Crownelius commited on
Commit
7de48b6
·
verified ·
1 Parent(s): 910f36a

Rebrand OpenDateBase to loveGPT

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +95 -95
  2. app.py +9 -8
  3. artifacts/prompts/openclaw.md +42 -42
  4. artifacts/schemas/openclaw-response.schema.json +78 -78
  5. backend/dist/app.js +34 -0
  6. backend/dist/config/env.js +52 -0
  7. backend/dist/config/supabase.js +17 -0
  8. backend/dist/middleware/auth.js +24 -0
  9. backend/dist/middleware/errors.js +37 -0
  10. backend/dist/routes/health.js +12 -0
  11. backend/dist/routes/matches.js +32 -0
  12. backend/dist/routes/messages.js +37 -0
  13. backend/dist/routes/openClaw.js +52 -0
  14. backend/dist/routes/payments.js +92 -0
  15. backend/dist/routes/profiles.js +59 -0
  16. backend/dist/server.js +8 -0
  17. backend/dist/services/embeddingService.js +29 -0
  18. backend/dist/services/matchService.js +144 -0
  19. backend/dist/services/messageService.js +89 -0
  20. backend/dist/services/openClawService.js +210 -0
  21. backend/dist/services/profileService.js +164 -0
  22. backend/dist/types/domain.js +2 -0
  23. backend/dist/utils/httpError.js +20 -0
  24. backend/dist/utils/privacyGuard.js +22 -0
  25. backend/dist/utils/questions.js +45 -0
  26. backend/package-lock.json +0 -0
  27. backend/package.json +33 -33
  28. backend/src/prompts/openclaw.md +42 -42
  29. backend/src/routes/health.ts +11 -11
  30. backend/src/routes/payments.ts +104 -104
  31. backend/src/server.ts +8 -8
  32. backend/src/utils/privacyGuard.ts +21 -21
  33. docs/huggingface-space.md +154 -154
  34. frontend/.gitignore +45 -0
  35. frontend/.metadata +36 -0
  36. frontend/README.md +1 -1
  37. frontend/android/.gitignore +14 -0
  38. frontend/android/app/build.gradle.kts +45 -0
  39. frontend/android/app/src/debug/AndroidManifest.xml +7 -0
  40. frontend/android/app/src/main/AndroidManifest.xml +45 -0
  41. frontend/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +54 -0
  42. frontend/android/app/src/main/kotlin/com/example/lovegpt/MainActivity.kt +5 -0
  43. frontend/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  44. frontend/android/app/src/main/res/drawable/launch_background.xml +12 -0
  45. frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  46. frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  47. frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  48. frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  49. frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  50. frontend/android/app/src/main/res/values-night/styles.xml +18 -0
README.md CHANGED
@@ -1,95 +1,95 @@
1
- ---
2
- title: OpenDateBase
3
- colorFrom: pink
4
- colorTo: yellow
5
- sdk: gradio
6
- sdk_version: 6.16.0
7
- python_version: 3.11
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- ---
12
-
13
- # OpenDateBase
14
-
15
- Premium AI dating platform powered by OpenClaw.
16
-
17
- ## Structure
18
-
19
- - `frontend/` Flutter app for web, Android, and iOS.
20
- - `backend/` Node.js, TypeScript, Express, Supabase, Stripe, AI, embeddings.
21
- - `backend/supabase/migrations/` PostgreSQL schema, RLS, pgvector matching RPC.
22
- - `shared/` full compatibility questionnaire and shared TypeScript contracts.
23
- - `artifacts/` OpenClaw prompt and response schema.
24
- - `docs/` setup, API, and deployment notes.
25
- - `app.py` Gradio/Hugging Face Space adapter.
26
-
27
- ## Gradio Space
28
-
29
- ```powershell
30
- python -m pip install -r requirements.txt
31
- python app.py
32
- ```
33
-
34
- The Space adapter runs session-local by default and does not require Supabase or Stripe secrets just to launch. Profile exports are encrypted JSONL rows, and questionnaire turns are logged into an encrypted matchmaker-only table. Set `DATACLAW_PROFILE_KEY` and `MATCHMAKER_TABLE_KEY` as Hugging Face secrets when rows must remain decryptable by Dataclaw and the matchmaker across restarts.
35
-
36
- Optional local Phi OpenClaw runtime:
37
-
38
- ```powershell
39
- llama-server `
40
- -hf ysn-rfd/Phi-4-mini-instruct-GGUF:Q4_0 `
41
- --alias microsoft/Phi-4-mini-instruct `
42
- --jinja `
43
- --host 127.0.0.1 `
44
- --port 8081 `
45
- -c 32768
46
-
47
- $env:OPENCLAW_PROVIDER="local"
48
- $env:OPENCLAW_LOCAL_MODEL="microsoft/Phi-4-mini-instruct"
49
- $env:OPENCLAW_LOCAL_BASE_URL="http://127.0.0.1:8081/v1"
50
- python app.py
51
- ```
52
-
53
- Dataclaw-only decrypt:
54
-
55
- ```powershell
56
- $env:DATACLAW_PROFILE_KEY="your-secret"
57
- $env:DATACLAW_ALLOW_PROFILE_DECRYPT="1"
58
- python tools/dataclaw_profile_vault.py encrypted-profile.jsonl.enc --output decrypted.jsonl --i-understand-raw-profile-output
59
- ```
60
-
61
- Do not expose decrypted JSONL through the Space UI or logs.
62
-
63
- ## Backend
64
-
65
- ```powershell
66
- cd backend
67
- Copy-Item .env.example .env
68
- npm install
69
- npm run typecheck
70
- npm run build
71
- npm run dev
72
- ```
73
-
74
- The default backend AI provider is `local` using `microsoft/Phi-4-mini-instruct` through `LOCAL_AI_BASE_URL=http://127.0.0.1:8081/v1`.
75
-
76
- ## Frontend
77
-
78
- Flutter is required locally.
79
-
80
- ```powershell
81
- cd frontend
82
- flutter pub get
83
- flutter run `
84
- --dart-define=SUPABASE_URL=https://your-project.supabase.co `
85
- --dart-define=SUPABASE_ANON_KEY=your-anon-key `
86
- --dart-define=BACKEND_URL=http://localhost:8080/api
87
- ```
88
-
89
- If platform folders are not present yet, run this from `frontend/` once:
90
-
91
- ```powershell
92
- flutter create --platforms=web,android,ios .
93
- ```
94
-
95
- Then keep the existing `lib/`, `pubspec.yaml`, and assets.
 
1
+ ---
2
+ title: loveGPT
3
+ colorFrom: pink
4
+ colorTo: yellow
5
+ sdk: gradio
6
+ sdk_version: 6.16.0
7
+ python_version: 3.11
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # loveGPT
14
+
15
+ Premium AI dating platform powered by OpenClaw.
16
+
17
+ ## Structure
18
+
19
+ - `frontend/` Flutter app for web, Android, and iOS.
20
+ - `backend/` Node.js, TypeScript, Express, Supabase, Stripe, AI, embeddings.
21
+ - `backend/supabase/migrations/` PostgreSQL schema, RLS, pgvector matching RPC.
22
+ - `shared/` full compatibility questionnaire and shared TypeScript contracts.
23
+ - `artifacts/` OpenClaw prompt and response schema.
24
+ - `docs/` setup, API, and deployment notes.
25
+ - `app.py` Gradio/Hugging Face Space adapter.
26
+
27
+ ## Gradio Space
28
+
29
+ ```powershell
30
+ python -m pip install -r requirements.txt
31
+ python app.py
32
+ ```
33
+
34
+ The Space adapter runs session-local by default and does not require Supabase or Stripe secrets just to launch. Profile exports are encrypted JSONL rows, and questionnaire turns are logged into an encrypted matchmaker-only table. Set `DATACLAW_PROFILE_KEY` and `MATCHMAKER_TABLE_KEY` as Hugging Face secrets when rows must remain decryptable by Dataclaw and the matchmaker across restarts.
35
+
36
+ Optional local Phi OpenClaw runtime:
37
+
38
+ ```powershell
39
+ llama-server `
40
+ -hf ysn-rfd/Phi-4-mini-instruct-GGUF:Q4_0 `
41
+ --alias microsoft/Phi-4-mini-instruct `
42
+ --jinja `
43
+ --host 127.0.0.1 `
44
+ --port 8081 `
45
+ -c 32768
46
+
47
+ $env:OPENCLAW_PROVIDER="local"
48
+ $env:OPENCLAW_LOCAL_MODEL="microsoft/Phi-4-mini-instruct"
49
+ $env:OPENCLAW_LOCAL_BASE_URL="http://127.0.0.1:8081/v1"
50
+ python app.py
51
+ ```
52
+
53
+ Dataclaw-only decrypt:
54
+
55
+ ```powershell
56
+ $env:DATACLAW_PROFILE_KEY="your-secret"
57
+ $env:DATACLAW_ALLOW_PROFILE_DECRYPT="1"
58
+ python tools/dataclaw_profile_vault.py encrypted-profile.jsonl.enc --output decrypted.jsonl --i-understand-raw-profile-output
59
+ ```
60
+
61
+ Do not expose decrypted JSONL through the Space UI or logs.
62
+
63
+ ## Backend
64
+
65
+ ```powershell
66
+ cd backend
67
+ Copy-Item .env.example .env
68
+ npm install
69
+ npm run typecheck
70
+ npm run build
71
+ npm run dev
72
+ ```
73
+
74
+ The default backend AI provider is `local` using `microsoft/Phi-4-mini-instruct` through `LOCAL_AI_BASE_URL=http://127.0.0.1:8081/v1`.
75
+
76
+ ## Frontend
77
+
78
+ Flutter is required locally.
79
+
80
+ ```powershell
81
+ cd frontend
82
+ flutter pub get
83
+ flutter run `
84
+ --dart-define=SUPABASE_URL=https://your-project.supabase.co `
85
+ --dart-define=SUPABASE_ANON_KEY=your-anon-key `
86
+ --dart-define=BACKEND_URL=http://localhost:8080/api
87
+ ```
88
+
89
+ If platform folders are not present yet, run this from `frontend/` once:
90
+
91
+ ```powershell
92
+ flutter create --platforms=web,android,ios .
93
+ ```
94
+
95
+ Then keep the existing `lib/`, `pubspec.yaml`, and assets.
app.py CHANGED
@@ -26,9 +26,10 @@ from huggingface_hub import HfApi
26
  ROOT = Path(__file__).resolve().parent
27
  QUESTIONS_PATH = ROOT / "shared" / "questions.json"
28
  EXPECTED_QUESTIONS = int(os.getenv("OPENDB_EXPECTED_QUESTIONS", "0"))
 
29
  VAULT_SCHEMA = "opendatebase.profile.vault.v1"
30
  MATCHMAKER_TABLE_SCHEMA = "opendatebase.matchmaker.table.v1"
31
- MATCHMAKER_TABLE_PATH = Path(os.getenv("MATCHMAKER_TABLE_PATH", str(Path(tempfile.gettempdir()) / "opendatebase-matchmaker-table.jsonl.enc")))
32
  SPEED_DATE_SECONDS = int(os.getenv("SPEED_DATE_SECONDS", "1200"))
33
  OPENCLAW_PROVIDER = os.getenv("OPENCLAW_PROVIDER", os.getenv("AI_PROVIDER", "deterministic")).strip().lower()
34
  OPENCLAW_LOCAL_BASE_URL = os.getenv("OPENCLAW_LOCAL_BASE_URL", os.getenv("LOCAL_AI_BASE_URL", "http://127.0.0.1:8081/v1")).rstrip("/")
@@ -172,7 +173,7 @@ def encrypt_profile_payload(payload: dict[str, Any]) -> dict[str, Any]:
172
  nonce = secrets.token_bytes(12)
173
  aad = {
174
  "schema": VAULT_SCHEMA,
175
- "source": payload.get("source", "opendatebase-gradio-space"),
176
  "answerCount": len(payload.get("answers", {})),
177
  "profileComplete": bool(payload.get("profileComplete")),
178
  }
@@ -275,7 +276,7 @@ def is_privacy_lookup_request(value: str) -> bool:
275
  def privacy_refusal() -> str:
276
  return (
277
  "I cannot help find, identify, or expose personal details about another person. "
278
- "OpenDateBase can only use profile information for consent-based compatibility work. "
279
  "Answer the current profile question about your own preferences and boundaries instead."
280
  )
281
 
@@ -790,7 +791,7 @@ def export_payload(state: dict[str, Any]) -> dict[str, Any]:
790
  state = state or new_state()
791
  answers = state.get("answers", {})
792
  return {
793
- "source": "opendatebase-gradio-space",
794
  "profileId": state.get("profile_id", ""),
795
  "exportedAt": now_iso(),
796
  "profile": state.get("profile", {}),
@@ -812,7 +813,7 @@ def export_profile_file(state: dict[str, Any]) -> str:
812
  mode="w",
813
  encoding="utf-8",
814
  suffix=".jsonl.enc",
815
- prefix="opendatebase-profile-vault-",
816
  delete=False,
817
  )
818
  with handle:
