Asish Karthikeya Gogineni commited on
Commit
2156541
·
1 Parent(s): 134ff3d

Refactor: Enhanced UI (Tabs/Explorer) and Modular Components

Browse files
app.py CHANGED
@@ -18,337 +18,8 @@ st.set_page_config(page_title="Code Chatbot", page_icon="💻", layout="wide", i
18
  logging.basicConfig(level=logging.INFO)
19
 
20
  # --- Custom CSS for Premium Slate UI ---
21
- import base64
22
- def get_base64_logo():
23
- try:
24
- with open("assets/logo.png", "rb") as f:
25
- data = f.read()
26
- return base64.b64encode(data).decode()
27
- except:
28
- return ""
29
-
30
- logo_b64 = get_base64_logo()
31
-
32
- css = """
33
- <style>
34
- /* -------------------------------------------------------------------------- */
35
- /* CORE ANIMATIONS */
36
- /* -------------------------------------------------------------------------- */
37
- @keyframes gradient-xy {
38
- 0% { background-position: 0% 50%; }
39
- 50% { background-position: 100% 50%; }
40
- 100% { background-position: 0% 50%; }
41
- }
42
-
43
- @keyframes fadeInUp {
44
- from { opacity: 0; transform: translateY(10px); }
45
- to { opacity: 1; transform: translateY(0); }
46
- }
47
-
48
- /* -------------------------------------------------------------------------- */
49
- /* GLOBAL THEME ENGINE */
50
- /* -------------------------------------------------------------------------- */
51
- @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap');
52
-
53
- :root {
54
- --primary-glow: 56, 189, 248; /* Sky Blue */
55
- --secondary-glow: 139, 92, 246; /* Violet */
56
- --bg-deep: #050608;
57
- --glass-border: rgba(255, 255, 255, 0.08);
58
- --glass-bg: rgba(15, 23, 42, 0.6);
59
- }
60
-
61
- .stApp {
62
- background: radial-gradient(circle at 10% 20%, rgba(13, 17, 28, 1) 0%, rgba(5, 6, 8, 1) 90%);
63
- font-family: 'Outfit', sans-serif;
64
- }
65
-
66
- /* BACKGROUND WATERMARK */
67
- .stApp::before {
68
- content: "";
69
- position: absolute;
70
- top: 50%;
71
- left: 50%;
72
- transform: translate(-50%, -50%);
73
- width: 70vh; /* Slightly smaller to fit nicely */
74
- height: 70vh;
75
- background-image: url("data:image/png;base64,LOGO_BASE64_PLACEHOLDER");
76
- background-position: center;
77
- background-repeat: no-repeat;
78
- background-size: contain;
79
- opacity: 0.08; /* Subtle but visible color */
80
- pointer-events: none;
81
- z-index: 0;
82
- border-radius: 50%; /* Force Circular Shape */
83
- }
84
-
85
- /* Sidebar Logo - Standard Shape */
86
- [data-testid="stSidebar"] img {
87
- border-radius: 12px; /* Slight rounded corners for better aesthetics, but not circular */
88
- box-shadow: 0 0 20px rgba(56, 189, 248, 0.3); /* Neon Glow */
89
- border: 1px solid rgba(56, 189, 248, 0.5);
90
- }
91
-
92
- /* Global Text Override */
93
- p, div, span, label, h1, h2, h3, h4, h5, h6, .stMarkdown {
94
- color: #E2E8F0 !important;
95
- text-shadow: 0 1px 2px rgba(0,0,0,0.3);
96
- }
97
-
98
- /* -------------------------------------------------------------------------- */
99
- /* SIDEBAR */
100
- /* -------------------------------------------------------------------------- */
101
- section[data-testid="stSidebar"] {
102
- background: rgba(11, 12, 16, 0.85);
103
- backdrop-filter: blur(20px);
104
- -webkit-backdrop-filter: blur(20px);
105
- border-right: 1px solid var(--glass-border);
106
- box-shadow: 5px 0 30px rgba(0,0,0,0.5);
107
- }
108
-
109
- section[data-testid="stSidebar"] h1 {
110
- background: linear-gradient(to right, #38BDF8, #8B5CF6);
111
- -webkit-background-clip: text;
112
- -webkit-text-fill-color: transparent;
113
- font-weight: 800;
114
- font-size: 2rem !important;
115
- padding-bottom: 0.5rem;
116
- }
117
-
118
- /* -------------------------------------------------------------------------- */
119
- /* INPUTS & FORMS */
120
- /* -------------------------------------------------------------------------- */
121
- .stTextInput input, .stSelectbox div[data-baseweb="select"], .stTextArea textarea {
122
- background-color: rgba(30, 41, 59, 0.5) !important;
123
- border: 1px solid var(--glass-border) !important;
124
- color: #F8FAFC !important;
125
- border-radius: 12px !important;
126
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
127
- backdrop-filter: blur(10px);
128
- }
129
-
130
- .stTextInput input:focus, .stTextArea textarea:focus, .stSelectbox div[data-baseweb="select"]:focus-within {
131
- border-color: #38BDF8 !important;
132
- box-shadow: 0 0 15px rgba(var(--primary-glow), 0.3);
133
- transform: translateY(-1px);
134
- }
135
-
136
- /* -------------------------------------------------------------------------- */
137
- /* MEDIA UPLOADS */
138
- /* -------------------------------------------------------------------------- */
139
- [data-testid="stFileUploader"] {
140
- background-color: rgba(30, 41, 59, 0.4);
141
- border: 1px dashed var(--glass-border);
142
- border-radius: 12px;
143
- padding: 20px;
144
- }
145
-
146
- /* FORCE TEXT COLOR FOR FILE UPLOADER */
147
- [data-testid="stFileUploader"] section > div,
148
- [data-testid="stFileUploader"] section > div > span,
149
- [data-testid="stFileUploader"] section > div > small,
150
- [data-testid="stFileUploader"] div[data-testid="stMarkdownContainer"] p {
151
- color: #E2E8F0 !important; /* Bright Slate */
152
- opacity: 1 !important;
153
- -webkit-text-fill-color: #E2E8F0 !important;
154
- }
155
-
156
- [data-testid="stFileUploader"] button {
157
- background: rgba(56, 189, 248, 0.2);
158
- color: #38BDF8 !important;
159
- border: 1px solid #38BDF8;
160
- }
161
-
162
- /* -------------------------------------------------------------------------- */
163
- /* DROPDOWN & SELECT */
164
- /* -------------------------------------------------------------------------- */
165
-
166
- /* 1. The Box Itself */
167
- .stSelectbox div[data-baseweb="select"] {
168
- background-color: #1E293B !important; /* Solid Slate-800 for contrast */
169
- border: 1px solid #475569 !important;
170
- color: white !important;
171
- }
172
-
173
- /* 2. The Text INSIDE the Box (Critical Fix) */
174
- .stSelectbox div[data-baseweb="select"] div[data-testid="stMarkdownContainer"] > p {
175
- color: #F8FAFC !important; /* White */
176
- font-weight: 500 !important;
177
- }
178
-
179
- /* 3. The Dropdown Menu (Popup) */
180
- div[data-baseweb="popover"], div[data-baseweb="menu"], ul[data-baseweb="menu"] {
181
- background-color: #0F172A !important;
182
- border: 1px solid #334155 !important;
183
- }
184
-
185
- /* 4. Options in the Menu */
186
- li[data-baseweb="option"], div[data-baseweb="option"] {
187
- color: #CBD5E1 !important; /* Light Slate */
188
- }
189
-
190
- /* 5. Start/Icons in Menu */
191
- li[data-baseweb="option"] *, div[data-baseweb="option"] * {
192
- color: #CBD5E1 !important;
193
- }
194
-
195
- /* 6. Selected/Hovered Option */
196
- li[data-baseweb="option"][aria-selected="true"],
197
- li[data-baseweb="option"]:hover,
198
- div[data-baseweb="option"]:hover {
199
- background-color: #38BDF8 !important;
200
- color: white !important;
201
- }
202
-
203
- /* 7. SVG Arrow Icon */
204
- .stSelectbox svg {
205
- fill: #94A3B8 !important;
206
- }
207
-
208
- /* -------------------------------------------------------------------------- */
209
- /* BUTTONS */
210
- /* -------------------------------------------------------------------------- */
211
- .stButton button {
212
- background: linear-gradient(135deg, #0EA5E9 0%, #2563EB 100%);
213
- color: white !important;
214
- border: none;
215
- border-radius: 12px;
216
- padding: 0.75rem 1.5rem;
217
- font-weight: 600;
218
- letter-spacing: 0.5px;
219
- transition: all 0.3s ease;
220
- box-shadow: 0 4px 14px rgba(14, 165, 233, 0.3);
221
- text-transform: uppercase;
222
- font-size: 0.85rem;
223
- }
224
-
225
- .stButton button:hover {
226
- transform: translateY(-2px) scale(1.02);
227
- box-shadow: 0 6px 20px rgba(14, 165, 233, 0.5);
228
- }
229
- .stButton button:active {
230
- transform: translateY(0);
231
- }
232
-
233
- /* -------------------------------------------------------------------------- */
234
- /* CHAT BUBBLES */
235
- /* -------------------------------------------------------------------------- */
236
- .stChatMessage {
237
- background: var(--glass-bg);
238
- border: 1px solid var(--glass-border);
239
- border-radius: 16px;
240
- backdrop-filter: blur(10px);
241
- margin-bottom: 1rem;
242
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
243
- animation: fadeInUp 0.4s ease-out forwards;
244
- }
245
-
246
- .stChatMessage[data-testid="stChatMessage"]:nth-child(even) {
247
- border-left: 3px solid #38BDF8;
248
- background: linear-gradient(90deg, rgba(56, 189, 248, 0.05) 0%, rgba(15, 23, 42, 0.6) 100%);
249
- }
250
-
251
- /* -------------------------------------------------------------------------- */
252
- /* CODE & CHIPS */
253
- /* -------------------------------------------------------------------------- */
254
- code {
255
- font-family: 'JetBrains Mono', monospace !important;
256
- background: #0B0E14 !important;
257
- border: 1px solid #1E293B;
258
- border-radius: 6px;
259
- color: #7DD3FC !important;
260
- }
261
-
262
- /* Source Chips with Glow */
263
- .source-container {
264
- display: flex;
265
- flex-wrap: wrap;
266
- gap: 8px;
267
- margin-bottom: 16px;
268
- padding-bottom: 12px;
269
- border-bottom: 1px solid rgba(255,255,255,0.05);
270
- }
271
-
272
- .source-chip {
273
- background: rgba(30, 41, 59, 0.6);
274
- border: 1px solid rgba(56, 189, 248, 0.2);
275
- border-radius: 20px;
276
- padding: 6px 14px;
277
- font-size: 0.8rem;
278
- color: #94A3B8;
279
- display: flex;
280
- align-items: center;
281
- transition: all 0.3s ease;
282
- cursor: pointer;
283
- backdrop-filter: blur(5px);
284
- }
285
-
286
- .source-chip:hover {
287
- background: rgba(56, 189, 248, 0.15);
288
- border-color: #38BDF8;
289
- color: #38BDF8;
290
- box-shadow: 0 0 10px rgba(56, 189, 248, 0.2);
291
- transform: translateY(-1px);
292
- }
293
-
294
- .source-icon {
295
- margin-right: 8px;
296
- opacity: 0.7;
297
- }
298
-
299
- /* Hiding Streamlit Branding */
300
- #MainMenu {visibility: hidden;}
301
- footer {visibility: hidden;}
302
- header {visibility: hidden;}
303
-
304
- /* -------------------------------------------------------------------------- */
305
- /* IDE-STYLE PANEL LAYOUT */
306
- /* -------------------------------------------------------------------------- */
307
-
308
- /* Main content area - fixed height */
309
- .main .block-container {
310
- max-height: calc(100vh - 80px);
311
- overflow: hidden;
312
- padding-top: 1rem;
313
- }
314
-
315
- /* Make columns scrollable independently */
316
- [data-testid="column"] {
317
- max-height: calc(100vh - 120px);
318
- overflow-y: auto;
319
- overflow-x: hidden;
320
- scrollbar-width: thin;
321
- scrollbar-color: #475569 transparent;
322
- }
323
-
324
- /* Custom scrollbar for webkit browsers */
325
- [data-testid="column"]::-webkit-scrollbar {
326
- width: 6px;
327
- }
328
-
329
- [data-testid="column"]::-webkit-scrollbar-track {
330
- background: transparent;
331
- }
332
-
333
- [data-testid="column"]::-webkit-scrollbar-thumb {
334
- background: #475569;
335
- border-radius: 3px;
336
- }
337
-
338
- [data-testid="column"]::-webkit-scrollbar-thumb:hover {
339
- background: #64748b;
340
- }
341
-
342
- /* Code viewer specific - scrollable code block */
343
- .stCode {
344
- max-height: 60vh;
345
- overflow-y: auto !important;
346
- }
347
-
348
- </style>
349
- """
350
-
351
- st.markdown(css.replace("LOGO_BASE64_PLACEHOLDER", logo_b64), unsafe_allow_html=True)
352
 
353
  # Session State
354
  if "messages" not in st.session_state:
@@ -358,132 +29,18 @@ if "chat_engine" not in st.session_state:
358
  if "processed_files" not in st.session_state:
359
  st.session_state.processed_files = False
360
 
361
- # Sidebar
362
- with st.sidebar:
363
- # Logo
364
- if os.path.exists("assets/logo.png"):
365
- st.image("assets/logo.png", use_column_width=True)
366
-
367
- st.title("🔧 Configuration")
368
-
369
- # Provider Selection (Gemini & Groq only as requested)
370
- provider = st.radio("LLM Provider", ["gemini", "groq"])
371
-
372
- # Model Selection for Gemini
373
- gemini_model = None
374
- if provider == "gemini":
375
- gemini_model = st.selectbox(
376
- "Gemini Model",
377
- [
378
- "gemini-2.5-flash", # This one was working!
379
- "gemini-2.0-flash",
380
- "gemini-1.5-pro",
381
- ],
382
- index=0, # Default to 2.5 Flash (confirmed working)
383
- help="""**Gemini 2.5 Flash** (Recommended): Latest, confirmed working
384
- **Gemini 2.0 Flash**: Newer model
385
- **Gemini 1.5 Pro**: More stable for complex tasks"""
386
- )
387
- st.caption(f"✨ Using {gemini_model}")
388
-
389
- # Agentic Mode Toggle
390
- use_agent = st.checkbox("Enable Agentic Reasoning 🤖", value=True, help="Allows the AI to browse files and reason multiple steps.")
391
-
392
- # Determine Env Key Name
393
- if provider == "gemini":
394
- env_key_name = "GOOGLE_API_KEY"
395
- elif provider == "groq":
396
- env_key_name = "GROQ_API_KEY"
397
-
398
- env_key = os.getenv(env_key_name)
399
- api_key = env_key
400
-
401
- if env_key:
402
- st.success(f"✅ {env_key_name} loaded from environment.")
403
- else:
404
- # API Key Input
405
- api_key_label = f"{provider.capitalize()} API Key"
406
- api_key_input = st.text_input(api_key_label, type="password")
407
- if api_key_input:
408
- api_key = api_key_input
409
- os.environ[env_key_name] = api_key
410
-
411
- # Vector Database Selection
412
- vector_db_type = st.selectbox("Vector Database", ["faiss", "chroma", "qdrant"])
413
-
414
- if vector_db_type == "qdrant":
415
- st.caption("☁️ connect to a hosted Qdrant cluster")
416
- qdrant_url = st.text_input("Qdrant URL", placeholder="https://xyz.qdrant.io:6333", value=os.getenv("QDRANT_URL", ""))
417
- qdrant_key = st.text_input("Qdrant API Key", type="password", value=os.getenv("QDRANT_API_KEY", ""))
418
-
419
- if qdrant_url:
420
- os.environ["QDRANT_URL"] = qdrant_url
421
- if qdrant_key:
422
- os.environ["QDRANT_API_KEY"] = qdrant_key
423
-
424
- # For Groq, we need an embedding provider
425
- # Use LOCAL embeddings by default - NO RATE LIMITS!
426
- embedding_provider = "local" # Use local HuggingFace embeddings
427
- embedding_api_key = api_key
428
-
429
- if provider == "groq":
430
- st.info(f"ℹ️ {provider.capitalize()} is used for Chat. Using LOCAL embeddings (no rate limits!).")
431
- embedding_provider = "local" # Use local embeddings for Groq too
432
-
433
- # Check Embedding Key for Gemini (not needed for local)
434
- emb_env_key = os.getenv("GOOGLE_API_KEY")
435
- if not emb_env_key and provider != "gemini":
436
- embedding_api_key = emb_env_key # Optional now
437
- else:
438
- embedding_api_key = emb_env_key
439
-
440
- st.divider()
441
-
442
- # Ingestion moved to main area
443
-
444
- if st.session_state.processed_files:
445
- st.success(f"✅ Codebase Ready ({provider}) + AST 🧠")
446
-
447
- # Show usage statistics if available
448
- if st.session_state.chat_engine:
449
- try:
450
- from code_chatbot.rate_limiter import get_rate_limiter
451
- limiter = get_rate_limiter(provider)
452
- stats = limiter.get_usage_stats()
453
-
454
- st.divider()
455
- st.subheader("📊 API Usage")
456
- col1, col2 = st.columns(2)
457
- with col1:
458
- st.metric("Requests/min", stats['requests_last_minute'])
459
- st.metric("Cache Hits", stats['cache_size'])
460
- with col2:
461
- st.metric("Total Tokens", f"{stats['total_tokens']:,}")
462
- rpm_limit = 15 if provider == "gemini" else 30
463
- usage_pct = (stats['requests_last_minute'] / rpm_limit) * 100
464
- st.progress(usage_pct / 100, text=f"{usage_pct:.0f}% of limit")
465
- except Exception as e:
466
- pass # Stats are optional
467
-
468
- st.divider()
469
- if st.button("🗑️ Clear Chat History"):
470
- st.session_state.messages = []
471
- st.rerun()
472
-
473
- if st.button("Reset"):
474
- # Clear disk data for a true reset
475
- try:
476
- if os.path.exists("chroma_db"):
477
- shutil.rmtree("chroma_db")
478
- if os.path.exists("data"):
479
- shutil.rmtree("data")
480
- except Exception as e:
481
- st.error(f"Error clearing data: {e}")
482
-
483
- st.session_state.processed_files = False
484
- st.session_state.messages = []
485
- st.session_state.chat_engine = None
486
- st.rerun()
487
 
488
  # ============================================================================
489
  # MAIN 3-PANEL LAYOUT
 
18
  logging.basicConfig(level=logging.INFO)
19
 
20
  # --- Custom CSS for Premium Slate UI ---
21
+ from components import style
22
+ style.apply_custom_css()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  # Session State
25
  if "messages" not in st.session_state:
 
29
  if "processed_files" not in st.session_state:
30
  st.session_state.processed_files = False
31
 
32
+ # --- Sidebar Configuration ---
33
+ from components import sidebar
34
+ config = sidebar.render_sidebar()
35
+
36
+ # Extract config values for easy access in main app
37
+ provider = config["provider"]
38
+ api_key = config["api_key"]
39
+ gemini_model = config["gemini_model"]
40
+ use_agent = config["use_agent"]
41
+ vector_db_type = config["vector_db_type"]
42
+ embedding_provider = config["embedding_provider"]
43
+ embedding_api_key = config["embedding_api_key"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  # ============================================================================
46
  # MAIN 3-PANEL LAYOUT
code_chatbot/db_connection.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ import logging
4
+ from typing import Optional
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ # Vector database fallback priority order
9
+ VECTOR_DB_FALLBACK_ORDER = ["chroma", "faiss"]
10
+
11
+ # Track which vector DB is currently active (for automatic fallback)
12
+ _active_vector_db = {"type": "chroma", "fallback_count": 0}
13
+
14
+ def get_active_vector_db() -> str:
15
+ """Get the currently active vector database type."""
16
+ return _active_vector_db["type"]
17
+
18
+ def set_active_vector_db(db_type: str):
19
+ """Set the active vector database type."""
20
+ _active_vector_db["type"] = db_type
21
+ logger.info(f"Active vector database set to: {db_type}")
22
+
23
+ def get_next_fallback_db(current_db: str) -> Optional[str]:
24
+ """Get the next fallback vector database in the priority order."""
25
+ try:
26
+ current_idx = VECTOR_DB_FALLBACK_ORDER.index(current_db)
27
+ if current_idx + 1 < len(VECTOR_DB_FALLBACK_ORDER):
28
+ return VECTOR_DB_FALLBACK_ORDER[current_idx + 1]
29
+ except ValueError:
30
+ pass
31
+ return None
32
+
33
+ # Global ChromaDB client cache to avoid "different settings" error
34
+ _chroma_clients = {}
35
+
36
+ def reset_chroma_clients():
37
+ """Reset all cached ChromaDB clients. Call when database corruption is detected."""
38
+ global _chroma_clients
39
+ _chroma_clients = {}
40
+ logger.info("Reset ChromaDB client cache")
41
+
42
+ def get_chroma_client(persist_directory: str):
43
+ """Get or create a shared ChromaDB client for a given path.
44
+
45
+ Includes automatic recovery for common ChromaDB errors:
46
+ - tenant default_tenant connection errors
47
+ - Database corruption
48
+ - Version mismatch issues
49
+ """
50
+ global _chroma_clients
51
+
52
+ # Ensure directory exists
53
+ os.makedirs(persist_directory, exist_ok=True)
54
+
55
+ if persist_directory not in _chroma_clients:
56
+ import chromadb
57
+ from chromadb.config import Settings
58
+
59
+ def create_client():
60
+ """Helper to create a new ChromaDB client."""
61
+ return chromadb.PersistentClient(
62
+ path=persist_directory,
63
+ settings=Settings(
64
+ anonymized_telemetry=False,
65
+ allow_reset=True
66
+ )
67
+ )
68
+
69
+ def clear_and_recreate():
70
+ """Clear corrupted database and create fresh client."""
71
+ logger.warning(f"Clearing corrupted ChromaDB at {persist_directory} and recreating...")
72
+ if os.path.exists(persist_directory):
73
+ shutil.rmtree(persist_directory)
74
+ os.makedirs(persist_directory, exist_ok=True)
75
+ return create_client()
76
+
77
+ def is_corruption_error(error: Exception) -> bool:
78
+ """Check if error indicates database corruption."""
79
+ error_str = str(error).lower()
80
+ corruption_indicators = [
81
+ 'tenant', # "Could not connect to tenant default_tenant"
82
+ 'default_tenant',
83
+ 'sqlite', # SQLite database issues
84
+ 'database',
85
+ 'corrupt',
86
+ 'no such table',
87
+ 'disk i/o error',
88
+ 'malformed',
89
+ 'locked',
90
+ ]
91
+ return any(indicator in error_str for indicator in corruption_indicators)
92
+
93
+ try:
94
+ _chroma_clients[persist_directory] = create_client()
95
+ # Verify the client works by attempting a simple operation
96
+ try:
97
+ _chroma_clients[persist_directory].heartbeat()
98
+ except Exception as verify_error:
99
+ if is_corruption_error(verify_error):
100
+ logger.error(f"ChromaDB verification failed: {verify_error}")
101
+ del _chroma_clients[persist_directory]
102
+ _chroma_clients[persist_directory] = clear_and_recreate()
103
+ else:
104
+ raise
105
+ except Exception as e:
106
+ logger.error(f"Failed to create ChromaDB client: {e}")
107
+ if is_corruption_error(e):
108
+ _chroma_clients[persist_directory] = clear_and_recreate()
109
+ else:
110
+ # For non-corruption errors, still try to recover
111
+ try:
112
+ _chroma_clients[persist_directory] = clear_and_recreate()
113
+ except Exception as recovery_error:
114
+ logger.error(f"Recovery also failed: {recovery_error}")
115
+ raise recovery_error
116
+
117
+ return _chroma_clients[persist_directory]
code_chatbot/indexer.py CHANGED
@@ -13,125 +13,13 @@ import logging
13
 
14
  logger = logging.getLogger(__name__)
15
 
16
- # Vector database fallback priority order
17
- # When primary DB fails, automatically try the next in list
18
- VECTOR_DB_FALLBACK_ORDER = ["chroma", "faiss"]
19
-
20
- # Track which vector DB is currently active (for automatic fallback)
21
- _active_vector_db = {"type": "chroma", "fallback_count": 0}
22
-
23
- def get_active_vector_db() -> str:
24
- """Get the currently active vector database type."""
25
- return _active_vector_db["type"]
26
-
27
- def set_active_vector_db(db_type: str):
28
- """Set the active vector database type."""
29
- _active_vector_db["type"] = db_type
30
- logger.info(f"Active vector database set to: {db_type}")
31
-
32
- def get_next_fallback_db(current_db: str) -> Optional[str]:
33
- """Get the next fallback vector database in the priority order.
34
-
35
- Args:
36
- current_db: Current vector database type that failed
37
-
38
- Returns:
39
- Next vector database type to try, or None if no more fallbacks
40
- """
41
- try:
42
- current_idx = VECTOR_DB_FALLBACK_ORDER.index(current_db)
43
- if current_idx + 1 < len(VECTOR_DB_FALLBACK_ORDER):
44
- return VECTOR_DB_FALLBACK_ORDER[current_idx + 1]
45
- except ValueError:
46
- pass
47
- return None
48
-
49
- # Global ChromaDB client cache to avoid "different settings" error
50
- _chroma_clients = {}
51
-
52
- def reset_chroma_clients():
53
- """Reset all cached ChromaDB clients. Call when database corruption is detected."""
54
- global _chroma_clients
55
- _chroma_clients = {}
56
- logger.info("Reset ChromaDB client cache")
57
-
58
- def get_chroma_client(persist_directory: str):
59
- """Get or create a shared ChromaDB client for a given path.
60
-
61
- Includes automatic recovery for common ChromaDB errors:
62
- - tenant default_tenant connection errors
63
- - Database corruption
64
- - Version mismatch issues
65
- """
66
- global _chroma_clients
67
-
68
- # Ensure directory exists
69
- os.makedirs(persist_directory, exist_ok=True)
70
-
71
- if persist_directory not in _chroma_clients:
72
- import chromadb
73
- from chromadb.config import Settings
74
-
75
- def create_client():
76
- """Helper to create a new ChromaDB client."""
77
- return chromadb.PersistentClient(
78
- path=persist_directory,
79
- settings=Settings(
80
- anonymized_telemetry=False,
81
- allow_reset=True
82
- )
83
- )
84
-
85
- def clear_and_recreate():
86
- """Clear corrupted database and create fresh client."""
87
- logger.warning(f"Clearing corrupted ChromaDB at {persist_directory} and recreating...")
88
- import shutil
89
- if os.path.exists(persist_directory):
90
- shutil.rmtree(persist_directory)
91
- os.makedirs(persist_directory, exist_ok=True)
92
- return create_client()
93
-
94
- def is_corruption_error(error: Exception) -> bool:
95
- """Check if error indicates database corruption."""
96
- error_str = str(error).lower()
97
- corruption_indicators = [
98
- 'tenant', # "Could not connect to tenant default_tenant"
99
- 'default_tenant',
100
- 'sqlite', # SQLite database issues
101
- 'database',
102
- 'corrupt',
103
- 'no such table',
104
- 'disk i/o error',
105
- 'malformed',
106
- 'locked',
107
- ]
108
- return any(indicator in error_str for indicator in corruption_indicators)
109
-
110
- try:
111
- _chroma_clients[persist_directory] = create_client()
112
- # Verify the client works by attempting a simple operation
113
- try:
114
- _chroma_clients[persist_directory].heartbeat()
115
- except Exception as verify_error:
116
- if is_corruption_error(verify_error):
117
- logger.error(f"ChromaDB verification failed: {verify_error}")
118
- del _chroma_clients[persist_directory]
119
- _chroma_clients[persist_directory] = clear_and_recreate()
120
- else:
121
- raise
122
- except Exception as e:
123
- logger.error(f"Failed to create ChromaDB client: {e}")
124
- if is_corruption_error(e):
125
- _chroma_clients[persist_directory] = clear_and_recreate()
126
- else:
127
- # For non-corruption errors, still try to recover
128
- try:
129
- _chroma_clients[persist_directory] = clear_and_recreate()
130
- except Exception as recovery_error:
131
- logger.error(f"Recovery also failed: {recovery_error}")
132
- raise recovery_error
133
-
134
- return _chroma_clients[persist_directory]
135
 
136
 
137
  class Indexer:
 
13
 
14
  logger = logging.getLogger(__name__)
15
 
16
+ from code_chatbot.db_connection import (
17
+ get_chroma_client,
18
+ reset_chroma_clients,
19
+ set_active_vector_db,
20
+ get_next_fallback_db,
21
+ VECTOR_DB_FALLBACK_ORDER
22
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
 
25
  class Indexer:
code_chatbot/prompts.py CHANGED
@@ -33,7 +33,7 @@ SYSTEM_PROMPT_AGENT = """You are an expert software engineer pair-programming wi
33
  - Briefly explain *why* it fits the existing patterns.
34
 
35
  **CRITICAL RULES**:
36
- 1. **NO HTML**: Use only Markdown.
37
  2. **NO HALLUCINATION**: Only cite files that exist in the retrieved context.
38
  3. **NO LECTURES**: Don't explain general programming concepts unless asked.
39
  """
@@ -56,7 +56,7 @@ SYSTEM_PROMPT_LINEAR_RAG = """You are an expert pair-programmer analyzing the co
56
 
57
  **CRITICAL RULES**:
58
  - **NO HALLUCINATION**: Only use code from the context above.
59
- - **NO HTML**: Use standard Markdown only.
60
  - **Keep it Short**: If a 2-sentence answer suffices, do not write a paragraph.
61
  """
62
 
 
33
  - Briefly explain *why* it fits the existing patterns.
34
 
35
  **CRITICAL RULES**:
36
+ 1. **NO HTML**: Use only Markdown. Do NOT generate HTML tags like <div> or <span>. Do NOT render "source chips".
37
  2. **NO HALLUCINATION**: Only cite files that exist in the retrieved context.
38
  3. **NO LECTURES**: Don't explain general programming concepts unless asked.
39
  """
 
56
 
57
  **CRITICAL RULES**:
58
  - **NO HALLUCINATION**: Only use code from the context above.
59
+ - **NO HTML**: Use standard Markdown only. Do NOT generate <div> tags.
60
  - **Keep it Short**: If a 2-sentence answer suffices, do not write a paragraph.
61
  """
62
 
code_chatbot/rag.py CHANGED
@@ -319,6 +319,15 @@ class ChatEngine:
319
  else:
320
  answer = raw_content
321
 
 
 
 
 
 
 
 
 
 
322
  # Update history
323
  self.chat_history.append(HumanMessage(content=question))
324
  self.chat_history.append(AIMessage(content=answer))
 
319
  else:
320
  answer = raw_content
321
 
322
+ # CLEANING: Remove hallucinated source chips
323
+ import re
324
+ # Remove the specific div block structure
325
+ answer = re.sub(r'<div class="source-chip">.*?</div>\s*</div>', '', answer, flags=re.DOTALL)
326
+ # Remove standalone chips if any remain
327
+ answer = re.sub(r'<div class="source-chip">.*?</div>', '', answer, flags=re.DOTALL)
328
+ # Clean up leading whitespace/newlines left behind
329
+ answer = answer.strip()
330
+
331
  # Update history
332
  self.chat_history.append(HumanMessage(content=question))
333
  self.chat_history.append(AIMessage(content=answer))
components/file_explorer.py CHANGED
@@ -56,33 +56,7 @@ def render_file_tree(indexed_files: List[str], base_path: str = ""):
56
  return
57
 
58
  # Custom CSS for tree styling
59
- st.markdown("""
60
- <style>
61
- .tree-item {
62
- padding: 2px 0;
63
- cursor: pointer;
64
- font-size: 13px;
65
- font-family: 'Segoe UI', sans-serif;
66
- color: #ccc;
67
- white-space: nowrap;
68
- overflow: hidden;
69
- text-overflow: ellipsis;
70
- }
71
- .tree-item:hover {
72
- background: rgba(255,255,255,0.1);
73
- }
74
- .tree-dir {
75
- font-weight: 500;
76
- }
77
- .tree-file {
78
- font-weight: 400;
79
- }
80
- .tree-selected {
81
- background: rgba(56, 189, 248, 0.2) !important;
82
- color: #38bdf8;
83
- }
84
- </style>
85
- """, unsafe_allow_html=True)
86
 
87
  st.markdown(f"**📁 Files** ({len(indexed_files)})")
88
 
@@ -136,7 +110,7 @@ def render_tree_items(tree: Dict, depth: int):
136
 
137
  # Render children if expanded
138
  if is_expanded:
139
- children = {k: v for k, v in node.items() if not k.startswith("_")}
140
  render_tree_items(children, depth + 1)
141
 
142
 
 
56
  return
57
 
58
  # Custom CSS for tree styling
59
+ # Styles are now handled globally in components/style.py for modularity
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
  st.markdown(f"**📁 Files** ({len(indexed_files)})")
62
 
 
110
 
111
  # Render children if expanded
112
  if is_expanded:
113
+ children = node.get("_children", {})
114
  render_tree_items(children, depth + 1)
115
 
116
 
components/sidebar.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ import shutil
4
+ from dotenv import load_dotenv
5
+
6
+ # Load Env
7
+ load_dotenv()
8
+
9
+ def render_sidebar():
10
+ """
11
+ Renders the sidebar configuration panel.
12
+ Returns:
13
+ dict: A dictionary containing the configuration settings:
14
+ - api_key (str)
15
+ - provider (str)
16
+ - gemini_model (str)
17
+ - use_agent (bool)
18
+ - vector_db_type (str)
19
+ - embedding_provider (str)
20
+ - embedding_api_key (str)
21
+ """
22
+ config = {}
23
+
24
+ with st.sidebar:
25
+ # Logo
26
+ if os.path.exists("assets/logo.png"):
27
+ st.image("assets/logo.png", use_column_width=True)
28
+
29
+ st.title("🔧 Configuration")
30
+
31
+ # Provider Selection (Gemini & Groq only as requested)
32
+ provider = st.radio("LLM Provider", ["gemini", "groq"])
33
+ config["provider"] = provider
34
+
35
+ # Model Selection for Gemini
36
+ gemini_model = None
37
+ if provider == "gemini":
38
+ gemini_model = st.selectbox(
39
+ "Gemini Model",
40
+ [
41
+ "gemini-2.5-flash", # This one was working!
42
+ "gemini-2.0-flash",
43
+ "gemini-1.5-pro",
44
+ ],
45
+ index=0, # Default to 2.5 Flash (confirmed working)
46
+ help="""**Gemini 2.5 Flash** (Recommended): Latest, confirmed working
47
+ **Gemini 2.0 Flash**: Newer model
48
+ **Gemini 1.5 Pro**: More stable for complex tasks"""
49
+ )
50
+ st.caption(f"✨ Using {gemini_model}")
51
+ config["gemini_model"] = gemini_model
52
+
53
+ # Agentic Mode Toggle
54
+ use_agent = st.checkbox("Enable Agentic Reasoning 🤖", value=True, help="Allows the AI to browse files and reason multiple steps.")
55
+ config["use_agent"] = use_agent
56
+
57
+ # Determine Env Key Name
58
+ if provider == "gemini":
59
+ env_key_name = "GOOGLE_API_KEY"
60
+ elif provider == "groq":
61
+ env_key_name = "GROQ_API_KEY"
62
+
63
+ env_key = os.getenv(env_key_name)
64
+ api_key = env_key
65
+
66
+ if env_key:
67
+ st.success(f"✅ {env_key_name} loaded from environment.")
68
+ else:
69
+ # API Key Input
70
+ api_key_label = f"{provider.capitalize()} API Key"
71
+ api_key_input = st.text_input(api_key_label, type="password")
72
+ if api_key_input:
73
+ api_key = api_key_input
74
+ os.environ[env_key_name] = api_key
75
+ config["api_key"] = api_key
76
+
77
+ # Vector Database Selection
78
+ vector_db_type = st.selectbox("Vector Database", ["faiss", "chroma", "qdrant"])
79
+ config["vector_db_type"] = vector_db_type
80
+
81
+ if vector_db_type == "qdrant":
82
+ st.caption("☁️ connect to a hosted Qdrant cluster")
83
+ qdrant_url = st.text_input("Qdrant URL", placeholder="https://xyz.qdrant.io:6333", value=os.getenv("QDRANT_URL", ""))
84
+ qdrant_key = st.text_input("Qdrant API Key", type="password", value=os.getenv("QDRANT_API_KEY", ""))
85
+
86
+ if qdrant_url:
87
+ os.environ["QDRANT_URL"] = qdrant_url
88
+ if qdrant_key:
89
+ os.environ["QDRANT_API_KEY"] = qdrant_key
90
+
91
+ # For Groq, we need an embedding provider
92
+ # Use LOCAL embeddings by default - NO RATE LIMITS!
93
+ embedding_provider = "local" # Use local HuggingFace embeddings
94
+ embedding_api_key = api_key
95
+
96
+ if provider == "groq":
97
+ st.info(f"ℹ️ {provider.capitalize()} is used for Chat. Using LOCAL embeddings (no rate limits!).")
98
+ embedding_provider = "local" # Use local embeddings for Groq too
99
+
100
+ # Check Embedding Key for Gemini (not needed for local)
101
+ emb_env_key = os.getenv("GOOGLE_API_KEY")
102
+ if not emb_env_key and provider != "gemini":
103
+ embedding_api_key = emb_env_key # Optional now
104
+ else:
105
+ embedding_api_key = emb_env_key
106
+
107
+ config["embedding_provider"] = embedding_provider
108
+ config["embedding_api_key"] = embedding_api_key
109
+
110
+ st.divider()
111
+
112
+ # Ingestion moved to main area
113
+
114
+ if st.session_state.processed_files:
115
+ st.success(f"✅ Codebase Ready ({provider}) + AST 🧠")
116
+
117
+ # Show usage statistics if available
118
+ if st.session_state.chat_engine:
119
+ try:
120
+ from code_chatbot.rate_limiter import get_rate_limiter
121
+ limiter = get_rate_limiter(provider)
122
+ stats = limiter.get_usage_stats()
123
+
124
+ st.divider()
125
+ st.subheader("📊 API Usage")
126
+ col1, col2 = st.columns(2)
127
+ with col1:
128
+ st.metric("Requests/min", stats['requests_last_minute'])
129
+ st.metric("Cache Hits", stats['cache_size'])
130
+ with col2:
131
+ st.metric("Total Tokens", f"{stats['total_tokens']:,}")
132
+ rpm_limit = 15 if provider == "gemini" else 30
133
+ usage_pct = (stats['requests_last_minute'] / rpm_limit) * 100
134
+ st.progress(usage_pct / 100, text=f"{usage_pct:.0f}% of limit")
135
+ except Exception as e:
136
+ pass # Stats are optional
137
+
138
+ st.divider()
139
+ if st.button("🗑️ Clear Chat History"):
140
+ st.session_state.messages = []
141
+ st.rerun()
142
+
143
+ if st.button("Reset"):
144
+ # Clear disk data for a true reset
145
+ try:
146
+ if os.path.exists("chroma_db"):
147
+ shutil.rmtree("chroma_db")
148
+ if os.path.exists("data"):
149
+ shutil.rmtree("data")
150
+ except Exception as e:
151
+ st.error(f"Error clearing data: {e}")
152
+
153
+ st.session_state.processed_files = False
154
+ st.session_state.messages = []
155
+ st.session_state.chat_engine = None
156
+ st.rerun()
157
+
158
+ return config
components/style.py CHANGED
@@ -14,110 +14,394 @@ def apply_custom_css():
14
 
15
  st.markdown("""
16
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  :root {
18
- --glass-bg: rgba(30, 41, 59, 0.7);
19
- --glass-border: rgba(255, 255, 255, 0.1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
 
22
- /* Global Text */
23
  p, div, span, label, h1, h2, h3, h4, h5, h6, .stMarkdown {
24
  color: #E2E8F0 !important;
 
25
  }
26
-
27
- /* Sidebar */
 
 
28
  section[data-testid="stSidebar"] {
29
- background: rgba(11, 12, 16, 0.95);
 
 
30
  border-right: 1px solid var(--glass-border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
 
33
- /* Buttons */
34
- .stButton button {
 
 
 
 
 
 
 
 
 
 
 
 
35
  background: linear-gradient(135deg, #0EA5E9 0%, #2563EB 100%);
36
  color: white !important;
37
  border: none;
38
  border-radius: 8px;
 
39
  font-weight: 600;
 
 
 
 
 
40
  }
41
- .stButton button:hover {
42
- transform: translateY(-1px);
43
- box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
- /* Chat Messages */
 
 
 
 
 
 
 
 
 
 
47
  .stChatMessage {
48
  background: var(--glass-bg);
49
  border: 1px solid var(--glass-border);
50
- border-radius: 12px;
 
 
 
 
51
  }
 
52
  .stChatMessage[data-testid="stChatMessage"]:nth-child(even) {
53
  border-left: 3px solid #38BDF8;
54
  background: linear-gradient(90deg, rgba(56, 189, 248, 0.05) 0%, rgba(15, 23, 42, 0.6) 100%);
55
  }
56
 
57
- /* IDE Layout & Scrolling */
58
- .main .block-container {
59
- max-width: 100% !important;
60
- padding-top: 1rem !important; /* Minimized top padding */
61
- padding-left: 1rem !important;
62
- padding-right: 1rem !important;
63
- max-height: 100vh;
64
- overflow: hidden;
 
65
  }
66
 
67
- /* Align Tabs with Editor */
68
- .stTabs [data-baseweb="tab-list"] {
69
- gap: 2px;
70
- background-color: transparent;
 
 
 
 
71
  }
72
- .stTabs [data-baseweb="tab"] {
73
- height: 50px;
74
- white-space: pre-wrap;
75
- border-radius: 4px 4px 0px 0px;
76
- gap: 2px;
77
- padding-top: 0px;
78
- padding-bottom: 0px;
79
- color: #f8fafc !important; /* Force white text */
80
- font-weight: 600;
 
 
 
 
 
 
 
 
 
 
 
 
81
  }
82
 
83
- div[data-testid="column"] {
84
- max-height: calc(100vh - 60px);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  overflow-y: auto;
86
  overflow-x: hidden;
87
  scrollbar-width: thin;
 
88
  }
89
 
90
- .stCode {
91
- max-height: 75vh !important;
92
- overflow-y: auto !important;
93
- }
94
-
95
- /* Scrollbar styling */
96
- ::-webkit-scrollbar {
97
  width: 6px;
98
- height: 6px;
99
  }
100
- ::-webkit-scrollbar-track {
 
101
  background: transparent;
102
  }
103
- ::-webkit-scrollbar-thumb {
 
104
  background: #475569;
105
  border-radius: 3px;
106
  }
107
 
108
- /* Source Chips */
109
- .source-chip {
110
- background: rgba(30, 41, 59, 0.4);
111
- border: 1px solid rgba(148, 163, 184, 0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  border-radius: 6px;
113
- padding: 4px 10px;
114
- font-size: 0.85em;
115
- color: #cbd5e1;
116
- display: inline-flex;
117
- align-items: center;
118
  gap: 6px;
119
- margin-right: 8px;
120
- margin-bottom: 8px;
 
 
 
 
 
 
 
 
 
 
121
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  </style>
123
- """, unsafe_allow_html=True)
 
14
 
15
  st.markdown("""
16
  <style>
17
+ /* -------------------------------------------------------------------------- */
18
+ /* CORE ANIMATIONS */
19
+ /* -------------------------------------------------------------------------- */
20
+ @keyframes gradient-xy {
21
+ 0% { background-position: 0% 50%; }
22
+ 50% { background-position: 100% 50%; }
23
+ 100% { background-position: 0% 50%; }
24
+ }
25
+
26
+ @keyframes fadeInUp {
27
+ from { opacity: 0; transform: translateY(10px); }
28
+ to { opacity: 1; transform: translateY(0); }
29
+ }
30
+
31
+ /* -------------------------------------------------------------------------- */
32
+ /* GLOBAL THEME ENGINE */
33
+ /* -------------------------------------------------------------------------- */
34
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;700&display=swap');
35
+
36
  :root {
37
+ --primary-glow: 56, 189, 248; /* Sky Blue */
38
+ --secondary-glow: 139, 92, 246; /* Violet */
39
+ --bg-deep: #050608;
40
+ --glass-border: rgba(255, 255, 255, 0.08);
41
+ --glass-bg: rgba(15, 23, 42, 0.6);
42
+ }
43
+
44
+ .stApp {
45
+ background: radial-gradient(circle at 10% 20%, rgba(13, 17, 28, 1) 0%, rgba(5, 6, 8, 1) 90%);
46
+ font-family: 'Outfit', sans-serif;
47
+ }
48
+
49
+ /* BACKGROUND WATERMARK */
50
+ .stApp::before {
51
+ content: "";
52
+ position: absolute;
53
+ top: 50%;
54
+ left: 50%;
55
+ transform: translate(-50%, -50%);
56
+ width: 70vh; /* Slightly smaller to fit nicely */
57
+ height: 70vh;
58
+ background-image: url("data:image/png;base64,LOGO_BASE64_PLACEHOLDER");
59
+ background-position: center;
60
+ background-repeat: no-repeat;
61
+ background-size: contain;
62
+ opacity: 0.08; /* Subtle but visible color */
63
+ pointer-events: none;
64
+ z-index: 0;
65
+ border-radius: 50%; /* Force Circular Shape */
66
+ }
67
+
68
+ /* Sidebar Logo - Standard Shape */
69
+ [data-testid="stSidebar"] img {
70
+ border-radius: 12px; /* Slight rounded corners for better aesthetics, but not circular */
71
+ box-shadow: 0 0 20px rgba(56, 189, 248, 0.3); /* Neon Glow */
72
+ border: 1px solid rgba(56, 189, 248, 0.5);
73
  }
74
 
75
+ /* Global Text Override */
76
  p, div, span, label, h1, h2, h3, h4, h5, h6, .stMarkdown {
77
  color: #E2E8F0 !important;
78
+ text-shadow: 0 1px 2px rgba(0,0,0,0.3);
79
  }
80
+
81
+ /* -------------------------------------------------------------------------- */
82
+ /* SIDEBAR */
83
+ /* -------------------------------------------------------------------------- */
84
  section[data-testid="stSidebar"] {
85
+ background: rgba(11, 12, 16, 0.85);
86
+ backdrop-filter: blur(20px);
87
+ -webkit-backdrop-filter: blur(20px);
88
  border-right: 1px solid var(--glass-border);
89
+ box-shadow: 5px 0 30px rgba(0,0,0,0.5);
90
+ }
91
+
92
+ section[data-testid="stSidebar"] h1 {
93
+ background: linear-gradient(to right, #38BDF8, #8B5CF6);
94
+ -webkit-background-clip: text;
95
+ -webkit-text-fill-color: transparent;
96
+ font-weight: 800;
97
+ font-size: 2rem !important;
98
+ padding-bottom: 0.5rem;
99
+ }
100
+
101
+ /* -------------------------------------------------------------------------- */
102
+ /* INPUTS & FORMS */
103
+ /* -------------------------------------------------------------------------- */
104
+ .stTextInput input, .stSelectbox div[data-baseweb="select"], .stTextArea textarea {
105
+ background-color: rgba(30, 41, 59, 0.5) !important;
106
+ border: 1px solid var(--glass-border) !important;
107
+ color: #F8FAFC !important;
108
+ border-radius: 12px !important;
109
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
110
+ backdrop-filter: blur(10px);
111
+ }
112
+
113
+ .stTextInput input:focus, .stTextArea textarea:focus, .stSelectbox div[data-baseweb="select"]:focus-within {
114
+ border-color: #38BDF8 !important;
115
+ box-shadow: 0 0 15px rgba(var(--primary-glow), 0.3);
116
+ transform: translateY(-1px);
117
+ }
118
+
119
+ /* -------------------------------------------------------------------------- */
120
+ /* MEDIA UPLOADS */
121
+ /* -------------------------------------------------------------------------- */
122
+ [data-testid="stFileUploader"] {
123
+ background-color: rgba(30, 41, 59, 0.4);
124
+ border: 1px dashed var(--glass-border);
125
+ border-radius: 12px;
126
+ padding: 20px;
127
+ }
128
+
129
+ /* FORCE TEXT COLOR FOR FILE UPLOADER */
130
+ [data-testid="stFileUploader"] section > div,
131
+ [data-testid="stFileUploader"] section > div > span,
132
+ [data-testid="stFileUploader"] section > div > small,
133
+ [data-testid="stFileUploader"] div[data-testid="stMarkdownContainer"] p {
134
+ color: #E2E8F0 !important; /* Bright Slate */
135
+ opacity: 1 !important;
136
+ -webkit-text-fill-color: #E2E8F0 !important;
137
+ }
138
+
139
+ [data-testid="stFileUploader"] button {
140
+ background: rgba(56, 189, 248, 0.2);
141
+ color: #38BDF8 !important;
142
+ border: 1px solid #38BDF8;
143
+ }
144
+
145
+ /* -------------------------------------------------------------------------- */
146
+ /* DROPDOWN & SELECT */
147
+ /* -------------------------------------------------------------------------- */
148
+
149
+ /* 1. The Box Itself */
150
+ .stSelectbox div[data-baseweb="select"] {
151
+ background-color: #1E293B !important; /* Solid Slate-800 for contrast */
152
+ border: 1px solid #475569 !important;
153
+ color: white !important;
154
+ }
155
+
156
+ /* 2. The Text INSIDE the Box (Critical Fix) */
157
+ .stSelectbox div[data-baseweb="select"] div[data-testid="stMarkdownContainer"] > p {
158
+ color: #F8FAFC !important; /* White */
159
+ font-weight: 500 !important;
160
+ }
161
+
162
+ /* 3. The Dropdown Menu (Popup) */
163
+ div[data-baseweb="popover"], div[data-baseweb="menu"], ul[data-baseweb="menu"] {
164
+ background-color: #0F172A !important;
165
+ border: 1px solid #334155 !important;
166
+ }
167
+
168
+ /* 4. Options in the Menu */
169
+ li[data-baseweb="option"], div[data-baseweb="option"] {
170
+ color: #CBD5E1 !important; /* Light Slate */
171
+ }
172
+
173
+ /* 5. Start/Icons in Menu */
174
+ li[data-baseweb="option"] *, div[data-baseweb="option"] * {
175
+ color: #CBD5E1 !important;
176
+ }
177
+
178
+ /* 6. Selected/Hovered Option */
179
+ li[data-baseweb="option"][aria-selected="true"],
180
+ li[data-baseweb="option"]:hover,
181
+ div[data-baseweb="option"]:hover {
182
+ background-color: #38BDF8 !important;
183
+ color: white !important;
184
  }
185
 
186
+ /* 7. SVG Arrow Icon */
187
+ .stSelectbox svg {
188
+ fill: #94A3B8 !important;
189
+ }
190
+
191
+ /* -------------------------------------------------------------------------- */
192
+ /* BUTTONS */
193
+ /* -------------------------------------------------------------------------- */
194
+ /* -------------------------------------------------------------------------- */
195
+ /* BUTTONS */
196
+ /* -------------------------------------------------------------------------- */
197
+
198
+ /* Primary Actions (Gradient) */
199
+ .stButton button[kind="primary"] {
200
  background: linear-gradient(135deg, #0EA5E9 0%, #2563EB 100%);
201
  color: white !important;
202
  border: none;
203
  border-radius: 8px;
204
+ padding: 0.6rem 1.2rem;
205
  font-weight: 600;
206
+ letter-spacing: 0.5px;
207
+ transition: all 0.3s ease;
208
+ box-shadow: 0 4px 14px rgba(14, 165, 233, 0.3);
209
+ text-transform: uppercase;
210
+ font-size: 0.85rem;
211
  }
212
+
213
+ /* Secondary Actions (File Explorer Items) */
214
+ .stButton button[kind="secondary"] {
215
+ background: transparent !important;
216
+ border: 1px solid transparent !important;
217
+ color: #94A3B8 !important; /* Slate 400 */
218
+ text-align: left !important;
219
+ display: flex !important;
220
+ justify-content: flex-start !important;
221
+ padding: 8px 12px !important; /* Bigger padding */
222
+ font-size: 15px !important; /* Bigger Font */
223
+ font-family: 'JetBrains Mono', monospace !important; /* Monospace for files */
224
+ transition: all 0.2s ease;
225
+ box-shadow: none !important;
226
+ }
227
+
228
+ .stButton button[kind="secondary"]:hover {
229
+ background: rgba(56, 189, 248, 0.1) !important;
230
+ color: #38BDF8 !important;
231
+ transform: translateX(4px); /* Slide effect */
232
  }
233
 
234
+ /* Generic overrides if kind is not caught */
235
+ .stButton button:hover {
236
+ /* Handled by specific kinds above */
237
+ }
238
+ .stButton button:active {
239
+ transform: translateY(0);
240
+ }
241
+
242
+ /* -------------------------------------------------------------------------- */
243
+ /* CHAT BUBBLES */
244
+ /* -------------------------------------------------------------------------- */
245
  .stChatMessage {
246
  background: var(--glass-bg);
247
  border: 1px solid var(--glass-border);
248
+ border-radius: 16px;
249
+ backdrop-filter: blur(10px);
250
+ margin-bottom: 1rem;
251
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
252
+ animation: fadeInUp 0.4s ease-out forwards;
253
  }
254
+
255
  .stChatMessage[data-testid="stChatMessage"]:nth-child(even) {
256
  border-left: 3px solid #38BDF8;
257
  background: linear-gradient(90deg, rgba(56, 189, 248, 0.05) 0%, rgba(15, 23, 42, 0.6) 100%);
258
  }
259
 
260
+ /* -------------------------------------------------------------------------- */
261
+ /* CODE & CHIPS */
262
+ /* -------------------------------------------------------------------------- */
263
+ code {
264
+ font-family: 'JetBrains Mono', monospace !important;
265
+ background: #0B0E14 !important;
266
+ border: 1px solid #1E293B;
267
+ border-radius: 6px;
268
+ color: #7DD3FC !important;
269
  }
270
 
271
+ /* Source Chips with Glow */
272
+ .source-container {
273
+ display: flex;
274
+ flex-wrap: wrap;
275
+ gap: 8px;
276
+ margin-bottom: 16px;
277
+ padding-bottom: 12px;
278
+ border-bottom: 1px solid rgba(255,255,255,0.05);
279
  }
280
+
281
+ .source-chip {
282
+ background: rgba(30, 41, 59, 0.6);
283
+ border: 1px solid rgba(56, 189, 248, 0.2);
284
+ border-radius: 20px;
285
+ padding: 6px 14px;
286
+ font-size: 0.8rem;
287
+ color: #94A3B8;
288
+ display: flex;
289
+ align-items: center;
290
+ transition: all 0.3s ease;
291
+ cursor: pointer;
292
+ backdrop-filter: blur(5px);
293
+ }
294
+
295
+ .source-chip:hover {
296
+ background: rgba(56, 189, 248, 0.15);
297
+ border-color: #38BDF8;
298
+ color: #38BDF8;
299
+ box-shadow: 0 0 10px rgba(56, 189, 248, 0.2);
300
+ transform: translateY(-1px);
301
  }
302
 
303
+ .source-icon {
304
+ margin-right: 8px;
305
+ opacity: 0.7;
306
+ }
307
+
308
+ /* Hiding Streamlit Branding */
309
+ #MainMenu {visibility: hidden;}
310
+ footer {visibility: hidden;}
311
+ header {visibility: hidden;}
312
+
313
+ /* -------------------------------------------------------------------------- */
314
+ /* IDE-STYLE PANEL LAYOUT */
315
+ /* -------------------------------------------------------------------------- */
316
+
317
+ /* Main content area - fixed height */
318
+ .main .block-container {
319
+ max-height: calc(100vh - 80px);
320
+ overflow: hidden;
321
+ padding-top: 1rem;
322
+ }
323
+
324
+ /* Make columns scrollable independently */
325
+ [data-testid="column"] {
326
+ max-height: calc(100vh - 120px);
327
  overflow-y: auto;
328
  overflow-x: hidden;
329
  scrollbar-width: thin;
330
+ scrollbar-color: #475569 transparent;
331
  }
332
 
333
+ /* Custom scrollbar for webkit browsers */
334
+ [data-testid="column"]::-webkit-scrollbar {
 
 
 
 
 
335
  width: 6px;
 
336
  }
337
+
338
+ [data-testid="column"]::-webkit-scrollbar-track {
339
  background: transparent;
340
  }
341
+
342
+ [data-testid="column"]::-webkit-scrollbar-thumb {
343
  background: #475569;
344
  border-radius: 3px;
345
  }
346
 
347
+ [data-testid="column"]::-webkit-scrollbar-thumb:hover {
348
+ background: #64748b;
349
+ }
350
+
351
+ /* Code viewer specific - scrollable code block */
352
+ .stCode {
353
+ max-height: 60vh;
354
+ overflow-y: auto !important;
355
+ }
356
+
357
+ /* -------------------------------------------------------------------------- */
358
+ /* HORIZONTAL TABS */
359
+ /* -------------------------------------------------------------------------- */
360
+ /* -------------------------------------------------------------------------- */
361
+ /* HORIZONTAL TABS */
362
+ /* -------------------------------------------------------------------------- */
363
+
364
+ /* Tab List Container */
365
+ .stTabs [data-baseweb="tab-list"] {
366
+ gap: 8px;
367
+ background-color: transparent;
368
+ padding-bottom: 4px;
369
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
370
+ }
371
+
372
+ /* Individual Tab (Inactive) */
373
+ .stTabs [data-baseweb="tab"] {
374
+ height: 40px;
375
+ white-space: pre-wrap;
376
  border-radius: 6px;
 
 
 
 
 
377
  gap: 6px;
378
+ padding: 0px 16px;
379
+ color: #94A3B8 !important; /* Slate 400 */
380
+ font-weight: 500;
381
+ background: transparent;
382
+ border: 1px solid transparent;
383
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
384
+ }
385
+
386
+ /* Tab Hover State */
387
+ .stTabs [data-baseweb="tab"]:hover {
388
+ background: rgba(255, 255, 255, 0.05);
389
+ color: #E2E8F0 !important;
390
  }
391
+
392
+ /* Active Tab (Selected) */
393
+ .stTabs [data-baseweb="tab"][aria-selected="true"] {
394
+ background: rgba(56, 189, 248, 0.15) !important;
395
+ color: #38BDF8 !important; /* Sky Blue */
396
+ border: 1px solid rgba(56, 189, 248, 0.3);
397
+ font-weight: 600;
398
+ box-shadow: 0 0 10px rgba(56, 189, 248, 0.1);
399
+ }
400
+
401
+ /* Tab Icons styling fix */
402
+ .stTabs [data-baseweb="tab"] p {
403
+ font-size: 0.95rem;
404
+ }
405
+
406
  </style>
407
+ """.replace("LOGO_BASE64_PLACEHOLDER", logo_b64), unsafe_allow_html=True)