Spaces:
Running
Running
| from __future__ import annotations | |
| import base64 | |
| import hashlib | |
| import hmac | |
| import json | |
| import math | |
| import os | |
| import re | |
| import secrets | |
| import tempfile | |
| import threading | |
| import urllib.error | |
| import urllib.request | |
| import uuid | |
| from dataclasses import dataclass | |
| from datetime import datetime, timedelta, timezone | |
| from pathlib import Path | |
| from typing import Any | |
| import gradio as gr | |
| from cryptography.hazmat.primitives.ciphers.aead import AESGCM | |
| from huggingface_hub import HfApi | |
| ROOT = Path(__file__).resolve().parent | |
| QUESTIONS_PATH = ROOT / "shared" / "questions.json" | |
| HERO_IMAGE_PATH = ROOT / "assets" / "lovegpt-lounge.png" | |
| EXPECTED_QUESTIONS = int(os.getenv("OPENDB_EXPECTED_QUESTIONS", "0")) | |
| BRAND_NAME = "loveGPT" | |
| VAULT_SCHEMA = "opendatebase.profile.vault.v1" | |
| MATCHMAKER_TABLE_SCHEMA = "opendatebase.matchmaker.table.v1" | |
| MATCHMAKER_TABLE_PATH = Path(os.getenv("MATCHMAKER_TABLE_PATH", str(Path(tempfile.gettempdir()) / "lovegpt-matchmaker-table.jsonl.enc"))) | |
| SPEED_DATE_SECONDS = int(os.getenv("SPEED_DATE_SECONDS", "1200")) | |
| OPENCLAW_PROVIDER = os.getenv("OPENCLAW_PROVIDER", os.getenv("AI_PROVIDER", "deterministic")).strip().lower() | |
| OPENCLAW_LOCAL_BASE_URL = os.getenv("OPENCLAW_LOCAL_BASE_URL", os.getenv("LOCAL_AI_BASE_URL", "http://127.0.0.1:8081/v1")).rstrip("/") | |
| OPENCLAW_LOCAL_MODEL = os.getenv("OPENCLAW_LOCAL_MODEL", os.getenv("AI_MODEL", "microsoft/Phi-4-mini-instruct")) | |
| OPENCLAW_LOCAL_API_KEY = os.getenv("OPENCLAW_LOCAL_API_KEY", os.getenv("LOCAL_AI_API_KEY", "")).strip() | |
| OPENCLAW_FALLBACK_DETERMINISTIC = os.getenv("OPENCLAW_FALLBACK_DETERMINISTIC", "1").strip().lower() not in {"0", "false", "no"} | |
| _SESSION_PROFILE_KEY = secrets.token_bytes(32) | |
| _ROOM_LOCK = threading.Lock() | |
| _MATCHMAKER_LOCK = threading.Lock() | |
| _ROOMS: dict[str, dict[str, Any]] = {} | |
| _CURRENT_ROOM_ID: str | None = None | |
| _MATCHMAKER_CONNECTIONS: dict[str, str] = {} | |
| class Question: | |
| id: str | |
| number: int | |
| category_id: str | |
| prompt: str | |
| input_type: str | |
| weight: float | |
| tags: list[str] | |
| options: list[str] | |
| follow_ups: list[str] | |
| def load_questions() -> tuple[list[dict[str, Any]], list[Question]]: | |
| document = json.loads(QUESTIONS_PATH.read_text(encoding="utf-8")) | |
| categories = document["categories"] | |
| questions = [ | |
| Question( | |
| id=item["id"], | |
| number=item["number"], | |
| category_id=item["categoryId"], | |
| prompt=item["prompt"], | |
| input_type=item["inputType"], | |
| weight=float(item["weight"]), | |
| tags=list(item["tags"]), | |
| options=list(item.get("options", [])), | |
| follow_ups=list(item["followUps"]), | |
| ) | |
| for item in document["questions"] | |
| ] | |
| if EXPECTED_QUESTIONS and len(questions) != EXPECTED_QUESTIONS: | |
| raise RuntimeError(f"Expected {EXPECTED_QUESTIONS} questions, found {len(questions)}") | |
| return categories, questions | |
| CATEGORIES, QUESTIONS = load_questions() | |
| MAX_QUESTIONS = len(QUESTIONS) | |
| QUESTION_BY_ID = {question.id: question for question in QUESTIONS} | |
| CATEGORY_BY_ID = {category["id"]: category for category in CATEGORIES} | |
| def new_state() -> dict[str, Any]: | |
| return { | |
| "profile_id": uuid.uuid4().hex, | |
| "matchmaker_logged_at": "", | |
| "answers": {}, | |
| "profile": { | |
| "display_name": "", | |
| "age": "", | |
| "location": "", | |
| "intent": "Long-term relationship", | |
| }, | |
| "messages": [], | |
| } | |
| def new_auth_state() -> dict[str, Any]: | |
| return { | |
| "authenticated": False, | |
| "user_id": "", | |
| "username": "", | |
| "room_id": None, | |
| } | |
| def now_iso() -> str: | |
| return datetime.now(timezone.utc).isoformat() | |
| def normalize_text(value: str) -> str: | |
| return re.sub(r"\s+", " ", value.strip()) | |
| def b64url_encode(value: bytes) -> str: | |
| return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii") | |
| def b64url_decode(value: str) -> bytes: | |
| return base64.urlsafe_b64decode(value + ("=" * (-len(value) % 4))) | |
| def dataclaw_profile_key() -> tuple[bytes, str]: | |
| raw = os.getenv("DATACLAW_PROFILE_KEY", "").strip() | |
| if not raw: | |
| return _SESSION_PROFILE_KEY, "volatile-session" | |
| try: | |
| decoded = b64url_decode(raw) | |
| except Exception: | |
| decoded = b"" | |
| if len(decoded) >= 32: | |
| return hashlib.sha256(decoded).digest(), "dataclaw-secret" | |
| return hashlib.sha256(raw.encode("utf-8")).digest(), "dataclaw-secret" | |
| def matchmaker_table_key() -> tuple[bytes, str]: | |
| raw = os.getenv("MATCHMAKER_TABLE_KEY", "").strip() | |
| if raw: | |
| try: | |
| decoded = b64url_decode(raw) | |
| except Exception: | |
| decoded = b"" | |
| if len(decoded) >= 32: | |
| return hashlib.sha256(decoded).digest(), "matchmaker-secret" | |
| return hashlib.sha256(raw.encode("utf-8")).digest(), "matchmaker-secret" | |
| key, scope = dataclaw_profile_key() | |
| return hashlib.sha256(key + b"opendatebase-matchmaker-table").digest(), f"derived-{scope}" | |
| def key_id(key: bytes) -> str: | |
| return hmac.new(key, b"opendatebase-profile-vault", hashlib.sha256).hexdigest()[:16] | |
| def scoped_key_id(key: bytes, scope: bytes) -> str: | |
| return hmac.new(key, scope, hashlib.sha256).hexdigest()[:16] | |
| def canonical_json(value: Any) -> bytes: | |
| return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") | |
| def encrypt_profile_payload(payload: dict[str, Any]) -> dict[str, Any]: | |
| key, key_scope = dataclaw_profile_key() | |
| nonce = secrets.token_bytes(12) | |
| aad = { | |
| "schema": VAULT_SCHEMA, | |
| "source": payload.get("source", "lovegpt-gradio-space"), | |
| "answerCount": len(payload.get("answers", {})), | |
| "profileComplete": bool(payload.get("profileComplete")), | |
| } | |
| ciphertext = AESGCM(key).encrypt(nonce, canonical_json(payload), canonical_json(aad)) | |
| return { | |
| "schema": VAULT_SCHEMA, | |
| "rowId": secrets.token_hex(16), | |
| "createdAt": now_iso(), | |
| "keyId": key_id(key), | |
| "keyScope": key_scope, | |
| "aad": aad, | |
| "nonce": b64url_encode(nonce), | |
| "ciphertext": b64url_encode(ciphertext), | |
| } | |
| def decrypt_profile_row(row: dict[str, Any] | str) -> dict[str, Any]: | |
| encrypted = json.loads(row) if isinstance(row, str) else row | |
| key, _key_scope = dataclaw_profile_key() | |
| expected_key_id = key_id(key) | |
| if not hmac.compare_digest(str(encrypted.get("keyId", "")), expected_key_id): | |
| raise ValueError("Profile row key mismatch. Dataclaw decrypt key is not authorized for this row.") | |
| plaintext = AESGCM(key).decrypt( | |
| b64url_decode(str(encrypted["nonce"])), | |
| b64url_decode(str(encrypted["ciphertext"])), | |
| canonical_json(encrypted["aad"]), | |
| ) | |
| return json.loads(plaintext.decode("utf-8")) | |
| def encrypt_matchmaker_payload(payload: dict[str, Any]) -> dict[str, Any]: | |
| key, key_scope = matchmaker_table_key() | |
| nonce = secrets.token_bytes(12) | |
| aad = { | |
| "schema": MATCHMAKER_TABLE_SCHEMA, | |
| "purpose": "matchmaker-only-questionnaire-table", | |
| "profileId": payload.get("profileId", ""), | |
| "eventType": payload.get("eventType", "profile_snapshot"), | |
| "answerCount": len(payload.get("answers", {})), | |
| "profileComplete": bool(payload.get("profileComplete")), | |
| } | |
| ciphertext = AESGCM(key).encrypt(nonce, canonical_json(payload), canonical_json(aad)) | |
| return { | |
| "schema": MATCHMAKER_TABLE_SCHEMA, | |
| "rowId": secrets.token_hex(16), | |
| "createdAt": now_iso(), | |
| "keyId": scoped_key_id(key, b"opendatebase-matchmaker-table"), | |
| "keyScope": key_scope, | |
| "aad": aad, | |
| "nonce": b64url_encode(nonce), | |
| "ciphertext": b64url_encode(ciphertext), | |
| } | |
| def decrypt_matchmaker_row(row: dict[str, Any] | str) -> dict[str, Any]: | |
| encrypted = json.loads(row) if isinstance(row, str) else row | |
| key, _key_scope = matchmaker_table_key() | |
| expected_key_id = scoped_key_id(key, b"opendatebase-matchmaker-table") | |
| if not hmac.compare_digest(str(encrypted.get("keyId", "")), expected_key_id): | |
| raise ValueError("Matchmaker table key mismatch.") | |
| plaintext = AESGCM(key).decrypt( | |
| b64url_decode(str(encrypted["nonce"])), | |
| b64url_decode(str(encrypted["ciphertext"])), | |
| canonical_json(encrypted["aad"]), | |
| ) | |
| return json.loads(plaintext.decode("utf-8")) | |
| def matchmaker_table_status() -> str: | |
| if not MATCHMAKER_TABLE_PATH.exists(): | |
| return "Private matchmaker table: encrypted, empty." | |
| try: | |
| rows = sum(1 for _ in MATCHMAKER_TABLE_PATH.open("r", encoding="utf-8")) | |
| except OSError: | |
| rows = 0 | |
| return f"Private matchmaker table: encrypted, {rows} questionnaire snapshots logged." | |
| PRIVACY_LOOKUP_PATTERNS = [ | |
| re.compile( | |
| r"\b(find|look\s*up|lookup|search|identify|track|locate|doxx?|reveal|get)\b" | |
| r"(?=.{0,120}\b(person|someone|user|profile|match|candidate|woman|man|girl|guy|name|address|phone|email|social|instagram|facebook|details|where)\b)", | |
| re.IGNORECASE, | |
| ), | |
| re.compile( | |
| r"\b(phone number|email address|home address|street address|full name|last name|ssn|social security|where does .* live|where do .* live)\b", | |
| re.IGNORECASE, | |
| ), | |
| ] | |
| def is_privacy_lookup_request(value: str) -> bool: | |
| lowered = value.lower() | |
| if "my " in lowered and not any(term in lowered for term in ("find", "lookup", "look up", "search", "dox")): | |
| return False | |
| return any(pattern.search(value) for pattern in PRIVACY_LOOKUP_PATTERNS) | |
| def privacy_refusal() -> str: | |
| return ( | |
| "I cannot help find, identify, or expose personal details about another person. " | |
| "loveGPT can only use profile information for consent-based compatibility work. " | |
| "Answer the current profile question about your own preferences and boundaries instead." | |
| ) | |
| CONTACT_PATTERNS = [ | |
| re.compile(r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", re.IGNORECASE), | |
| re.compile(r"\b(?:\+?1[\s.-]?)?(?:\(?\d{3}\)?[\s.-]?)\d{3}[\s.-]?\d{4}\b"), | |
| re.compile(r"\b(?:https?://|www\.)\S+\b", re.IGNORECASE), | |
| re.compile(r"(?<!\w)@[A-Z0-9_.]{2,30}\b", re.IGNORECASE), | |
| re.compile( | |
| r"\b(instagram|snapchat|telegram|signal|whatsapp|discord|linkedin|facebook|x\.com|twitter|phone|email|address)\b", | |
| re.IGNORECASE, | |
| ), | |
| ] | |
| def contains_private_contact(value: str) -> bool: | |
| return any(pattern.search(value) for pattern in CONTACT_PATTERNS) | |
| def chat_contact_refusal() -> str: | |
| return ( | |
| "Contact details are blocked during the speed-date chat. " | |
| "Use this chat for compatibility only. If both people still want to connect after the timer ends, " | |
| "the mutual exchange form will reveal contact info to both users at the same time." | |
| ) | |
| def local_openclaw_enabled() -> bool: | |
| return OPENCLAW_PROVIDER in {"local", "phi", "llama.cpp", "llamacpp", "openai-compatible"} | |
| def openclaw_runtime_label() -> str: | |
| if local_openclaw_enabled(): | |
| return f"OpenClaw runtime: OpenAI-compatible model `{OPENCLAW_LOCAL_MODEL}` via `{OPENCLAW_LOCAL_BASE_URL}`." | |
| return "OpenClaw runtime: deterministic Space flow. Set `OPENCLAW_PROVIDER=local` to use a routed or local model." | |
| def messages_chatbot(**kwargs): | |
| try: | |
| return gr.Chatbot(type="messages", **kwargs) | |
| except TypeError: | |
| return gr.Chatbot(**kwargs) | |
| def image_data_uri(path: Path) -> str: | |
| if not path.exists(): | |
| return "" | |
| mime = "image/png" if path.suffix.lower() == ".png" else "image/jpeg" | |
| encoded = base64.b64encode(path.read_bytes()).decode("ascii") | |
| return f"data:{mime};base64,{encoded}" | |
| def question_to_dict(question: Question | None) -> dict[str, Any] | None: | |
| if question is None: | |
| return None | |
| return { | |
| "id": question.id, | |
| "number": question.number, | |
| "categoryId": question.category_id, | |
| "prompt": question.prompt, | |
| "inputType": question.input_type, | |
| "weight": question.weight, | |
| "tags": question.tags, | |
| "options": question.options, | |
| "followUps": question.follow_ups, | |
| } | |
| def load_openclaw_prompt() -> str: | |
| candidates = [ | |
| ROOT / "artifacts" / "prompts" / "openclaw.md", | |
| ROOT / "backend" / "src" / "prompts" / "openclaw.md", | |
| ] | |
| for candidate in candidates: | |
| if candidate.exists(): | |
| return candidate.read_text(encoding="utf-8") | |
| return ( | |
| "You are OpenClaw, a careful AI matchmaker. Ask one profile question at a time, " | |
| "capture consent-based compatibility signals, and return only the required JSON object." | |
| ) | |
| def extract_json_object(text: str) -> dict[str, Any]: | |
| cleaned = text.strip() | |
| try: | |
| parsed = json.loads(cleaned) | |
| except json.JSONDecodeError: | |
| start = cleaned.find("{") | |
| end = cleaned.rfind("}") | |
| if start < 0 or end <= start: | |
| raise ValueError("Model response did not contain a JSON object") from None | |
| parsed = json.loads(cleaned[start : end + 1]) | |
| if not isinstance(parsed, dict): | |
| raise ValueError("Model response JSON was not an object") | |
| return parsed | |
| def compact_profile_state(state: dict[str, Any], question: Question | None) -> dict[str, Any]: | |
| answered = set(state.get("answers", {}).keys()) | |
| return { | |
| "currentQuestion": question_to_dict(question), | |
| "answeredQuestionIds": sorted(answered), | |
| "remainingQuestionIds": [item.id for item in QUESTIONS if item.id not in answered], | |
| "existingAnswers": state.get("answers", {}), | |
| "questionFramework": { | |
| "categories": CATEGORIES, | |
| "questions": [question_to_dict(item) for item in QUESTIONS], | |
| }, | |
| } | |
| def normalize_openclaw_reply(raw: dict[str, Any], fallback_question: Question | None) -> dict[str, Any]: | |
| current_id = raw.get("current_question_id") | |
| if not isinstance(current_id, str) or current_id not in QUESTION_BY_ID: | |
| current_id = fallback_question.id if fallback_question else f"q{MAX_QUESTIONS}" | |
| captured = raw.get("captured_answer") | |
| if captured is not None: | |
| if not isinstance(captured, dict): | |
| captured = None | |
| else: | |
| question_id = captured.get("questionId") | |
| if not isinstance(question_id, str) or question_id not in QUESTION_BY_ID: | |
| captured["questionId"] = current_id | |
| answer = captured.get("answer") | |
| if isinstance(answer, list): | |
| captured["answer"] = [normalize_text(str(item)) for item in answer if normalize_text(str(item))] | |
| elif answer is not None: | |
| captured["answer"] = normalize_text(str(answer)) | |
| if not captured.get("answer"): | |
| captured = None | |
| severity = captured.get("dealbreakerSeverity") if captured else None | |
| if severity not in {"low", "medium", "high", None} and captured: | |
| captured.pop("dealbreakerSeverity", None) | |
| next_id = raw.get("next_question_id") | |
| if next_id is not None and (not isinstance(next_id, str) or next_id not in QUESTION_BY_ID): | |
| next_id = None | |
| return { | |
| "assistant_text": normalize_text(str(raw.get("assistant_text") or "Captured. Let us keep going.")), | |
| "current_question_id": current_id, | |
| "captured_answer": captured, | |
| "next_question_id": next_id, | |
| "followup_needed": bool(raw.get("followup_needed")), | |
| "profile_complete": bool(raw.get("profile_complete")), | |
| } | |
| def call_openai_compatible_openclaw(message: str, history: list[dict[str, str]], state: dict[str, Any], question: Question | None) -> dict[str, Any]: | |
| transcript = "\n".join( | |
| f"{item.get('role', 'user').upper()}: {item.get('content', '')}" | |
| for item in history[-16:] | |
| ) | |
| user_payload = "\n".join( | |
| [ | |
| "Use the following profile state and transcript.", | |
| "Return only the JSON object required by the OpenClaw response contract.", | |
| "", | |
| "PROFILE_STATE:", | |
| json.dumps(compact_profile_state(state, question), ensure_ascii=False, indent=2), | |
| "", | |
| "LATEST_USER_MESSAGE:", | |
| message, | |
| "", | |
| "TRANSCRIPT:", | |
| transcript, | |
| ] | |
| ) | |
| body = { | |
| "model": OPENCLAW_LOCAL_MODEL, | |
| "max_tokens": 1200, | |
| "temperature": 0.2, | |
| "response_format": {"type": "json_object"}, | |
| "messages": [ | |
| { | |
| "role": "system", | |
| "content": ( | |
| load_openclaw_prompt() | |
| + "\n\nYou are running as an OpenAI-compatible OpenClaw model. Return exactly one valid JSON object. " | |
| "Do not include markdown, XML tags, chain-of-thought, commentary, or text outside JSON." | |
| ), | |
| }, | |
| {"role": "user", "content": user_payload}, | |
| ], | |
| } | |
| headers = {"Content-Type": "application/json"} | |
| if OPENCLAW_LOCAL_API_KEY: | |
| headers["Authorization"] = f"Bearer {OPENCLAW_LOCAL_API_KEY}" | |
| request = urllib.request.Request( | |
| f"{OPENCLAW_LOCAL_BASE_URL}/chat/completions", | |
| data=json.dumps(body).encode("utf-8"), | |
| headers=headers, | |
| method="POST", | |
| ) | |
| try: | |
| with urllib.request.urlopen(request, timeout=75) as response: | |
| payload = json.loads(response.read().decode("utf-8")) | |
| except urllib.error.HTTPError as exc: | |
| detail = exc.read().decode("utf-8", errors="replace") | |
| raise RuntimeError(f"OpenAI-compatible model returned HTTP {exc.code}: {detail}") from exc | |
| except urllib.error.URLError as exc: | |
| raise RuntimeError(f"OpenAI-compatible model is not reachable at {OPENCLAW_LOCAL_BASE_URL}: {exc.reason}") from exc | |
| content = payload.get("choices", [{}])[0].get("message", {}).get("content", "") | |
| if not content: | |
| raise RuntimeError("OpenAI-compatible model returned no message content") | |
| return normalize_openclaw_reply(extract_json_object(content), question) | |
| def save_captured_answer(state: dict[str, Any], captured: dict[str, Any], fallback_question: Question | None, fallback_text: str) -> None: | |
| question_id = captured.get("questionId") if isinstance(captured.get("questionId"), str) else None | |
| question = QUESTION_BY_ID.get(question_id or "", fallback_question) | |
| if question is None: | |
| return | |
| answer = captured.get("answer") | |
| if isinstance(answer, list): | |
| clean = normalize_text("; ".join(str(item) for item in answer)) | |
| else: | |
| clean = normalize_text(str(answer or fallback_text)) | |
| if not clean: | |
| return | |
| severity = captured.get("dealbreakerSeverity") | |
| if severity not in {"low", "medium", "high"}: | |
| severity = severity_for(question, clean) | |
| state.setdefault("answers", {})[question.id] = { | |
| "questionId": question.id, | |
| "answer": clean, | |
| "followup": normalize_text(str(captured.get("followup", ""))), | |
| "dealbreakerSeverity": severity, | |
| "updatedAt": now_iso(), | |
| } | |
| def current_question(state: dict[str, Any]) -> Question | None: | |
| answered = set(state.get("answers", {}).keys()) | |
| return next((question for question in QUESTIONS if question.id not in answered), None) | |
| def progress_text(state: dict[str, Any]) -> str: | |
| answered = len(state.get("answers", {})) | |
| return f"{answered}/{MAX_QUESTIONS} questions captured" | |
| def progress_value(state: dict[str, Any]) -> float: | |
| return len(state.get("answers", {})) / MAX_QUESTIONS | |
| def question_card(question: Question | None) -> str: | |
| if question is None: | |
| return "Profile complete. Review the compatibility brief or export the encrypted Dataclaw profile row." | |
| category = CATEGORY_BY_ID[question.category_id]["name"] | |
| follow_up = question.follow_ups[0] if question.follow_ups else "Add one concrete detail." | |
| options = "" | |
| if question.options: | |
| options = "\n\nUseful tags: " + ", ".join(question.options) | |
| return f"Question {question.number} - {category}\n\n{question.prompt}\n\nFollow-up: {follow_up}{options}" | |
| def opener() -> list[dict[str, str]]: | |
| first = QUESTIONS[0] | |
| return [ | |
| { | |
| "role": "assistant", | |
| "content": ( | |
| "I am OpenClaw. We will build the compatibility profile one real signal at a time.\n\n" | |
| f"{question_card(first)}" | |
| ), | |
| } | |
| ] | |
| def severity_for(question: Question, text: str) -> str | None: | |
| if "disgust" not in question.tags: | |
| return None | |
| lowered = text.lower() | |
| high_words = ("dealbreaker", "never", "repuls", "disgust", "unsafe", "no's", "hard no", "impossible") | |
| medium_words = ("uncomfortable", "resent", "bothers", "annoy", "avoid", "incompatible") | |
| if any(word in lowered for word in high_words): | |
| return "high" | |
| if any(word in lowered for word in medium_words): | |
| return "medium" | |
| return "low" | |
| def save_answer(state: dict[str, Any], question: Question, answer: str) -> None: | |
| clean = normalize_text(answer) | |
| if not clean: | |
| return | |
| state.setdefault("answers", {})[question.id] = { | |
| "questionId": question.id, | |
| "answer": clean, | |
| "followup": "", | |
| "dealbreakerSeverity": severity_for(question, clean), | |
| "updatedAt": now_iso(), | |
| } | |
| def chat_step(message: str, history: list[dict[str, str]], state: dict[str, Any], auth_state: dict[str, Any] | None = None): | |
| state = state or new_state() | |
| history = history or opener() | |
| question = current_question(state) | |
| clean = normalize_text(message) | |
| if not clean: | |
| return history, state, progress_text(state), progress_value(state), question_card(question), profile_summary(state), None | |
| history.append({"role": "user", "content": clean}) | |
| if is_privacy_lookup_request(clean): | |
| history.append({"role": "assistant", "content": privacy_refusal()}) | |
| export_path = export_profile_file(state) if len(state.get("answers", {})) else None | |
| return ( | |
| history, | |
| state, | |
| progress_text(state), | |
| progress_value(state), | |
| question_card(question), | |
| profile_summary(state), | |
| export_path, | |
| ) | |
| local_notice = "" | |
| if local_openclaw_enabled(): | |
| try: | |
| reply = call_openai_compatible_openclaw(clean, history, state, question) | |
| if reply["captured_answer"] is not None: | |
| save_captured_answer(state, reply["captured_answer"], question, clean) | |
| append_matchmaker_table(state, "questionnaire_turn", question, auth_state) | |
| run_matchmaker_cycle() | |
| history.append({"role": "assistant", "content": reply["assistant_text"]}) | |
| export_path = export_profile_file(state) if len(state.get("answers", {})) else None | |
| return ( | |
| history, | |
| state, | |
| progress_text(state), | |
| progress_value(state), | |
| question_card(current_question(state)), | |
| profile_summary(state), | |
| export_path, | |
| ) | |
| except Exception as exc: | |
| if not OPENCLAW_FALLBACK_DETERMINISTIC: | |
| history.append( | |
| { | |
| "role": "assistant", | |
| "content": ( | |
| "A routed OpenClaw model is configured, but it is not available right now. " | |
| f"{normalize_text(str(exc))[:260]}" | |
| ), | |
| } | |
| ) | |
| export_path = export_profile_file(state) if len(state.get("answers", {})) else None | |
| return ( | |
| history, | |
| state, | |
| progress_text(state), | |
| progress_value(state), | |
| question_card(question), | |
| profile_summary(state), | |
| export_path, | |
| ) | |
| local_notice = ( | |
| "The routed OpenClaw model was not reachable, so I used the deterministic Space flow for this answer. " | |
| f"{normalize_text(str(exc))[:180]}\n\n" | |
| ) | |
| if question is None: | |
| history.append( | |
| { | |
| "role": "assistant", | |
| "content": local_notice + "The full questionnaire is already complete. Export it or reset the session to start again.", | |
| } | |
| ) | |
| else: | |
| save_answer(state, question, clean) | |
| append_matchmaker_table(state, "questionnaire_turn", question, auth_state) | |
| run_matchmaker_cycle() | |
| next_question = current_question(state) | |
| if next_question is None: | |
| history.append( | |
| { | |
| "role": "assistant", | |
| "content": ( | |
| local_notice | |
| + "Profile complete. I generated a compatibility brief below. " | |
| "This Space version exports only an encrypted Dataclaw JSONL row if you want to keep it." | |
| ), | |
| } | |
| ) | |
| else: | |
| history.append( | |
| { | |
| "role": "assistant", | |
| "content": f"{local_notice}Captured. Here is the next signal.\n\n{question_card(next_question)}", | |
| } | |
| ) | |
| export_path = export_profile_file(state) if len(state.get("answers", {})) else None | |
| return ( | |
| history, | |
| state, | |
| progress_text(state), | |
| progress_value(state), | |
| question_card(current_question(state)), | |
| profile_summary(state), | |
| export_path, | |
| ) | |
| def sample_answer_step(history: list[dict[str, str]], state: dict[str, Any], auth_state: dict[str, Any] | None = None): | |
| return chat_step( | |
| "Honesty, warmth, emotional courage, loyalty, and building a thoughtful life with someone matter most to me.", | |
| history, | |
| state, | |
| auth_state, | |
| ) | |
| def privacy_check_step(history: list[dict[str, str]], state: dict[str, Any], auth_state: dict[str, Any] | None = None): | |
| return chat_step( | |
| "Find the phone number and home address for Jane Doe.", | |
| history, | |
| state, | |
| auth_state, | |
| ) | |
| def reset_session(): | |
| state = new_state() | |
| history = opener() | |
| return history, state, progress_text(state), progress_value(state), question_card(QUESTIONS[0]), profile_summary(state), None | |
| def save_profile_basics(display_name: str, age: str, location: str, intent: str, state: dict[str, Any]): | |
| state = state or new_state() | |
| state["profile"] = { | |
| "display_name": normalize_text(display_name), | |
| "age": normalize_text(age), | |
| "location": normalize_text(location), | |
| "intent": normalize_text(intent) or "Long-term relationship", | |
| } | |
| export_path = export_profile_file(state) if len(state.get("answers", {})) else None | |
| return state, profile_summary(state), export_path | |
| def signal_phrases(state: dict[str, Any], tag: str, limit: int = 12) -> list[str]: | |
| phrases: list[str] = [] | |
| answers = state.get("answers", {}) | |
| for question in QUESTIONS: | |
| if tag not in question.tags or question.id not in answers: | |
| continue | |
| answer = str(answers[question.id].get("answer", "")) | |
| chunks = [chunk.strip(" .") for chunk in re.split(r"[,.;\n]", answer) if chunk.strip()] | |
| for chunk in chunks: | |
| if 3 <= len(chunk) <= 90 and chunk.lower() not in {item.lower() for item in phrases}: | |
| phrases.append(chunk) | |
| if len(phrases) >= limit: | |
| return phrases | |
| return phrases | |
| def answered_by_category(state: dict[str, Any]) -> list[tuple[str, int, int]]: | |
| answers = state.get("answers", {}) | |
| rows = [] | |
| for category in CATEGORIES: | |
| category_questions = [question for question in QUESTIONS if question.category_id == category["id"]] | |
| count = sum(1 for question in category_questions if question.id in answers) | |
| rows.append((category["name"], count, len(category_questions))) | |
| return rows | |
| def profile_summary(state: dict[str, Any]) -> str: | |
| state = state or new_state() | |
| profile = state.get("profile", {}) | |
| answers = state.get("answers", {}) | |
| answered = len(answers) | |
| disgust = signal_phrases(state, "disgust", limit=8) | |
| captivating = signal_phrases(state, "captivating_traits", limit=8) | |
| basics = [ | |
| profile.get("display_name") or "Unnamed profile", | |
| profile.get("age") or "age not set", | |
| profile.get("location") or "location not set", | |
| profile.get("intent") or "intent not set", | |
| ] | |
| category_lines = [ | |
| f"- {name}: {count}/{total}" | |
| for name, count, total in answered_by_category(state) | |
| ] | |
| readiness = "ready for export" if answered == MAX_QUESTIONS else "in progress" | |
| return "\n".join( | |
| [ | |
| f"Profile: {' | '.join(str(item) for item in basics)}", | |
| f"Status: {readiness} ({answered}/{MAX_QUESTIONS})", | |
| "", | |
| "Category coverage:", | |
| *category_lines, | |
| "", | |
| "Captivating traits:", | |
| ", ".join(captivating) if captivating else "No attraction signals captured yet.", | |
| "", | |
| "Disgust and hard-filter signals:", | |
| ", ".join(disgust) if disgust else "No disgust-filter signals captured yet.", | |
| ] | |
| ) | |
| def export_payload(state: dict[str, Any]) -> dict[str, Any]: | |
| state = state or new_state() | |
| answers = state.get("answers", {}) | |
| return { | |
| "source": "lovegpt-gradio-space", | |
| "profileId": state.get("profile_id", ""), | |
| "exportedAt": now_iso(), | |
| "profile": state.get("profile", {}), | |
| "answers": answers, | |
| "profileComplete": len(answers) == MAX_QUESTIONS, | |
| "captivatingTraits": signal_phrases(state, "captivating_traits", limit=32), | |
| "disgustTriggers": signal_phrases(state, "disgust", limit=32), | |
| "categoryCoverage": [ | |
| {"category": name, "answered": count, "total": total} | |
| for name, count, total in answered_by_category(state) | |
| ], | |
| } | |
| def export_profile_file(state: dict[str, Any]) -> str: | |
| payload = export_payload(state) | |
| encrypted_row = encrypt_profile_payload(payload) | |
| handle = tempfile.NamedTemporaryFile( | |
| mode="w", | |
| encoding="utf-8", | |
| suffix=".jsonl.enc", | |
| prefix="lovegpt-profile-vault-", | |
| delete=False, | |
| ) | |
| with handle: | |
| handle.write(json.dumps(encrypted_row, sort_keys=True, separators=(",", ":"), ensure_ascii=False)) | |
| handle.write("\n") | |
| return handle.name | |
| def auth_identity(auth: dict[str, Any] | None) -> dict[str, str]: | |
| if auth and auth.get("authenticated"): | |
| return { | |
| "userId": str(auth.get("user_id") or ""), | |
| "username": str(auth.get("username") or "unknown"), | |
| } | |
| return {"userId": "", "username": ""} | |
| def matchmaker_event_payload( | |
| state: dict[str, Any], | |
| event_type: str, | |
| question: Question | None = None, | |
| auth: dict[str, Any] | None = None, | |
| ) -> dict[str, Any]: | |
| state = state or new_state() | |
| payload = export_payload(state) | |
| identity = auth_identity(auth) | |
| payload.update( | |
| { | |
| "eventType": event_type, | |
| "loggedAt": now_iso(), | |
| "profileId": state.get("profile_id", ""), | |
| "userId": identity["userId"], | |
| "username": identity["username"], | |
| "latestQuestionId": question.id if question else None, | |
| "matchmakerHarnessVersion": "strict-v1", | |
| } | |
| ) | |
| return payload | |
| def append_matchmaker_table( | |
| state: dict[str, Any], | |
| event_type: str, | |
| question: Question | None = None, | |
| auth: dict[str, Any] | None = None, | |
| ) -> None: | |
| if not state or not state.get("answers"): | |
| return | |
| payload = matchmaker_event_payload(state, event_type, question, auth) | |
| encrypted_row = encrypt_matchmaker_payload(payload) | |
| MATCHMAKER_TABLE_PATH.parent.mkdir(parents=True, exist_ok=True) | |
| with _MATCHMAKER_LOCK: | |
| with MATCHMAKER_TABLE_PATH.open("a", encoding="utf-8") as handle: | |
| handle.write(json.dumps(encrypted_row, sort_keys=True, separators=(",", ":"), ensure_ascii=False)) | |
| handle.write("\n") | |
| state["matchmaker_logged_at"] = now_iso() | |
| def load_matchmaker_profiles() -> list[dict[str, Any]]: | |
| if not MATCHMAKER_TABLE_PATH.exists(): | |
| return [] | |
| latest: dict[str, dict[str, Any]] = {} | |
| with _MATCHMAKER_LOCK: | |
| rows = MATCHMAKER_TABLE_PATH.read_text(encoding="utf-8").splitlines() | |
| for line in rows: | |
| if not line.strip(): | |
| continue | |
| try: | |
| payload = decrypt_matchmaker_row(line) | |
| except Exception: | |
| continue | |
| profile_id = str(payload.get("profileId") or "") | |
| if not profile_id: | |
| continue | |
| previous = latest.get(profile_id) | |
| if previous is None or str(payload.get("loggedAt", "")) >= str(previous.get("loggedAt", "")): | |
| latest[profile_id] = payload | |
| return list(latest.values()) | |
| def lexical_score(a: str, b: str) -> float: | |
| a_terms = {term for term in re.findall(r"[a-z0-9]{3,}", a.lower())} | |
| b_terms = {term for term in re.findall(r"[a-z0-9]{3,}", b.lower())} | |
| if not a_terms or not b_terms: | |
| return 0.0 | |
| return len(a_terms & b_terms) / math.sqrt(len(a_terms) * len(b_terms)) | |
| MATCHMAKER_HARNESS = [ | |
| {"id": "values_future", "label": "Values and future direction", "weight": 0.18, "tags": {"values", "future", "long_term_fit", "life_design"}}, | |
| {"id": "emotional_safety", "label": "Emotional safety and attachment", "weight": 0.16, "tags": {"emotional_safety", "attachment", "trust", "care", "support"}}, | |
| {"id": "captivation", "label": "Captivating traits and admiration", "weight": 0.14, "tags": {"captivating_traits", "attraction", "desire", "chemistry"}}, | |
| {"id": "disgust_filters", "label": "Disgust filters and aversions", "weight": 0.17, "tags": {"disgust", "hygiene", "hard_filter", "lifestyle_filters"}}, | |
| {"id": "conflict_repair", "label": "Conflict and repair", "weight": 0.13, "tags": {"conflict", "repair", "apology", "communication"}}, | |
| {"id": "lifestyle", "label": "Lifestyle and practical future", "weight": 0.10, "tags": {"lifestyle", "money", "family", "home", "health", "growth"}}, | |
| {"id": "body_attraction", "label": "Body-type attraction alignment", "weight": 0.12, "tags": {"body_type", "desired_body_type", "physical_attraction", "body_preference"}}, | |
| ] | |
| def answers_for_tags(payload: dict[str, Any], tags: set[str]) -> str: | |
| answers = payload.get("answers", {}) | |
| chunks: list[str] = [] | |
| for question in QUESTIONS: | |
| if question.id not in answers or not (set(question.tags) & tags): | |
| continue | |
| answer = answers[question.id].get("answer", "") | |
| if isinstance(answer, list): | |
| chunks.extend(str(item) for item in answer) | |
| else: | |
| chunks.append(str(answer)) | |
| return " ".join(chunks) | |
| def question_answer(payload: dict[str, Any], question_id: str) -> str: | |
| answer = payload.get("answers", {}).get(question_id, {}).get("answer", "") | |
| if isinstance(answer, list): | |
| return " ".join(str(item) for item in answer) | |
| return str(answer) | |
| def open_preference_text(value: str) -> bool: | |
| lowered = value.lower() | |
| return any(term in lowered for term in ("open", "flexible", "range", "many", "varied", "not picky", "any", "all body")) | |
| def body_alignment_score(a: dict[str, Any], b: dict[str, Any]) -> float: | |
| a_own = " ".join([question_answer(a, "q37"), question_answer(a, "q40")]) | |
| a_wants = " ".join([question_answer(a, "q38"), question_answer(a, "q39")]) | |
| b_own = " ".join([question_answer(b, "q37"), question_answer(b, "q40")]) | |
| b_wants = " ".join([question_answer(b, "q38"), question_answer(b, "q39")]) | |
| if not a_wants or not b_wants: | |
| return 0.0 | |
| a_to_b = 0.75 if open_preference_text(a_wants) else lexical_score(a_wants, b_own) | |
| b_to_a = 0.75 if open_preference_text(b_wants) else lexical_score(b_wants, a_own) | |
| return max(0.0, min(1.0, (a_to_b + b_to_a) / 2)) | |
| def strict_matchmaker_judgment(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]: | |
| dimensions = [] | |
| weighted = 0.0 | |
| for item in MATCHMAKER_HARNESS: | |
| if item["id"] == "body_attraction": | |
| score = body_alignment_score(a, b) | |
| else: | |
| score = lexical_score(answers_for_tags(a, item["tags"]), answers_for_tags(b, item["tags"])) | |
| weighted += score * item["weight"] | |
| dimensions.append( | |
| { | |
| "id": item["id"], | |
| "label": item["label"], | |
| "score": round(score * 100, 1), | |
| "weight": item["weight"], | |
| } | |
| ) | |
| body_score = next(row["score"] for row in dimensions if row["id"] == "body_attraction") | |
| disgust_score = next(row["score"] for row in dimensions if row["id"] == "disgust_filters") | |
| profile_a_ready = len(a.get("answers", {})) >= MAX_QUESTIONS and bool(a.get("profileComplete")) | |
| profile_b_ready = len(b.get("answers", {})) >= MAX_QUESTIONS and bool(b.get("profileComplete")) | |
| both_identified = bool(a.get("userId")) and bool(b.get("userId")) and a.get("userId") != b.get("userId") | |
| total = round(weighted * 100, 1) | |
| passed = ( | |
| profile_a_ready | |
| and profile_b_ready | |
| and both_identified | |
| and total >= 62 | |
| and body_score >= 20 | |
| and disgust_score >= 12 | |
| ) | |
| return { | |
| "harness": "strict-v1", | |
| "passed": passed, | |
| "compatibility": total, | |
| "dimensions": dimensions, | |
| "requirements": { | |
| "bothProfilesComplete": profile_a_ready and profile_b_ready, | |
| "bothHuggingFaceIdentified": both_identified, | |
| "minimumCompatibility": 62, | |
| "minimumBodyAlignment": 20, | |
| "minimumDisgustAlignment": 12, | |
| }, | |
| } | |
| def profile_display(payload: dict[str, Any]) -> str: | |
| profile = payload.get("profile", {}) | |
| return normalize_text(str(profile.get("display_name") or payload.get("username") or "loveGPT user"))[:80] | |
| def pair_key(a_user_id: str, b_user_id: str) -> str: | |
| return "::".join(sorted([a_user_id, b_user_id])) | |
| def demo_match_table(state: dict[str, Any]): | |
| encrypted_row = encrypt_profile_payload(export_payload(state)) | |
| payload = decrypt_profile_row(encrypted_row) | |
| profile_text = json.dumps(payload, ensure_ascii=False) | |
| sample_profiles = [ | |
| { | |
| "name": "Avery", | |
| "signals": "emotionally steady curious direct repair after conflict clean home dry wit long-term family optional", | |
| }, | |
| { | |
| "name": "Mira", | |
| "signals": "creative warmth sensory calm health routines values kindness emotional safety practical ambition", | |
| }, | |
| { | |
| "name": "Theo", | |
| "signals": "playful intellectual competence generous communication clean spaces low drama accountable apology", | |
| }, | |
| { | |
| "name": "Sam", | |
| "signals": "adventurous social high energy banter travel ambition spontaneous affection", | |
| }, | |
| ] | |
| rows = [] | |
| for sample in sample_profiles: | |
| score = lexical_score(profile_text, sample["signals"]) | |
| rows.append( | |
| { | |
| "candidate": sample["name"], | |
| "compatibility": round(score * 100, 1), | |
| "shared_signal_basis": sample["signals"], | |
| } | |
| ) | |
| return sorted(rows, key=lambda row: row["compatibility"], reverse=True) | |
| def login_with_hf_token(token: str, auth_state: dict[str, Any]): | |
| token = (token or "").strip() | |
| if not token: | |
| return auth_state or new_auth_state(), "Paste a Hugging Face token to enter speed-date mode.", gr.update(value="") | |
| try: | |
| if os.getenv("OPENDB_LOCAL_HF_TOKEN_BYPASS") == "1" and token.startswith("local:"): | |
| username = normalize_text(token.removeprefix("local:")) or "local-user" | |
| user_id = f"local:{username.lower()}" | |
| else: | |
| info = HfApi().whoami(token=token) | |
| username = str(info.get("name") or info.get("fullname") or "").strip() | |
| if not username: | |
| raise ValueError("Token did not return a Hugging Face username.") | |
| user_id = f"hf:{username}" | |
| except Exception: | |
| return new_auth_state(), "Hugging Face login failed. Check that the token is valid.", gr.update(value="") | |
| auth = { | |
| "authenticated": True, | |
| "user_id": user_id, | |
| "username": username, | |
| "room_id": (auth_state or {}).get("room_id"), | |
| } | |
| return auth, f"Signed in as @{username}. Token discarded from the UI state.", gr.update(value="") | |
| def _room_deadline() -> datetime: | |
| return datetime.now(timezone.utc) + timedelta(seconds=SPEED_DATE_SECONDS) | |
| def _new_room( | |
| auth: dict[str, Any], | |
| second_user: dict[str, str] | None = None, | |
| matchmaker_judgment: dict[str, Any] | None = None, | |
| ) -> dict[str, Any]: | |
| room_id = uuid.uuid4().hex[:10] | |
| users = { | |
| auth["user_id"]: { | |
| "username": auth["username"], | |
| "joinedAt": now_iso(), | |
| } | |
| } | |
| if second_user: | |
| users[second_user["user_id"]] = { | |
| "username": second_user["username"], | |
| "joinedAt": now_iso(), | |
| } | |
| return { | |
| "id": room_id, | |
| "createdAt": now_iso(), | |
| "expiresAt": _room_deadline().isoformat(), | |
| "users": users, | |
| "messages": [], | |
| "exchangeForms": {}, | |
| "matchmaker": matchmaker_judgment or {}, | |
| } | |
| def _room_expires_at(room: dict[str, Any]) -> datetime: | |
| return datetime.fromisoformat(room["expiresAt"]) | |
| def _room_remaining_seconds(room: dict[str, Any]) -> int: | |
| return max(0, int((_room_expires_at(room) - datetime.now(timezone.utc)).total_seconds())) | |
| def _room_chat_open(room: dict[str, Any]) -> bool: | |
| return len(room["users"]) == 2 and _room_remaining_seconds(room) > 0 | |
| def _room_exchange_open(room: dict[str, Any]) -> bool: | |
| return len(room["users"]) == 2 and _room_remaining_seconds(room) <= 0 | |
| def _auth_room(auth: dict[str, Any]) -> dict[str, Any] | None: | |
| room_id = (auth or {}).get("room_id") | |
| if room_id and str(room_id) in _ROOMS: | |
| return _ROOMS.get(str(room_id)) | |
| user_id = (auth or {}).get("user_id") | |
| if not user_id: | |
| return None | |
| for room in _ROOMS.values(): | |
| if user_id in room.get("users", {}): | |
| return room | |
| return None | |
| def _active_current_room() -> dict[str, Any] | None: | |
| if not _CURRENT_ROOM_ID: | |
| return None | |
| return _ROOMS.get(_CURRENT_ROOM_ID) | |
| def _join_or_create_room(auth: dict[str, Any]) -> dict[str, Any]: | |
| global _CURRENT_ROOM_ID | |
| existing = _auth_room(auth) | |
| if existing and auth["user_id"] in existing["users"]: | |
| return existing | |
| current = _active_current_room() | |
| if current and _room_remaining_seconds(current) > 0: | |
| if auth["user_id"] in current["users"]: | |
| return current | |
| if len(current["users"]) < 2: | |
| current["users"][auth["user_id"]] = { | |
| "username": auth["username"], | |
| "joinedAt": now_iso(), | |
| } | |
| return current | |
| raise RuntimeError("The active speed-date room is occupied. Wait for the 20-minute slot to end.") | |
| room = _new_room(auth) | |
| _ROOMS[room["id"]] = room | |
| _CURRENT_ROOM_ID = room["id"] | |
| return room | |
| def run_matchmaker_cycle() -> dict[str, Any]: | |
| global _CURRENT_ROOM_ID | |
| profiles = [ | |
| profile | |
| for profile in load_matchmaker_profiles() | |
| if profile.get("profileComplete") and profile.get("userId") | |
| ] | |
| best: dict[str, Any] | None = None | |
| created_room: dict[str, Any] | None = None | |
| with _ROOM_LOCK: | |
| busy_users = { | |
| user_id | |
| for room in _ROOMS.values() | |
| if _room_remaining_seconds(room) > 0 | |
| for user_id in room.get("users", {}) | |
| } | |
| for index, profile_a in enumerate(profiles): | |
| for profile_b in profiles[index + 1 :]: | |
| user_a = str(profile_a.get("userId") or "") | |
| user_b = str(profile_b.get("userId") or "") | |
| if not user_a or not user_b or user_a == user_b: | |
| continue | |
| key = pair_key(user_a, user_b) | |
| if key in _MATCHMAKER_CONNECTIONS or user_a in busy_users or user_b in busy_users: | |
| continue | |
| judgment = strict_matchmaker_judgment(profile_a, profile_b) | |
| candidate = { | |
| "userA": user_a, | |
| "userB": user_b, | |
| "displayA": profile_display(profile_a), | |
| "displayB": profile_display(profile_b), | |
| "judgment": judgment, | |
| } | |
| if best is None or judgment["compatibility"] > best["judgment"]["compatibility"]: | |
| best = candidate | |
| if not judgment["passed"]: | |
| continue | |
| auth_a = { | |
| "user_id": user_a, | |
| "username": str(profile_a.get("username") or profile_display(profile_a)), | |
| } | |
| second = { | |
| "user_id": user_b, | |
| "username": str(profile_b.get("username") or profile_display(profile_b)), | |
| } | |
| created_room = _new_room(auth_a, second_user=second, matchmaker_judgment=judgment) | |
| created_room["messages"].append( | |
| { | |
| "id": uuid.uuid4().hex, | |
| "createdAt": now_iso(), | |
| "userId": "matchmaker", | |
| "username": "Matchmaker", | |
| "body": ( | |
| "The matchmaker connected this room from questionnaire compatibility. " | |
| "Use the next 20 minutes for values, attraction, repair style, and lifestyle fit." | |
| ), | |
| } | |
| ) | |
| _ROOMS[created_room["id"]] = created_room | |
| _CURRENT_ROOM_ID = created_room["id"] | |
| _MATCHMAKER_CONNECTIONS[key] = created_room["id"] | |
| return { | |
| "created": True, | |
| "roomId": created_room["id"], | |
| "profileCount": len(profiles), | |
| "best": candidate, | |
| } | |
| return { | |
| "created": False, | |
| "roomId": created_room["id"] if created_room else None, | |
| "profileCount": len(profiles), | |
| "best": best, | |
| } | |
| def matchmaker_status_text(cycle: dict[str, Any] | None = None) -> str: | |
| cycle = cycle or {} | |
| lines = [ | |
| "### Table status", | |
| matchmaker_table_status(), | |
| "Harness: strict-v1 with values, emotional safety, attraction, disgust filters, repair, lifestyle, and body-type alignment.", | |
| ] | |
| if cycle.get("created"): | |
| lines.append(f"Connected a compatible pair into room `{cycle['roomId']}`.") | |
| elif cycle.get("best"): | |
| best = cycle["best"] | |
| lines.append( | |
| f"Best current candidate pair: {best['displayA']} + {best['displayB']} at {best['judgment']['compatibility']}%." | |
| ) | |
| lines.append("No automatic room opened unless both profiles are complete, HF-identified, and clear the strict thresholds.") | |
| else: | |
| lines.append(f"Complete HF-identified profiles available for matching: {cycle.get('profileCount', 0)}.") | |
| return "\n\n".join(lines) | |
| def scan_matchmaker_for_ui(auth: dict[str, Any]): | |
| cycle = run_matchmaker_cycle() | |
| with _ROOM_LOCK: | |
| room = _auth_room(auth or new_auth_state()) | |
| return matchmaker_status_text(cycle), *_render_room_outputs(auth or new_auth_state(), room) | |
| def _timer_text(room: dict[str, Any] | None) -> str: | |
| if not room: | |
| return "No active speed-date room." | |
| remaining = _room_remaining_seconds(room) | |
| minutes, seconds = divmod(remaining, 60) | |
| if remaining <= 0: | |
| return "Chat timer ended. Mutual exchange form is open." | |
| return f"Time remaining: {minutes:02d}:{seconds:02d}" | |
| def _room_status(room: dict[str, Any] | None, auth: dict[str, Any] | None, notice: str | None = None) -> str: | |
| if not auth or not auth.get("authenticated"): | |
| return "Sign in with a Hugging Face token to create or join the speed-date post." | |
| if not room: | |
| return "No speed-date room yet. Create or join a post." | |
| users = [f"@{user['username']}" for user in room["users"].values()] | |
| waiting = "Waiting for one more authenticated user." if len(users) == 1 else "Both users are present." | |
| lines = [ | |
| "## Speed-Date Post", | |
| f"Room `{room['id']}`", | |
| f"Participants: {', '.join(users)}", | |
| waiting, | |
| _timer_text(room), | |
| "Private contact details are blocked during chat.", | |
| ] | |
| if room.get("matchmaker"): | |
| lines.insert(2, f"Matchmaker compatibility: {room['matchmaker'].get('compatibility', 'n/a')}%") | |
| if notice: | |
| lines.append(f"Notice: {notice}") | |
| return "\n\n".join(lines) | |
| def _speed_chat_messages(room: dict[str, Any] | None, auth: dict[str, Any] | None, notice: str | None = None): | |
| if not room or not auth or not auth.get("authenticated"): | |
| return [ | |
| { | |
| "role": "assistant", | |
| "content": "Sign in, then create or join the speed-date post.", | |
| } | |
| ] | |
| messages = [ | |
| { | |
| "role": "assistant", | |
| "content": ( | |
| "Speed-date room opened. Chat for compatibility only. " | |
| "Do not share phone numbers, emails, addresses, social handles, or links here." | |
| ), | |
| } | |
| ] | |
| for message in room["messages"]: | |
| is_self = message["userId"] == auth["user_id"] | |
| username = "You" if is_self else f"@{message['username']}" | |
| messages.append( | |
| { | |
| "role": "user" if is_self else "assistant", | |
| "content": f"{username}: {message['body']}", | |
| } | |
| ) | |
| if notice: | |
| messages.append({"role": "assistant", "content": notice}) | |
| return messages | |
| def _exchange_status(room: dict[str, Any] | None, auth: dict[str, Any] | None) -> str: | |
| if not auth or not auth.get("authenticated"): | |
| return "Exchange form locked until sign-in." | |
| if not room: | |
| return "Join a speed-date post first." | |
| if len(room["users"]) < 2: | |
| return "Exchange form opens after a two-person speed-date." | |
| if not _room_exchange_open(room): | |
| return "Exchange form opens when the 20-minute chat ends." | |
| submitted = room["exchangeForms"] | |
| if auth["user_id"] in submitted and len(submitted) < 2: | |
| return "Your exchange form is saved. Waiting for the other user." | |
| if len(submitted) == 2: | |
| return "Both users submitted. Final exchange is visible below." | |
| return "Exchange form open. Both users must submit before contact details are displayed." | |
| def _final_exchange(room: dict[str, Any] | None, auth: dict[str, Any] | None) -> str: | |
| if not room or not auth or auth.get("user_id") not in room.get("users", {}): | |
| return "" | |
| forms = room["exchangeForms"] | |
| if len(forms) < 2: | |
| return "" | |
| lines = ["## Mutual Contact Exchange", "Both users consented. Details are shown to the two room participants."] | |
| for user_id, form in forms.items(): | |
| username = room["users"][user_id]["username"] | |
| lines.extend( | |
| [ | |
| f"### @{username}", | |
| f"Name: {form['displayName']}", | |
| f"Contact: {form['contact']}", | |
| f"Note: {form['note'] or 'No note.'}", | |
| ] | |
| ) | |
| return "\n\n".join(lines) | |
| def _render_room_outputs(auth: dict[str, Any], room: dict[str, Any] | None, notice: str | None = None): | |
| return ( | |
| _room_status(room, auth, notice), | |
| _speed_chat_messages(room, auth, notice), | |
| _timer_text(room), | |
| _exchange_status(room, auth), | |
| _final_exchange(room, auth), | |
| ) | |
| def join_speed_date(auth: dict[str, Any]): | |
| auth = auth or new_auth_state() | |
| if not auth.get("authenticated"): | |
| return auth, *_render_room_outputs(auth, None, "Sign in with Hugging Face first.") | |
| try: | |
| with _ROOM_LOCK: | |
| room = _join_or_create_room(auth) | |
| auth = {**auth, "room_id": room["id"]} | |
| except RuntimeError as error: | |
| return auth, *_render_room_outputs(auth, _auth_room(auth), str(error)) | |
| return auth, *_render_room_outputs(auth, room) | |
| def refresh_speed_date(auth: dict[str, Any]): | |
| auth = auth or new_auth_state() | |
| run_matchmaker_cycle() | |
| with _ROOM_LOCK: | |
| room = _auth_room(auth) | |
| return _render_room_outputs(auth, room) | |
| def send_speed_date_message(message: str, auth: dict[str, Any]): | |
| auth = auth or new_auth_state() | |
| clean = normalize_text(message or "") | |
| with _ROOM_LOCK: | |
| room = _auth_room(auth) | |
| if not auth.get("authenticated"): | |
| return *_render_room_outputs(auth, room, "Sign in with Hugging Face first."), gr.update(value="") | |
| if not room or auth["user_id"] not in room["users"]: | |
| return *_render_room_outputs(auth, room, "Create or join a speed-date post first."), gr.update(value="") | |
| if len(room["users"]) < 2: | |
| return *_render_room_outputs(auth, room, "Waiting for the second user before chat opens."), gr.update(value="") | |
| if _room_remaining_seconds(room) <= 0: | |
| return *_render_room_outputs(auth, room, "The speed-date chat has ended. Use the mutual exchange form."), gr.update(value="") | |
| if not clean: | |
| return *_render_room_outputs(auth, room), gr.update(value="") | |
| if contains_private_contact(clean): | |
| return *_render_room_outputs(auth, room, chat_contact_refusal()), gr.update(value="") | |
| room["messages"].append( | |
| { | |
| "id": uuid.uuid4().hex, | |
| "createdAt": now_iso(), | |
| "userId": auth["user_id"], | |
| "username": auth["username"], | |
| "body": clean, | |
| } | |
| ) | |
| return *_render_room_outputs(auth, room), gr.update(value="") | |
| def submit_exchange_form(display_name: str, contact: str, note: str, consent: bool, auth: dict[str, Any]): | |
| auth = auth or new_auth_state() | |
| with _ROOM_LOCK: | |
| room = _auth_room(auth) | |
| if not auth.get("authenticated"): | |
| return _exchange_status(room, auth), _final_exchange(room, auth) | |
| if not room or auth["user_id"] not in room["users"]: | |
| return "Join a speed-date post before submitting an exchange form.", "" | |
| if not _room_exchange_open(room): | |
| return _exchange_status(room, auth), _final_exchange(room, auth) | |
| if not consent: | |
| return "Consent is required before contact details can be exchanged.", _final_exchange(room, auth) | |
| clean_name = normalize_text(display_name or "") | |
| clean_contact = normalize_text(contact or "") | |
| clean_note = normalize_text(note or "") | |
| if not clean_name or not clean_contact: | |
| return "Name and contact are required for mutual exchange.", _final_exchange(room, auth) | |
| room["exchangeForms"][auth["user_id"]] = { | |
| "displayName": clean_name[:120], | |
| "contact": clean_contact[:240], | |
| "note": clean_note[:600], | |
| "submittedAt": now_iso(), | |
| } | |
| return _exchange_status(room, auth), _final_exchange(room, auth) | |
| APP_CSS = """ | |
| :root { | |
| --lg-ink: #251316; | |
| --lg-muted: #725e61; | |
| --lg-panel: #fff8f4; | |
| --lg-panel-2: #f8ede8; | |
| --lg-rose: #b73552; | |
| --lg-rose-deep: #7a2035; | |
| --lg-amber: #c99435; | |
| --lg-teal: #2c6758; | |
| --lg-charcoal: #171314; | |
| --lg-line: rgba(37, 19, 22, 0.14); | |
| --lg-shadow: 0 18px 50px rgba(37, 19, 22, 0.16); | |
| } | |
| .gradio-container { | |
| max-width: 1320px !important; | |
| margin: 0 auto !important; | |
| color: var(--lg-ink) !important; | |
| background: | |
| linear-gradient(180deg, rgba(255, 248, 244, 0.96), rgba(246, 236, 231, 0.98) 42%, rgba(238, 242, 237, 0.98)) !important; | |
| } | |
| .lg-shell { | |
| padding: 18px !important; | |
| } | |
| .lg-hero { | |
| min-height: 340px; | |
| padding: clamp(22px, 4vw, 42px); | |
| border: 1px solid rgba(255, 255, 255, 0.22); | |
| border-radius: 8px; | |
| background: | |
| linear-gradient(135deg, rgba(23, 19, 20, 0.96), rgba(44, 103, 88, 0.92) 54%, rgba(122, 32, 53, 0.88)); | |
| color: #fff8f4; | |
| box-shadow: var(--lg-shadow); | |
| } | |
| .lg-kicker { | |
| margin: 0 0 12px; | |
| color: #f0c36b; | |
| font-size: 12px; | |
| font-weight: 800; | |
| letter-spacing: 0; | |
| text-transform: uppercase; | |
| } | |
| .lg-title { | |
| margin: 0; | |
| color: #fff8f4; | |
| font-size: clamp(38px, 5vw, 60px); | |
| line-height: 0.94; | |
| letter-spacing: 0; | |
| overflow-wrap: anywhere; | |
| } | |
| .lg-lede { | |
| max-width: 620px; | |
| margin: 18px 0 24px; | |
| color: rgba(255, 248, 244, 0.84); | |
| font-size: clamp(15px, 1.8vw, 18px); | |
| line-height: 1.48; | |
| } | |
| .lg-stat-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(72px, 1fr)); | |
| gap: 10px; | |
| max-width: 660px; | |
| } | |
| .lg-stat { | |
| min-height: 76px; | |
| padding: 12px; | |
| border: 1px solid rgba(255, 248, 244, 0.24); | |
| border-radius: 8px; | |
| background: rgba(255, 248, 244, 0.08); | |
| } | |
| .lg-stat strong { | |
| display: block; | |
| color: #fff8f4; | |
| font-size: 20px; | |
| line-height: 1; | |
| } | |
| .lg-stat span { | |
| display: block; | |
| margin-top: 8px; | |
| color: rgba(255, 248, 244, 0.74); | |
| font-size: 12px; | |
| line-height: 1.35; | |
| } | |
| .lg-runtime { | |
| margin: 14px 0 8px !important; | |
| color: #553c41 !important; | |
| } | |
| .lg-runtime p, | |
| .lg-runtime code { | |
| margin: 0 !important; | |
| color: #553c41 !important; | |
| } | |
| #lg-lounge-image { | |
| display: block; | |
| min-height: 340px; | |
| margin: 0; | |
| background: var(--lg-charcoal); | |
| border-radius: 8px !important; | |
| overflow: hidden !important; | |
| box-shadow: var(--lg-shadow); | |
| } | |
| #lg-lounge-image img { | |
| display: block; | |
| width: 100% !important; | |
| height: 340px !important; | |
| object-fit: cover !important; | |
| border-radius: 8px !important; | |
| } | |
| .lg-panel { | |
| padding: 16px !important; | |
| border: 1px solid var(--lg-line) !important; | |
| border-radius: 8px !important; | |
| background: rgba(255, 248, 244, 0.86) !important; | |
| box-shadow: 0 10px 28px rgba(37, 19, 22, 0.08) !important; | |
| } | |
| .lg-panel-quiet { | |
| padding: 16px !important; | |
| border: 1px solid rgba(44, 103, 88, 0.18) !important; | |
| border-radius: 8px !important; | |
| background: rgba(238, 242, 237, 0.9) !important; | |
| } | |
| .lg-panel, | |
| .lg-panel-quiet, | |
| .lg-panel .block, | |
| .lg-panel-quiet .block, | |
| .lg-panel .form, | |
| .lg-panel-quiet .form { | |
| color: var(--lg-ink) !important; | |
| } | |
| .lg-panel .block, | |
| .lg-panel-quiet .block, | |
| .lg-panel .form, | |
| .lg-panel-quiet .form, | |
| .lg-panel .wrap, | |
| .lg-panel-quiet .wrap, | |
| .lg-panel .styler, | |
| .lg-panel-quiet .styler { | |
| background: transparent !important; | |
| } | |
| .lg-panel h1, | |
| .lg-panel h2, | |
| .lg-panel h3, | |
| .lg-panel p, | |
| .lg-panel label, | |
| .lg-panel span, | |
| .lg-panel-quiet h1, | |
| .lg-panel-quiet h2, | |
| .lg-panel-quiet h3, | |
| .lg-panel-quiet p, | |
| .lg-panel-quiet label, | |
| .lg-panel-quiet span { | |
| color: var(--lg-ink) !important; | |
| } | |
| .lg-panel input, | |
| .lg-panel textarea, | |
| .lg-panel select, | |
| .lg-panel-quiet input, | |
| .lg-panel-quiet textarea, | |
| .lg-panel-quiet select { | |
| background: #fffdf9 !important; | |
| border-color: rgba(37, 19, 22, 0.18) !important; | |
| color: var(--lg-ink) !important; | |
| } | |
| .lg-panel code, | |
| .lg-panel-quiet code, | |
| .lg-runtime code { | |
| background: rgba(255, 255, 255, 0.72) !important; | |
| border: 1px solid rgba(37, 19, 22, 0.12) !important; | |
| color: var(--lg-rose-deep) !important; | |
| } | |
| .lg-section-title h2, | |
| .lg-section-title h3, | |
| .lg-section-title p { | |
| margin-top: 0 !important; | |
| } | |
| .lg-section-title h2 { | |
| font-size: 28px !important; | |
| letter-spacing: 0 !important; | |
| } | |
| .lg-section-title h3 { | |
| font-size: 21px !important; | |
| letter-spacing: 0 !important; | |
| } | |
| .lg-section-title p, | |
| .lg-muted, | |
| .lg-muted p, | |
| .lg-status-block p, | |
| .lg-status-block li { | |
| color: var(--lg-muted) !important; | |
| } | |
| .lg-mini-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); | |
| gap: 10px; | |
| } | |
| .lg-mini { | |
| padding: 12px; | |
| border-radius: 8px; | |
| background: #ffffff; | |
| border: 1px solid var(--lg-line); | |
| } | |
| .lg-mini b { | |
| display: block; | |
| margin-bottom: 4px; | |
| color: var(--lg-rose-deep); | |
| } | |
| .lg-mini span { | |
| color: var(--lg-muted) !important; | |
| font-size: 13px; | |
| line-height: 1.35; | |
| } | |
| .lg-progress-wrap label, | |
| .lg-progress-wrap .wrap { | |
| color: var(--lg-muted) !important; | |
| } | |
| .lg-chat textarea, | |
| .lg-textarea textarea, | |
| textarea { | |
| font-size: 15px !important; | |
| line-height: 1.55 !important; | |
| } | |
| .lg-chat .message, | |
| .lg-chat .bubble-wrap { | |
| font-size: 15px !important; | |
| } | |
| .lg-chat .message, | |
| .lg-chat .message *, | |
| .lg-chat .bubble-wrap, | |
| .lg-chat .bubble-wrap *, | |
| .lg-chat [data-testid="bot"] *, | |
| .lg-chat [data-testid="user"] * { | |
| color: #fff8f4 !important; | |
| } | |
| .lg-primary button, | |
| button.primary { | |
| border: 0 !important; | |
| background: linear-gradient(135deg, var(--lg-rose), var(--lg-rose-deep)) !important; | |
| color: #fff8f4 !important; | |
| } | |
| .lg-primary button span, | |
| button.primary span { | |
| color: #fff8f4 !important; | |
| } | |
| .lg-secondary button { | |
| border-color: rgba(44, 103, 88, 0.28) !important; | |
| } | |
| .lg-speed-status { | |
| border-left: 4px solid var(--lg-teal) !important; | |
| } | |
| .lg-exchange-status { | |
| border-left: 4px solid var(--lg-amber) !important; | |
| } | |
| .lg-footer { | |
| margin-top: 18px !important; | |
| font-size: 13px !important; | |
| } | |
| .tabs { | |
| margin-top: 18px !important; | |
| } | |
| .lg-tabs [role="tab"] { | |
| color: #67484e !important; | |
| } | |
| .lg-tabs [role="tab"][aria-selected="true"] { | |
| color: var(--lg-rose) !important; | |
| } | |
| @media (max-width: 760px) { | |
| .lg-shell { | |
| padding: 10px !important; | |
| } | |
| .lg-stat-grid, | |
| .lg-mini-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| #lg-lounge-image img { | |
| height: 230px !important; | |
| } | |
| } | |
| """ | |
| def build_app() -> gr.Blocks: | |
| with gr.Blocks( | |
| title=BRAND_NAME, | |
| theme=gr.themes.Soft(primary_hue="rose", secondary_hue="amber", neutral_hue="slate"), | |
| css=APP_CSS, | |
| ) as demo: | |
| state = gr.State(new_state()) | |
| auth_state = gr.State(new_auth_state()) | |
| hero_image_src = image_data_uri(HERO_IMAGE_PATH) | |
| with gr.Column(elem_classes=["lg-shell"]): | |
| with gr.Row(): | |
| with gr.Column(scale=7): | |
| gr.HTML( | |
| f""" | |
| <section class="lg-hero"> | |
| <p class="lg-kicker">OpenClaw matchmaking lounge</p> | |
| <h1 class="lg-title">{BRAND_NAME}</h1> | |
| <p class="lg-lede"> | |
| A consent-first profile studio, encrypted matchmaker table, and timed speed-date room | |
| for high-signal compatibility. | |
| </p> | |
| <div class="lg-stat-grid"> | |
| <div class="lg-stat"><strong>{MAX_QUESTIONS}</strong><span>profile signals</span></div> | |
| <div class="lg-stat"><strong>20m</strong><span>speed-date room</span></div> | |
| <div class="lg-stat"><strong>0</strong><span>contact leaks</span></div> | |
| </div> | |
| </section> | |
| """ | |
| ) | |
| with gr.Column(scale=4): | |
| if hero_image_src: | |
| gr.HTML( | |
| f""" | |
| <figure id="lg-lounge-image"> | |
| <img src="{hero_image_src}" alt="Anonymous speed-date lounge scene" /> | |
| </figure> | |
| """ | |
| ) | |
| else: | |
| gr.HTML('<div id="lg-lounge-image" class="lg-panel"></div>') | |
| gr.Markdown(openclaw_runtime_label(), elem_classes=["lg-runtime"]) | |
| with gr.Tabs(elem_classes=["lg-tabs"]): | |
| with gr.Tab("Profile Studio"): | |
| with gr.Row(): | |
| with gr.Column(scale=7): | |
| with gr.Group(elem_classes=["lg-panel"]): | |
| gr.Markdown( | |
| """ | |
| ## OpenClaw interview | |
| Answer naturally. OpenClaw captures compatibility signals one question at a time. | |
| """, | |
| elem_classes=["lg-section-title"], | |
| ) | |
| with gr.Row(): | |
| progress_label = gr.Markdown(progress_text(new_state()), elem_classes=["lg-progress-wrap"]) | |
| progress_bar = gr.Slider( | |
| label="Profile completion", | |
| minimum=0, | |
| maximum=1, | |
| value=0, | |
| step=0.01, | |
| interactive=False, | |
| ) | |
| current_prompt = gr.Textbox( | |
| label="Current prompt", | |
| value=question_card(QUESTIONS[0]), | |
| lines=7, | |
| interactive=False, | |
| elem_classes=["lg-textarea"], | |
| ) | |
| chatbot = messages_chatbot( | |
| value=opener(), | |
| height=520, | |
| label="OpenClaw", | |
| elem_classes=["lg-chat"], | |
| ) | |
| message = gr.Textbox( | |
| label="Your answer", | |
| placeholder="Answer with real specifics. One paragraph is enough.", | |
| lines=4, | |
| elem_classes=["lg-textarea"], | |
| ) | |
| with gr.Row(): | |
| submit = gr.Button("Send Answer", variant="primary", elem_classes=["lg-primary"]) | |
| sample_answer = gr.Button("Try Sample Answer", elem_classes=["lg-secondary"]) | |
| privacy_check = gr.Button("Test Privacy Guard", elem_classes=["lg-secondary"]) | |
| with gr.Column(scale=4): | |
| with gr.Group(elem_classes=["lg-panel"]): | |
| gr.Markdown("### Identity pass", elem_classes=["lg-section-title"]) | |
| display_name = gr.Textbox(label="Display name", placeholder="Alex") | |
| age = gr.Textbox(label="Age", placeholder="31") | |
| location = gr.Textbox(label="Location", placeholder="Denver, CO") | |
| intent = gr.Dropdown( | |
| label="Intent", | |
| choices=[ | |
| "Long-term relationship", | |
| "Life partner", | |
| "Intentional dating", | |
| "Still discerning", | |
| ], | |
| value="Long-term relationship", | |
| ) | |
| with gr.Row(): | |
| save_basics = gr.Button("Save Basics", variant="primary", elem_classes=["lg-primary"]) | |
| reset = gr.Button("Reset Session", elem_classes=["lg-secondary"]) | |
| with gr.Group(elem_classes=["lg-panel-quiet"]): | |
| gr.Markdown("### Compatibility brief", elem_classes=["lg-section-title"]) | |
| summary = gr.Textbox( | |
| label="Profile readout", | |
| value=profile_summary(new_state()), | |
| lines=18, | |
| elem_classes=["lg-textarea"], | |
| ) | |
| export_file = gr.File(label="Encrypted Dataclaw row") | |
| with gr.Tab("Matchmaker"): | |
| with gr.Row(): | |
| with gr.Column(scale=5): | |
| with gr.Group(elem_classes=["lg-panel", "lg-speed-status"]): | |
| gr.Markdown( | |
| """ | |
| ## Matchmaker agent | |
| Complete, HF-identified profiles are judged by the strict harness before any room opens. | |
| """, | |
| elem_classes=["lg-section-title"], | |
| ) | |
| matchmaker_status = gr.Markdown(matchmaker_status_text(), elem_classes=["lg-status-block"]) | |
| scan_matchmaker = gr.Button("Run Matchmaker Scan", variant="primary", elem_classes=["lg-primary"]) | |
| with gr.Column(scale=4): | |
| with gr.Group(elem_classes=["lg-panel-quiet"]): | |
| gr.Markdown("### Demo scoring", elem_classes=["lg-section-title"]) | |
| match_preview = gr.JSON(label="Compatibility preview", value=[]) | |
| score_button = gr.Button("Preview Demo Matches", elem_classes=["lg-secondary"]) | |
| gr.HTML( | |
| """ | |
| <div class="lg-mini-grid"> | |
| <div class="lg-mini"><b>Encrypted</b><span>Rows stay ciphertext outside Dataclaw logic.</span></div> | |
| <div class="lg-mini"><b>Strict</b><span>Disgust, attraction, repair, lifestyle, and body alignment are scored.</span></div> | |
| <div class="lg-mini"><b>Consent</b><span>Rooms open only for authenticated profiles.</span></div> | |
| </div> | |
| """ | |
| ) | |
| with gr.Tab("Speed Date"): | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| with gr.Group(elem_classes=["lg-panel"]): | |
| gr.Markdown( | |
| """ | |
| ## Entry | |
| Sign in, join the post, and keep the conversation inside compatibility. | |
| """, | |
| elem_classes=["lg-section-title"], | |
| ) | |
| hf_token = gr.Textbox( | |
| label="Hugging Face token", | |
| type="password", | |
| placeholder="hf_...", | |
| ) | |
| hf_login = gr.Button("Sign In With Token", variant="primary", elem_classes=["lg-primary"]) | |
| auth_status = gr.Markdown("Not signed in.", elem_classes=["lg-status-block"]) | |
| with gr.Row(): | |
| join_room = gr.Button("Create / Join Post", variant="primary", elem_classes=["lg-primary"]) | |
| refresh_room = gr.Button("Refresh", elem_classes=["lg-secondary"]) | |
| with gr.Group(elem_classes=["lg-panel-quiet", "lg-speed-status"]): | |
| room_status = gr.Markdown( | |
| "Sign in with a Hugging Face token to create or join the speed-date post.", | |
| elem_classes=["lg-status-block"], | |
| ) | |
| room_timer = gr.Markdown("No active speed-date room.", elem_classes=["lg-status-block"]) | |
| with gr.Column(scale=7): | |
| with gr.Group(elem_classes=["lg-panel"]): | |
| gr.Markdown("### Speed-date chat", elem_classes=["lg-section-title"]) | |
| speed_chat = messages_chatbot( | |
| value=[{"role": "assistant", "content": "Sign in, then create or join the speed-date post."}], | |
| height=420, | |
| label="Room conversation", | |
| elem_classes=["lg-chat"], | |
| ) | |
| speed_message = gr.Textbox( | |
| label="Message", | |
| placeholder="Ask about values, pace, conflict repair, attraction, and lifestyle. Do not share contact details.", | |
| lines=3, | |
| elem_classes=["lg-textarea"], | |
| ) | |
| send_speed = gr.Button("Send Message", variant="primary", elem_classes=["lg-primary"]) | |
| with gr.Group(elem_classes=["lg-panel", "lg-exchange-status"]): | |
| gr.Markdown( | |
| """ | |
| ### Mutual exchange | |
| Opens after the speed-date ends and reveals contact details only after both users submit. | |
| """, | |
| elem_classes=["lg-section-title"], | |
| ) | |
| with gr.Row(): | |
| exchange_name = gr.Textbox(label="Name to share", placeholder="Your preferred name") | |
| exchange_contact = gr.Textbox( | |
| label="Contact after mutual consent", | |
| placeholder="Email, phone, or handle", | |
| ) | |
| exchange_note = gr.Textbox(label="Optional note", lines=3, elem_classes=["lg-textarea"]) | |
| exchange_consent = gr.Checkbox( | |
| label="I consent to share this contact info with the other speed-date participant." | |
| ) | |
| submit_exchange = gr.Button("Submit Mutual Exchange", variant="primary", elem_classes=["lg-primary"]) | |
| exchange_status = gr.Markdown( | |
| "Exchange form locked until a two-person speed-date ends.", | |
| elem_classes=["lg-status-block"], | |
| ) | |
| final_exchange = gr.Markdown("", elem_classes=["lg-status-block"]) | |
| room_auto_refresh = gr.Timer(value=3, active=True) if hasattr(gr, "Timer") else None | |
| with gr.Accordion("Security notes", open=False, elem_classes=["lg-footer"]): | |
| gr.Markdown( | |
| """ | |
| - Exported profile rows are encrypted JSONL. Raw JSONL is not exposed through the user interface. | |
| - Rows are decrypted only inside functions that need them, such as matching, then discarded. | |
| - Set `DATACLAW_PROFILE_KEY` as a Hugging Face secret so Dataclaw can decrypt rows across restarts. | |
| - The production app still uses Flutter, Node/TypeScript, Supabase, Stripe, and pgvector. | |
| - Chat requests to find, identify, or expose a person's private details are refused. | |
| """, | |
| elem_classes=["lg-muted"], | |
| ) | |
| save_basics.click( | |
| save_profile_basics, | |
| inputs=[display_name, age, location, intent, state], | |
| outputs=[state, summary, export_file], | |
| ) | |
| submit.click( | |
| chat_step, | |
| inputs=[message, chatbot, state, auth_state], | |
| outputs=[chatbot, state, progress_label, progress_bar, current_prompt, summary, export_file], | |
| ).then(lambda: "", outputs=message) | |
| message.submit( | |
| chat_step, | |
| inputs=[message, chatbot, state, auth_state], | |
| outputs=[chatbot, state, progress_label, progress_bar, current_prompt, summary, export_file], | |
| ).then(lambda: "", outputs=message) | |
| sample_answer.click( | |
| sample_answer_step, | |
| inputs=[chatbot, state, auth_state], | |
| outputs=[chatbot, state, progress_label, progress_bar, current_prompt, summary, export_file], | |
| ) | |
| privacy_check.click( | |
| privacy_check_step, | |
| inputs=[chatbot, state, auth_state], | |
| outputs=[chatbot, state, progress_label, progress_bar, current_prompt, summary, export_file], | |
| ) | |
| reset.click( | |
| reset_session, | |
| outputs=[chatbot, state, progress_label, progress_bar, current_prompt, summary, export_file], | |
| ) | |
| score_button.click(demo_match_table, inputs=state, outputs=match_preview) | |
| scan_matchmaker.click( | |
| scan_matchmaker_for_ui, | |
| inputs=auth_state, | |
| outputs=[matchmaker_status, room_status, speed_chat, room_timer, exchange_status, final_exchange], | |
| ) | |
| hf_login.click( | |
| login_with_hf_token, | |
| inputs=[hf_token, auth_state], | |
| outputs=[auth_state, auth_status, hf_token], | |
| ) | |
| join_room.click( | |
| join_speed_date, | |
| inputs=auth_state, | |
| outputs=[auth_state, room_status, speed_chat, room_timer, exchange_status, final_exchange], | |
| ) | |
| refresh_room.click( | |
| refresh_speed_date, | |
| inputs=auth_state, | |
| outputs=[room_status, speed_chat, room_timer, exchange_status, final_exchange], | |
| ) | |
| if room_auto_refresh is not None: | |
| room_auto_refresh.tick( | |
| refresh_speed_date, | |
| inputs=auth_state, | |
| outputs=[room_status, speed_chat, room_timer, exchange_status, final_exchange], | |
| ) | |
| send_speed.click( | |
| send_speed_date_message, | |
| inputs=[speed_message, auth_state], | |
| outputs=[room_status, speed_chat, room_timer, exchange_status, final_exchange, speed_message], | |
| ) | |
| speed_message.submit( | |
| send_speed_date_message, | |
| inputs=[speed_message, auth_state], | |
| outputs=[room_status, speed_chat, room_timer, exchange_status, final_exchange, speed_message], | |
| ) | |
| submit_exchange.click( | |
| submit_exchange_form, | |
| inputs=[exchange_name, exchange_contact, exchange_note, exchange_consent, auth_state], | |
| outputs=[exchange_status, final_exchange], | |
| ) | |
| return demo | |
| demo = build_app() | |
| if __name__ == "__main__": | |
| demo.queue(default_concurrency_limit=8).launch( | |
| server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"), | |
| server_port=int(os.getenv("GRADIO_SERVER_PORT", "7860")), | |
| show_error=True, | |
| ) | |