@@ -999,7 +1000,7 @@ def strict_matchmaker_judgment(a: dict[str, Any], b: dict[str, Any]) -> dict[str
999
 
1000
  def profile_display(payload: dict[str, Any]) -> str:
1001
  profile = payload.get("profile", {})
1002
- return normalize_text(str(profile.get("display_name") or payload.get("username") or "OpenDateBase user"))[:80]
1003
 
1004
 
1005
  def pair_key(a_user_id: str, b_user_id: str) -> str:
@@ -1462,7 +1463,7 @@ def submit_exchange_form(display_name: str, contact: str, note: str, consent: bo
1462
 
1463
  def build_app() -> gr.Blocks:
1464
  with gr.Blocks(
1465
- title="OpenDateBase",
1466
  theme=gr.themes.Soft(primary_hue="rose", secondary_hue="amber", neutral_hue="slate"),
1467
  css="""
1468
  .odp-hero {max-width: 980px}
@@ -1475,7 +1476,7 @@ def build_app() -> gr.Blocks:
1475
 
1476
  gr.Markdown(
1477
  """
1478
- # OpenDateBase
1479
  Premium AI dating profile builder powered by OpenClaw.
1480
 
1481
  This Hugging Face Space is a session-local Gradio version of the profile workflow.
 
26
  ROOT = Path(__file__).resolve().parent
27
  QUESTIONS_PATH = ROOT / "shared" / "questions.json"
28
  EXPECTED_QUESTIONS = int(os.getenv("OPENDB_EXPECTED_QUESTIONS", "0"))
29
+ BRAND_NAME = "loveGPT"
30
  VAULT_SCHEMA = "opendatebase.profile.vault.v1"
31
  MATCHMAKER_TABLE_SCHEMA = "opendatebase.matchmaker.table.v1"
32
+ MATCHMAKER_TABLE_PATH = Path(os.getenv("MATCHMAKER_TABLE_PATH", str(Path(tempfile.gettempdir()) / "lovegpt-matchmaker-table.jsonl.enc")))
33
  SPEED_DATE_SECONDS = int(os.getenv("SPEED_DATE_SECONDS", "1200"))
34
  OPENCLAW_PROVIDER = os.getenv("OPENCLAW_PROVIDER", os.getenv("AI_PROVIDER", "deterministic")).strip().lower()
35
  OPENCLAW_LOCAL_BASE_URL = os.getenv("OPENCLAW_LOCAL_BASE_URL", os.getenv("LOCAL_AI_BASE_URL", "http://127.0.0.1:8081/v1")).rstrip("/")
 
173
  nonce = secrets.token_bytes(12)
174
  aad = {
175
  "schema": VAULT_SCHEMA,
176
+ "source": payload.get("source", "lovegpt-gradio-space"),
177
  "answerCount": len(payload.get("answers", {})),
178
  "profileComplete": bool(payload.get("profileComplete")),
179
  }
 
276
  def privacy_refusal() -> str:
277
  return (
278
  "I cannot help find, identify, or expose personal details about another person. "
279
+ "loveGPT can only use profile information for consent-based compatibility work. "
280
  "Answer the current profile question about your own preferences and boundaries instead."
281
  )
282
 
 
791
  state = state or new_state()
792
  answers = state.get("answers", {})
793
  return {
794
+ "source": "lovegpt-gradio-space",
795
  "profileId": state.get("profile_id", ""),
796
  "exportedAt": now_iso(),
797
  "profile": state.get("profile", {}),
 
813
  mode="w",
814
  encoding="utf-8",
815
  suffix=".jsonl.enc",
816
+ prefix="lovegpt-profile-vault-",
817
  delete=False,
818
  )
819
  with handle:
 
1000
 
1001
  def profile_display(payload: dict[str, Any]) -> str:
1002
  profile = payload.get("profile", {})
1003
+ return normalize_text(str(profile.get("display_name") or payload.get("username") or "loveGPT user"))[:80]
1004
 
1005
 
1006
  def pair_key(a_user_id: str, b_user_id: str) -> str:
 
1463
 
1464
  def build_app() -> gr.Blocks:
1465
  with gr.Blocks(
1466
+ title=BRAND_NAME,
1467
  theme=gr.themes.Soft(primary_hue="rose", secondary_hue="amber", neutral_hue="slate"),
1468
  css="""
1469
  .odp-hero {max-width: 980px}
 
1476
 
1477
  gr.Markdown(
1478
  """
1479
+ # loveGPT
1480
  Premium AI dating profile builder powered by OpenClaw.
1481
 
1482
  This Hugging Face Space is a session-local Gradio version of the profile workflow.
artifacts/prompts/openclaw.md CHANGED
@@ -1,42 +1,42 @@
1
- # OpenClaw System Prompt
2
-
3
- You are OpenClaw, the warm, direct, psychologically precise AI matchmaker inside OpenDateBase. Your job is to guide a dating user through the complete 40-question compatibility profile in a natural conversation.
4
-
5
- Core behavior:
6
- - Ask one meaningful question at a time.
7
- - Reflect briefly before moving on.
8
- - Ask follow-up questions when the answer is vague, performative, contradictory, unsafe, or too thin to support matching.
9
- - Treat disgust triggers as important compatibility data, not as shameful confessions.
10
- - Never moralize ordinary preferences. Do challenge cruelty, coercion, dehumanization, or unsafe intent.
11
- - Keep the conversation grounded, elegant, and human. Do not sound like a quiz form.
12
- - Do not promise a perfect partner. Promise careful matching and honest signal.
13
- - Never help a user find, identify, dox, locate, or expose private details about another person. Refuse those requests and redirect to consent-based compatibility profiling.
14
-
15
- Profile priorities:
16
- 1. Long-term values and future direction.
17
- 2. Emotional safety, attachment, and conflict repair.
18
- 3. Captivating traits that create admiration and desire.
19
- 4. Disgust triggers, repulsions, hygiene needs, moral aversions, sensory aversions, and intimacy boundaries.
20
- 5. Lifestyle practicality.
21
- 6. Readiness, boundaries, and consent.
22
- 7. Respectful body-type self-description, desired body-type range, and physical attraction flexibility.
23
-
24
- Response contract:
25
- Return a single JSON object with no markdown.
26
-
27
- Schema:
28
- {
29
- "assistant_text": "Message shown to the user.",
30
- "current_question_id": "q1",
31
- "captured_answer": {
32
- "questionId": "q1",
33
- "answer": "The user's answer, normalized without changing meaning.",
34
- "followup": "Optional concise follow-up detail.",
35
- "dealbreakerSeverity": "low | medium | high"
36
- },
37
- "next_question_id": "q2",
38
- "followup_needed": false,
39
- "profile_complete": false
40
- }
41
-
42
- If the user has not answered the current question, set captured_answer to null. If all 40 answers are complete, set profile_complete to true and produce a concise completion message.
 
1
+ # OpenClaw System Prompt
2
+
3
+ You are OpenClaw, the warm, direct, psychologically precise AI matchmaker inside loveGPT. Your job is to guide a dating user through the complete 40-question compatibility profile in a natural conversation.
4
+
5
+ Core behavior:
6
+ - Ask one meaningful question at a time.
7
+ - Reflect briefly before moving on.
8
+ - Ask follow-up questions when the answer is vague, performative, contradictory, unsafe, or too thin to support matching.
9
+ - Treat disgust triggers as important compatibility data, not as shameful confessions.
10
+ - Never moralize ordinary preferences. Do challenge cruelty, coercion, dehumanization, or unsafe intent.
11
+ - Keep the conversation grounded, elegant, and human. Do not sound like a quiz form.
12
+ - Do not promise a perfect partner. Promise careful matching and honest signal.
13
+ - Never help a user find, identify, dox, locate, or expose private details about another person. Refuse those requests and redirect to consent-based compatibility profiling.
14
+
15
+ Profile priorities:
16
+ 1. Long-term values and future direction.
17
+ 2. Emotional safety, attachment, and conflict repair.
18
+ 3. Captivating traits that create admiration and desire.
19
+ 4. Disgust triggers, repulsions, hygiene needs, moral aversions, sensory aversions, and intimacy boundaries.
20
+ 5. Lifestyle practicality.
21
+ 6. Readiness, boundaries, and consent.
22
+ 7. Respectful body-type self-description, desired body-type range, and physical attraction flexibility.
23
+
24
+ Response contract:
25
+ Return a single JSON object with no markdown.
26
+
27
+ Schema:
28
+ {
29
+ "assistant_text": "Message shown to the user.",
30
+ "current_question_id": "q1",
31
+ "captured_answer": {
32
+ "questionId": "q1",
33
+ "answer": "The user's answer, normalized without changing meaning.",
34
+ "followup": "Optional concise follow-up detail.",
35
+ "dealbreakerSeverity": "low | medium | high"
36
+ },
37
+ "next_question_id": "q2",
38
+ "followup_needed": false,
39
+ "profile_complete": false
40
+ }
41
+
42
+ If the user has not answered the current question, set captured_answer to null. If all 40 answers are complete, set profile_complete to true and produce a concise completion message.
artifacts/schemas/openclaw-response.schema.json CHANGED
@@ -1,78 +1,78 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://opendatebase.com/schemas/openclaw-response.schema.json",
4
- "type": "object",
5
- "required": [
6
- "assistant_text",
7
- "current_question_id",
8
- "captured_answer",
9
- "next_question_id",
10
- "followup_needed",
11
- "profile_complete"
12
- ],
13
- "properties": {
14
- "assistant_text": {
15
- "type": "string",
16
- "minLength": 1
17
- },
18
- "current_question_id": {
19
- "type": "string",
20
- "pattern": "^q([1-9]|[1-3][0-9]|40)$"
21
- },
22
- "captured_answer": {
23
- "anyOf": [
24
- {
25
- "type": "null"
26
- },
27
- {
28
- "type": "object",
29
- "required": ["questionId", "answer"],
30
- "properties": {
31
- "questionId": {
32
- "type": "string",
33
- "pattern": "^q([1-9]|[1-3][0-9]|40)$"
34
- },
35
- "answer": {
36
- "anyOf": [
37
- {
38
- "type": "string"
39
- },
40
- {
41
- "type": "array",
42
- "items": {
43
- "type": "string"
44
- }
45
- }
46
- ]
47
- },
48
- "followup": {
49
- "type": "string"
50
- },
51
- "dealbreakerSeverity": {
52
- "enum": ["low", "medium", "high"]
53
- }
54
- },
55
- "additionalProperties": false
56
- }
57
- ]
58
- },
59
- "next_question_id": {
60
- "anyOf": [
61
- {
62
- "type": "null"
63
- },
64
- {
65
- "type": "string",
66
- "pattern": "^q([1-9]|[1-3][0-9]|40)$"
67
- }
68
- ]
69
- },
70
- "followup_needed": {
71
- "type": "boolean"
72
- },
73
- "profile_complete": {
74
- "type": "boolean"
75
- }
76
- },
77
- "additionalProperties": false
78
- }
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://lovegpt.app/schemas/openclaw-response.schema.json",
4
+ "type": "object",
5
+ "required": [
6
+ "assistant_text",
7
+ "current_question_id",
8
+ "captured_answer",
9
+ "next_question_id",
10
+ "followup_needed",
11
+ "profile_complete"
12
+ ],
13
+ "properties": {
14
+ "assistant_text": {
15
+ "type": "string",
16
+ "minLength": 1
17
+ },
18
+ "current_question_id": {
19
+ "type": "string",
20
+ "pattern": "^q([1-9]|[1-3][0-9]|40)$"
21
+ },
22
+ "captured_answer": {
23
+ "anyOf": [
24
+ {
25
+ "type": "null"
26
+ },
27
+ {
28
+ "type": "object",
29
+ "required": ["questionId", "answer"],
30
+ "properties": {
31
+ "questionId": {
32
+ "type": "string",
33
+ "pattern": "^q([1-9]|[1-3][0-9]|40)$"
34
+ },
35
+ "answer": {
36
+ "anyOf": [
37
+ {
38
+ "type": "string"
39
+ },
40
+ {
41
+ "type": "array",
42
+ "items": {
43
+ "type": "string"
44
+ }
45
+ }
46
+ ]
47
+ },
48
+ "followup": {
49
+ "type": "string"
50
+ },
51
+ "dealbreakerSeverity": {
52
+ "enum": ["low", "medium", "high"]
53
+ }
54
+ },
55
+ "additionalProperties": false
56
+ }
57
+ ]
58
+ },
59
+ "next_question_id": {
60
+ "anyOf": [
61
+ {
62
+ "type": "null"
63
+ },
64
+ {
65
+ "type": "string",
66
+ "pattern": "^q([1-9]|[1-3][0-9]|40)$"
67
+ }
68
+ ]
69
+ },
70
+ "followup_needed": {
71
+ "type": "boolean"
72
+ },
73
+ "profile_complete": {
74
+ "type": "boolean"
75
+ }
76
+ },
77
+ "additionalProperties": false
78
+ }
backend/dist/app.js ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createApp = createApp;
7
+ const cors_1 = __importDefault(require("cors"));
8
+ const express_1 = __importDefault(require("express"));
9
+ const helmet_1 = __importDefault(require("helmet"));
10
+ const morgan_1 = __importDefault(require("morgan"));
11
+ const health_1 = require("./routes/health");
12
+ const matches_1 = require("./routes/matches");
13
+ const messages_1 = require("./routes/messages");
14
+ const openClaw_1 = require("./routes/openClaw");
15
+ const payments_1 = require("./routes/payments");
16
+ const profiles_1 = require("./routes/profiles");
17
+ const errors_1 = require("./middleware/errors");
18
+ function createApp() {
19
+ const app = (0, express_1.default)();
20
+ app.use((0, helmet_1.default)());
21
+ app.use((0, cors_1.default)());
22
+ app.use((0, morgan_1.default)("tiny"));
23
+ app.use("/api/payments/webhook", express_1.default.raw({ type: "application/json" }), payments_1.paymentWebhookRouter);
24
+ app.use(express_1.default.json({ limit: "1mb" }));
25
+ app.use("/health", health_1.healthRouter);
26
+ app.use("/api/openclaw", openClaw_1.openClawRouter);
27
+ app.use("/api/profiles", profiles_1.profilesRouter);
28
+ app.use("/api/payments", payments_1.paymentsRouter);
29
+ app.use("/api/matches", matches_1.matchesRouter);
30
+ app.use("/api/messages", messages_1.messagesRouter);
31
+ app.use(errors_1.notFoundHandler);
32
+ app.use(errors_1.errorHandler);
33
+ return app;
34
+ }
backend/dist/config/env.js ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isProduction = exports.env = void 0;
7
+ exports.assertAiConfigured = assertAiConfigured;
8
+ exports.assertEmbeddingsConfigured = assertEmbeddingsConfigured;
9
+ const dotenv_1 = __importDefault(require("dotenv"));
10
+ const zod_1 = require("zod");
11
+ dotenv_1.default.config();
12
+ const envSchema = zod_1.z.object({
13
+ NODE_ENV: zod_1.z.enum(["development", "test", "production"]).default("development"),
14
+ PORT: zod_1.z.coerce.number().int().positive().default(8080),
15
+ PUBLIC_APP_URL: zod_1.z.string().url().default("http://localhost:8080"),
16
+ FRONTEND_SUCCESS_URL: zod_1.z.string().url().default("http://localhost:3000/#/payment/success"),
17
+ FRONTEND_CANCEL_URL: zod_1.z.string().url().default("http://localhost:3000/#/payment/cancel"),
18
+ SUPABASE_URL: zod_1.z.string().url(),
19
+ SUPABASE_ANON_KEY: zod_1.z.string().min(1),
20
+ SUPABASE_SERVICE_ROLE_KEY: zod_1.z.string().min(1),
21
+ AI_PROVIDER: zod_1.z.enum(["anthropic", "grok", "local"]).default("local"),
22
+ AI_MODEL: zod_1.z.string().min(1).default("microsoft/Phi-4-mini-instruct"),
23
+ ANTHROPIC_API_KEY: zod_1.z.string().optional(),
24
+ GROK_API_KEY: zod_1.z.string().optional(),
25
+ GROK_BASE_URL: zod_1.z.string().url().default("https://api.x.ai/v1"),
26
+ LOCAL_AI_BASE_URL: zod_1.z.string().url().default("http://127.0.0.1:8081/v1"),
27
+ LOCAL_AI_API_KEY: zod_1.z.string().optional(),
28
+ EMBEDDING_BASE_URL: zod_1.z.string().url().default("https://api.openai.com/v1"),
29
+ EMBEDDING_API_KEY: zod_1.z.string().optional(),
30
+ EMBEDDING_MODEL: zod_1.z.string().min(1).default("text-embedding-3-small"),
31
+ EMBEDDING_DIMENSIONS: zod_1.z.coerce.number().int().positive().default(1536),
32
+ STRIPE_SECRET_KEY: zod_1.z.string().min(1),
33
+ STRIPE_WEBHOOK_SECRET: zod_1.z.string().min(1),
34
+ STRIPE_PRICE_ID: zod_1.z.string().optional(),
35
+ ENTRY_FEE_CENTS: zod_1.z.coerce.number().int().positive().default(2400),
36
+ ENTRY_FEE_CURRENCY: zod_1.z.string().min(3).max(3).default("usd")
37
+ });
38
+ exports.env = envSchema.parse(process.env);
39
+ exports.isProduction = exports.env.NODE_ENV === "production";
40
+ function assertAiConfigured() {
41
+ if (exports.env.AI_PROVIDER === "anthropic" && !exports.env.ANTHROPIC_API_KEY) {
42
+ throw new Error("ANTHROPIC_API_KEY is required when AI_PROVIDER=anthropic");
43
+ }
44
+ if (exports.env.AI_PROVIDER === "grok" && !exports.env.GROK_API_KEY) {
45
+ throw new Error("GROK_API_KEY is required when AI_PROVIDER=grok");
46
+ }
47
+ }
48
+ function assertEmbeddingsConfigured() {
49
+ if (!exports.env.EMBEDDING_API_KEY) {
50
+ throw new Error("EMBEDDING_API_KEY is required to complete profiles and curate matches");
51
+ }
52
+ }
backend/dist/config/supabase.js ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.supabasePublic = exports.supabaseAdmin = void 0;
4
+ const supabase_js_1 = require("@supabase/supabase-js");
5
+ const env_1 = require("./env");
6
+ exports.supabaseAdmin = (0, supabase_js_1.createClient)(env_1.env.SUPABASE_URL, env_1.env.SUPABASE_SERVICE_ROLE_KEY, {
7
+ auth: {
8
+ autoRefreshToken: false,
9
+ persistSession: false
10
+ }
11
+ });
12
+ exports.supabasePublic = (0, supabase_js_1.createClient)(env_1.env.SUPABASE_URL, env_1.env.SUPABASE_ANON_KEY, {
13
+ auth: {
14
+ autoRefreshToken: false,
15
+ persistSession: false
16
+ }
17
+ });
backend/dist/middleware/auth.js ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireAuth = requireAuth;
4
+ const supabase_1 = require("../config/supabase");
5
+ const httpError_1 = require("../utils/httpError");
6
+ async function requireAuth(req, _res, next) {
7
+ try {
8
+ const header = req.header("authorization") ?? "";
9
+ const [scheme, token] = header.split(" ");
10
+ if (scheme?.toLowerCase() !== "bearer" || !token) {
11
+ throw new httpError_1.HttpError(401, "auth_missing", "A Supabase bearer token is required");
12
+ }
13
+ const { data, error } = await supabase_1.supabaseAdmin.auth.getUser(token);
14
+ if (error || !data.user) {
15
+ throw new httpError_1.HttpError(401, "auth_invalid", "The Supabase bearer token is invalid");
16
+ }
17
+ req.authUser = data.user;
18
+ req.accessToken = token;
19
+ next();
20
+ }
21
+ catch (error) {
22
+ next(error);
23
+ }
24
+ }
backend/dist/middleware/errors.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.notFoundHandler = notFoundHandler;
4
+ exports.errorHandler = errorHandler;
5
+ const zod_1 = require("zod");
6
+ const env_1 = require("../config/env");
7
+ const httpError_1 = require("../utils/httpError");
8
+ function notFoundHandler(req, _res, next) {
9
+ next(new httpError_1.HttpError(404, "not_found", `Route not found: ${req.method} ${req.path}`));
10
+ }
11
+ function errorHandler(error, _req, res, _next) {
12
+ if (error instanceof zod_1.ZodError) {
13
+ res.status(400).json({
14
+ error: {
15
+ code: "validation_error",
16
+ message: "Request validation failed",
17
+ issues: error.issues
18
+ }
19
+ });
20
+ return;
21
+ }
22
+ if (error instanceof httpError_1.HttpError) {
23
+ res.status(error.status).json({
24
+ error: {
25
+ code: error.code,
26
+ message: error.message
27
+ }
28
+ });
29
+ return;
30
+ }
31
+ res.status(500).json({
32
+ error: {
33
+ code: "internal_error",
34
+ message: env_1.isProduction ? "Internal server error" : (0, httpError_1.getErrorMessage)(error)
35
+ }
36
+ });
37
+ }
backend/dist/routes/health.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.healthRouter = void 0;
4
+ const express_1 = require("express");
5
+ exports.healthRouter = (0, express_1.Router)();
6
+ exports.healthRouter.get("/", (_req, res) => {
7
+ res.json({
8
+ ok: true,
9
+ service: "lovegpt-backend",
10
+ checkedAt: new Date().toISOString()
11
+ });
12
+ });
backend/dist/routes/matches.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchesRouter = void 0;
4
+ const express_1 = require("express");
5
+ const auth_1 = require("../middleware/auth");
6
+ const matchService_1 = require("../services/matchService");
7
+ exports.matchesRouter = (0, express_1.Router)();
8
+ exports.matchesRouter.use(auth_1.requireAuth);
9
+ exports.matchesRouter.get("/", async (req, res, next) => {
10
+ try {
11
+ res.json(await (0, matchService_1.listMyMatches)(req.authUser.id));
12
+ }
13
+ catch (error) {
14
+ next(error);
15
+ }
16
+ });
17
+ exports.matchesRouter.post("/curate", async (req, res, next) => {
18
+ try {
19
+ res.json(await (0, matchService_1.curateMatches)(req.authUser.id));
20
+ }
21
+ catch (error) {
22
+ next(error);
23
+ }
24
+ });
25
+ exports.matchesRouter.post("/:matchId/accept", async (req, res, next) => {
26
+ try {
27
+ res.json(await (0, matchService_1.acceptMatch)(req.authUser.id, req.params.matchId));
28
+ }
29
+ catch (error) {
30
+ next(error);
31
+ }
32
+ });
backend/dist/routes/messages.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.messagesRouter = void 0;
4
+ const express_1 = require("express");
5
+ const zod_1 = require("zod");
6
+ const auth_1 = require("../middleware/auth");
7
+ const messageService_1 = require("../services/messageService");
8
+ const sendMessageSchema = zod_1.z.object({
9
+ body: zod_1.z.string().trim().min(1).max(4000)
10
+ });
11
+ exports.messagesRouter = (0, express_1.Router)();
12
+ exports.messagesRouter.use(auth_1.requireAuth);
13
+ exports.messagesRouter.get("/conversations", async (req, res, next) => {
14
+ try {
15
+ res.json(await (0, messageService_1.listConversations)(req.authUser.id));
16
+ }
17
+ catch (error) {
18
+ next(error);
19
+ }
20
+ });
21
+ exports.messagesRouter.get("/conversations/:conversationId/messages", async (req, res, next) => {
22
+ try {
23
+ res.json(await (0, messageService_1.listMessages)(req.authUser.id, req.params.conversationId));
24
+ }
25
+ catch (error) {
26
+ next(error);
27
+ }
28
+ });
29
+ exports.messagesRouter.post("/conversations/:conversationId/messages", async (req, res, next) => {
30
+ try {
31
+ const input = sendMessageSchema.parse(req.body);
32
+ res.json(await (0, messageService_1.sendMessage)(req.authUser.id, req.params.conversationId, input.body));
33
+ }
34
+ catch (error) {
35
+ next(error);
36
+ }
37
+ });
backend/dist/routes/openClaw.js ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.openClawRouter = void 0;
4
+ const express_1 = require("express");
5
+ const zod_1 = require("zod");
6
+ const auth_1 = require("../middleware/auth");
7
+ const openClawService_1 = require("../services/openClawService");
8
+ const profileService_1 = require("../services/profileService");
9
+ const privacyGuard_1 = require("../utils/privacyGuard");
10
+ const chatSchema = zod_1.z.object({
11
+ currentQuestionId: zod_1.z.string().optional().nullable(),
12
+ messages: zod_1.z.array(zod_1.z.object({
13
+ role: zod_1.z.enum(["user", "assistant"]),
14
+ content: zod_1.z.string().min(1).max(8000)
15
+ })).min(1).max(40)
16
+ });
17
+ exports.openClawRouter = (0, express_1.Router)();
18
+ exports.openClawRouter.post("/chat", auth_1.requireAuth, async (req, res, next) => {
19
+ try {
20
+ const userId = req.authUser.id;
21
+ const input = chatSchema.parse(req.body);
22
+ const latestUserMessage = [...input.messages].reverse().find((message) => message.role === "user");
23
+ if (latestUserMessage && (0, privacyGuard_1.isPrivacyLookupRequest)(latestUserMessage.content)) {
24
+ res.json({
25
+ assistant_text: (0, privacyGuard_1.privacyRefusalText)(),
26
+ current_question_id: input.currentQuestionId ?? "q1",
27
+ captured_answer: null,
28
+ next_question_id: input.currentQuestionId ?? "q1",
29
+ followup_needed: true,
30
+ profile_complete: false
31
+ });
32
+ return;
33
+ }
34
+ const profile = await (0, profileService_1.getMyProfile)(userId);
35
+ const reply = await (0, openClawService_1.chatWithOpenClaw)({
36
+ messages: input.messages,
37
+ answers: (profile?.answers ?? {}),
38
+ currentQuestionId: input.currentQuestionId
39
+ });
40
+ if (reply.captured_answer) {
41
+ await (0, profileService_1.saveProfileAnswer)(userId, reply.captured_answer.questionId, {
42
+ answer: reply.captured_answer.answer,
43
+ followup: reply.captured_answer.followup,
44
+ dealbreakerSeverity: reply.captured_answer.dealbreakerSeverity
45
+ });
46
+ }
47
+ res.json(reply);
48
+ }
49
+ catch (error) {
50
+ next(error);
51
+ }
52
+ });
backend/dist/routes/payments.js ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.paymentWebhookRouter = exports.paymentsRouter = void 0;
7
+ const express_1 = require("express");
8
+ const stripe_1 = __importDefault(require("stripe"));
9
+ const env_1 = require("../config/env");
10
+ const supabase_1 = require("../config/supabase");
11
+ const auth_1 = require("../middleware/auth");
12
+ const profileService_1 = require("../services/profileService");
13
+ const httpError_1 = require("../utils/httpError");
14
+ const stripe = new stripe_1.default(env_1.env.STRIPE_SECRET_KEY, {
15
+ typescript: true
16
+ });
17
+ exports.paymentsRouter = (0, express_1.Router)();
18
+ exports.paymentWebhookRouter = (0, express_1.Router)();
19
+ exports.paymentsRouter.use(auth_1.requireAuth);
20
+ exports.paymentsRouter.post("/checkout-session", async (req, res, next) => {
21
+ try {
22
+ const user = req.authUser;
23
+ const session = await stripe.checkout.sessions.create({
24
+ mode: "payment",
25
+ customer_email: user.email ?? undefined,
26
+ client_reference_id: user.id,
27
+ metadata: {
28
+ user_id: user.id,
29
+ product: "lovegpt_entry"
30
+ },
31
+ success_url: env_1.env.FRONTEND_SUCCESS_URL,
32
+ cancel_url: env_1.env.FRONTEND_CANCEL_URL,
33
+ line_items: [
34
+ env_1.env.STRIPE_PRICE_ID
35
+ ? {
36
+ price: env_1.env.STRIPE_PRICE_ID,
37
+ quantity: 1
38
+ }
39
+ : {
40
+ quantity: 1,
41
+ price_data: {
42
+ currency: env_1.env.ENTRY_FEE_CURRENCY,
43
+ unit_amount: env_1.env.ENTRY_FEE_CENTS,
44
+ product_data: {
45
+ name: "loveGPT Entry",
46
+ description: "One-time access to ten curated high-compatibility matches."
47
+ }
48
+ }
49
+ }
50
+ ]
51
+ });
52
+ res.json({
53
+ id: session.id,
54
+ url: session.url
55
+ });
56
+ }
57
+ catch (error) {
58
+ next(error);
59
+ }
60
+ });
61
+ exports.paymentWebhookRouter.post("/", async (req, res, next) => {
62
+ try {
63
+ const signature = req.header("stripe-signature");
64
+ if (!signature) {
65
+ throw new httpError_1.HttpError(400, "stripe_signature_missing", "Missing Stripe signature");
66
+ }
67
+ const event = stripe.webhooks.constructEvent(req.body, signature, env_1.env.STRIPE_WEBHOOK_SECRET);
68
+ if (event.type === "checkout.session.completed") {
69
+ const session = event.data.object;
70
+ const userId = session.metadata?.user_id ?? session.client_reference_id;
71
+ if (!userId) {
72
+ throw new httpError_1.HttpError(400, "stripe_user_missing", "Stripe session does not include a user id");
73
+ }
74
+ await (0, profileService_1.markUserPaid)(userId, typeof session.customer === "string" ? session.customer : session.customer?.id, session.id);
75
+ const { error } = await supabase_1.supabaseAdmin.from("payments").upsert({
76
+ user_id: userId,
77
+ stripe_event_id: event.id,
78
+ stripe_checkout_session_id: session.id,
79
+ amount: session.amount_total ?? env_1.env.ENTRY_FEE_CENTS,
80
+ currency: session.currency ?? env_1.env.ENTRY_FEE_CURRENCY,
81
+ status: "paid"
82
+ }, { onConflict: "stripe_event_id" });
83
+ if (error) {
84
+ throw error;
85
+ }
86
+ }
87
+ res.json({ received: true });
88
+ }
89
+ catch (error) {
90
+ next(error);
91
+ }
92
+ });
backend/dist/routes/profiles.js ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.profilesRouter = void 0;
4
+ const express_1 = require("express");
5
+ const zod_1 = require("zod");
6
+ const auth_1 = require("../middleware/auth");
7
+ const profileService_1 = require("../services/profileService");
8
+ const questions_1 = require("../utils/questions");
9
+ const upsertProfileSchema = zod_1.z.object({
10
+ displayName: zod_1.z.string().trim().min(1).max(80),
11
+ age: zod_1.z.number().int().min(18).max(120),
12
+ location: zod_1.z.string().trim().max(120).optional(),
13
+ gender: zod_1.z.string().trim().max(80).optional(),
14
+ interestedIn: zod_1.z.array(zod_1.z.string().trim().min(1).max(80)).max(12).optional()
15
+ });
16
+ const answerSchema = zod_1.z.object({
17
+ answer: zod_1.z.union([zod_1.z.string().trim().min(1).max(8000), zod_1.z.array(zod_1.z.string().trim().min(1).max(500)).min(1).max(24)]),
18
+ followup: zod_1.z.string().trim().max(4000).optional(),
19
+ dealbreakerSeverity: zod_1.z.enum(["low", "medium", "high"]).optional()
20
+ });
21
+ exports.profilesRouter = (0, express_1.Router)();
22
+ exports.profilesRouter.use(auth_1.requireAuth);
23
+ exports.profilesRouter.get("/questions", (_req, res) => {
24
+ res.json((0, questions_1.getQuestionsDocument)());
25
+ });
26
+ exports.profilesRouter.get("/me", async (req, res, next) => {
27
+ try {
28
+ res.json(await (0, profileService_1.getMyProfile)(req.authUser.id));
29
+ }
30
+ catch (error) {
31
+ next(error);
32
+ }
33
+ });
34
+ exports.profilesRouter.post("/me", async (req, res, next) => {
35
+ try {
36
+ const input = upsertProfileSchema.parse(req.body);
37
+ res.json(await (0, profileService_1.upsertMyProfile)(req.authUser.id, input));
38
+ }
39
+ catch (error) {
40
+ next(error);
41
+ }
42
+ });
43
+ exports.profilesRouter.patch("/answers/:questionId", async (req, res, next) => {
44
+ try {
45
+ const input = answerSchema.parse(req.body);
46
+ res.json(await (0, profileService_1.saveProfileAnswer)(req.authUser.id, req.params.questionId, input));
47
+ }
48
+ catch (error) {
49
+ next(error);
50
+ }
51
+ });
52
+ exports.profilesRouter.post("/complete", async (req, res, next) => {
53
+ try {
54
+ res.json(await (0, profileService_1.completeMyProfile)(req.authUser.id));
55
+ }
56
+ catch (error) {
57
+ next(error);
58
+ }
59
+ });
backend/dist/server.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const env_1 = require("./config/env");
4
+ const app_1 = require("./app");
5
+ const app = (0, app_1.createApp)();
6
+ app.listen(env_1.env.PORT, () => {
7
+ console.log(`loveGPT backend listening on port ${env_1.env.PORT}`);
8
+ });
backend/dist/services/embeddingService.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createEmbedding = createEmbedding;
4
+ const env_1 = require("../config/env");
5
+ async function createEmbedding(input) {
6
+ (0, env_1.assertEmbeddingsConfigured)();
7
+ const response = await fetch(`${env_1.env.EMBEDDING_BASE_URL.replace(/\/$/, "")}/embeddings`, {
8
+ method: "POST",
9
+ headers: {
10
+ Authorization: `Bearer ${env_1.env.EMBEDDING_API_KEY}`,
11
+ "Content-Type": "application/json"
12
+ },
13
+ body: JSON.stringify({
14
+ model: env_1.env.EMBEDDING_MODEL,
15
+ input,
16
+ dimensions: env_1.env.EMBEDDING_DIMENSIONS
17
+ })
18
+ });
19
+ if (!response.ok) {
20
+ const text = await response.text();
21
+ throw new Error(`Embedding provider failed with ${response.status}: ${text}`);
22
+ }
23
+ const payload = (await response.json());
24
+ const embedding = payload.data?.[0]?.embedding;
25
+ if (!embedding || embedding.length !== env_1.env.EMBEDDING_DIMENSIONS) {
26
+ throw new Error(`Embedding provider returned an invalid vector. Expected ${env_1.env.EMBEDDING_DIMENSIONS} dimensions.`);
27
+ }
28
+ return embedding;
29
+ }
backend/dist/services/matchService.js ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listMyMatches = listMyMatches;
4
+ exports.curateMatches = curateMatches;
5
+ exports.acceptMatch = acceptMatch;
6
+ const supabase_1 = require("../config/supabase");
7
+ const httpError_1 = require("../utils/httpError");
8
+ function orderedPair(a, b) {
9
+ return a < b ? [a, b] : [b, a];
10
+ }
11
+ async function listMyMatches(userId) {
12
+ const { data: profile, error: profileError } = await supabase_1.supabaseAdmin
13
+ .from("profiles")
14
+ .select("id")
15
+ .eq("user_id", userId)
16
+ .maybeSingle();
17
+ if (profileError) {
18
+ throw profileError;
19
+ }
20
+ if (!profile) {
21
+ return [];
22
+ }
23
+ const { data, error } = await supabase_1.supabaseAdmin
24
+ .from("matches")
25
+ .select(`
26
+ id,
27
+ compatibility_score,
28
+ shared_traits,
29
+ caution_notes,
30
+ profile_a_accepted,
31
+ profile_b_accepted,
32
+ status,
33
+ profile_a_id,
34
+ profile_b_id,
35
+ profile_a:profiles!matches_profile_a_id_fkey(id, display_name, age, location, narrative_summary),
36
+ profile_b:profiles!matches_profile_b_id_fkey(id, display_name, age, location, narrative_summary)
37
+ `)
38
+ .or(`profile_a_id.eq.${profile.id},profile_b_id.eq.${profile.id}`)
39
+ .order("compatibility_score", { ascending: false });
40
+ if (error) {
41
+ throw error;
42
+ }
43
+ return (data ?? []).map((match) => {
44
+ const isA = match.profile_a_id === profile.id;
45
+ return {
46
+ id: match.id,
47
+ compatibility_score: match.compatibility_score,
48
+ shared_traits: match.shared_traits,
49
+ caution_notes: match.caution_notes,
50
+ status: match.status,
51
+ my_accepted: isA ? match.profile_a_accepted : match.profile_b_accepted,
52
+ their_accepted: isA ? match.profile_b_accepted : match.profile_a_accepted,
53
+ profile: isA ? match.profile_b : match.profile_a
54
+ };
55
+ });
56
+ }
57
+ async function curateMatches(userId) {
58
+ const { data: profile, error: profileError } = await supabase_1.supabaseAdmin
59
+ .from("profiles")
60
+ .select("id, profile_complete, paid_entry")
61
+ .eq("user_id", userId)
62
+ .maybeSingle();
63
+ if (profileError) {
64
+ throw profileError;
65
+ }
66
+ if (!profile) {
67
+ throw new httpError_1.HttpError(409, "profile_required", "Create and complete a profile before curating matches");
68
+ }
69
+ if (!profile.profile_complete) {
70
+ throw new httpError_1.HttpError(422, "profile_incomplete", "Complete the full questionnaire before curating matches");
71
+ }
72
+ if (!profile.paid_entry) {
73
+ throw new httpError_1.HttpError(402, "entry_fee_required", "The one-time entry fee is required before match curation");
74
+ }
75
+ const { data: candidates, error: rpcError } = await supabase_1.supabaseAdmin.rpc("match_candidates", {
76
+ source_profile_id: profile.id,
77
+ match_count: 10
78
+ });
79
+ if (rpcError) {
80
+ throw rpcError;
81
+ }
82
+ for (const candidate of (candidates ?? [])) {
83
+ const [profileAId, profileBId] = orderedPair(profile.id, candidate.candidate_profile_id);
84
+ const { error } = await supabase_1.supabaseAdmin.from("matches").upsert({
85
+ profile_a_id: profileAId,
86
+ profile_b_id: profileBId,
87
+ compatibility_score: candidate.compatibility_score,
88
+ shared_traits: candidate.shared_traits,
89
+ caution_notes: candidate.caution_notes
90
+ }, { onConflict: "profile_a_id,profile_b_id" });
91
+ if (error) {
92
+ throw error;
93
+ }
94
+ }
95
+ return listMyMatches(userId);
96
+ }
97
+ async function acceptMatch(userId, matchId) {
98
+ const { data: profile, error: profileError } = await supabase_1.supabaseAdmin
99
+ .from("profiles")
100
+ .select("id")
101
+ .eq("user_id", userId)
102
+ .maybeSingle();
103
+ if (profileError) {
104
+ throw profileError;
105
+ }
106
+ if (!profile) {
107
+ throw new httpError_1.HttpError(404, "profile_not_found", "Profile not found");
108
+ }
109
+ const { data: match, error: matchError } = await supabase_1.supabaseAdmin
110
+ .from("matches")
111
+ .select("*")
112
+ .eq("id", matchId)
113
+ .or(`profile_a_id.eq.${profile.id},profile_b_id.eq.${profile.id}`)
114
+ .single();
115
+ if (matchError || !match) {
116
+ throw new httpError_1.HttpError(404, "match_not_found", "Match not found");
117
+ }
118
+ const isProfileA = match.profile_a_id === profile.id;
119
+ const profileAAccepted = isProfileA ? true : match.profile_a_accepted;
120
+ const profileBAccepted = isProfileA ? match.profile_b_accepted : true;
121
+ const nextStatus = profileAAccepted && profileBAccepted ? "active" : "accepted";
122
+ const { data, error } = await supabase_1.supabaseAdmin
123
+ .from("matches")
124
+ .update({
125
+ profile_a_accepted: profileAAccepted,
126
+ profile_b_accepted: profileBAccepted,
127
+ status: nextStatus
128
+ })
129
+ .eq("id", matchId)
130
+ .select("*")
131
+ .single();
132
+ if (error) {
133
+ throw error;
134
+ }
135
+ if (nextStatus === "active") {
136
+ const { error: conversationError } = await supabase_1.supabaseAdmin
137
+ .from("conversations")
138
+ .upsert({ match_id: matchId }, { onConflict: "match_id" });
139
+ if (conversationError) {
140
+ throw conversationError;
141
+ }
142
+ }
143
+ return data;
144
+ }
backend/dist/services/messageService.js ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listConversations = listConversations;
4
+ exports.listMessages = listMessages;
5
+ exports.sendMessage = sendMessage;
6
+ const supabase_1 = require("../config/supabase");
7
+ const httpError_1 = require("../utils/httpError");
8
+ async function listConversations(userId) {
9
+ const { data, error } = await supabase_1.supabaseAdmin
10
+ .from("conversations")
11
+ .select(`
12
+ id,
13
+ match_id,
14
+ created_at,
15
+ updated_at,
16
+ match:matches(id, status, compatibility_score, profile_a_id, profile_b_id)
17
+ `)
18
+ .order("updated_at", { ascending: false });
19
+ if (error) {
20
+ throw error;
21
+ }
22
+ const { data: profile, error: profileError } = await supabase_1.supabaseAdmin
23
+ .from("profiles")
24
+ .select("id")
25
+ .eq("user_id", userId)
26
+ .maybeSingle();
27
+ if (profileError) {
28
+ throw profileError;
29
+ }
30
+ return (data ?? []).filter((conversation) => {
31
+ const match = conversation.match;
32
+ return match.profile_a_id === profile?.id || match.profile_b_id === profile?.id;
33
+ });
34
+ }
35
+ async function listMessages(userId, conversationId) {
36
+ await assertConversationParticipant(userId, conversationId);
37
+ const { data, error } = await supabase_1.supabaseAdmin
38
+ .from("messages")
39
+ .select("*")
40
+ .eq("conversation_id", conversationId)
41
+ .order("created_at", { ascending: true });
42
+ if (error) {
43
+ throw error;
44
+ }
45
+ return data ?? [];
46
+ }
47
+ async function sendMessage(userId, conversationId, body) {
48
+ await assertConversationParticipant(userId, conversationId);
49
+ const { data, error } = await supabase_1.supabaseAdmin
50
+ .from("messages")
51
+ .insert({
52
+ conversation_id: conversationId,
53
+ sender_user_id: userId,
54
+ body
55
+ })
56
+ .select("*")
57
+ .single();
58
+ if (error) {
59
+ throw error;
60
+ }
61
+ await supabase_1.supabaseAdmin.from("conversations").update({ updated_at: new Date().toISOString() }).eq("id", conversationId);
62
+ return data;
63
+ }
64
+ async function assertConversationParticipant(userId, conversationId) {
65
+ const { data: profile, error: profileError } = await supabase_1.supabaseAdmin
66
+ .from("profiles")
67
+ .select("id")
68
+ .eq("user_id", userId)
69
+ .maybeSingle();
70
+ if (profileError) {
71
+ throw profileError;
72
+ }
73
+ if (!profile) {
74
+ throw new httpError_1.HttpError(404, "profile_not_found", "Profile not found");
75
+ }
76
+ const { data, error } = await supabase_1.supabaseAdmin
77
+ .from("conversations")
78
+ .select("id, match:matches(profile_a_id, profile_b_id, status)")
79
+ .eq("id", conversationId)
80
+ .maybeSingle();
81
+ if (error) {
82
+ throw error;
83
+ }
84
+ const match = data?.match;
85
+ const isParticipant = match?.profile_a_id === profile.id || match?.profile_b_id === profile.id;
86
+ if (!data || !match || !isParticipant || !["accepted", "active"].includes(match.status ?? "")) {
87
+ throw new httpError_1.HttpError(403, "conversation_forbidden", "Conversation is not available for this user");
88
+ }
89
+ }
backend/dist/services/openClawService.js ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.openClawReplySchema = void 0;
7
+ exports.chatWithOpenClaw = chatWithOpenClaw;
8
+ exports.summarizeProfile = summarizeProfile;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const zod_1 = require("zod");
12
+ const env_1 = require("../config/env");
13
+ const questions_1 = require("../utils/questions");
14
+ const capturedAnswerSchema = zod_1.z.object({
15
+ questionId: zod_1.z.string(),
16
+ answer: zod_1.z.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())]),
17
+ followup: zod_1.z.string().optional(),
18
+ dealbreakerSeverity: zod_1.z.enum(["low", "medium", "high"]).optional()
19
+ });
20
+ exports.openClawReplySchema = zod_1.z.object({
21
+ assistant_text: zod_1.z.string().min(1),
22
+ current_question_id: zod_1.z.string(),
23
+ captured_answer: capturedAnswerSchema.nullable(),
24
+ next_question_id: zod_1.z.string().nullable(),
25
+ followup_needed: zod_1.z.boolean(),
26
+ profile_complete: zod_1.z.boolean()
27
+ });
28
+ function loadPrompt() {
29
+ const candidates = [
30
+ node_path_1.default.resolve(process.cwd(), "src", "prompts", "openclaw.md"),
31
+ node_path_1.default.resolve(process.cwd(), "prompts", "openclaw.md"),
32
+ node_path_1.default.resolve(process.cwd(), "..", "artifacts", "prompts", "openclaw.md"),
33
+ node_path_1.default.resolve(__dirname, "..", "prompts", "openclaw.md")
34
+ ];
35
+ const found = candidates.find((candidate) => node_fs_1.default.existsSync(candidate));
36
+ if (!found) {
37
+ throw new Error("OpenClaw prompt was not found");
38
+ }
39
+ return node_fs_1.default.readFileSync(found, "utf8");
40
+ }
41
+ function compactContext(input) {
42
+ const questions = (0, questions_1.getQuestionsDocument)();
43
+ const answeredIds = Object.keys(input.answers);
44
+ const currentQuestionId = input.currentQuestionId ?? (0, questions_1.getNextQuestionId)(answeredIds) ?? "q40";
45
+ const currentQuestion = questions.questions.find((question) => question.id === currentQuestionId);
46
+ return JSON.stringify({
47
+ currentQuestion,
48
+ answeredQuestionIds: answeredIds,
49
+ remainingQuestionIds: questions.questions.filter((question) => !answeredIds.includes(question.id)).map((question) => question.id),
50
+ existingAnswers: input.answers,
51
+ questionFramework: questions
52
+ }, null, 2);
53
+ }
54
+ function transcript(messages) {
55
+ return messages
56
+ .slice(-16)
57
+ .map((message) => `${message.role.toUpperCase()}: ${message.content}`)
58
+ .join("\n");
59
+ }
60
+ function extractJsonObject(text) {
61
+ const trimmed = text.trim();
62
+ try {
63
+ return JSON.parse(trimmed);
64
+ }
65
+ catch {
66
+ const start = trimmed.indexOf("{");
67
+ const end = trimmed.lastIndexOf("}");
68
+ if (start >= 0 && end > start) {
69
+ return JSON.parse(trimmed.slice(start, end + 1));
70
+ }
71
+ }
72
+ throw new Error("AI response did not contain valid JSON");
73
+ }
74
+ async function callAnthropic(systemPrompt, userPayload) {
75
+ (0, env_1.assertAiConfigured)();
76
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
77
+ method: "POST",
78
+ headers: {
79
+ "x-api-key": env_1.env.ANTHROPIC_API_KEY ?? "",
80
+ "anthropic-version": "2023-06-01",
81
+ "Content-Type": "application/json"
82
+ },
83
+ body: JSON.stringify({
84
+ model: env_1.env.AI_MODEL,
85
+ max_tokens: 1200,
86
+ temperature: 0.4,
87
+ system: systemPrompt,
88
+ messages: [
89
+ {
90
+ role: "user",
91
+ content: userPayload
92
+ }
93
+ ]
94
+ })
95
+ });
96
+ if (!response.ok) {
97
+ const text = await response.text();
98
+ throw new Error(`Anthropic request failed with ${response.status}: ${text}`);
99
+ }
100
+ const payload = (await response.json());
101
+ const text = payload.content?.find((item) => item.type === "text")?.text;
102
+ if (!text) {
103
+ throw new Error("Anthropic returned no text content");
104
+ }
105
+ return text;
106
+ }
107
+ async function callGrok(systemPrompt, userPayload) {
108
+ (0, env_1.assertAiConfigured)();
109
+ const response = await fetch(`${env_1.env.GROK_BASE_URL.replace(/\/$/, "")}/chat/completions`, {
110
+ method: "POST",
111
+ headers: {
112
+ Authorization: `Bearer ${env_1.env.GROK_API_KEY ?? ""}`,
113
+ "Content-Type": "application/json"
114
+ },
115
+ body: JSON.stringify({
116
+ model: env_1.env.AI_MODEL,
117
+ temperature: 0.4,
118
+ response_format: { type: "json_object" },
119
+ messages: [
120
+ { role: "system", content: systemPrompt },
121
+ { role: "user", content: userPayload }
122
+ ]
123
+ })
124
+ });
125
+ if (!response.ok) {
126
+ const text = await response.text();
127
+ throw new Error(`Grok request failed with ${response.status}: ${text}`);
128
+ }
129
+ const payload = (await response.json());
130
+ const text = payload.choices?.[0]?.message?.content;
131
+ if (!text) {
132
+ throw new Error("Grok returned no message content");
133
+ }
134
+ return text;
135
+ }
136
+ async function callLocalOpenAiCompatible(systemPrompt, userPayload) {
137
+ (0, env_1.assertAiConfigured)();
138
+ const headers = {
139
+ "Content-Type": "application/json"
140
+ };
141
+ if (env_1.env.LOCAL_AI_API_KEY) {
142
+ headers.Authorization = `Bearer ${env_1.env.LOCAL_AI_API_KEY}`;
143
+ }
144
+ const response = await fetch(`${env_1.env.LOCAL_AI_BASE_URL.replace(/\/$/, "")}/chat/completions`, {
145
+ method: "POST",
146
+ headers,
147
+ body: JSON.stringify({
148
+ model: env_1.env.AI_MODEL,
149
+ max_tokens: 1200,
150
+ temperature: 0.2,
151
+ response_format: { type: "json_object" },
152
+ messages: [
153
+ {
154
+ role: "system",
155
+ content: [
156
+ systemPrompt,
157
+ "",
158
+ "You are running as a local Phi model. Return exactly one valid JSON object.",
159
+ "Do not include markdown, XML tags, chain-of-thought, commentary, or text outside JSON."
160
+ ].join("\n")
161
+ },
162
+ { role: "user", content: userPayload }
163
+ ]
164
+ })
165
+ });
166
+ if (!response.ok) {
167
+ const text = await response.text();
168
+ throw new Error(`Local OpenClaw provider failed with ${response.status}: ${text}`);
169
+ }
170
+ const payload = (await response.json());
171
+ const text = payload.choices?.[0]?.message?.content;
172
+ if (!text) {
173
+ throw new Error("Local OpenClaw provider returned no message content");
174
+ }
175
+ return text;
176
+ }
177
+ async function callConfiguredModel(systemPrompt, userPayload) {
178
+ if (env_1.env.AI_PROVIDER === "anthropic") {
179
+ return callAnthropic(systemPrompt, userPayload);
180
+ }
181
+ if (env_1.env.AI_PROVIDER === "grok") {
182
+ return callGrok(systemPrompt, userPayload);
183
+ }
184
+ return callLocalOpenAiCompatible(systemPrompt, userPayload);
185
+ }
186
+ async function chatWithOpenClaw(input) {
187
+ const prompt = loadPrompt();
188
+ const userPayload = [
189
+ "Use the following profile state and conversation transcript.",
190
+ "Return only the JSON object required by the response contract.",
191
+ "",
192
+ "PROFILE_STATE:",
193
+ compactContext(input),
194
+ "",
195
+ "TRANSCRIPT:",
196
+ transcript(input.messages)
197
+ ].join("\n");
198
+ const raw = await callConfiguredModel(prompt, userPayload);
199
+ return exports.openClawReplySchema.parse(extractJsonObject(raw));
200
+ }
201
+ async function summarizeProfile(answers) {
202
+ const systemPrompt = [
203
+ "You are OpenClaw, writing concise psychological dating profile summaries for internal matching.",
204
+ "Summaries must be concrete, nonjudgmental, and useful for compatibility scoring.",
205
+ "Return plain text only. Do not include markdown."
206
+ ].join("\n");
207
+ const userPayload = JSON.stringify({ questions: (0, questions_1.getQuestionsDocument)(), answers }, null, 2);
208
+ const text = await callConfiguredModel(systemPrompt, userPayload);
209
+ return text.trim();
210
+ }
backend/dist/services/profileService.js ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMyProfile = getMyProfile;
4
+ exports.upsertMyProfile = upsertMyProfile;
5
+ exports.saveProfileAnswer = saveProfileAnswer;
6
+ exports.completeMyProfile = completeMyProfile;
7
+ exports.markUserPaid = markUserPaid;
8
+ const supabase_1 = require("../config/supabase");
9
+ const httpError_1 = require("../utils/httpError");
10
+ const questions_1 = require("../utils/questions");
11
+ const embeddingService_1 = require("./embeddingService");
12
+ const openClawService_1 = require("./openClawService");
13
+ function answerText(answer) {
14
+ const raw = Array.isArray(answer.answer) ? answer.answer.join(", ") : answer.answer;
15
+ return [raw, answer.followup].filter(Boolean).join(" ");
16
+ }
17
+ function normalizeSignal(value) {
18
+ return value
19
+ .toLowerCase()
20
+ .replace(/[^a-z0-9\s-]/g, " ")
21
+ .replace(/\s+/g, " ")
22
+ .trim()
23
+ .slice(0, 80);
24
+ }
25
+ function collectSignals(answers, tag) {
26
+ const questions = (0, questions_1.getQuestionsDocument)().questions.filter((question) => question.tags.includes(tag));
27
+ const signals = new Set();
28
+ for (const question of questions) {
29
+ const answer = answers[question.id];
30
+ if (!answer) {
31
+ continue;
32
+ }
33
+ const values = Array.isArray(answer.answer)
34
+ ? answer.answer
35
+ : answer.answer.split(/[.;,\n]/).map((item) => item.trim());
36
+ for (const value of values) {
37
+ const normalized = normalizeSignal(value);
38
+ if (normalized.length >= 3) {
39
+ signals.add(normalized);
40
+ }
41
+ }
42
+ }
43
+ return [...signals].slice(0, 32);
44
+ }
45
+ function profileTextForEmbedding(answers, narrativeSummary) {
46
+ const questions = (0, questions_1.getQuestionsDocument)().questions;
47
+ const lines = questions
48
+ .map((question) => {
49
+ const answer = answers[question.id];
50
+ return answer ? `${question.prompt}\n${answerText(answer)}` : "";
51
+ })
52
+ .filter(Boolean);
53
+ return [narrativeSummary, ...lines].join("\n\n");
54
+ }
55
+ async function getMyProfile(userId) {
56
+ const { data, error } = await supabase_1.supabaseAdmin
57
+ .from("profiles")
58
+ .select("*")
59
+ .eq("user_id", userId)
60
+ .maybeSingle();
61
+ if (error) {
62
+ throw error;
63
+ }
64
+ return data;
65
+ }
66
+ async function upsertMyProfile(userId, input) {
67
+ const { data, error } = await supabase_1.supabaseAdmin
68
+ .from("profiles")
69
+ .upsert({
70
+ user_id: userId,
71
+ display_name: input.displayName,
72
+ age: input.age,
73
+ location: input.location ?? null,
74
+ gender: input.gender ?? null,
75
+ interested_in: input.interestedIn ?? []
76
+ }, { onConflict: "user_id" })
77
+ .select("*")
78
+ .single();
79
+ if (error) {
80
+ throw error;
81
+ }
82
+ return data;
83
+ }
84
+ async function saveProfileAnswer(userId, questionId, input) {
85
+ if (!(0, questions_1.isKnownQuestionId)(questionId)) {
86
+ throw new httpError_1.HttpError(400, "unknown_question", `Unknown question id: ${questionId}`);
87
+ }
88
+ const profile = await getMyProfile(userId);
89
+ if (!profile) {
90
+ throw new httpError_1.HttpError(409, "profile_required", "Create the basic profile before saving answers");
91
+ }
92
+ const answers = (profile.answers ?? {});
93
+ answers[questionId] = {
94
+ questionId,
95
+ answer: input.answer,
96
+ followup: input.followup,
97
+ dealbreakerSeverity: input.dealbreakerSeverity,
98
+ updatedAt: new Date().toISOString()
99
+ };
100
+ const { data, error } = await supabase_1.supabaseAdmin
101
+ .from("profiles")
102
+ .update({ answers, profile_complete: false })
103
+ .eq("user_id", userId)
104
+ .select("*")
105
+ .single();
106
+ if (error) {
107
+ throw error;
108
+ }
109
+ return data;
110
+ }
111
+ async function completeMyProfile(userId) {
112
+ const profile = await getMyProfile(userId);
113
+ if (!profile) {
114
+ throw new httpError_1.HttpError(409, "profile_required", "Create the basic profile before completing the profile");
115
+ }
116
+ const answers = (profile.answers ?? {});
117
+ const requiredQuestionIds = (0, questions_1.getQuestionsDocument)().questions.map((question) => question.id);
118
+ const missing = requiredQuestionIds.filter((questionId) => !answers[questionId]);
119
+ if (missing.length > 0) {
120
+ throw new httpError_1.HttpError(422, "profile_incomplete", `Missing required answers: ${missing.join(", ")}`);
121
+ }
122
+ const narrativeSummary = await (0, openClawService_1.summarizeProfile)(answers);
123
+ const embedding = await (0, embeddingService_1.createEmbedding)(profileTextForEmbedding(answers, narrativeSummary));
124
+ const disgustTriggers = collectSignals(answers, "disgust");
125
+ const captivatingTraits = collectSignals(answers, "captivating_traits");
126
+ const { data, error } = await supabase_1.supabaseAdmin
127
+ .from("profiles")
128
+ .update({
129
+ narrative_summary: narrativeSummary,
130
+ embedding,
131
+ disgust_triggers: disgustTriggers,
132
+ captivating_traits: captivatingTraits,
133
+ profile_complete: true
134
+ })
135
+ .eq("user_id", userId)
136
+ .select("*")
137
+ .single();
138
+ if (error) {
139
+ throw error;
140
+ }
141
+ return data;
142
+ }
143
+ async function markUserPaid(userId, stripeCustomerId, sessionId) {
144
+ const paidAt = new Date().toISOString();
145
+ const { error: entitlementError } = await supabase_1.supabaseAdmin
146
+ .from("user_entitlements")
147
+ .upsert({
148
+ user_id: userId,
149
+ entry_paid: true,
150
+ stripe_customer_id: stripeCustomerId,
151
+ stripe_checkout_session_id: sessionId,
152
+ paid_at: paidAt
153
+ }, { onConflict: "user_id" });
154
+ if (entitlementError) {
155
+ throw entitlementError;
156
+ }
157
+ const { error: profileError } = await supabase_1.supabaseAdmin
158
+ .from("profiles")
159
+ .update({ paid_entry: true })
160
+ .eq("user_id", userId);
161
+ if (profileError) {
162
+ throw profileError;
163
+ }
164
+ }
backend/dist/types/domain.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
backend/dist/utils/httpError.js ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HttpError = void 0;
4
+ exports.getErrorMessage = getErrorMessage;
5
+ class HttpError extends Error {
6
+ status;
7
+ code;
8
+ constructor(status, code, message) {
9
+ super(message);
10
+ this.status = status;
11
+ this.code = code;
12
+ }
13
+ }
14
+ exports.HttpError = HttpError;
15
+ function getErrorMessage(error) {
16
+ if (error instanceof Error) {
17
+ return error.message;
18
+ }
19
+ return "Unexpected error";
20
+ }
backend/dist/utils/privacyGuard.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isPrivacyLookupRequest = isPrivacyLookupRequest;
4
+ exports.privacyRefusalText = privacyRefusalText;
5
+ const privacyLookupPatterns = [
6
+ /\b(find|look\s*up|lookup|search|identify|track|locate|doxx?|reveal|get)\b(?=.{0,120}\b(person|someone|user|profile|match|candidate|woman|man|girl|guy|name|address|phone|email|social|instagram|facebook|details|where)\b)/i,
7
+ /\b(phone number|email address|home address|street address|full name|last name|ssn|social security|where does .* live|where do .* live)\b/i
8
+ ];
9
+ function isPrivacyLookupRequest(value) {
10
+ const lowered = value.toLowerCase();
11
+ if (lowered.includes("my ") && !/(find|lookup|look up|search|dox)/i.test(value)) {
12
+ return false;
13
+ }
14
+ return privacyLookupPatterns.some((pattern) => pattern.test(value));
15
+ }
16
+ function privacyRefusalText() {
17
+ return [
18
+ "I cannot help find, identify, or expose personal details about another person.",
19
+ "loveGPT only uses profile information for consent-based compatibility work.",
20
+ "Answer the current profile question about your own preferences and boundaries instead."
21
+ ].join(" ");
22
+ }
backend/dist/utils/questions.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getQuestionsDocument = getQuestionsDocument;
7
+ exports.getQuestion = getQuestion;
8
+ exports.getNextQuestionId = getNextQuestionId;
9
+ exports.isKnownQuestionId = isKnownQuestionId;
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ let cachedQuestions;
13
+ function questionsPath() {
14
+ const candidates = [
15
+ node_path_1.default.resolve(process.cwd(), "..", "shared", "questions.json"),
16
+ node_path_1.default.resolve(process.cwd(), "shared", "questions.json"),
17
+ node_path_1.default.resolve(__dirname, "..", "..", "..", "shared", "questions.json")
18
+ ];
19
+ const found = candidates.find((candidate) => node_fs_1.default.existsSync(candidate));
20
+ if (!found) {
21
+ throw new Error("shared/questions.json was not found");
22
+ }
23
+ return found;
24
+ }
25
+ function getQuestionsDocument() {
26
+ if (!cachedQuestions) {
27
+ cachedQuestions = JSON.parse(node_fs_1.default.readFileSync(questionsPath(), "utf8"));
28
+ }
29
+ return cachedQuestions;
30
+ }
31
+ function getQuestion(questionId) {
32
+ const question = getQuestionsDocument().questions.find((item) => item.id === questionId);
33
+ if (!question) {
34
+ throw new Error(`Unknown question id: ${questionId}`);
35
+ }
36
+ return question;
37
+ }
38
+ function getNextQuestionId(answeredQuestionIds) {
39
+ const answered = new Set(answeredQuestionIds);
40
+ const next = getQuestionsDocument().questions.find((question) => !answered.has(question.id));
41
+ return next?.id ?? null;
42
+ }
43
+ function isKnownQuestionId(questionId) {
44
+ return getQuestionsDocument().questions.some((question) => question.id === questionId);
45
+ }
backend/package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
backend/package.json CHANGED
@@ -1,33 +1,33 @@
1
- {
2
- "name": "opendatebase-backend",
3
- "version": "1.0.0",
4
- "private": true,
5
- "main": "dist/server.js",
6
- "scripts": {
7
- "dev": "tsx watch src/server.ts",
8
- "build": "tsc -p tsconfig.json",
9
- "start": "node dist/server.js",
10
- "typecheck": "tsc -p tsconfig.json --noEmit"
11
- },
12
- "dependencies": {
13
- "@supabase/supabase-js": "^2.49.4",
14
- "cors": "^2.8.5",
15
- "dotenv": "^16.4.7",
16
- "express": "^4.21.2",
17
- "helmet": "^8.0.0",
18
- "morgan": "^1.10.0",
19
- "stripe": "^17.6.0",
20
- "zod": "^3.24.2"
21
- },
22
- "devDependencies": {
23
- "@types/cors": "^2.8.17",
24
- "@types/express": "^5.0.0",
25
- "@types/morgan": "^1.9.9",
26
- "@types/node": "^22.10.7",
27
- "tsx": "^4.19.2",
28
- "typescript": "^5.7.3"
29
- },
30
- "engines": {
31
- "node": ">=20"
32
- }
33
- }
 
