BasitAliii commited on
Commit
577035a
·
verified ·
1 Parent(s): 1b3d8f9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +142 -664
app.py CHANGED
@@ -1,16 +1,14 @@
1
- from __future__ import annotations
2
-
3
- import streamlit as st
 
 
 
 
 
 
4
 
5
- # Set page config as the VERY FIRST Streamlit command
6
- st.set_page_config(
7
- page_title="AI Skill Swap",
8
- page_icon="🤝",
9
- layout="wide",
10
- initial_sidebar_state="expanded"
11
- )
12
-
13
- # Now import other modules
14
  import os
15
  import json
16
  import uuid
@@ -20,11 +18,13 @@ from dataclasses import dataclass, asdict
20
  from pathlib import Path
21
  from typing import List, Dict, Any, Optional, Tuple
22
 
23
- # Lazy import Groq
 
 
24
  try:
25
  from groq import Groq
26
  except Exception:
27
- Groq = None
28
 
29
  # ---------- Config ----------
30
  DATA_FILE = Path("users.json")
@@ -32,6 +32,7 @@ MODEL = "llama-3.3-70b-versatile"
32
  if not DATA_FILE.exists():
33
  DATA_FILE.write_text("[]", encoding="utf-8")
34
 
 
35
  # ---------- Data model ----------
36
  @dataclass
37
  class Profile:
@@ -56,13 +57,16 @@ class Profile:
56
  def to_dict(self) -> Dict[str, Any]:
57
  return asdict(self)
58
 
 
59
  # ---------- Storage & Validation ----------
60
  class ProfileStore:
61
- def __init__(self, path: Path = DATA_FILE):
 
 
62
  self.path = path
63
  self._ensure_file()
64
 
65
- def _ensure_file(self):
66
  if not self.path.exists():
67
  self.path.write_text("[]", encoding="utf-8")
68
 
@@ -71,9 +75,10 @@ class ProfileStore:
71
  data = json.loads(self.path.read_text(encoding="utf-8"))
72
  return [Profile.from_dict(d) for d in data if isinstance(d, dict)]
73
  except json.JSONDecodeError:
 
74
  return []
75
 
76
- def save_all(self, profiles: List[Profile]):
77
  data = [p.to_dict() for p in profiles]
78
  self.path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
79
 
@@ -98,13 +103,13 @@ class ProfileStore:
98
  existing.availability = profile.availability
99
  existing.preferences = profile.preferences
100
  self.save_all(profiles)
101
- return True, "Profile updated successfully! 🎉"
102
  else:
103
  if not profile.id:
104
  profile.id = str(uuid.uuid4())
105
  profiles.append(profile)
106
  self.save_all(profiles)
107
- return True, "Profile created successfully! 🎉"
108
 
109
  def delete(self, username: str) -> Tuple[bool, str]:
110
  profiles = self.load_all()
@@ -112,7 +117,8 @@ class ProfileStore:
112
  if len(new) == len(profiles):
113
  return False, "Profile not found."
114
  self.save_all(new)
115
- return True, "Profile deleted successfully."
 
116
 
117
  def validate_profile(profile: Profile) -> Tuple[bool, Optional[str]]:
118
  if not profile.username or not profile.username.strip():
@@ -128,6 +134,7 @@ def validate_profile(profile: Profile) -> Tuple[bool, Optional[str]]:
128
  return False, "Individual skill entries must be 120 characters or fewer."
129
  return True, None
130
 
 
131
  # ---------- Utilities ----------
132
  def normalize_skill_list(text: Optional[str]) -> List[str]:
133
  if not text:
@@ -143,6 +150,7 @@ def normalize_skill_list(text: Optional[str]) -> List[str]:
143
  out.append(it)
144
  return out
145
 
 
146
  def make_prompt_for_matching(current_user: Profile, all_users: List[Profile], top_k: int = 5) -> Tuple[str, str]:
147
  users_desc = []
148
  for u in all_users:
