Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -208,6 +208,40 @@ def save_feedback(entry: Dict[str, Any]):
|
|
| 208 |
data.append(entry)
|
| 209 |
FEEDBACK_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
| 210 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
# ---------- Streamlit UI ----------
|
| 212 |
st.set_page_config(page_title="AI Skill Swap", page_icon="🤝", layout="wide")
|
| 213 |
|
|
@@ -338,51 +372,77 @@ with col2:
|
|
| 338 |
if st.button("Run AI matchmaking"):
|
| 339 |
with st.spinner("Finding matches..."):
|
| 340 |
current = store.find_by_username(pick)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
|
| 342 |
-
sys_ins, user_msg = make_prompt_for_matching(current, profiles, top_k)
|
| 343 |
try:
|
|
|
|
|
|
|
| 344 |
matches = ask_groq_for_matches(sys_ins, user_msg)
|
| 345 |
-
|
| 346 |
-
#
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
cur_wants = set(w.lower() for w in current.wants)
|
| 356 |
-
|
| 357 |
-
score += len(offers_set & cur_wants) * 30
|
| 358 |
-
score += len(cur_offers & wants_set) * 30
|
| 359 |
-
if cand.availability and current.availability and cand.availability.lower() in current.availability.lower():
|
| 360 |
-
score += 20
|
| 361 |
-
if cand.preferences and current.preferences and cand.preferences.lower() in current.preferences.lower():
|
| 362 |
-
score += 20
|
| 363 |
-
|
| 364 |
-
matches.append({
|
| 365 |
-
"id": cand.id,
|
| 366 |
-
"username": cand.username,
|
| 367 |
-
"score": min(100, score),
|
| 368 |
-
"reason": "Local scoring",
|
| 369 |
-
"avatar": cand.avatar
|
| 370 |
-
})
|
| 371 |
-
|
| 372 |
-
matches = sorted(matches, key=lambda x: x["score"], reverse=True)[:top_k]
|
| 373 |
|
| 374 |
st.markdown("<div class='card'><b>Top Matches</b></div>", unsafe_allow_html=True)
|
| 375 |
|
| 376 |
for m in matches:
|
|
|
|
|
|
|
|
|
|
| 377 |
c1, c2 = st.columns([1, 5])
|
| 378 |
with c1:
|
| 379 |
-
|
| 380 |
-
|
|
|
|
| 381 |
else:
|
| 382 |
st.image("https://via.placeholder.com/72?text=User", width=72)
|
| 383 |
|
| 384 |
with c2:
|
| 385 |
-
st.markdown(f"### {m
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
data.append(entry)
|
| 209 |
FEEDBACK_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
| 210 |
|
| 211 |
+
# ---------- Local matching fallback ----------
|
| 212 |
+
def calculate_local_matches(current: Profile, candidates: List[Profile], top_k: int = 3) -> List[Dict[str, Any]]:
|
| 213 |
+
matches = []
|
| 214 |
+
for cand in candidates:
|
| 215 |
+
if cand.id == current.id:
|
| 216 |
+
continue
|
| 217 |
+
|
| 218 |
+
score = 0
|
| 219 |
+
offers_set = set(o.lower() for o in cand.offers)
|
| 220 |
+
wants_set = set(w.lower() for w in cand.wants)
|
| 221 |
+
cur_offers = set(o.lower() for o in current.offers)
|
| 222 |
+
cur_wants = set(w.lower() for w in current.wants)
|
| 223 |
+
|
| 224 |
+
score += len(offers_set & cur_wants) * 30
|
| 225 |
+
score += len(cur_offers & wants_set) * 30
|
| 226 |
+
|
| 227 |
+
if cand.availability and current.availability:
|
| 228 |
+
if cand.availability.lower() in current.availability.lower() or current.availability.lower() in cand.availability.lower():
|
| 229 |
+
score += 20
|
| 230 |
+
|
| 231 |
+
if cand.preferences and current.preferences:
|
| 232 |
+
if cand.preferences.lower() in current.preferences.lower() or current.preferences.lower() in cand.preferences.lower():
|
| 233 |
+
score += 20
|
| 234 |
+
|
| 235 |
+
matches.append({
|
| 236 |
+
"id": cand.id,
|
| 237 |
+
"username": cand.username,
|
| 238 |
+
"score": min(100, score),
|
| 239 |
+
"reason": "Local scoring",
|
| 240 |
+
"avatar": cand.avatar
|
| 241 |
+
})
|
| 242 |
+
|
| 243 |
+
return sorted(matches, key=lambda x: x["score"], reverse=True)[:top_k]
|
| 244 |
+
|
| 245 |
# ---------- Streamlit UI ----------
|
| 246 |
st.set_page_config(page_title="AI Skill Swap", page_icon="🤝", layout="wide")
|
| 247 |
|
|
|
|
| 372 |
if st.button("Run AI matchmaking"):
|
| 373 |
with st.spinner("Finding matches..."):
|
| 374 |
current = store.find_by_username(pick)
|
| 375 |
+
|
| 376 |
+
if not current:
|
| 377 |
+
st.error("Selected profile not found!")
|
| 378 |
+
st.stop()
|
| 379 |
|
|
|
|
| 380 |
try:
|
| 381 |
+
# Try AI matching first
|
| 382 |
+
sys_ins, user_msg = make_prompt_for_matching(current, profiles, top_k)
|
| 383 |
matches = ask_groq_for_matches(sys_ins, user_msg)
|
| 384 |
+
|
| 385 |
+
# Ensure matches have score field
|
| 386 |
+
for match in matches:
|
| 387 |
+
if "score" not in match:
|
| 388 |
+
match["score"] = 0
|
| 389 |
+
except Exception as e:
|
| 390 |
+
st.warning(f"AI matching failed: {e}. Using local matching...")
|
| 391 |
+
# Fallback to local matching
|
| 392 |
+
candidates = [p for p in profiles if p.id != current.id]
|
| 393 |
+
matches = calculate_local_matches(current, candidates, top_k)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
|
| 395 |
st.markdown("<div class='card'><b>Top Matches</b></div>", unsafe_allow_html=True)
|
| 396 |
|
| 397 |
for m in matches:
|
| 398 |
+
if not isinstance(m, dict):
|
| 399 |
+
continue
|
| 400 |
+
|
| 401 |
c1, c2 = st.columns([1, 5])
|
| 402 |
with c1:
|
| 403 |
+
avatar = m.get("avatar")
|
| 404 |
+
if avatar and Path(avatar).exists():
|
| 405 |
+
st.image(avatar, width=72)
|
| 406 |
else:
|
| 407 |
st.image("https://via.placeholder.com/72?text=User", width=72)
|
| 408 |
|
| 409 |
with c2:
|
| 410 |
+
st.markdown(f"### {m.get('username', 'Unknown User')}")
|
| 411 |
+
|
| 412 |
+
# Safely get score with default value
|
| 413 |
+
score = m.get("score", 0)
|
| 414 |
+
if not isinstance(score, (int, float)):
|
| 415 |
+
try:
|
| 416 |
+
score = float(score)
|
| 417 |
+
except:
|
| 418 |
+
score = 0
|
| 419 |
+
score = max(0, min(100, score))
|
| 420 |
+
|
| 421 |
+
st.markdown(f"**Match score:** <span class='match-score'>{score}%</span>", unsafe_allow_html=True)
|
| 422 |
+
st.progress(score / 100)
|
| 423 |
+
|
| 424 |
+
reason = m.get("reason", "AI matchmaking")
|
| 425 |
+
st.caption(f"*{reason}*")
|
| 426 |
+
|
| 427 |
+
# Feedback button (floating)
|
| 428 |
+
st.markdown("""
|
| 429 |
+
<div class='feedback-floating' onclick="document.querySelector('.feedback-form').style.display='block'">💬</div>
|
| 430 |
+
""", unsafe_allow_html=True)
|
| 431 |
+
|
| 432 |
+
# Hidden feedback form
|
| 433 |
+
with st.sidebar:
|
| 434 |
+
st.markdown("---")
|
| 435 |
+
with st.expander("💬 Feedback"):
|
| 436 |
+
feedback = st.text_area("Your feedback")
|
| 437 |
+
if st.button("Submit Feedback"):
|
| 438 |
+
if feedback.strip():
|
| 439 |
+
save_feedback({
|
| 440 |
+
"timestamp": time.time(),
|
| 441 |
+
"feedback": feedback.strip(),
|
| 442 |
+
"username": st.session_state.get("username", "anonymous")
|
| 443 |
+
})
|
| 444 |
+
st.success("Thank you for your feedback!")
|
| 445 |
+
time.sleep(1)
|
| 446 |
+
st.rerun()
|
| 447 |
+
else:
|
| 448 |
+
st.warning("Please enter some feedback.")
|