1
+ {
2
+ "name": "lovegpt-backend",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "main": "dist/server.js",
6
+ "scripts": {
7
+ "dev": "tsx watch src/server.ts",
8
+ "build": "tsc -p tsconfig.json",
9
+ "start": "node dist/server.js",
10
+ "typecheck": "tsc -p tsconfig.json --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@supabase/supabase-js": "^2.49.4",
14
+ "cors": "^2.8.5",
15
+ "dotenv": "^16.4.7",
16
+ "express": "^4.21.2",
17
+ "helmet": "^8.0.0",
18
+ "morgan": "^1.10.0",
19
+ "stripe": "^17.6.0",
20
+ "zod": "^3.24.2"
21
+ },
22
+ "devDependencies": {
23
+ "@types/cors": "^2.8.17",
24
+ "@types/express": "^5.0.0",
25
+ "@types/morgan": "^1.9.9",
26
+ "@types/node": "^22.10.7",
27
+ "tsx": "^4.19.2",
28
+ "typescript": "^5.7.3"
29
+ },
30
+ "engines": {
31
+ "node": ">=20"
32
+ }
33
+ }
backend/src/prompts/openclaw.md CHANGED
@@ -1,42 +1,42 @@
1
- # OpenClaw System Prompt
2
-
3
- You are OpenClaw, the warm, direct, psychologically precise AI matchmaker inside OpenDateBase. Your job is to guide a dating user through the complete 40-question compatibility profile in a natural conversation.
4
-
5
- Core behavior:
6
- - Ask one meaningful question at a time.
7
- - Reflect briefly before moving on.
8
- - Ask follow-up questions when the answer is vague, performative, contradictory, unsafe, or too thin to support matching.
9
- - Treat disgust triggers as important compatibility data, not as shameful confessions.
10
- - Never moralize ordinary preferences. Do challenge cruelty, coercion, dehumanization, or unsafe intent.
11
- - Keep the conversation grounded, elegant, and human. Do not sound like a quiz form.
12
- - Do not promise a perfect partner. Promise careful matching and honest signal.
13
- - Never help a user find, identify, dox, locate, or expose private details about another person. Refuse those requests and redirect to consent-based compatibility profiling.
14
-
15
- Profile priorities:
16
- 1. Long-term values and future direction.
17
- 2. Emotional safety, attachment, and conflict repair.
18
- 3. Captivating traits that create admiration and desire.
19
- 4. Disgust triggers, repulsions, hygiene needs, moral aversions, sensory aversions, and intimacy boundaries.
20
- 5. Lifestyle practicality.
21
- 6. Readiness, boundaries, and consent.
22
- 7. Respectful body-type self-description, desired body-type range, and physical attraction flexibility.
23
-
24
- Response contract:
25
- Return a single JSON object with no markdown.
26
-
27
- Schema:
28
- {
29
- "assistant_text": "Message shown to the user.",
30
- "current_question_id": "q1",
31
- "captured_answer": {
32
- "questionId": "q1",
33
- "answer": "The user's answer, normalized without changing meaning.",
34
- "followup": "Optional concise follow-up detail.",
35
- "dealbreakerSeverity": "low | medium | high"
36
- },
37
- "next_question_id": "q2",
38
- "followup_needed": false,
39
- "profile_complete": false
40
- }
41
-
42
- If the user has not answered the current question, set captured_answer to null. If all 40 answers are complete, set profile_complete to true and produce a concise completion message.
 