@@ -174,6 +182,7 @@ def make_prompt_for_matching(current_user: Profile, all_users: List[Profile], to
174
 
175
  return system_instructions, user_message
176
 
 
177
  # ---------- Groq LLM helper ----------
178
  def init_groq_client():
179
  api_key = os.environ.get("GROQ_API_KEY")
@@ -186,6 +195,7 @@ def init_groq_client():
186
  except Exception:
187
  return None
188
 
 
189
  def ask_groq_for_matches(system_instructions: str, user_message: str, model: str = MODEL) -> List[Dict[str, Any]]:
190
  client = init_groq_client()
191
  if client is None:
@@ -199,6 +209,7 @@ def ask_groq_for_matches(system_instructions: str, user_message: str, model: str
199
  except Exception as e:
200
  raise RuntimeError(f"Groq call failed: {e}")
201
  content = (resp.choices[0].message.content or "")
 
202
  json_match = re.search(r"(\[\s*\{[\s\S]*?\}\s*\])", content)
203
  if not json_match:
204
  raise RuntimeError(f"No JSON array found in LLM response. Raw output:\n{content[:1000]}")
@@ -210,653 +221,120 @@ def ask_groq_for_matches(system_instructions: str, user_message: str, model: str
210
  except json.JSONDecodeError as e:
211
  raise RuntimeError(f"Failed to parse JSON from LLM output: {e}\nRaw:\n{content[:1000]}")
212
 
213
- # ---------- Modern UI Components ----------
214
- def inject_custom_css():
215
- st.markdown("""
216
- <style>
217
- /* Modern header styling */
218
- .main-header {
219
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
220
- padding: 2rem;
221
- border-radius: 15px;
222
- color: white;
223
- margin-bottom: 2rem;
224
- text-align: center;
225
- box-shadow: 0 8px 25px rgba(0,0,0,0.1);
226
- }
227
-
228
- /* Tab styling */
229
- .stTabs [data-baseweb="tab-list"] {
230
- gap: 8px;
231
- background: transparent;
232
- }
233
-
234
- .stTabs [data-baseweb="tab"] {
235
- height: 50px;
236
- white-space: pre-wrap;
237
- border-radius: 10px 10px 0px 0px;
238
- gap: 8px;
239
- padding: 8px 16px;
240
- background: #f8f9fa;
241
- border: 1px solid #e9ecef;
242
- font-weight: 600;
243
- transition: all 0.3s ease;
244
- }
245
-
246
- .stTabs [aria-selected="true"] {
247
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
248
- color: white !important;
249
- border-color: #667eea !important;
250
- transform: translateY(-2px);
251
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
252
- }
253
-
254
- /* Card styling */
255
- .feature-card {
256
- background: white;
257
- padding: 1.5rem;
258
- border-radius: 15px;
259
- box-shadow: 0 4px 20px rgba(0,0,0,0.1);
260
- border-left: 4px solid #667eea;
261
- margin-bottom: 1rem;
262
- transition: transform 0.3s ease;
263
- height: 100%;
264
- }
265
-
266
- .feature-card:hover {
267
- transform: translateY(-5px);
268
- }
269
-
270
- .profile-card {
271
- background: white;
272
- padding: 1.5rem;
273
- border-radius: 15px;
274
- box-shadow: 0 4px 15px rgba(0,0,0,0.08);
275
- margin-bottom: 1rem;
276
- border: 1px solid #e0e0e0;
277
- transition: all 0.3s ease;
278
- }
279
-
280
- .profile-card:hover {
281
- box-shadow: 0 8px 25px rgba(0,0,0,0.15);
282
- transform: translateY(-2px);
283
- }
284
-
285
- .match-card {
286
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
287
- color: white;
288
- padding: 1.5rem;
289
- border-radius: 15px;
290
- margin-bottom: 1rem;
291
- box-shadow: 0 4px 15px rgba(0,0,0,0.1);
292
- }
293
-
294
- /* Skill tags */
295
- .skill-tag {
296
- display: inline-block;
297
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
298
- color: white;
299
- padding: 0.3rem 0.8rem;
300
- border-radius: 20px;
301
- margin: 0.2rem;
302
- font-size: 0.8rem;
303
- font-weight: 500;
304
- box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
305
- }
306
-
307
- .skill-tag-want {
308
- background: linear-gradient(135deg, #fd746c 0%, #ff9068 100%);
309
- }
310
-
311
- /* Stats cards */
312
- .stats-card {
313
- background: white;
314
- padding: 1.5rem;
315
- border-radius: 15px;
316
- text-align: center;
317
- box-shadow: 0 4px 15px rgba(0,0,0,0.08);
318
- border-top: 4px solid #667eea;
319
- transition: transform 0.3s ease;
320
- }
321
-
322
- .stats-card:hover {
323
- transform: translateY(-3px);
324
- }
325
-
326
- /* Form styling */
327
- .stTextInput input, .stTextArea textarea {
328
- border-radius: 10px !important;
329
- border: 2px solid #e0e0e0 !important;
330
- padding: 0.8rem !important;
331
- transition: all 0.3s ease;
332
- }
333
-
334
- .stTextInput input:focus, .stTextArea textarea:focus {
335
- border-color: #667eea !important;
336
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
337
- }
338
-
339
- /* Button styling */
340
- .stButton button {
341
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
342
- color: white;
343
- border: none;
344
- padding: 0.7rem 1.5rem;
345
- border-radius: 10px;
346
- font-weight: 600;
347
- transition: all 0.3s ease;
348
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
349
- }
350
-
351
- .stButton button:hover {
352
- transform: translateY(-2px);
353
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
354
- }
355
-
356
- /* Secondary button */
357
- .stButton button[kind="secondary"] {
358
- background: #6c757d;
359
- border: 1px solid #6c757d;
360
- }
361
-
362
- .stButton button[kind="secondary"]:hover {
363
- background: #5a6268;
364
- border-color: #545b62;
365
- }
366
-
367
- /* Animations */
368
- @keyframes fadeIn {
369
- from { opacity: 0; transform: translateY(20px); }
370
- to { opacity: 1; transform: translateY(0); }
371
- }
372
-
373
- .fade-in {
374
- animation: fadeIn 0.6s ease-out;
375
- }
376
-
377
- /* Action buttons in cards */
378
- .action-buttons {
379
- display: flex;
380
- gap: 8px;
381
- margin-top: 1rem;
382
- }
383
-
384
- /* Responsive design */
385
- @media (max-width: 768px) {
386
- .main-header {
387
- padding: 1rem;
388
- margin-bottom: 1rem;
389
- }
390
-
391
- .stTabs [data-baseweb="tab"] {
392
- font-size: 0.8rem;
393
- padding: 6px 12px;
394
- }
395
- }
396
-
397
- /* Modal-like form styling */
398
- .edit-form-container {
399
- background: white;
400
- padding: 2rem;
401
- border-radius: 15px;
402
- box-shadow: 0 10px 30px rgba(0,0,0,0.2);
403
- border: 1px solid #e0e0e0;
404
- margin: 1rem 0;
405
- }
406
- </style>
407
- """, unsafe_allow_html=True)
408
-
409
- def render_modern_header():
410
- st.markdown("""
411
- <div class="main-header fade-in">
412
- <h1 style="margin:0; font-size: 2.5rem;">🤝 AI Skill Swap</h1>
413
- <p style="margin:0; font-size: 1.2rem; opacity: 0.9;">Connect • Learn • Grow Together</p>
414
- </div>
415
- """, unsafe_allow_html=True)
416
-
417
- def render_dashboard_tab():
418
- """Dashboard tab with stats and features"""
419
- st.markdown("## 🎯 Dashboard Overview")
420
-
421
- store = ProfileStore()
422
- profiles = store.load_all()
423
- current_user = None
424
- if st.session_state.get("username"):
425
- current_user = store.find_by_username(st.session_state["username"])
426
-
427
- # Stats cards
428
- col1, col2, col3, col4 = st.columns(4)
429
- with col1:
430
- st.markdown(f"""
431
- <div class="stats-card">
432
- <h3>👥</h3>
433
- <h2 style="margin:0; color: #667eea;">{len(profiles)}</h2>
434
- <p style="margin:0; color: #666;">Community Members</p>
435
- </div>
436
- """, unsafe_allow_html=True)
437
-
438
- with col2:
439
- total_skills = sum(len(p.offers) + len(p.wants) for p in profiles)
440
- st.markdown(f"""
441
- <div class="stats-card">
442
- <h3>🎯</h3>
443
- <h2 style="margin:0; color: #667eea;">{total_skills}</h2>
444
- <p style="margin:0; color: #666;">Skills Shared</p>
445
- </div>
446
- """, unsafe_allow_html=True)
447
-
448
- with col3:
449
- active_matches = len([p for p in profiles if p.offers and p.wants])
450
- st.markdown(f"""
451
- <div class="stats-card">
452
- <h3>🤝</h3>
453
- <h2 style="margin:0; color: #667eea;">{active_matches}</h2>
454
- <p style="margin:0; color: #666;">Active Learners</p>
455
- </div>
456
- """, unsafe_allow_html=True)
457
-
458
- with col4:
459
- status = "Ready!" if current_user else "Create Profile"
460
- st.markdown(f"""
461
- <div class="stats-card">
462
- <h3>👤</h3>
463
- <h2 style="margin:0; color: #667eea;">{status}</h2>
464
- <p style="margin:0; color: #666;">Your Status</p>
465
- </div>
466
- """, unsafe_allow_html=True)
467
-
468
- # Feature cards
469
- st.markdown("## 🚀 How It Works")
470
- col1, col2, col3 = st.columns(3)
471
-
472
- with col1:
473
- st.markdown("""
474
- <div class="feature-card">
475
- <h3 style="color: #667eea;">📝 Create Profile</h3>
476
- <p style="color: #555;">List skills you can teach and want to learn. Set your availability and preferences.</p>
477
- </div>
478
- """, unsafe_allow_html=True)
479
-
480
- with col2:
481
- st.markdown("""
482
- <div class="feature-card">
483
- <h3 style="color: #667eea;">🤖 AI Matching</h3>
484
- <p style="color: #555;">Our AI finds perfect skill exchange partners based on mutual interests.</p>
485
- </div>
486
- """, unsafe_allow_html=True)
487
-
488
- with col3:
489
- st.markdown("""
490
- <div class="feature-card">
491
- <h3 style="color: #667eea;">🎯 Connect & Learn</h3>
492
- <p style="color: #555;">Connect with matches and start your skill exchange journey together!</p>
493
- </div>
494
- """, unsafe_allow_html=True)
495
-
496
- # Quick start guide
497
- if not current_user:
498
- st.markdown("""
499
- <div class="feature-card" style="background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); border: none;">
500
- <h3 style="color: #333;">🎉 Get Started!</h3>
501
- <p style="color: #555;">Go to the <strong>Create Account</strong> tab to create your profile and start finding perfect skill exchange partners!</p>
502
- </div>
503
- """, unsafe_allow_html=True)
504
-
505
- def render_create_account_tab():
506
- """Create Account tab with profile form"""
507
- st.markdown("## 👤 Create Your Profile")
508
-
509
- store = ProfileStore()
510
-
511
- # Check if we're in edit mode
512
- edit_mode = st.session_state.get("edit_profile", False)
513
- current_profile = None
514
- if edit_mode and st.session_state.get("username"):
515
- current_profile = store.find_by_username(st.session_state["username"])
516
-
517
- with st.form("create_account_form", clear_on_submit=not edit_mode):
518
- col1, col2 = st.columns(2)
519
-
520
- with col1:
521
- username = st.text_input("👤 Username*",
522
- value=st.session_state.get("username", ""),
523
- placeholder="Enter your unique username",
524
- disabled=edit_mode) # Cannot change username in edit mode
525
-
526
- offers_text = st.text_area("🎯 Skills I Can Teach*",
527
- value=st.session_state.get("offers_text", ""),
528
- placeholder="Python programming\nWeb design\nPublic speaking\nData analysis",
529
- height=120,
530
- help="Enter one skill per line")
531
-
532
- availability = st.text_input("⏰ Availability",
533
- value=st.session_state.get("availability", ""),
534
- placeholder="Weekends, Evenings after 6 PM")
535
-
536
- with col2:
537
- # Show current username if editing
538
- if edit_mode and current_profile:
539
- st.info(f"✏️ Editing profile: **{current_profile.username}**")
540
-
541
- wants_text = st.text_area("🎓 Skills I Want to Learn*",
542
- value=st.session_state.get("wants_text", ""),
543
- placeholder="Machine learning\nGraphic design\nDigital marketing\nSpanish language",
544
- height=120,
545
- help="Enter one skill per line")
546
-
547
- preferences = st.text_input("💫 Preferences",
548
- value=st.session_state.get("preferences", ""),
549
- placeholder="Online sessions, English language")
550
-
551
- col1, col2 = st.columns(2)
552
- with col1:
553
- if edit_mode:
554
- submit_button = st.form_submit_button("💾 Update Profile", use_container_width=True)
555
- else:
556
- submit_button = st.form_submit_button("🚀 Create Profile", use_container_width=True)
557
-
558
- with col2:
559
- if edit_mode:
560
- cancel_button = st.form_submit_button("❌ Cancel Edit", use_container_width=True, type="secondary")
561
- else:
562
- # Only show delete button if user has a profile
563
- current_username = st.session_state.get("username", "")
564
- if current_username and store.find_by_username(current_username):
565
- delete_button = st.form_submit_button("🗑️ Delete Profile", use_container_width=True, type="secondary")
566
-
567
- if submit_button:
568
- if not username.strip():
569
- st.error("❌ Please enter a username!")
570
- elif not offers_text.strip() and not wants_text.strip():
571
- st.error("❌ Please add at least one skill you can teach or want to learn!")
572
  else:
573
- offers = normalize_skill_list(offers_text)
574
- wants = normalize_skill_list(wants_text)
575
- profile = Profile(
576
- id=str(uuid.uuid4()),
577
- username=username.strip(),
578
- offers=offers,
579
- wants=wants,
580
- availability=availability.strip(),
581
- preferences=preferences.strip()
582
- )
583
- ok, msg = store.add_or_update(profile)
584
- if ok:
585
- st.session_state["username"] = username
586
- st.session_state["offers_text"] = offers_text
587
- st.session_state["wants_text"] = wants_text
588
- st.session_state["availability"] = availability
589
- st.session_state["preferences"] = preferences
590
- st.session_state["edit_profile"] = False # Exit edit mode
591
- st.success(" " + msg)
592
- # Auto-redirect to Community tab
593
- st.rerun()
594
- else:
595
- st.error("❌ " + msg)
596
-
597
- if edit_mode and cancel_button:
598
- st.session_state["edit_profile"] = False
599
- st.rerun()
600
-
601
- if not edit_mode and 'delete_button' in locals() and delete_button:
602
- current_username = st.session_state.get("username", "")
603
- if current_username:
604
- ok, msg = store.delete(current_username)
605
- if ok:
606
- st.session_state["username"] = ""
607
- st.session_state["offers_text"] = ""
608
- st.session_state["wants_text"] = ""
609
- st.session_state["availability"] = ""
610
- st.session_state["preferences"] = ""
611
- st.success("✅ " + msg)
612
- st.rerun()
613
- else:
614
- st.error("❌ " + msg)
615
 
616
- def render_community_tab():
617
- """Community tab with modern card layout"""
618
- st.markdown("## 🌐 Community Members")
619
-
620
- store = ProfileStore()
 
621
  profiles = store.load_all()
622
-
623
  if not profiles:
624
- st.info("🌟 Be the first to join our community! Create your profile in the **Create Account** tab.")
625
- return
626
-
627
- # Search and filter
628
- col1, col2 = st.columns([2, 1])
629
- with col1:
630
- search_term = st.text_input("🔍 Search by name...", placeholder="Enter username to search")
631
- with col2:
632
- all_skills = list(set(s for p in profiles for s in p.offers + p.wants))
633
- filter_skill = st.selectbox("Filter by skill", ["All skills"] + sorted(all_skills))
634
-
635
- # Display profiles in modern cards
636
- filtered_profiles = []
637
- for p in profiles:
638
- if search_term and search_term.lower() not in p.username.lower():
639
- continue
640
- if filter_skill != "All skills" and filter_skill not in p.offers + p.wants:
641
- continue
642
- filtered_profiles.append(p)
643
-
644
- st.write(f"**Showing {len(filtered_profiles)} of {len(profiles)} members**")
645
-
646
- # Display profiles in columns for better layout
647
- cols = st.columns(2)
648
- for idx, profile in enumerate(filtered_profiles):
649
- with cols[idx % 2]:
650
- render_profile_card(profile, store)
651
-
652
- def render_profile_card(profile: Profile, store: ProfileStore):
653
- """Render a single profile card with proper data display"""
654
- current_user = st.session_state.get("username", "")
655
- is_current_user = current_user.lower() == profile.username.lower()
656
-
657
- # Create the card content using Streamlit components instead of raw HTML
658
- with st.container():
659
- st.markdown(f"""
660
- <div class="profile-card fade-in">
661
- <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
662
- <h3 style="margin: 0; color: #333;">👤 {profile.username}</h3>
663
- {'<span style="background: #667eea; color: white; padding: 0.3rem 0.8rem; border-radius: 15px; font-size: 0.8rem;">You</span>' if is_current_user else ''}
664
- </div>
665
- </div>
666
- """, unsafe_allow_html=True)
667
-
668
- # Skills section
669
- col1, col2 = st.columns(2)
670
- with col1:
671
- st.write("**🎯 Teaches:**")
672
- if profile.offers:
673
- for skill in profile.offers:
674
- st.markdown(f'<span class="skill-tag">{skill}</span>', unsafe_allow_html=True)
675
- else:
676
- st.write("No skills listed")
677
-
678
- with col2:
679
- st.write("**🎓 Wants to Learn:**")
680
- if profile.wants:
681
- for skill in profile.wants:
682
- st.markdown(f'<span class="skill-tag skill-tag-want">{skill}</span>', unsafe_allow_html=True)
683
- else:
684
- st.write("No skills listed")
685
-
686
- # Additional details
687
- if profile.availability or profile.preferences:
688
- st.write("---")
689
- if profile.availability:
690
- st.write(f"**⏰ Available:** {profile.availability}")
691
- if profile.preferences:
692
- st.write(f"**💫 Preferences:** {profile.preferences}")
693
-
694
- # Action buttons for current user's profile
695
- if is_current_user:
696
- st.write("---")
697
- col1, col2 = st.columns(2)
698
- with col1:
699
- if st.button("✏️ Edit", key=f"edit_{profile.id}", use_container_width=True):
700
- st.session_state["username"] = profile.username
701
- st.session_state["offers_text"] = "\n".join(profile.offers)
702
- st.session_state["wants_text"] = "\n".join(profile.wants)
703
- st.session_state["availability"] = profile.availability
704
- st.session_state["preferences"] = profile.preferences
705
- st.session_state["edit_profile"] = True
706
- st.rerun()
707
- with col2:
708
- if st.button("🗑️ Delete", key=f"delete_{profile.id}", use_container_width=True, type="secondary"):
709
- ok, msg = store.delete(profile.username)
710
- if ok:
711
- if st.session_state.get("username") == profile.username:
712
- st.session_state["username"] = ""
713
- st.session_state["offers_text"] = ""
714
- st.session_state["wants_text"] = ""
715
- st.session_state["availability"] = ""
716
- st.session_state["preferences"] = ""
717
- st.session_state["edit_profile"] = False
718
- st.success("✅ " + msg)
719
- st.rerun()
720
- else:
721
- st.error("❌ " + msg)
722
-
723
- def render_matches_tab():
724
- """AI Matches tab"""
725
- st.markdown("## 🤖 AI-Powered Skill Matching")
726
-
727
- store = ProfileStore()
728
  profiles = store.load_all()
729
-
730
  if not profiles:
731
- st.info("👥 Add some profiles in the **Create Account** tab to start finding amazing skill matches!")
732
- return
733
-
734
- # Matching interface
735
- col1, col2 = st.columns([2, 1])
736
- with col1:
737
- profile_options = [p.username for p in profiles]
738
- selected_profile = st.selectbox("👤 Select profile for matching", profile_options)
739
- with col2:
740
- top_k = st.slider("Number of matches", 1, 10, 5)
741
-
742
- if st.button("🎯 Find AI Matches", type="primary", use_container_width=True):
743
- with st.spinner("🔍 Analyzing profiles and finding perfect matches..."):
744
- time.sleep(1)
745
- current = store.find_by_username(selected_profile)
746
- if not current:
747
- st.error(" Profile not found.")
748
- else:
749
- try:
750
- sys_ins, user_msg = make_prompt_for_matching(current, profiles, top_k=top_k)
751
- matches = ask_groq_for_matches(sys_ins, user_msg)
752
-
753
- st.success(f"🎉 Found {len(matches)} perfect matches for {selected_profile}!")
754
-
755
- for i, match in enumerate(matches, 1):
756
- score = match.get('score', 0)
757
- username = match.get('username', 'Unknown')
758
- reason = match.get('reason', 'Great compatibility based on shared interests!')
759
-
760
- # Color based on score
761
- if score >= 80:
762
- color = "linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
763
- emoji = "💎"
764
- elif score >= 60:
765
- color = "linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)"
766
- emoji = "⭐"
767
- else:
768
- color = "linear-gradient(135deg, #fa709a 0%, #fee140 100%)"
769
- emoji = "👍"
770
-
771
- st.markdown(f"""
772
- <div class="match-card" style="background: {color};">
773
- <div style="display: flex; justify-content: space-between; align-items: center;">
774
- <div>
775
- <h3 style="margin:0; color: white;">{emoji} Match #{i}: {username}</h3>
776
- <h1 style="margin:0; color: white; font-size: 2.5rem;">{score}/100</h1>
777
- </div>
778
- <div style="font-size: 3rem;">{emoji}</div>
779
- </div>
780
- <p style="margin: 1rem 0 0 0; color: white; opacity: 0.9;">{reason}</p>
781
- </div>
782
- """, unsafe_allow_html=True)
783
-
784
- except Exception as e:
785
- st.error(f"❌ Error generating matches: {str(e)}")
786
-
787
- def render_settings_tab():
788
- """Settings tab"""
789
- st.markdown("## ⚙️ Settings & Information")
790
-
791
- col1, col2 = st.columns(2)
792
-
793
- with col1:
794
- st.markdown("""
795
- <div class="feature-card">
796
- <h3 style="color: #667eea;">ℹ️ About AI Skill Swap</h3>
797
- <p style="color: #555;">Connect with amazing people and exchange skills in a friendly community.</p>
798
- <p style="color: #555;"><strong>Features:</strong></p>
799
- <ul style="color: #555;">
800
- <li>🤖 AI-powered matching</li>
801
- <li>👥 Community discovery</li>
802
- <li>🎯 Skill-based connections</li>
803
- <li>💫 Personalized preferences</li>
804
- </ul>
805
- </div>
806
- """, unsafe_allow_html=True)
807
-
808
- with col2:
809
- st.markdown("""
810
- <div class="feature-card">
811
- <h3 style="color: #667eea;">🛠️ Technical Info</h3>
812
- <p style="color: #555;"><strong>Built with:</strong></p>
813
- <ul style="color: #555;">
814
- <li>Streamlit - Modern web framework</li>
815
- <li>Groq LLM - AI matching engine</li>
816
- <li>Python - Backend logic</li>
817
- <li>JSON - Data storage</li>
818
- </ul>
819
- </div>
820
- """, unsafe_allow_html=True)
821
-
822
- # ---------- Main App ----------
823
- def main():
824
- # Initialize session state
825
- if "current_tab" not in st.session_state:
826
- st.session_state.current_tab = "Dashboard"
827
- if "edit_profile" not in st.session_state:
828
- st.session_state.edit_profile = False
829
-
830
- # Inject custom CSS
831
- inject_custom_css()
832
-
833
- # Render modern header
834
- render_modern_header()
835
-
836
- # Create tabs for main navigation
837
- tab1, tab2, tab3, tab4, tab5 = st.tabs([
838
- "🏠 Dashboard",
839
- "👤 Create Account",
840
- "🌐 Community",
841
- "🤖 AI Matches",
842
- "⚙️ Settings"
843
- ])
844
-
845
- # Render content based on selected tab
846
- with tab1:
847
- render_dashboard_tab()
848
-
849
- with tab2:
850
- render_create_account_tab()
851
-
852
- with tab3:
853
- render_community_tab()
854
-
855
- with tab4:
856
- render_matches_tab()
857
-
858
- with tab5:
859
- render_settings_tab()
860
-
861
- if __name__ == "__main__":
862
- main()
 
1
+ # app.py
2
+ """
3
+ AI Skill Swap — Unified App (Backend + Module 2 + Module 3 UI)
4
+ Features:
5
+ - JSON-backed ProfileStore (create / read / update / delete)
6
+ - Groq LLM integration with robust error handling
7
+ - Streamlit UI with animated success messages
8
+ - Single-file, drop-in replacement for your previous app.py
9
+ """
10
 
11
+ from __future__ import annotations
 
 
 
 
 
 
 
 
12
  import os
13
  import json
14
  import uuid
 
18
  from pathlib import Path
19
  from typing import List, Dict, Any, Optional, Tuple
20
 
21
+ import streamlit as st
22
+
23
+ # Lazy import Groq so app runs without the package if not installed.
24
  try:
25
  from groq import Groq
26
  except Exception:
27
+ Groq = None # type: ignore
28
 
29
  # ---------- Config ----------
30
  DATA_FILE = Path("users.json")
 
32
  if not DATA_FILE.exists():
33
  DATA_FILE.write_text("[]", encoding="utf-8")
34
 
35
+
36
  # ---------- Data model ----------
37
  @dataclass
38
  class Profile:
 
57
  def to_dict(self) -> Dict[str, Any]:
58
  return asdict(self)
59
 
60
+
61
  # ---------- Storage & Validation ----------
62
  class ProfileStore:
63
+ """JSON file-backed profile store."""
64
+
65
+ def __init__(self, path: Path = DATA_FILE) -> None:
66
  self.path = path
67
  self._ensure_file()
68
 
69
+ def _ensure_file(self) -> None:
70
  if not self.path.exists():
71
  self.path.write_text("[]", encoding="utf-8")
72
 
 
75
  data = json.loads(self.path.read_text(encoding="utf-8"))
76
  return [Profile.from_dict(d) for d in data if isinstance(d, dict)]
77
  except json.JSONDecodeError:
78
+ # Corrupted file -> return empty list
79
  return []
80
 
81
+ def save_all(self, profiles: List[Profile]) -> None:
82
  data = [p.to_dict() for p in profiles]
83
  self.path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
84
 
 
103
  existing.availability = profile.availability
104
  existing.preferences = profile.preferences
105
  self.save_all(profiles)
106
+ return True, "Profile updated."
107
  else:
108
  if not profile.id:
109
  profile.id = str(uuid.uuid4())
110
  profiles.append(profile)
111
  self.save_all(profiles)
112
+ return True, "Profile created."
113
 
114
  def delete(self, username: str) -> Tuple[bool, str]:
115
  profiles = self.load_all()
 
117
  if len(new) == len(profiles):
118
  return False, "Profile not found."
119
  self.save_all(new)
120
+ return True, "Profile deleted."
121
+
122
 
123
  def validate_profile(profile: Profile) -> Tuple[bool, Optional[str]]:
124
  if not profile.username or not profile.username.strip():
 
134
  return False, "Individual skill entries must be 120 characters or fewer."
135
  return True, None
136
 
137
+
138
  # ---------- Utilities ----------
139
  def normalize_skill_list(text: Optional[str]) -> List[str]:
140
  if not text:
 
150
  out.append(it)
151
  return out
152
 
153
+
154
  def make_prompt_for_matching(current_user: Profile, all_users: List[Profile], top_k: int = 5) -> Tuple[str, str]:
155
  users_desc = []
156
  for u in all_users:
 
182
 
183
  return system_instructions, user_message
184
 
185
+
186
  # ---------- Groq LLM helper ----------
187
  def init_groq_client():
188
  api_key = os.environ.get("GROQ_API_KEY")
 
195
  except Exception:
196
  return None
197
 
198
+
199
  def ask_groq_for_matches(system_instructions: str, user_message: str, model: str = MODEL) -> List[Dict[str, Any]]:
200
  client = init_groq_client()
201
  if client is None:
 
209
  except Exception as e:
210
  raise RuntimeError(f"Groq call failed: {e}")
211
  content = (resp.choices[0].message.content or "")
212
+ # Try to extract JSON array from model response
213
  json_match = re.search(r"(\[\s*\{[\s\S]*?\}\s*\])", content)
214
  if not json_match:
215
  raise RuntimeError(f"No JSON array found in LLM response. Raw output:\n{content[:1000]}")
 
221
  except json.JSONDecodeError as e:
222
  raise RuntimeError(f"Failed to parse JSON from LLM output: {e}\nRaw:\n{content[:1000]}")
223
 
224
+
225
+ # ---------- UI (Streamlit) ----------
226
+ st.set_page_config(page_title="AI Skill Swap", page_icon="🤝", layout="wide")
227
+ st.title("AI Skill Swap — Match & Exchange Skills")
228
+
229
+ # Insert CSS for animations only (dark mode removed)
230
+ CSS = """
231
+ <style>
232
+ .card{padding:18px;border-radius:12px;background:#ffffff;box-shadow:0 6px 18px rgba(0,0,0,0.08);margin-bottom:12px}
233
+ .success-animation{animation:pop 0.45s cubic-bezier(.2,.9,.2,1)}
234
+ @keyframes pop{0%{transform:scale(.85);opacity:0}100%{transform:scale(1);opacity:1}}
235
+ .small-muted{font-size:12px;color:grey;margin-top:6px}
236
+ .btn-primary{background:linear-gradient(90deg,#6d28d9,#4f46e5);color:white;padding:8px 12px;border-radius:8px;border:none}
237
+ </style>
238
+ """
239
+ st.markdown(CSS, unsafe_allow_html=True)
240
+
241
+ # Layout: sidebar for profiles, main area for matches
242
+ store = ProfileStore()
243
+
244
+ with st.sidebar:
245
+ st.header("Your profile")
246
+ with st.form("profile_form"):
247
+ username = st.text_input("Username", value=st.session_state.get("username", ""))
248
+ offers_text = st.text_area("Skills you can teach (one per line or comma-separated)", value=st.session_state.get("offers_text", ""))
249
+ wants_text = st.text_area("Skills you want to learn (one per line or comma-separated)", value=st.session_state.get("wants_text", ""))
250
+ availability = st.text_input("Availability (e.g., Weekends)", value=st.session_state.get("availability", ""))
251
+ preferences = st.text_input("Preferences (e.g., language, online)", value=st.session_state.get("preferences", ""))
252
+ save = st.form_submit_button("Save / Update profile", use_container_width=True)
253
+ if save:
254
+ offers = normalize_skill_list(offers_text)
255
+ wants = normalize_skill_list(wants_text)
256
+ profile = Profile(id=str(uuid.uuid4()), username=username.strip(), offers=offers, wants=wants, availability=availability.strip(), preferences=preferences.strip())
257
+ ok, msg = store.add_or_update(profile)
258
+ if ok:
259
+ st.session_state["username"] = username
260
+ st.session_state["offers_text"] = offers_text
261
+ st.session_state["wants_text"] = wants_text
262
+ st.session_state["availability"] = availability
263
+ st.session_state["preferences"] = preferences
264
+ st.success(msg)
265
+ st.markdown(f"<div class='card success-animation' style='border-left:6px solid #4F46E5;'><b>Saved</b><div class='small-muted'>{username} saved to local storage.</div></div>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  else:
267
+ st.error(msg)
268
+
269
+ st.markdown("---")
270
+ st.header("Load / Delete")
271
+ profiles = store.load_all()
272
+ options = ["-- new profile --"] + [p.username for p in profiles]
273
+ selected = st.selectbox("Choose profile", options, index=0)
274
+ if st.button("Load profile") and selected != "-- new profile --":
275
+ p = store.find_by_username(selected)
276
+ if p:
277
+ st.session_state["username"] = p.username
278
+ st.session_state["offers_text"] = "\n".join(p.offers)
279
+ st.session_state["wants_text"] = "\n".join(p.wants)
280
+ st.session_state["availability"] = p.availability
281
+ st.session_state["preferences"] = p.preferences
282
+ st.experimental_rerun()
283
+ else:
284
+ st.warning("Profile not found.")
285
+ if st.button("Delete profile") and selected != "-- new profile --":
286
+ ok, m = store.delete(selected)
287
+ if ok:
288
+ st.success(m)
289
+ # clear session state for form
290
+ st.session_state["username"] = ""
291
+ st.session_state["offers_text"] = ""
292
+ st.session_state["wants_text"] = ""
293
+ st.session_state["availability"] = ""
294
+ st.session_state["preferences"] = ""
295
+ time.sleep(0.2)
296
+ st.experimental_rerun()
297
+ else:
298
+ st.error(m)
 
 
 
 
 
 
 
 
 
 
299
 
300
+ st.markdown("---")
301
+
302
+ col1, col2 = st.columns([2, 3])
303
+
304
+ with col1:
305
+ st.subheader("Community profiles")
306
  profiles = store.load_all()
 
307
  if not profiles:
308
+ st.info("No profiles yet create your profile in the sidebar.")
309
+ else:
310
+ for p in profiles:
311
+ st.markdown(f"**{p.username}** offers: {', '.join(p.offers) or '—'}; wants: {', '.join(p.wants) or '—'}")
312
+ with st.expander("Details"):
313
+ st.write(p.to_dict())
314
+
315
+ with col2:
316
+ st.subheader("Find Matches (AI)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  profiles = store.load_all()
 
318
  if not profiles:
319
+ st.info("Add some profiles to test matchmaking.")
320
+ else:
321
+ pick = st.selectbox("Match for profile", [p.username for p in profiles])
322
+ top_k = st.slider("Top K matches", 1, 10, 3)
323
+ if st.button("Run AI matchmaking"):
324
+ # animated spinner + success card
325
+ with st.spinner("Generating matches via Groq LLM..."):
326
+ time.sleep(0.6)
327
+ current = store.find_by_username(pick)
328
+ if not current:
329
+ st.error("Profile not found.")
330
+ else:
331
+ try:
332
+ sys_ins, user_msg = make_prompt_for_matching(current, profiles, top_k=top_k)
333
+ # ask groq
334
+ matches = ask_groq_for_matches(sys_ins, user_msg)
335
+ st.markdown("<div class='card success-animation' style='border-left:6px solid #4F46E5;'><b>Matches Found</b><div class='small-muted'>Below are the top matches returned by the AI.</div></div>", unsafe_allow_html=True)
336
+ st.json(matches)
337
+ except Exception as e:
338
+ st.error(str(e))
339
+
340
+ st.markdown("---")