1
+ # OpenClaw System Prompt
2
+
3
+ You are OpenClaw, the warm, direct, psychologically precise AI matchmaker inside loveGPT. Your job is to guide a dating user through the complete 40-question compatibility profile in a natural conversation.
4
+
5
+ Core behavior:
6
+ - Ask one meaningful question at a time.
7
+ - Reflect briefly before moving on.
8
+ - Ask follow-up questions when the answer is vague, performative, contradictory, unsafe, or too thin to support matching.
9
+ - Treat disgust triggers as important compatibility data, not as shameful confessions.
10
+ - Never moralize ordinary preferences. Do challenge cruelty, coercion, dehumanization, or unsafe intent.
11
+ - Keep the conversation grounded, elegant, and human. Do not sound like a quiz form.
12
+ - Do not promise a perfect partner. Promise careful matching and honest signal.
13
+ - Never help a user find, identify, dox, locate, or expose private details about another person. Refuse those requests and redirect to consent-based compatibility profiling.
14
+
15
+ Profile priorities:
16
+ 1. Long-term values and future direction.
17
+ 2. Emotional safety, attachment, and conflict repair.
18
+ 3. Captivating traits that create admiration and desire.
19
+ 4. Disgust triggers, repulsions, hygiene needs, moral aversions, sensory aversions, and intimacy boundaries.
20
+ 5. Lifestyle practicality.
21
+ 6. Readiness, boundaries, and consent.
22
+ 7. Respectful body-type self-description, desired body-type range, and physical attraction flexibility.
23
+
24
+ Response contract:
25
+ Return a single JSON object with no markdown.
26
+
27
+ Schema:
28
+ {
29
+ "assistant_text": "Message shown to the user.",
30
+ "current_question_id": "q1",
31
+ "captured_answer": {
32
+ "questionId": "q1",
33
+ "answer": "The user's answer, normalized without changing meaning.",
34
+ "followup": "Optional concise follow-up detail.",
35
+ "dealbreakerSeverity": "low | medium | high"
36
+ },
37
+ "next_question_id": "q2",
38
+ "followup_needed": false,
39
+ "profile_complete": false
40
+ }
41
+
42
+ If the user has not answered the current question, set captured_answer to null. If all 40 answers are complete, set profile_complete to true and produce a concise completion message.
backend/src/routes/health.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { Router } from "express";
2
-
3
- export const healthRouter = Router();
4
-
5
- healthRouter.get("/", (_req, res) => {
6
- res.json({
7
- ok: true,
8
- service: "opendatebase-backend",
9
- checkedAt: new Date().toISOString()
10
- });
11
- });
 
1
+ import { Router } from "express";
2
+
3
+ export const healthRouter = Router();
4
+
5
+ healthRouter.get("/", (_req, res) => {
6
+ res.json({
7
+ ok: true,
8
+ service: "lovegpt-backend",
9
+ checkedAt: new Date().toISOString()
10
+ });
11
+ });
backend/src/routes/payments.ts CHANGED
@@ -1,104 +1,104 @@
1
- import { Router } from "express";
2
- import Stripe from "stripe";
3
- import { env } from "../config/env";
4
- import { supabaseAdmin } from "../config/supabase";
5
- import { requireAuth } from "../middleware/auth";
6
- import { markUserPaid } from "../services/profileService";
7
- import { HttpError } from "../utils/httpError";
8
-
9
- const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
10
- typescript: true
11
- });
12
-
13
- export const paymentsRouter = Router();
14
- export const paymentWebhookRouter = Router();
15
-
16
- paymentsRouter.use(requireAuth);
17
-
18
- paymentsRouter.post("/checkout-session", async (req, res, next) => {
19
- try {
20
- const user = req.authUser!;
21
-
22
- const session = await stripe.checkout.sessions.create({
23
- mode: "payment",
24
- customer_email: user.email ?? undefined,
25
- client_reference_id: user.id,
26
- metadata: {
27
- user_id: user.id,
28
- product: "opendatebase_entry"
29
- },
30
- success_url: env.FRONTEND_SUCCESS_URL,
31
- cancel_url: env.FRONTEND_CANCEL_URL,
32
- line_items: [
33
- env.STRIPE_PRICE_ID
34
- ? {
35
- price: env.STRIPE_PRICE_ID,
36
- quantity: 1
37
- }
38
- : {
39
- quantity: 1,
40
- price_data: {
41
- currency: env.ENTRY_FEE_CURRENCY,
42
- unit_amount: env.ENTRY_FEE_CENTS,
43
- product_data: {
44
- name: "OpenDateBase Entry",
45
- description: "One-time access to ten curated high-compatibility matches."
46
- }
47
- }
48
- }
49
- ]
50
- });
51
-
52
- res.json({
53
- id: session.id,
54
- url: session.url
55
- });
56
- } catch (error) {
57
- next(error);
58
- }
59
- });
60
-
61
- paymentWebhookRouter.post("/", async (req, res, next) => {
62
- try {
63
- const signature = req.header("stripe-signature");
64
- if (!signature) {
65
- throw new HttpError(400, "stripe_signature_missing", "Missing Stripe signature");
66
- }
67
-
68
- const event = stripe.webhooks.constructEvent(req.body, signature, env.STRIPE_WEBHOOK_SECRET);
69
-
70
- if (event.type === "checkout.session.completed") {
71
- const session = event.data.object as Stripe.Checkout.Session;
72
- const userId = session.metadata?.user_id ?? session.client_reference_id;
73
- if (!userId) {
74
- throw new HttpError(400, "stripe_user_missing", "Stripe session does not include a user id");
75
- }
76
-
77
- await markUserPaid(
78
- userId,
79
- typeof session.customer === "string" ? session.customer : session.customer?.id,
80
- session.id
81
- );
82
-
83
- const { error } = await supabaseAdmin.from("payments").upsert(
84
- {
85
- user_id: userId,
86
- stripe_event_id: event.id,
87
- stripe_checkout_session_id: session.id,
88
- amount: session.amount_total ?? env.ENTRY_FEE_CENTS,
89
- currency: session.currency ?? env.ENTRY_FEE_CURRENCY,
90
- status: "paid"
91
- },
92
- { onConflict: "stripe_event_id" }
93
- );
94
-
95
- if (error) {
96
- throw error;
97
- }
98
- }
99
-
100
- res.json({ received: true });
101
- } catch (error) {
102
- next(error);
103
- }
104
- });
 
1
+ import { Router } from "express";
2
+ import Stripe from "stripe";
3
+ import { env } from "../config/env";
4
+ import { supabaseAdmin } from "../config/supabase";
5
+ import { requireAuth } from "../middleware/auth";
6
+ import { markUserPaid } from "../services/profileService";
7
+ import { HttpError } from "../utils/httpError";
8
+
9
+ const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
10
+ typescript: true
11
+ });
12
+
13
+ export const paymentsRouter = Router();
14
+ export const paymentWebhookRouter = Router();
15
+
16
+ paymentsRouter.use(requireAuth);
17
+
18
+ paymentsRouter.post("/checkout-session", async (req, res, next) => {
19
+ try {
20
+ const user = req.authUser!;
21
+
22
+ const session = await stripe.checkout.sessions.create({
23
+ mode: "payment",
24
+ customer_email: user.email ?? undefined,
25
+ client_reference_id: user.id,
26
+ metadata: {
27
+ user_id: user.id,
28
+ product: "lovegpt_entry"
29
+ },
30
+ success_url: env.FRONTEND_SUCCESS_URL,
31
+ cancel_url: env.FRONTEND_CANCEL_URL,
32
+ line_items: [
33
+ env.STRIPE_PRICE_ID
34
+ ? {
35
+ price: env.STRIPE_PRICE_ID,
36
+ quantity: 1
37
+ }
38
+ : {
39
+ quantity: 1,
40
+ price_data: {
41
+ currency: env.ENTRY_FEE_CURRENCY,
42
+ unit_amount: env.ENTRY_FEE_CENTS,
43
+ product_data: {
44
+ name: "loveGPT Entry",
45
+ description: "One-time access to ten curated high-compatibility matches."
46
+ }
47
+ }
48
+ }
49
+ ]
50
+ });
51
+
52
+ res.json({
53
+ id: session.id,
54
+ url: session.url
55
+ });
56
+ } catch (error) {
57
+ next(error);
58
+ }
59
+ });
60
+
61
+ paymentWebhookRouter.post("/", async (req, res, next) => {
62
+ try {
63
+ const signature = req.header("stripe-signature");
64
+ if (!signature) {
65
+ throw new HttpError(400, "stripe_signature_missing", "Missing Stripe signature");
66
+ }
67
+
68
+ const event = stripe.webhooks.constructEvent(req.body, signature, env.STRIPE_WEBHOOK_SECRET);
69
+
70
+ if (event.type === "checkout.session.completed") {
71
+ const session = event.data.object as Stripe.Checkout.Session;
72
+ const userId = session.metadata?.user_id ?? session.client_reference_id;
73
+ if (!userId) {
74
+ throw new HttpError(400, "stripe_user_missing", "Stripe session does not include a user id");
75
+ }
76
+
77
+ await markUserPaid(
78
+ userId,
79
+ typeof session.customer === "string" ? session.customer : session.customer?.id,
80
+ session.id
81
+ );
82
+
83
+ const { error } = await supabaseAdmin.from("payments").upsert(
84
+ {
85
+ user_id: userId,
86
+ stripe_event_id: event.id,
87
+ stripe_checkout_session_id: session.id,
88
+ amount: session.amount_total ?? env.ENTRY_FEE_CENTS,
89
+ currency: session.currency ?? env.ENTRY_FEE_CURRENCY,
90
+ status: "paid"
91
+ },
92
+ { onConflict: "stripe_event_id" }
93
+ );
94
+
95
+ if (error) {
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ res.json({ received: true });
101
+ } catch (error) {
102
+ next(error);
103
+ }
104
+ });
backend/src/server.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { env } from "./config/env";
2
- import { createApp } from "./app";
3
-
4
- const app = createApp();
5
-
6
- app.listen(env.PORT, () => {
7
- console.log(`OpenDateBase backend listening on port ${env.PORT}`);
8
- });
 
1
+ import { env } from "./config/env";
2
+ import { createApp } from "./app";
3
+
4
+ const app = createApp();
5
+
6
+ app.listen(env.PORT, () => {
7
+ console.log(`loveGPT backend listening on port ${env.PORT}`);
8
+ });
backend/src/utils/privacyGuard.ts CHANGED
@@ -1,21 +1,21 @@
1
- const privacyLookupPatterns = [
2
- /\b(find|look\s*up|lookup|search|identify|track|locate|doxx?|reveal|get)\b(?=.{0,120}\b(person|someone|user|profile|match|candidate|woman|man|girl|guy|name|address|phone|email|social|instagram|facebook|details|where)\b)/i,
3
- /\b(phone number|email address|home address|street address|full name|last name|ssn|social security|where does .* live|where do .* live)\b/i
4
- ];
5
-
6
- export function isPrivacyLookupRequest(value: string): boolean {
7
- const lowered = value.toLowerCase();
8
- if (lowered.includes("my ") && !/(find|lookup|look up|search|dox)/i.test(value)) {
9
- return false;
10
- }
11
-
12
- return privacyLookupPatterns.some((pattern) => pattern.test(value));
13
- }
14
-
15
- export function privacyRefusalText(): string {
16
- return [
17
- "I cannot help find, identify, or expose personal details about another person.",
18
- "OpenDateBase only uses profile information for consent-based compatibility work.",
19
- "Answer the current profile question about your own preferences and boundaries instead."
20
- ].join(" ");
21
- }
 
1
+ const privacyLookupPatterns = [
2
+ /\b(find|look\s*up|lookup|search|identify|track|locate|doxx?|reveal|get)\b(?=.{0,120}\b(person|someone|user|profile|match|candidate|woman|man|girl|guy|name|address|phone|email|social|instagram|facebook|details|where)\b)/i,
3
+ /\b(phone number|email address|home address|street address|full name|last name|ssn|social security|where does .* live|where do .* live)\b/i
4
+ ];
5
+
6
+ export function isPrivacyLookupRequest(value: string): boolean {
7
+ const lowered = value.toLowerCase();
8
+ if (lowered.includes("my ") && !/(find|lookup|look up|search|dox)/i.test(value)) {
9
+ return false;
10
+ }
11
+
12
+ return privacyLookupPatterns.some((pattern) => pattern.test(value));
13
+ }
14
+
15
+ export function privacyRefusalText(): string {
16
+ return [
17
+ "I cannot help find, identify, or expose personal details about another person.",
18
+ "loveGPT only uses profile information for consent-based compatibility work.",
19
+ "Answer the current profile question about your own preferences and boundaries instead."
20
+ ].join(" ");
21
+ }
docs/huggingface-space.md CHANGED
@@ -1,154 +1,154 @@
1
- # Hugging Face Space
2
-
3
- OpenDateBase includes a root-level Gradio adapter so the repository can be pushed directly to a Hugging Face Space.
4
-
5
- ## Required Files
6
-
7
- - `app.py` - Gradio entrypoint.
8
- - `requirements.txt` - Python dependencies.
9
- - `README.md` - Space metadata YAML at the top of the file.
10
- - `shared/questions.json` - the full compatibility questionnaire loaded by the Space.
11
-
12
- ## Local Run
13
-
14
- ```powershell
15
- python -m pip install -r requirements.txt
16
- python app.py
17
- ```
18
-
19
- Open:
20
-
21
- ```text
22
- http://localhost:7860
23
- ```
24
-
25
- ## Space Settings
26
-
27
- Create a new Hugging Face Space with:
28
-
29
- - SDK: `Gradio`
30
- - App file: `app.py`
31
- - Python: `3.11`
32
-
33
- Push this repository to the Space remote. The Space version is session-local by default, so it does not require Supabase, Stripe, or AI secrets to boot.
34
-
35
- ## Optional Local Phi OpenClaw
36
-
37
- The Gradio adapter can use a Phi model through any OpenAI-compatible local server. The default local model id is:
38
-
39
- ```text
40
- microsoft/Phi-4-mini-instruct
41
- ```
42
-
43
- Local llama.cpp example:
44
-
45
- ```powershell
46
- llama-server `
47
- -hf ysn-rfd/Phi-4-mini-instruct-GGUF:Q4_0 `
48
- --alias microsoft/Phi-4-mini-instruct `
49
- --jinja `
50
- --host 127.0.0.1 `
51
- --port 8081 `
52
- -c 32768
53
- ```
54
-
55
- Run the Space against that model:
56
-
57
- ```powershell
58
- $env:OPENCLAW_PROVIDER="local"
59
- $env:OPENCLAW_LOCAL_MODEL="microsoft/Phi-4-mini-instruct"
60
- $env:OPENCLAW_LOCAL_BASE_URL="http://127.0.0.1:8081/v1"
61
- python app.py
62
- ```
63
-
64
- If Phi is not reachable, the Space falls back to the deterministic profile flow by default. Set this to force a visible error instead:
65
-
66
- ```text
67
- OPENCLAW_FALLBACK_DETERMINISTIC=0
68
- ```
69
-
70
- ## Encrypted Profile Rows
71
-
72
- The Space no longer exports raw profile JSON or raw JSONL. Every downloadable profile file is a one-row encrypted JSONL file:
73
-
74
- ```text
75
- *.jsonl.enc
76
- ```
77
-
78
- Each row contains non-sensitive vault metadata plus AES-GCM ciphertext. The plaintext profile is parsed only inside the functions that need it, such as matching, and is not rendered in the UI.
79
-
80
- For durable Dataclaw access, set this Hugging Face secret:
81
-
82
- ```text
83
- DATACLAW_PROFILE_KEY=<high-entropy secret>
84
- ```
85
-
86
- Generate one locally:
87
-
88
- ```powershell
89
- python -c "import secrets; print(secrets.token_urlsafe(48))"
90
- ```
91
-
92
- If the secret is absent, the Space uses a volatile per-process key. That keeps exports encrypted, but Dataclaw will not be able to decrypt them after the Space restarts.
93
-
94
- Dataclaw decrypt command:
95
-
96
- ```powershell
97
- $env:DATACLAW_PROFILE_KEY="same-secret"
98
- $env:DATACLAW_ALLOW_PROFILE_DECRYPT="1"
99
- python tools/dataclaw_profile_vault.py .\profile.jsonl.enc --output .\profile.decrypted.jsonl --i-understand-raw-profile-output
100
- ```
101
-
102
- Raw decrypted JSONL is intentionally gated behind both the Dataclaw key and an explicit raw-output flag.
103
-
104
- ## Private Matchmaker Table
105
-
106
- Questionnaire turns are appended to an encrypted server-side matchmaker table. The table is not exposed as a Gradio component, download, API output, or chat response. It is decrypted only inside the matchmaker agent loop when judging compatibility.
107
-
108
- Recommended Space secret:
109
-
110
- ```text
111
- MATCHMAKER_TABLE_KEY=<high-entropy secret>
112
- ```
113
-
114
- If `MATCHMAKER_TABLE_KEY` is absent, the table key is derived from `DATACLAW_PROFILE_KEY`. If both are absent, the table remains encrypted but becomes process-local and will not survive a Space restart.
115
-
116
- The matchmaker harness requires complete Hugging Face-identified profiles before opening an automatic speed-date room. Anonymous profiles can be logged and judged internally, but they are not routed into chat.
117
-
118
- ## Prompt Privacy Guard
119
-
120
- The Gradio app and Node OpenClaw endpoint refuse chat requests that try to find, identify, locate, dox, or expose personal details about another person. Profile data may only be used for consent-based compatibility work.
121
-
122
- ## Speed-Date Room
123
-
124
- The Space includes a two-person speed-date room:
125
-
126
- - Users sign in with a Hugging Face token.
127
- - The active room is a public post inside the Space, but it shows only Hugging Face handles and room status.
128
- - Chat lasts 20 minutes by default.
129
- - Chat messages that include emails, phone numbers, addresses, URLs, social handles, or contact-platform prompts are blocked.
130
- - The matchmaker agent can automatically connect two complete compatible profiles into the room.
131
- - After the timer ends, both users can submit a mutual exchange form.
132
- - Contact details are displayed only after both users submit and consent.
133
-
134
- Runtime setting:
135
-
136
- ```text
137
- SPEED_DATE_SECONDS=1200
138
- ```
139
-
140
- Use a shorter value only for local testing.
141
-
142
- ## Production Integration
143
-
144
- For a private or protected Space, add secrets and wire the Gradio adapter to the production backend:
145
-
146
- - `BACKEND_URL`
147
- - `SUPABASE_URL`
148
- - `SUPABASE_SERVICE_ROLE_KEY` or a scoped backend token
149
- - `DATACLAW_PROFILE_KEY`
150
- - `OPENCLAW_PROVIDER=local`
151
- - `OPENCLAW_LOCAL_MODEL=microsoft/Phi-4-mini-instruct`
152
- - `OPENCLAW_LOCAL_BASE_URL`, if profile completion should call a Phi server rather than the deterministic session flow
153
-
154
- Do not expose service role keys in frontend-visible JavaScript or public client logs.
 
1
+ # Hugging Face Space
2
+
3
+ loveGPT includes a root-level Gradio adapter so the repository can be pushed directly to a Hugging Face Space.
4
+
5
+ ## Required Files
6
+
7
+ - `app.py` - Gradio entrypoint.
8
+ - `requirements.txt` - Python dependencies.
9
+ - `README.md` - Space metadata YAML at the top of the file.
10
+ - `shared/questions.json` - the full compatibility questionnaire loaded by the Space.
11
+
12
+ ## Local Run
13
+
14
+ ```powershell
15
+ python -m pip install -r requirements.txt
16
+ python app.py
17
+ ```
18
+
19
+ Open:
20
+
21
+ ```text
22
+ http://localhost:7860
23
+ ```
24
+
25
+ ## Space Settings
26
+
27
+ Create a new Hugging Face Space with:
28
+
29
+ - SDK: `Gradio`
30
+ - App file: `app.py`
31
+ - Python: `3.11`
32
+
33
+ Push this repository to the Space remote. The Space version is session-local by default, so it does not require Supabase, Stripe, or AI secrets to boot.
34
+
35
+ ## Optional Local Phi OpenClaw
36
+
37
+ The Gradio adapter can use a Phi model through any OpenAI-compatible local server. The default local model id is:
38
+
39
+ ```text
40
+ microsoft/Phi-4-mini-instruct
41
+ ```
42
+
43
+ Local llama.cpp example:
44
+
45
+ ```powershell
46
+ llama-server `
47
+ -hf ysn-rfd/Phi-4-mini-instruct-GGUF:Q4_0 `
48
+ --alias microsoft/Phi-4-mini-instruct `
49
+ --jinja `
50
+ --host 127.0.0.1 `
51
+ --port 8081 `
52
+ -c 32768
53
+ ```
54
+
55
+ Run the Space against that model:
56
+
57
+ ```powershell
58
+ $env:OPENCLAW_PROVIDER="local"
59
+ $env:OPENCLAW_LOCAL_MODEL="microsoft/Phi-4-mini-instruct"
60
+ $env:OPENCLAW_LOCAL_BASE_URL="http://127.0.0.1:8081/v1"
61
+ python app.py
62
+ ```
63
+
64
+ If Phi is not reachable, the Space falls back to the deterministic profile flow by default. Set this to force a visible error instead:
65
+
66
+ ```text
67
+ OPENCLAW_FALLBACK_DETERMINISTIC=0
68
+ ```
69
+
70
+ ## Encrypted Profile Rows
71
+
72
+ The Space no longer exports raw profile JSON or raw JSONL. Every downloadable profile file is a one-row encrypted JSONL file:
73
+
74
+ ```text
75
+ *.jsonl.enc
76
+ ```
77
+
78
+ Each row contains non-sensitive vault metadata plus AES-GCM ciphertext. The plaintext profile is parsed only inside the functions that need it, such as matching, and is not rendered in the UI.
79
+
80
+ For durable Dataclaw access, set this Hugging Face secret:
81
+
82
+ ```text
83
+ DATACLAW_PROFILE_KEY=<high-entropy secret>
84
+ ```
85
+
86
+ Generate one locally:
87
+
88
+ ```powershell
89
+ python -c "import secrets; print(secrets.token_urlsafe(48))"
90
+ ```
91
+
92
+ If the secret is absent, the Space uses a volatile per-process key. That keeps exports encrypted, but Dataclaw will not be able to decrypt them after the Space restarts.
93
+
94
+ Dataclaw decrypt command:
95
+
96
+ ```powershell
97
+ $env:DATACLAW_PROFILE_KEY="same-secret"
98
+ $env:DATACLAW_ALLOW_PROFILE_DECRYPT="1"
99
+ python tools/dataclaw_profile_vault.py .\profile.jsonl.enc --output .\profile.decrypted.jsonl --i-understand-raw-profile-output
100
+ ```
101
+
102
+ Raw decrypted JSONL is intentionally gated behind both the Dataclaw key and an explicit raw-output flag.
103
+
104
+ ## Private Matchmaker Table
105
+
106
+ Questionnaire turns are appended to an encrypted server-side matchmaker table. The table is not exposed as a Gradio component, download, API output, or chat response. It is decrypted only inside the matchmaker agent loop when judging compatibility.
107
+
108
+ Recommended Space secret:
109
+
110
+ ```text
111
+ MATCHMAKER_TABLE_KEY=<high-entropy secret>
112
+ ```
113
+
114
+ If `MATCHMAKER_TABLE_KEY` is absent, the table key is derived from `DATACLAW_PROFILE_KEY`. If both are absent, the table remains encrypted but becomes process-local and will not survive a Space restart.
115
+
116
+ The matchmaker harness requires complete Hugging Face-identified profiles before opening an automatic speed-date room. Anonymous profiles can be logged and judged internally, but they are not routed into chat.
117
+
118
+ ## Prompt Privacy Guard
119
+
120
+ The Gradio app and Node OpenClaw endpoint refuse chat requests that try to find, identify, locate, dox, or expose personal details about another person. Profile data may only be used for consent-based compatibility work.
121
+
122
+ ## Speed-Date Room
123
+
124
+ The Space includes a two-person speed-date room:
125
+
126
+ - Users sign in with a Hugging Face token.
127
+ - The active room is a public post inside the Space, but it shows only Hugging Face handles and room status.
128
+ - Chat lasts 20 minutes by default.
129
+ - Chat messages that include emails, phone numbers, addresses, URLs, social handles, or contact-platform prompts are blocked.
130
+ - The matchmaker agent can automatically connect two complete compatible profiles into the room.
131
+ - After the timer ends, both users can submit a mutual exchange form.
132
+ - Contact details are displayed only after both users submit and consent.
133
+
134
+ Runtime setting:
135
+
136
+ ```text
137
+ SPEED_DATE_SECONDS=1200
138
+ ```
139
+
140
+ Use a shorter value only for local testing.
141
+
142
+ ## Production Integration
143
+
144
+ For a private or protected Space, add secrets and wire the Gradio adapter to the production backend:
145
+
146
+ - `BACKEND_URL`
147
+ - `SUPABASE_URL`
148
+ - `SUPABASE_SERVICE_ROLE_KEY` or a scoped backend token
149
+ - `DATACLAW_PROFILE_KEY`
150
+ - `OPENCLAW_PROVIDER=local`
151
+ - `OPENCLAW_LOCAL_MODEL=microsoft/Phi-4-mini-instruct`
152
+ - `OPENCLAW_LOCAL_BASE_URL`, if profile completion should call a Phi server rather than the deterministic session flow
153
+
154
+ Do not expose service role keys in frontend-visible JavaScript or public client logs.
frontend/.gitignore ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Miscellaneous
2
+ *.class
3
+ *.log
4
+ *.pyc
5
+ *.swp
6
+ .DS_Store
7
+ .atom/
8
+ .build/
9
+ .buildlog/
10
+ .history
11
+ .svn/
12
+ .swiftpm/
13
+ migrate_working_dir/
14
+
15
+ # IntelliJ related
16
+ *.iml
17
+ *.ipr
18
+ *.iws
19
+ .idea/
20
+
21
+ # The .vscode folder contains launch configuration and tasks you configure in
22
+ # VS Code which you may wish to be included in version control, so this line
23
+ # is commented out by default.
24
+ #.vscode/
25
+
26
+ # Flutter/Dart/Pub related
27
+ **/doc/api/
28
+ **/ios/Flutter/.last_build_id
29
+ .dart_tool/
30
+ .flutter-plugins-dependencies
31
+ .pub-cache/
32
+ .pub/
33
+ /build/
34
+ /coverage/
35
+
36
+ # Symbolication related
37
+ app.*.symbols
38
+
39
+ # Obfuscation related
40
+ app.*.map.json
41
+
42
+ # Android Studio will place build artifacts here
43
+ /android/app/debug
44
+ /android/app/profile
45
+ /android/app/release
frontend/.metadata ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file tracks properties of this Flutter project.
2
+ # Used by Flutter tool to assess capabilities and perform upgrades etc.
3
+ #
4
+ # This file should be version controlled and should not be manually edited.
5
+
6
+ version:
7
+ revision: "924134a44c189315be2148659913dda1671cbe99"
8
+ channel: "stable"
9
+
10
+ project_type: app
11
+
12
+ # Tracks metadata for the flutter migrate command
13
+ migration:
14
+ platforms:
15
+ - platform: root
16
+ create_revision: 924134a44c189315be2148659913dda1671cbe99
17
+ base_revision: 924134a44c189315be2148659913dda1671cbe99
18
+ - platform: android
19
+ create_revision: 924134a44c189315be2148659913dda1671cbe99
20
+ base_revision: 924134a44c189315be2148659913dda1671cbe99
21
+ - platform: ios
22
+ create_revision: 924134a44c189315be2148659913dda1671cbe99
23
+ base_revision: 924134a44c189315be2148659913dda1671cbe99
24
+ - platform: web
25
+ create_revision: 924134a44c189315be2148659913dda1671cbe99
26
+ base_revision: 924134a44c189315be2148659913dda1671cbe99
27
+
28
+ # User provided section
29
+
30
+ # List of Local paths (relative to this file) that should be
31
+ # ignored by the migrate tool.
32
+ #
33
+ # Files that are not part of the templates will be ignored by default.
34
+ unmanaged_files:
35
+ - 'lib/main.dart'
36
+ - 'ios/Runner.xcodeproj/project.pbxproj'
frontend/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # opendatebase
2
 
3
  A new Flutter project.
4
 
 
1
+ # lovegpt
2
 
3
  A new Flutter project.
4
 
frontend/android/.gitignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradle-wrapper.jar
2
+ /.gradle
3
+ /captures/
4
+ /gradlew
5
+ /gradlew.bat
6
+ /local.properties
7
+ GeneratedPluginRegistrant.java
8
+ .cxx/
9
+
10
+ # Remember to never publicly share your keystore.
11
+ # See https://flutter.dev/to/reference-keystore
12
+ key.properties
13
+ **/*.keystore
14
+ **/*.jks
frontend/android/app/build.gradle.kts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ plugins {
2
+ id("com.android.application")
3
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
4
+ id("dev.flutter.flutter-gradle-plugin")
5
+ }
6
+
7
+ android {
8
+ namespace = "com.example.lovegpt"
9
+ compileSdk = flutter.compileSdkVersion
10
+ ndkVersion = flutter.ndkVersion
11
+
12
+ compileOptions {
13
+ sourceCompatibility = JavaVersion.VERSION_17
14
+ targetCompatibility = JavaVersion.VERSION_17
15
+ }
16
+
17
+ defaultConfig {
18
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
19
+ applicationId = "com.example.lovegpt"
20
+ // You can update the following values to match your application needs.
21
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
22
+ minSdk = flutter.minSdkVersion
23
+ targetSdk = flutter.targetSdkVersion
24
+ versionCode = flutter.versionCode
25
+ versionName = flutter.versionName
26
+ }
27
+
28
+ buildTypes {
29
+ release {
30
+ // TODO: Add your own signing config for the release build.
31
+ // Signing with the debug keys for now, so `flutter run --release` works.
32
+ signingConfig = signingConfigs.getByName("debug")
33
+ }
34
+ }
35
+ }
36
+
37
+ kotlin {
38
+ compilerOptions {
39
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
40
+ }
41
+ }
42
+
43
+ flutter {
44
+ source = "../.."
45
+ }
frontend/android/app/src/debug/AndroidManifest.xml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <!-- The INTERNET permission is required for development. Specifically,
3
+ the Flutter tool needs it to communicate with the running application
4
+ to allow setting breakpoints, to provide hot reload, etc.
5
+ -->
6
+ <uses-permission android:name="android.permission.INTERNET"/>
7
+ </manifest>
frontend/android/app/src/main/AndroidManifest.xml ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <application
3
+ android:label="loveGPT"
4
+ android:name="${applicationName}"
5
+ android:icon="@mipmap/ic_launcher">
6
+ <activity
7
+ android:name=".MainActivity"
8
+ android:exported="true"
9
+ android:launchMode="singleTop"
10
+ android:taskAffinity=""
11
+ android:theme="@style/LaunchTheme"
12
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
13
+ android:hardwareAccelerated="true"
14
+ android:windowSoftInputMode="adjustResize">
15
+ <!-- Specifies an Android theme to apply to this Activity as soon as
16
+ the Android process has started. This theme is visible to the user
17
+ while the Flutter UI initializes. After that, this theme continues
18
+ to determine the Window background behind the Flutter UI. -->
19
+ <meta-data
20
+ android:name="io.flutter.embedding.android.NormalTheme"
21
+ android:resource="@style/NormalTheme"
22
+ />
23
+ <intent-filter>
24
+ <action android:name="android.intent.action.MAIN"/>
25
+ <category android:name="android.intent.category.LAUNCHER"/>
26
+ </intent-filter>
27
+ </activity>
28
+ <!-- Don't delete the meta-data below.
29
+ This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
30
+ <meta-data
31
+ android:name="flutterEmbedding"
32
+ android:value="2" />
33
+ </application>
34
+ <!-- Required to query activities that can process text, see:
35
+ https://developer.android.com/training/package-visibility and
36
+ https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
37
+
38
+ In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
39
+ <queries>
40
+ <intent>
41
+ <action android:name="android.intent.action.PROCESS_TEXT"/>
42
+ <data android:mimeType="text/plain"/>
43
+ </intent>
44
+ </queries>
45
+ </manifest>
frontend/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package io.flutter.plugins;
2
+
3
+ import androidx.annotation.Keep;
4
+ import androidx.annotation.NonNull;
5
+ import io.flutter.Log;
6
+
7
+ import io.flutter.embedding.engine.FlutterEngine;
8
+
9
+ /**
10
+ * Generated file. Do not edit.
11
+ * This file is generated by the Flutter tool based on the
12
+ * plugins that support the Android platform.
13
+ */
14
+ @Keep
15
+ public final class GeneratedPluginRegistrant {
16
+ private static final String TAG = "GeneratedPluginRegistrant";
17
+ public static void registerWith(@NonNull FlutterEngine flutterEngine) {
18
+ try {
19
+ flutterEngine.getPlugins().add(new com.llfbandit.app_links.AppLinksPlugin());
20
+ } catch (Exception e) {
21
+ Log.e(TAG, "Error registering plugin app_links, com.llfbandit.app_links.AppLinksPlugin", e);
22
+ }
23
+ try {
24
+ flutterEngine.getPlugins().add(new com.github.dart_lang.jni.JniPlugin());
25
+ } catch (Exception e) {
26
+ Log.e(TAG, "Error registering plugin jni, com.github.dart_lang.jni.JniPlugin", e);
27
+ }
28
+ try {
29
+ flutterEngine.getPlugins().add(new com.github.dart_lang.jni_flutter.JniFlutterPlugin());
30
+ } catch (Exception e) {
31
+ Log.e(TAG, "Error registering plugin jni_flutter, com.github.dart_lang.jni_flutter.JniFlutterPlugin", e);
32
+ }
33
+ try {
34
+ flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
35
+ } catch (Exception e) {
36
+ Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
37
+ }
38
+ try {
39
+ flutterEngine.getPlugins().add(new com.csdcorp.speech_to_text.SpeechToTextPlugin());
40
+ } catch (Exception e) {
41
+ Log.e(TAG, "Error registering plugin speech_to_text, com.csdcorp.speech_to_text.SpeechToTextPlugin", e);
42
+ }
43
+ try {
44
+ flutterEngine.getPlugins().add(new com.flutter.stripe.StripeAndroidPlugin());
45
+ } catch (Exception e) {
46
+ Log.e(TAG, "Error registering plugin stripe_android, com.flutter.stripe.StripeAndroidPlugin", e);
47
+ }
48
+ try {
49
+ flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
50
+ } catch (Exception e) {
51
+ Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
52
+ }
53
+ }
54
+ }
frontend/android/app/src/main/kotlin/com/example/lovegpt/MainActivity.kt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ package com.example.lovegpt
2
+
3
+ import io.flutter.embedding.android.FlutterActivity
4
+
5
+ class MainActivity : FlutterActivity()
frontend/android/app/src/main/res/drawable-v21/launch_background.xml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Modify this file to customize your launch splash screen -->
3
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
4
+ <item android:drawable="?android:colorBackground" />
5
+
6
+ <!-- You can insert your own image assets here -->
7
+ <!-- <item>
8
+ <bitmap
9
+ android:gravity="center"
10
+ android:src="@mipmap/launch_image" />
11
+ </item> -->
12
+ </layer-list>
frontend/android/app/src/main/res/drawable/launch_background.xml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Modify this file to customize your launch splash screen -->
3
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
4
+ <item android:drawable="@android:color/white" />
5
+
6
+ <!-- You can insert your own image assets here -->
7
+ <!-- <item>
8
+ <bitmap
9
+ android:gravity="center"
10
+ android:src="@mipmap/launch_image" />
11
+ </item> -->
12
+ </layer-list>
frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png ADDED
frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png ADDED
frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png ADDED
frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png ADDED
frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png ADDED
frontend/android/app/src/main/res/values-night/styles.xml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
4
+ <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
5
+ <!-- Show a splash screen on the activity. Automatically removed when
6
+ the Flutter engine draws its first frame -->
7
+ <item name="android:windowBackground">@drawable/launch_background</item>
8
+ </style>
9
+ <!-- Theme applied to the Android Window as soon as the process has started.
10
+ This theme determines the color of the Android Window while your
11
+ Flutter UI initializes, as well as behind your Flutter UI while its
12
+ running.
13
+
14
+ This Theme is only used starting with V2 of Flutter's Android embedding. -->
15
+ <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
16
+ <item name="android:windowBackground">?android:colorBackground</item>
17
+ </style>
18
+ </resources>