AliSakr9997 commited on
Commit
27c8ef8
·
verified ·
1 Parent(s): d4bef91

Add files using upload-large-folder tool

Browse files
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ ENV PYTHONDONTWRITEBYTECODE=1
6
+ ENV PYTHONUNBUFFERED=1
7
+
8
+ RUN apt-get update \
9
+ && apt-get install -y --no-install-recommends build-essential \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ COPY requirements.txt ./
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ COPY . ./
16
+
17
+ EXPOSE 7860
18
+
19
+ CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "7860"]
PRODUCTION_READINESS.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Production Readiness Audit
2
+
3
+ Scope: frontend, database, features, and user experience.
4
+
5
+ ## High Priority Gaps
6
+ - Auth is not secure or enforced: APIs accept `user_id` from client and return user history without verifying identity. Any client can access other users' data. Implement token-based auth and derive user_id server-side.
7
+ - Passwords stored insecurely: client stores plaintext password in SharedPreferences; server allows plaintext fallback and uses unsalted SHA-256. Use bcrypt/argon2; remove plaintext fallback; store tokens in secure storage only.
8
+ - Check-ins not persisted in DB: daily check-ins stored locally only. Add a `checkins` table and persist via API.
9
+ - Database schema incomplete for core features: missing tables for check-ins, goals, sessions, etc. Add required tables and foreign keys.
10
+
11
+ ## Medium Priority Gaps
12
+ - Hardcoded API base URL: no staging/production environment separation. Move to environment config.
13
+ - Logout does not clear stored user state: it navigates without clearing saved data. Call a clear/reset routine on logout.
14
+ - DASS scale mismatch risk: UI uses 0-4 while DASS-42 is typically 0-3. Align scale or adjust scoring logic.
15
+ - Error handling is weak: network failures often return empty lists or generic errors. Add timeouts, retries, and user-friendly error states.
16
+
17
+ ## UX and Feature Completeness
18
+ - Auto-login missing: splash always routes to onboarding even if user is logged in. Route based on `AppState.isLoggedIn`.
19
+ - Placeholder actions: "Forgot Password", social login, profile edits are non-functional. Implement or remove until ready.
20
+
21
+ ## Data and Analytics Limitations
22
+ - Analyses not fully stored: minimal fields saved; missing detailed scores/recommendations and client timestamps. Persist full output fields for analytics and debugging.
23
+ - Indexing missing: no indexes on `created_at`/`user_id` for history queries. Add indexes for performance.
24
+
25
+ ## Backend Gaps
26
+ - No auth enforcement: endpoints accept user_id from clients and return data without verifying identity. Add JWT/session auth and authorization checks.
27
+ - Password handling is weak: unsalted SHA-256 and plaintext fallback. Move to bcrypt/argon2 and remove plaintext path.
28
+ - CORS is wide open: `allow_origins=["*"]` with credentials. Restrict origins and limit credentials.
29
+ - No rate limiting or abuse protection: add per-IP/user throttling on auth and analysis endpoints.
30
+ - Error handling leaks details: raw exceptions are returned. Normalize error responses and hide internals.
31
+ - No database migrations: relies on `create_all`. Introduce Alembic migrations and schema versioning.
32
+ - Missing structured logging/monitoring: add request logs, tracing, and health checks.
README.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ ---
3
+ title: SafeSpace API
4
+ emoji: 🧠
5
+ colorFrom: blue
6
+ colorTo: green
7
+ sdk: docker
8
+ python_version: "3.11"
9
+ pinned: false
10
+ ---
11
+
12
+ # 🧠 Mental Health Prediction System
13
+
14
+ ## 📌 Project Overview
15
+
16
+ This project builds a **Machine Learning system** that predicts mental health conditions based on questionnaire answers.
17
+
18
+ The system predicts:
19
+
20
+ - Depression Level
21
+ - Anxiety Level
22
+ - Stress Level
23
+
24
+ The model takes questionnaire answers as input and outputs:
25
+
26
+ - Percentage of Depression
27
+ - Percentage of Anxiety
28
+ - Percentage of Stress
29
+
30
+ ---
31
+
32
+ # 🗺️ Project Roadmap
33
+
34
+ ## Phase 1 — Problem Definition
35
+
36
+ ### Goal
37
+
38
+ Build a Machine Learning model that predicts mental health conditions from questionnaire answers.
39
+
40
+ The system should:
41
+
42
+ - Accept user answers
43
+ - Predict mental health scores
44
+ - Show percentages
45
+ - Classify severity levels
46
+
47
+ Predicted Conditions:
48
+
49
+ - Depression
50
+ - Anxiety
51
+ - Stress
52
+
53
+ ---
54
+
55
+ ## Phase 2 — Dataset
56
+
57
+ ### Dataset Description
58
+
59
+ The dataset contains questionnaire answers and mental health scores.
60
+
61
+ ### Dataset Size
62
+
63
+ - Samples: **39,775**
64
+ - Features: **42 questions**
65
+ - Targets: **3 scores**
66
+
67
+ Targets:
68
+
69
+ - Depression Score
70
+ - Anxiety Score
71
+ - Stress Score
72
+
73
+ Example Features:
clean_git_repo/Untitled.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
index.html ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>SafeSpace API Test</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0f1a16;
10
+ --panel: #16231f;
11
+ --accent: #4fd1a5;
12
+ --accent-2: #7ce3c3;
13
+ --text: #e8f6f1;
14
+ --muted: #9bb7ad;
15
+ --danger: #f87171;
16
+ --border: #274136;
17
+ }
18
+
19
+ * { box-sizing: border-box; }
20
+
21
+ body {
22
+ margin: 0;
23
+ font-family: "Space Grotesk", "Figtree", "Montserrat", sans-serif;
24
+ background: radial-gradient(circle at 15% 10%, #193229, #0f1a16 55%);
25
+ color: var(--text);
26
+ }
27
+
28
+ header {
29
+ padding: 32px 24px 16px;
30
+ text-align: center;
31
+ }
32
+
33
+ header h1 {
34
+ margin: 0 0 8px;
35
+ font-size: 30px;
36
+ letter-spacing: 0.4px;
37
+ }
38
+
39
+ header p {
40
+ margin: 0;
41
+ color: var(--muted);
42
+ }
43
+
44
+ main {
45
+ max-width: 980px;
46
+ margin: 0 auto;
47
+ padding: 24px;
48
+ display: grid;
49
+ gap: 18px;
50
+ }
51
+
52
+ .panel {
53
+ background: var(--panel);
54
+ border: 1px solid var(--border);
55
+ border-radius: 18px;
56
+ padding: 20px;
57
+ box-shadow: 0 14px 34px rgba(0, 0, 0, 0.25);
58
+ }
59
+
60
+ label {
61
+ display: block;
62
+ font-size: 13px;
63
+ text-transform: uppercase;
64
+ letter-spacing: 1.5px;
65
+ color: var(--muted);
66
+ margin-bottom: 8px;
67
+ }
68
+
69
+ input, textarea {
70
+ width: 100%;
71
+ padding: 12px 14px;
72
+ border-radius: 12px;
73
+ border: 1px solid var(--border);
74
+ background: #0c1512;
75
+ color: var(--text);
76
+ font-size: 15px;
77
+ }
78
+
79
+ textarea { min-height: 120px; resize: vertical; }
80
+
81
+ .row { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
82
+
83
+ button {
84
+ border: none;
85
+ background: linear-gradient(120deg, var(--accent), var(--accent-2));
86
+ color: #062318;
87
+ font-weight: 700;
88
+ font-size: 15px;
89
+ padding: 12px 18px;
90
+ border-radius: 12px;
91
+ cursor: pointer;
92
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
93
+ width: 100%;
94
+ }
95
+
96
+ button:hover { transform: translateY(-1px); box-shadow: 0 10px 18px rgba(79, 209, 165, 0.2); }
97
+
98
+ pre {
99
+ white-space: pre-wrap;
100
+ word-break: break-word;
101
+ background: #0b1512;
102
+ border: 1px solid #1f352d;
103
+ border-radius: 12px;
104
+ padding: 16px;
105
+ min-height: 120px;
106
+ }
107
+
108
+ .error { color: var(--danger); font-weight: 600; }
109
+ </style>
110
+ </head>
111
+ <body>
112
+ <header>
113
+ <h1>SafeSpace API Test</h1>
114
+ <p>Quickly validate AI endpoints on your Hugging Face Space.</p>
115
+ </header>
116
+
117
+ <main>
118
+ <section class="panel">
119
+ <div class="row">
120
+ <div>
121
+ <label for="baseUrl">Base URL</label>
122
+ <input id="baseUrl" value="https://AliSakr9997-safespace.hf.space" />
123
+ </div>
124
+ <div>
125
+ <label for="userId">User ID</label>
126
+ <input id="userId" value="1" />
127
+ </div>
128
+ </div>
129
+ </section>
130
+
131
+ <section class="panel">
132
+ <label for="textInput">User Text</label>
133
+ <textarea id="textInput">I have been feeling overwhelmed at work and can't sleep.</textarea>
134
+ </section>
135
+
136
+ <section class="panel">
137
+ <label for="survey">Survey Answers (42 values, 0-4, comma-separated)</label>
138
+ <textarea id="survey">0,1,2,1,0,2,1,0,1,2,1,0,2,1,0,2,1,1,0,2,1,2,1,0,2,1,0,1,2,1,0,2,1,0,1,2,1,0,2,1,0,1</textarea>
139
+ </section>
140
+
141
+ <section class="panel">
142
+ <div class="row">
143
+ <button id="analyzeBtn">POST /v1/analysis</button>
144
+ <button id="historyBtn">GET /v1/users/{id}/analyses</button>
145
+ </div>
146
+ </section>
147
+
148
+ <section class="panel">
149
+ <label>Response</label>
150
+ <pre id="output">Waiting for request...</pre>
151
+ </section>
152
+ </main>
153
+
154
+ <script>
155
+ const output = document.getElementById('output');
156
+ const baseUrl = document.getElementById('baseUrl');
157
+ const userId = document.getElementById('userId');
158
+ const textInput = document.getElementById('textInput');
159
+ const survey = document.getElementById('survey');
160
+
161
+ function setOutput(data, isError = false) {
162
+ if (isError) {
163
+ output.innerHTML = '<span class="error">' + data + '</span>';
164
+ } else {
165
+ output.textContent = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
166
+ }
167
+ }
168
+
169
+ function parseSurvey() {
170
+ return survey.value
171
+ .split(',')
172
+ .map((v) => parseInt(v.trim(), 10))
173
+ .filter((v) => Number.isFinite(v));
174
+ }
175
+
176
+ document.getElementById('analyzeBtn').addEventListener('click', async () => {
177
+ setOutput('Sending request...');
178
+ try {
179
+ const payload = {
180
+ user_id: userId.value,
181
+ text: textInput.value,
182
+ survey_answers: parseSurvey(),
183
+ locale: 'en',
184
+ client_ts: new Date().toISOString(),
185
+ };
186
+ const res = await fetch(`${baseUrl.value}/v1/analysis`, {
187
+ method: 'POST',
188
+ headers: { 'Content-Type': 'application/json' },
189
+ body: JSON.stringify(payload),
190
+ });
191
+ const json = await res.json();
192
+ setOutput(json);
193
+ } catch (err) {
194
+ setOutput(err.message || String(err), true);
195
+ }
196
+ });
197
+
198
+ document.getElementById('historyBtn').addEventListener('click', async () => {
199
+ setOutput('Fetching history...');
200
+ try {
201
+ const res = await fetch(`${baseUrl.value}/v1/users/${userId.value}/analyses?limit=20&offset=0`);
202
+ const json = await res.json();
203
+ setOutput(json);
204
+ } catch (err) {
205
+ setOutput(err.message || String(err), true);
206
+ }
207
+ });
208
+ </script>
209
+ </body>
210
+ </html>
mental_xlmr_final/checkpoint-3670 ADDED
@@ -0,0 +1 @@
 
 
1
+
mental_xlmr_final/config.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_cross_attention": false,
3
+ "architectures": [
4
+ "XLMRobertaForSequenceClassification"
5
+ ],
6
+ "attention_probs_dropout_prob": 0.1,
7
+ "bos_token_id": 0,
8
+ "classifier_dropout": null,
9
+ "dtype": "float32",
10
+ "eos_token_id": 2,
11
+ "hidden_act": "gelu",
12
+ "hidden_dropout_prob": 0.1,
13
+ "hidden_size": 768,
14
+ "id2label": {
15
+ "0": "LABEL_0",
16
+ "1": "LABEL_1",
17
+ "2": "LABEL_2"
18
+ },
19
+ "initializer_range": 0.02,
20
+ "intermediate_size": 3072,
21
+ "is_decoder": false,
22
+ "label2id": {
23
+ "LABEL_0": 0,
24
+ "LABEL_1": 1,
25
+ "LABEL_2": 2
26
+ },
27
+ "layer_norm_eps": 1e-05,
28
+ "max_position_embeddings": 514,
29
+ "model_type": "xlm-roberta",
30
+ "num_attention_heads": 12,
31
+ "num_hidden_layers": 12,
32
+ "output_past": true,
33
+ "pad_token_id": 1,
34
+ "position_embedding_type": "absolute",
35
+ "tie_word_embeddings": true,
36
+ "transformers_version": "5.0.0",
37
+ "type_vocab_size": 1,
38
+ "use_cache": false,
39
+ "vocab_size": 250002
40
+ }
mental_xlmr_final/tokenizer_config.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": true,
3
+ "backend": "tokenizers",
4
+ "bos_token": "<s>",
5
+ "cls_token": "<s>",
6
+ "eos_token": "</s>",
7
+ "is_local": false,
8
+ "mask_token": "<mask>",
9
+ "model_max_length": 512,
10
+ "pad_token": "<pad>",
11
+ "sep_token": "</s>",
12
+ "tokenizer_class": "XLMRobertaTokenizer",
13
+ "unk_token": "<unk>"
14
+ }
migrations/20260508_init.sql ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Initial schema and indexes for production (Postgres compatible)
2
+
3
+ CREATE TABLE IF NOT EXISTS users (
4
+ id SERIAL PRIMARY KEY,
5
+ email VARCHAR(255),
6
+ password VARCHAR(255),
7
+ created_at TIMESTAMP WITHOUT TIME ZONE,
8
+ name TEXT
9
+ );
10
+
11
+ CREATE TABLE IF NOT EXISTS analyses (
12
+ id SERIAL PRIMARY KEY,
13
+ user_id INTEGER NULL,
14
+ primary_condition VARCHAR(255),
15
+ clinical_scoring JSON,
16
+ created_at TIMESTAMP WITHOUT TIME ZONE,
17
+ text_input TEXT,
18
+ text_input_hash TEXT,
19
+ text_scores JSONB,
20
+ survey_scores JSONB,
21
+ fused_scores JSONB,
22
+ severity TEXT,
23
+ cause TEXT,
24
+ suicidal_flag BOOLEAN DEFAULT FALSE,
25
+ model_version TEXT,
26
+ app_version TEXT,
27
+ locale TEXT
28
+ );
29
+
30
+ CREATE TABLE IF NOT EXISTS checkins (
31
+ id SERIAL PRIMARY KEY,
32
+ user_id INTEGER NOT NULL,
33
+ mood INTEGER NOT NULL,
34
+ sleep INTEGER NOT NULL,
35
+ energy DOUBLE PRECISION NOT NULL,
36
+ created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW()
37
+ );
38
+
39
+ CREATE TABLE IF NOT EXISTS journal_entries (
40
+ id SERIAL PRIMARY KEY,
41
+ user_id INTEGER,
42
+ content TEXT NOT NULL,
43
+ created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
44
+ updated_at TIMESTAMP WITHOUT TIME ZONE
45
+ );
46
+
47
+ CREATE TABLE IF NOT EXISTS user_preferences (
48
+ user_id INTEGER PRIMARY KEY,
49
+ theme TEXT DEFAULT 'dark',
50
+ language TEXT DEFAULT 'en',
51
+ notifications_enabled BOOLEAN DEFAULT TRUE,
52
+ crisis_locale TEXT
53
+ );
54
+
55
+ CREATE TABLE IF NOT EXISTS consents (
56
+ id SERIAL PRIMARY KEY,
57
+ user_id INTEGER,
58
+ consent_type TEXT NOT NULL,
59
+ granted BOOLEAN NOT NULL DEFAULT FALSE,
60
+ created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
61
+ );
62
+
63
+ CREATE INDEX IF NOT EXISTS ix_analyses_user_id_created_at ON analyses (user_id, created_at);
64
+ CREATE INDEX IF NOT EXISTS ix_checkins_user_id_created_at ON checkins (user_id, created_at);
65
+
66
+ DO $$
67
+ BEGIN
68
+ IF NOT EXISTS (
69
+ SELECT 1 FROM information_schema.table_constraints
70
+ WHERE constraint_name = 'fk_analyses_user'
71
+ ) THEN
72
+ ALTER TABLE analyses
73
+ ADD CONSTRAINT fk_analyses_user
74
+ FOREIGN KEY (user_id) REFERENCES users(id);
75
+ END IF;
76
+
77
+ IF NOT EXISTS (
78
+ SELECT 1 FROM information_schema.table_constraints
79
+ WHERE constraint_name = 'fk_checkins_user'
80
+ ) THEN
81
+ ALTER TABLE checkins
82
+ ADD CONSTRAINT fk_checkins_user
83
+ FOREIGN KEY (user_id) REFERENCES users(id);
84
+ END IF;
85
+
86
+ IF NOT EXISTS (
87
+ SELECT 1 FROM information_schema.table_constraints
88
+ WHERE constraint_name = 'fk_journal_entries_user'
89
+ ) THEN
90
+ ALTER TABLE journal_entries
91
+ ADD CONSTRAINT fk_journal_entries_user
92
+ FOREIGN KEY (user_id) REFERENCES users(id);
93
+ END IF;
94
+
95
+ IF NOT EXISTS (
96
+ SELECT 1 FROM information_schema.table_constraints
97
+ WHERE constraint_name = 'fk_consents_user'
98
+ ) THEN
99
+ ALTER TABLE consents
100
+ ADD CONSTRAINT fk_consents_user
101
+ FOREIGN KEY (user_id) REFERENCES users(id);
102
+ END IF;
103
+
104
+ IF NOT EXISTS (
105
+ SELECT 1 FROM information_schema.table_constraints
106
+ WHERE constraint_name = 'fk_user_preferences_user'
107
+ ) THEN
108
+ ALTER TABLE user_preferences
109
+ ADD CONSTRAINT fk_user_preferences_user
110
+ FOREIGN KEY (user_id) REFERENCES users(id);
111
+ END IF;
112
+ END $$;
model2.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
scratch/check_data.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine, text
2
+ import json
3
+
4
+ DATABASE_URL = "postgresql://safespace:AdminAdmin@postgresql-208383-0.cloudclusters.net:19712/safespace"
5
+ engine = create_engine(DATABASE_URL)
6
+
7
+ with engine.connect() as conn:
8
+ result = conn.execute(text("SELECT id, user_id, primary_condition, clinical_scoring, created_at FROM analyses WHERE user_id = 102 ORDER BY created_at DESC LIMIT 10"))
9
+ rows = [dict(row._mapping) for row in result]
10
+
11
+ print(f"Found {len(rows)} records for user 102")
12
+ for r in rows:
13
+ print(f"ID: {r['id']}, Date: {r['created_at']}, Primary: {r['primary_condition']}, Scores: {r['clinical_scoring']}")
temp_hf_space/api.py ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from datetime import datetime
3
+ import hashlib
4
+
5
+ import httpx
6
+ from fastapi import FastAPI, HTTPException, Depends
7
+ from fastapi.responses import HTMLResponse
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from pydantic import BaseModel, Field
10
+ from typing import Optional
11
+
12
+ from core_ai import predict_text, predict_survey, fuse_scores
13
+ from recommendations import get_recommendations
14
+
15
+ # --- DATABASE SETUP ---
16
+ from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, JSON
17
+ from sqlalchemy.orm import declarative_base, sessionmaker, Session
18
+
19
+ DATABASE_URL = os.environ.get("DATABASE_URL")
20
+ if DATABASE_URL and DATABASE_URL.startswith("postgres://"):
21
+ DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://", 1)
22
+
23
+ engine = create_engine(DATABASE_URL, connect_args={'connect_timeout': 5}) if DATABASE_URL else None
24
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) if engine else None
25
+ Base = declarative_base()
26
+
27
+
28
+ class DBUser(Base):
29
+ __tablename__ = "users"
30
+ id = Column(Integer, primary_key=True, index=True)
31
+ name = Column(String, nullable=True)
32
+ email = Column(String, unique=True, index=True)
33
+ password = Column(String)
34
+ created_at = Column(DateTime, default=datetime.utcnow)
35
+
36
+
37
+ class DBAnalysis(Base):
38
+ __tablename__ = "analyses"
39
+ id = Column(Integer, primary_key=True, index=True)
40
+ user_id = Column(Integer, index=True, nullable=True)
41
+ primary_condition = Column(String)
42
+ clinical_scoring = Column(JSON)
43
+ created_at = Column(DateTime, default=datetime.utcnow)
44
+
45
+ # --- APP SETUP ---
46
+ app = FastAPI(title="SafeSpace API", version="1.0.0")
47
+
48
+
49
+ @app.on_event("startup")
50
+ async def startup_event():
51
+ import asyncio
52
+ if engine:
53
+ try:
54
+ await asyncio.wait_for(
55
+ asyncio.to_thread(Base.metadata.create_all, bind=engine),
56
+ timeout=8.0
57
+ )
58
+ print("Database connected and tables verified.")
59
+ except asyncio.TimeoutError:
60
+ print("Database connection timed out during startup - server will start without DB verification.")
61
+ except Exception as e:
62
+ print(f"Database connection failed during startup: {e}")
63
+ print("Application startup complete.")
64
+
65
+
66
+ def get_db():
67
+ if not SessionLocal:
68
+ yield None
69
+ else:
70
+ db = SessionLocal()
71
+ try:
72
+ yield db
73
+ finally:
74
+ db.close()
75
+
76
+ # Add CORS so Flutter app can communicate with it
77
+ app.add_middleware(
78
+ CORSMiddleware,
79
+ allow_origins=["*"],
80
+ allow_credentials=True,
81
+ allow_methods=["*"],
82
+ allow_headers=["*"],
83
+ )
84
+
85
+ # --- Password Hashing ---
86
+ def hash_password(password: str) -> str:
87
+ return hashlib.sha256(password.encode()).hexdigest()
88
+
89
+ # --- DASS-42 Clinical Scoring ---
90
+ def calculate_dass_clinical_score(answers: list) -> dict:
91
+ dep_idx = [2, 4, 9, 12, 15, 16, 20, 23, 25, 30, 33, 36, 37, 41]
92
+ anx_idx = [1, 3, 6, 8, 14, 18, 19, 22, 24, 27, 29, 35, 39, 40]
93
+ str_idx = [0, 5, 7, 10, 11, 13, 17, 21, 26, 28, 31, 32, 34, 38]
94
+
95
+ dep_score = sum(answers[i] for i in dep_idx)
96
+ anx_score = sum(answers[i] for i in anx_idx)
97
+ str_score = sum(answers[i] for i in str_idx)
98
+
99
+ def get_severity(score, bounds):
100
+ if score <= bounds[0]: return "Normal"
101
+ if score <= bounds[1]: return "Mild"
102
+ if score <= bounds[2]: return "Moderate"
103
+ if score <= bounds[3]: return "Severe"
104
+ return "Extremely Severe"
105
+
106
+ return {
107
+ "depression": {"score": dep_score, "severity": get_severity(dep_score, [9, 13, 20, 27])},
108
+ "anxiety": {"score": anx_score, "severity": get_severity(anx_score, [7, 9, 14, 19])},
109
+ "stress": {"score": str_score, "severity": get_severity(str_score, [14, 18, 25, 33])}
110
+ }
111
+
112
+ # --- API MODELS ---
113
+ class AnalysisRequest(BaseModel):
114
+ user_id: str | int = Field(default=None, description="User identifier")
115
+ text: str = Field(..., min_length=1)
116
+ survey_answers: list[int] = Field(..., min_items=42, max_items=42)
117
+ locale: str = Field(default="en")
118
+ client_ts: str | None = None
119
+
120
+
121
+ class AnalyzeRequest(BaseModel):
122
+ text: str = Field(..., description="The user's response in text (Arabic/English)")
123
+ survey_answers: list[int] = Field(..., min_items=42, max_items=42, description="List of 42 integers (0-4) representing DASS-42 survey answers")
124
+ user_id: int | None = Field(default=None, description="Optional user ID to link analysis to a user")
125
+
126
+
127
+ class ChatRequest(BaseModel):
128
+ message: str
129
+ session_id: Optional[str] = "default"
130
+
131
+
132
+ class ChatResponse(BaseModel):
133
+ reply: str
134
+
135
+
136
+ class SignupRequest(BaseModel):
137
+ name: str = Field(..., min_length=1)
138
+ email: str = Field(..., min_length=5)
139
+ password: str = Field(..., min_length=4)
140
+
141
+
142
+ class LoginRequest(BaseModel):
143
+ email: str = Field(..., min_length=5)
144
+ password: str = Field(..., min_length=1)
145
+
146
+
147
+ # --- ENDPOINTS ---
148
+ @app.get("/")
149
+ def root():
150
+ return {"status": "ok", "message": "SafeSpace API"}
151
+
152
+
153
+ @app.get("/test", response_class=HTMLResponse)
154
+ def test_page():
155
+ html_path = os.path.join(os.path.dirname(__file__), "index.html")
156
+ if not os.path.exists(html_path):
157
+ raise HTTPException(status_code=404, detail="index.html not found")
158
+ with open(html_path, "r", encoding="utf-8") as f:
159
+ return f.read()
160
+
161
+
162
+ # --- AUTH ENDPOINTS ---
163
+ @app.post("/api/v1/auth/signup")
164
+ async def signup(request: SignupRequest, db: Session = Depends(get_db)):
165
+ if not db:
166
+ raise HTTPException(status_code=500, detail="Database not available")
167
+
168
+ # Check if email already exists
169
+ existing = db.query(DBUser).filter(DBUser.email == request.email).first()
170
+ if existing:
171
+ raise HTTPException(status_code=400, detail="Email already registered")
172
+
173
+ # Create new user
174
+ try:
175
+ new_user = DBUser(
176
+ name=request.name,
177
+ email=request.email,
178
+ password=hash_password(request.password),
179
+ )
180
+ db.add(new_user)
181
+ db.commit()
182
+ db.refresh(new_user)
183
+
184
+ return {
185
+ "user_id": new_user.id,
186
+ "email": new_user.email,
187
+ "name": new_user.name,
188
+ "message": "Account created successfully"
189
+ }
190
+ except Exception as e:
191
+ db.rollback()
192
+ raise HTTPException(status_code=500, detail=f"Failed to create account: {str(e)}")
193
+
194
+
195
+ @app.post("/api/v1/auth/login")
196
+ async def login(request: LoginRequest, db: Session = Depends(get_db)):
197
+ if not db:
198
+ raise HTTPException(status_code=500, detail="Database not available")
199
+
200
+ user = db.query(DBUser).filter(DBUser.email == request.email).first()
201
+ if not user:
202
+ raise HTTPException(status_code=401, detail="Email not found")
203
+
204
+ if user.password != hash_password(request.password):
205
+ # Also try plain-text match for legacy users who signed up before hashing
206
+ if user.password != request.password:
207
+ raise HTTPException(status_code=401, detail="Incorrect password")
208
+
209
+ return {
210
+ "user_id": user.id,
211
+ "email": user.email,
212
+ "name": user.name or "",
213
+ "message": "Login successful"
214
+ }
215
+
216
+
217
+ # New-style endpoint (used by index.html test page)
218
+ @app.post("/v1/analysis")
219
+ def analyze(payload: AnalysisRequest, db: Session = Depends(get_db)):
220
+ text_scores = predict_text(payload.text)
221
+ survey_scores = predict_survey(payload.survey_answers)
222
+ final_scores = fuse_scores(text_scores, survey_scores)
223
+ primary = max(final_scores, key=final_scores.get)
224
+ clinical = calculate_dass_clinical_score(payload.survey_answers)
225
+ rec = get_recommendations(primary, final_scores[primary], payload.text)
226
+
227
+ # Save to PostgreSQL if DB is connected
228
+ if db:
229
+ try:
230
+ new_analysis = DBAnalysis(
231
+ primary_condition=primary,
232
+ clinical_scoring=clinical
233
+ )
234
+ db.add(new_analysis)
235
+ db.commit()
236
+ except Exception as e:
237
+ print(f"DB save error: {e}")
238
+
239
+ return {
240
+ "analysis_id": None,
241
+ "primary": primary,
242
+ "scores": final_scores,
243
+ "severity": rec.get("severity"),
244
+ "cause": rec.get("cause"),
245
+ "recommendations": {
246
+ "tips_en": rec.get("tips_en", []),
247
+ "tips_ar": rec.get("tips_ar", []),
248
+ "resources_en": rec.get("resources_en", []),
249
+ "resources_ar": rec.get("resources_ar", []),
250
+ "referral_en": rec.get("referral_en", ""),
251
+ "referral_ar": rec.get("referral_ar", ""),
252
+ },
253
+ "suicidal_flag": rec.get("suicidal_flag", False),
254
+ "created_at": datetime.utcnow().isoformat() + "Z",
255
+ }
256
+
257
+ # Flutter-compatible endpoint (used by api_service.dart)
258
+ @app.post("/api/v1/analyze")
259
+ async def analyze_mental_health(request: AnalyzeRequest, db: Session = Depends(get_db)):
260
+ try:
261
+ text_scores = predict_text(request.text)
262
+ survey_scores = predict_survey(request.survey_answers)
263
+ final_scores = fuse_scores(text_scores, survey_scores)
264
+ primary = max(final_scores, key=final_scores.get)
265
+ clinical = calculate_dass_clinical_score(request.survey_answers)
266
+ rec = get_recommendations(primary, final_scores[primary], request.text)
267
+
268
+ # Save to PostgreSQL if DB is connected
269
+ if db:
270
+ try:
271
+ new_analysis = DBAnalysis(
272
+ user_id=request.user_id,
273
+ primary_condition=primary,
274
+ clinical_scoring=clinical
275
+ )
276
+ db.add(new_analysis)
277
+ db.commit()
278
+ except Exception as e:
279
+ print(f"DB save error: {e}")
280
+
281
+ return {
282
+ "primary_condition": primary,
283
+ "fused_scores": final_scores,
284
+ "text_scores": text_scores,
285
+ "survey_scores": survey_scores,
286
+ "clinical_scoring": clinical,
287
+ "recommendations": rec
288
+ }
289
+ except Exception as e:
290
+ raise HTTPException(status_code=500, detail=str(e))
291
+
292
+ # Flutter-compatible history endpoint
293
+ @app.get("/api/v1/analyses/history")
294
+ async def get_analyses_history(user_id: int = None, db: Session = Depends(get_db)):
295
+ try:
296
+ if not db:
297
+ return []
298
+
299
+ query = db.query(DBAnalysis)
300
+
301
+ # Filter by user_id if provided
302
+ if user_id is not None:
303
+ query = query.filter(DBAnalysis.user_id == user_id)
304
+
305
+ # Get the 10 most recent analyses, sorted by created_at ascending (oldest first for graphing)
306
+ records = query.order_by(DBAnalysis.created_at.desc()).limit(10).all()
307
+
308
+ history = []
309
+ for r in reversed(records): # Reverse so oldest is first
310
+ if r.clinical_scoring:
311
+ history.append({
312
+ "id": r.id,
313
+ "date": r.created_at.strftime("%b %d"),
314
+ "depression": r.clinical_scoring.get("depression", {}).get("score", 0),
315
+ "anxiety": r.clinical_scoring.get("anxiety", {}).get("score", 0),
316
+ "stress": r.clinical_scoring.get("stress", {}).get("score", 0),
317
+ "primary": r.primary_condition
318
+ })
319
+ return history
320
+ except Exception as e:
321
+ raise HTTPException(status_code=500, detail=str(e))
322
+
323
+ @app.post("/api/v1/chat", response_model=ChatResponse)
324
+ async def chat_with_ai(request: ChatRequest):
325
+ api_url = os.environ.get("AI_API_URL")
326
+ api_key = os.environ.get("AI_API_KEY")
327
+ chatflow_id = os.environ.get("AI_CHATFLOW_ID")
328
+
329
+ if not api_url or not api_key or not chatflow_id:
330
+ raise HTTPException(status_code=500, detail="AI API credentials are not configured in Secrets.")
331
+
332
+ endpoint = f"{api_url}/api/v1/prediction/{chatflow_id}"
333
+ headers = {"Authorization": f"Bearer {api_key}"}
334
+ payload = {"question": request.message, "overrideConfig": {"sessionId": request.session_id}}
335
+
336
+ async with httpx.AsyncClient() as client:
337
+ try:
338
+ response = await client.post(endpoint, json=payload, headers=headers, timeout=30.0)
339
+ response.raise_for_status()
340
+ data = response.json()
341
+ return ChatResponse(reply=data.get("text") or data.get("answer") or str(data))
342
+ except Exception as e:
343
+ raise HTTPException(status_code=502, detail=f"Failed to communicate with AI API: {str(e)}")
temp_space/.gitattributes ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ clean_git_repo/data.csv filter=lfs diff=lfs merge=lfs -text
37
+ clean_git_repo/mental_xlmr_final/tokenizer.json filter=lfs diff=lfs merge=lfs -text
38
+ clean_git_repo/UI/safespace/.dart_tool/chrome-device/Default/History filter=lfs diff=lfs merge=lfs -text
39
+ clean_git_repo/UI/safespace/.dart_tool/chrome-device/Default/shared_proto_db/000003.log filter=lfs diff=lfs merge=lfs -text
40
+ clean_git_repo/UI/safespace/.dart_tool/chrome-device/Default/Web[[:space:]]Data filter=lfs diff=lfs merge=lfs -text
41
+ clean_git_repo/UI/safespace/build/8730b13ca0249799964d0a71255a8f3b.cache.dill.track.dill filter=lfs diff=lfs merge=lfs -text
42
+ clean_git_repo/UI/safespace/build/flutter_assets/fonts/MaterialIcons-Regular.otf filter=lfs diff=lfs merge=lfs -text
43
+ clean_git_repo/UI/safespace/build/flutter_assets/packages/cupertino_icons/assets/CupertinoIcons.ttf filter=lfs diff=lfs merge=lfs -text
44
+ clean_git_repo/UI/safespace/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png filter=lfs diff=lfs merge=lfs -text
45
+ data.csv filter=lfs diff=lfs merge=lfs -text
46
+ mental_xlmr_final/tokenizer.json filter=lfs diff=lfs merge=lfs -text
47
+ UI/safespace/.dart_tool/chrome-device/Default/History filter=lfs diff=lfs merge=lfs -text
48
+ UI/safespace/.dart_tool/chrome-device/Default/shared_proto_db/000003.log filter=lfs diff=lfs merge=lfs -text
49
+ UI/safespace/.dart_tool/chrome-device/Default/Web[[:space:]]Data filter=lfs diff=lfs merge=lfs -text
50
+ UI/safespace/build/8730b13ca0249799964d0a71255a8f3b.cache.dill.track.dill filter=lfs diff=lfs merge=lfs -text
51
+ UI/safespace/build/flutter_assets/fonts/MaterialIcons-Regular.otf filter=lfs diff=lfs merge=lfs -text
52
+ UI/safespace/build/flutter_assets/packages/cupertino_icons/assets/CupertinoIcons.ttf filter=lfs diff=lfs merge=lfs -text
53
+ UI/safespace/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png filter=lfs diff=lfs merge=lfs -text
temp_space/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+
2
+ push_to_space.py
temp_space/Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ ENV PYTHONDONTWRITEBYTECODE=1
6
+ ENV PYTHONUNBUFFERED=1
7
+
8
+ RUN apt-get update \
9
+ && apt-get install -y --no-install-recommends build-essential \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ COPY requirements.txt ./
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ COPY . ./
16
+
17
+ EXPOSE 7860
18
+
19
+ CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "7860"]
temp_space/api.py ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from datetime import datetime
3
+ import hashlib
4
+
5
+ import httpx
6
+ from fastapi import FastAPI, HTTPException, Depends
7
+ from fastapi.responses import HTMLResponse
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from pydantic import BaseModel, Field
10
+ from typing import Optional
11
+
12
+ from core_ai import predict_text, predict_survey, fuse_scores
13
+ from recommendations import get_recommendations
14
+
15
+ # --- DATABASE SETUP ---
16
+ from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, JSON
17
+ from sqlalchemy.orm import declarative_base, sessionmaker, Session
18
+
19
+ DATABASE_URL = os.environ.get("DATABASE_URL")
20
+ if DATABASE_URL and DATABASE_URL.startswith("postgres://"):
21
+ DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://", 1)
22
+
23
+ engine = create_engine(DATABASE_URL, connect_args={'connect_timeout': 5}) if DATABASE_URL else None
24
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) if engine else None
25
+ Base = declarative_base()
26
+
27
+
28
+ class DBUser(Base):
29
+ __tablename__ = "users"
30
+ id = Column(Integer, primary_key=True, index=True)
31
+ name = Column(String, nullable=True)
32
+ email = Column(String, unique=True, index=True)
33
+ password = Column(String)
34
+ created_at = Column(DateTime, default=datetime.utcnow)
35
+
36
+
37
+ class DBAnalysis(Base):
38
+ __tablename__ = "analyses"
39
+ id = Column(Integer, primary_key=True, index=True)
40
+ user_id = Column(Integer, index=True, nullable=True)
41
+ primary_condition = Column(String)
42
+ clinical_scoring = Column(JSON)
43
+ created_at = Column(DateTime, default=datetime.utcnow)
44
+
45
+ # --- APP SETUP ---
46
+ app = FastAPI(title="SafeSpace API", version="1.0.0")
47
+
48
+
49
+ @app.on_event("startup")
50
+ async def startup_event():
51
+ import asyncio
52
+ if engine:
53
+ try:
54
+ await asyncio.wait_for(
55
+ asyncio.to_thread(Base.metadata.create_all, bind=engine),
56
+ timeout=8.0
57
+ )
58
+ print("Database connected and tables verified.")
59
+ except asyncio.TimeoutError:
60
+ print("Database connection timed out during startup - server will start without DB verification.")
61
+ except Exception as e:
62
+ print(f"Database connection failed during startup: {e}")
63
+ print("Application startup complete.")
64
+
65
+
66
+ def get_db():
67
+ if not SessionLocal:
68
+ yield None
69
+ else:
70
+ db = SessionLocal()
71
+ try:
72
+ yield db
73
+ finally:
74
+ db.close()
75
+
76
+ # Add CORS so Flutter app can communicate with it
77
+ app.add_middleware(
78
+ CORSMiddleware,
79
+ allow_origins=["*"],
80
+ allow_credentials=True,
81
+ allow_methods=["*"],
82
+ allow_headers=["*"],
83
+ )
84
+
85
+ # --- Password Hashing ---
86
+ def hash_password(password: str) -> str:
87
+ return hashlib.sha256(password.encode()).hexdigest()
88
+
89
+ # --- DASS-42 Clinical Scoring ---
90
+ def calculate_dass_clinical_score(answers: list) -> dict:
91
+ dep_idx = [2, 4, 9, 12, 15, 16, 20, 23, 25, 30, 33, 36, 37, 41]
92
+ anx_idx = [1, 3, 6, 8, 14, 18, 19, 22, 24, 27, 29, 35, 39, 40]
93
+ str_idx = [0, 5, 7, 10, 11, 13, 17, 21, 26, 28, 31, 32, 34, 38]
94
+
95
+ dep_score = sum(answers[i] for i in dep_idx)
96
+ anx_score = sum(answers[i] for i in anx_idx)
97
+ str_score = sum(answers[i] for i in str_idx)
98
+
99
+ def get_severity(score, bounds):
100
+ if score <= bounds[0]: return "Normal"
101
+ if score <= bounds[1]: return "Mild"
102
+ if score <= bounds[2]: return "Moderate"
103
+ if score <= bounds[3]: return "Severe"
104
+ return "Extremely Severe"
105
+
106
+ return {
107
+ "depression": {"score": dep_score, "severity": get_severity(dep_score, [9, 13, 20, 27])},
108
+ "anxiety": {"score": anx_score, "severity": get_severity(anx_score, [7, 9, 14, 19])},
109
+ "stress": {"score": str_score, "severity": get_severity(str_score, [14, 18, 25, 33])}
110
+ }
111
+
112
+ # --- API MODELS ---
113
+ class AnalysisRequest(BaseModel):
114
+ user_id: str | int = Field(default=None, description="User identifier")
115
+ text: str = Field(..., min_length=1)
116
+ survey_answers: list[int] = Field(..., min_items=42, max_items=42)
117
+ locale: str = Field(default="en")
118
+ client_ts: str | None = None
119
+
120
+
121
+ class AnalyzeRequest(BaseModel):
122
+ text: str = Field(..., description="The user's response in text (Arabic/English)")
123
+ survey_answers: list[int] = Field(..., min_items=42, max_items=42, description="List of 42 integers (0-4) representing DASS-42 survey answers")
124
+ user_id: int | None = Field(default=None, description="Optional user ID to link analysis to a user")
125
+
126
+
127
+ class ChatRequest(BaseModel):
128
+ message: str
129
+ session_id: Optional[str] = "default"
130
+
131
+
132
+ class ChatResponse(BaseModel):
133
+ reply: str
134
+
135
+
136
+ class SignupRequest(BaseModel):
137
+ name: str = Field(..., min_length=1)
138
+ email: str = Field(..., min_length=5)
139
+ password: str = Field(..., min_length=4)
140
+
141
+
142
+ class LoginRequest(BaseModel):
143
+ email: str = Field(..., min_length=5)
144
+ password: str = Field(..., min_length=1)
145
+
146
+
147
+ # --- ENDPOINTS ---
148
+ @app.get("/")
149
+ def root():
150
+ return {"status": "ok", "message": "SafeSpace API"}
151
+
152
+
153
+ @app.get("/test", response_class=HTMLResponse)
154
+ def test_page():
155
+ html_path = os.path.join(os.path.dirname(__file__), "index.html")
156
+ if not os.path.exists(html_path):
157
+ raise HTTPException(status_code=404, detail="index.html not found")
158
+ with open(html_path, "r", encoding="utf-8") as f:
159
+ return f.read()
160
+
161
+
162
+ # --- AUTH ENDPOINTS ---
163
+ @app.post("/api/v1/auth/signup")
164
+ async def signup(request: SignupRequest, db: Session = Depends(get_db)):
165
+ if not db:
166
+ raise HTTPException(status_code=500, detail="Database not available")
167
+
168
+ # Check if email already exists
169
+ existing = db.query(DBUser).filter(DBUser.email == request.email).first()
170
+ if existing:
171
+ raise HTTPException(status_code=400, detail="Email already registered")
172
+
173
+ # Create new user
174
+ try:
175
+ new_user = DBUser(
176
+ name=request.name,
177
+ email=request.email,
178
+ password=hash_password(request.password),
179
+ )
180
+ db.add(new_user)
181
+ db.commit()
182
+ db.refresh(new_user)
183
+
184
+ return {
185
+ "user_id": new_user.id,
186
+ "email": new_user.email,
187
+ "name": new_user.name,
188
+ "message": "Account created successfully"
189
+ }
190
+ except Exception as e:
191
+ db.rollback()
192
+ raise HTTPException(status_code=500, detail=f"Failed to create account: {str(e)}")
193
+
194
+
195
+ @app.post("/api/v1/auth/login")
196
+ async def login(request: LoginRequest, db: Session = Depends(get_db)):
197
+ if not db:
198
+ raise HTTPException(status_code=500, detail="Database not available")
199
+
200
+ user = db.query(DBUser).filter(DBUser.email == request.email).first()
201
+ if not user:
202
+ raise HTTPException(status_code=401, detail="Email not found")
203
+
204
+ if user.password != hash_password(request.password):
205
+ # Also try plain-text match for legacy users who signed up before hashing
206
+ if user.password != request.password:
207
+ raise HTTPException(status_code=401, detail="Incorrect password")
208
+
209
+ return {
210
+ "user_id": user.id,
211
+ "email": user.email,
212
+ "name": user.name or "",
213
+ "message": "Login successful"
214
+ }
215
+
216
+
217
+ # New-style endpoint (used by index.html test page)
218
+ @app.post("/v1/analysis")
219
+ def analyze(payload: AnalysisRequest, db: Session = Depends(get_db)):
220
+ text_scores = predict_text(payload.text)
221
+ survey_scores = predict_survey(payload.survey_answers)
222
+ final_scores = fuse_scores(text_scores, survey_scores)
223
+ primary = max(final_scores, key=final_scores.get)
224
+ clinical = calculate_dass_clinical_score(payload.survey_answers)
225
+ rec = get_recommendations(primary, final_scores[primary], payload.text)
226
+ created_at = datetime.utcnow().isoformat() + "Z"
227
+
228
+ # Save to PostgreSQL if DB is connected
229
+ if db:
230
+ try:
231
+ new_analysis = DBAnalysis(
232
+ primary_condition=primary,
233
+ clinical_scoring=clinical
234
+ )
235
+ db.add(new_analysis)
236
+ db.commit()
237
+ except Exception as e:
238
+ print(f"DB save error: {e}")
239
+
240
+ return {
241
+ "analysis_id": None,
242
+ "primary_condition": primary,
243
+ "fused_scores": final_scores,
244
+ "text_scores": text_scores,
245
+ "survey_scores": survey_scores,
246
+ "clinical_scoring": clinical,
247
+ "severity": rec.get("severity"),
248
+ "cause": rec.get("cause"),
249
+ "recommendations": {
250
+ "tips_en": rec.get("tips_en", []),
251
+ "tips_ar": rec.get("tips_ar", []),
252
+ "resources_en": rec.get("resources_en", []),
253
+ "resources_ar": rec.get("resources_ar", []),
254
+ "referral_en": rec.get("referral_en", ""),
255
+ "referral_ar": rec.get("referral_ar", ""),
256
+ },
257
+ "suicidal_flag": rec.get("suicidal_flag", False),
258
+ "created_at": created_at,
259
+ }
260
+
261
+ # Flutter-compatible endpoint (used by api_service.dart)
262
+ @app.post("/api/v1/analyze")
263
+ async def analyze_mental_health(request: AnalyzeRequest, db: Session = Depends(get_db)):
264
+ try:
265
+ text_scores = predict_text(request.text)
266
+ survey_scores = predict_survey(request.survey_answers)
267
+ final_scores = fuse_scores(text_scores, survey_scores)
268
+ primary = max(final_scores, key=final_scores.get)
269
+ clinical = calculate_dass_clinical_score(request.survey_answers)
270
+ rec = get_recommendations(primary, final_scores[primary], request.text)
271
+ created_at = datetime.utcnow().isoformat() + "Z"
272
+
273
+ # Save to PostgreSQL if DB is connected
274
+ if db:
275
+ try:
276
+ new_analysis = DBAnalysis(
277
+ user_id=request.user_id,
278
+ primary_condition=primary,
279
+ clinical_scoring=clinical
280
+ )
281
+ db.add(new_analysis)
282
+ db.commit()
283
+ except Exception as e:
284
+ print(f"DB save error: {e}")
285
+
286
+ return {
287
+ "analysis_id": None,
288
+ "primary_condition": primary,
289
+ "fused_scores": final_scores,
290
+ "text_scores": text_scores,
291
+ "survey_scores": survey_scores,
292
+ "clinical_scoring": clinical,
293
+ "severity": rec.get("severity"),
294
+ "cause": rec.get("cause"),
295
+ "recommendations": {
296
+ "tips_en": rec.get("tips_en", []),
297
+ "tips_ar": rec.get("tips_ar", []),
298
+ "resources_en": rec.get("resources_en", []),
299
+ "resources_ar": rec.get("resources_ar", []),
300
+ "referral_en": rec.get("referral_en", ""),
301
+ "referral_ar": rec.get("referral_ar", ""),
302
+ },
303
+ "suicidal_flag": rec.get("suicidal_flag", False),
304
+ "created_at": created_at,
305
+ }
306
+ except Exception as e:
307
+ raise HTTPException(status_code=500, detail=str(e))
308
+
309
+ # Flutter-compatible history endpoint
310
+ @app.get("/api/v1/analyses/history")
311
+ async def get_analyses_history(user_id: int = None, db: Session = Depends(get_db)):
312
+ try:
313
+ if not db:
314
+ return []
315
+
316
+ query = db.query(DBAnalysis)
317
+
318
+ # Filter by user_id if provided
319
+ if user_id is not None:
320
+ query = query.filter(DBAnalysis.user_id == user_id)
321
+
322
+ # Get the 10 most recent analyses, sorted by created_at ascending (oldest first for graphing)
323
+ records = query.order_by(DBAnalysis.created_at.desc()).limit(10).all()
324
+
325
+ history = []
326
+ for r in reversed(records): # Reverse so oldest is first
327
+ if r.clinical_scoring:
328
+ history.append({
329
+ "id": r.id,
330
+ "date": r.created_at.strftime("%b %d"),
331
+ "depression": r.clinical_scoring.get("depression", {}).get("score", 0),
332
+ "anxiety": r.clinical_scoring.get("anxiety", {}).get("score", 0),
333
+ "stress": r.clinical_scoring.get("stress", {}).get("score", 0),
334
+ "primary": r.primary_condition
335
+ })
336
+ return history
337
+ except Exception as e:
338
+ raise HTTPException(status_code=500, detail=str(e))
339
+
340
+ @app.post("/api/v1/chat", response_model=ChatResponse)
341
+ async def chat_with_ai(request: ChatRequest):
342
+ api_url = os.environ.get("AI_API_URL")
343
+ api_key = os.environ.get("AI_API_KEY")
344
+ chatflow_id = os.environ.get("AI_CHATFLOW_ID")
345
+
346
+ if not api_url or not api_key or not chatflow_id:
347
+ raise HTTPException(status_code=500, detail="AI API credentials are not configured in Secrets.")
348
+
349
+ endpoint = f"{api_url}/api/v1/prediction/{chatflow_id}"
350
+ headers = {"Authorization": f"Bearer {api_key}"}
351
+ payload = {"question": request.message, "overrideConfig": {"sessionId": request.session_id}}
352
+
353
+ async with httpx.AsyncClient() as client:
354
+ try:
355
+ response = await client.post(endpoint, json=payload, headers=headers, timeout=30.0)
356
+ response.raise_for_status()
357
+ data = response.json()
358
+ return ChatResponse(reply=data.get("text") or data.get("answer") or str(data))
359
+ except Exception as e:
360
+ raise HTTPException(status_code=502, detail=f"Failed to communicate with AI API: {str(e)}")
temp_space/core_ai.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import pickle
4
+ import warnings
5
+ from functools import lru_cache
6
+
7
+ import numpy as np
8
+ import torch
9
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
10
+ from deep_translator import GoogleTranslator
11
+
12
+ warnings.filterwarnings("ignore")
13
+
14
+ CLASSES = ["anxiety", "depression", "stress"]
15
+
16
+
17
+ @lru_cache(maxsize=1)
18
+ def load_xlmr():
19
+ model_id = os.getenv("HF_MODEL_ID", "AliSakr9997/Mental-XLMR-Model")
20
+ token = os.getenv("HF_TOKEN")
21
+ kwargs = {"token": token} if token else {}
22
+ local_dir = os.path.join(os.path.dirname(__file__), "mental_xlmr_final")
23
+ local_weights = any(
24
+ os.path.exists(os.path.join(local_dir, fname))
25
+ for fname in ("pytorch_model.bin", "model.safetensors")
26
+ )
27
+ source = local_dir if local_weights else model_id
28
+ tokenizer = AutoTokenizer.from_pretrained(source, **kwargs)
29
+ model = AutoModelForSequenceClassification.from_pretrained(source, **kwargs)
30
+ le_path = os.path.join(os.path.dirname(__file__), "mental_xlmr_final", "label_encoder.pkl")
31
+ with open(le_path, "rb") as f:
32
+ le = pickle.load(f)
33
+ model.eval()
34
+ return tokenizer, model, le
35
+
36
+
37
+ @lru_cache(maxsize=1)
38
+ def load_survey():
39
+ scaler = pickle.load(open(os.path.join(os.path.dirname(__file__), "scaler.pkl"), "rb"))
40
+ weights = pickle.load(open(os.path.join(os.path.dirname(__file__), "model_weights.pkl"), "rb"))
41
+
42
+ def predict(x):
43
+ for w in weights:
44
+ if len(w) == 2:
45
+ x = np.dot(x, w[0]) + w[1]
46
+ x = np.maximum(0, x)
47
+ x = np.exp(x) / np.sum(np.exp(x))
48
+ return x
49
+
50
+ return scaler, predict
51
+
52
+
53
+ def clean_text(text: str) -> str:
54
+ text = re.sub(r"(.)\1{2,}", r"\1\1", text)
55
+ text = re.sub(r"[^\w\s\u0600-\u06FF\[\]]", " ", text)
56
+ return re.sub(r"\s+", " ", text).strip()
57
+
58
+
59
+ def translate_to_en(text: str) -> str:
60
+ try:
61
+ return GoogleTranslator(source="auto", target="en").translate(text)
62
+ except Exception:
63
+ return ""
64
+
65
+
66
+ def predict_text(text: str) -> dict:
67
+ tokenizer, model, le = load_xlmr()
68
+ cleaned = clean_text(text)
69
+ text_en = translate_to_en(cleaned)
70
+ combined = (text_en + " [SEP] " + cleaned) if text_en else cleaned
71
+ inputs = tokenizer(combined, return_tensors="pt", truncation=True, max_length=192, padding=True)
72
+ with torch.no_grad():
73
+ probs = torch.softmax(model(**inputs).logits, dim=-1).squeeze().numpy()
74
+ return {c: round(float(p), 4) for c, p in zip(le.classes_, probs)}
75
+
76
+
77
+ def predict_survey(answers: list) -> dict:
78
+ scaler, survey_predict = load_survey()
79
+ data = scaler.transform(np.array(answers).reshape(1, -1))
80
+ pred = survey_predict(data)[0]
81
+ return {
82
+ "depression": round(float(pred[0]), 4),
83
+ "anxiety": round(float(pred[1]), 4),
84
+ "stress": round(float(pred[2]), 4),
85
+ }
86
+
87
+
88
+ def fuse_scores(text_s, survey_s, w_text=0.4, w_survey=0.6):
89
+ return {c: round(w_text * text_s[c] + w_survey * survey_s[c], 4) for c in CLASSES}
temp_space/index.html ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>SafeSpace API Test</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0f1a16;
10
+ --panel: #16231f;
11
+ --accent: #4fd1a5;
12
+ --accent-2: #7ce3c3;
13
+ --text: #e8f6f1;
14
+ --muted: #9bb7ad;
15
+ --danger: #f87171;
16
+ --border: #274136;
17
+ }
18
+
19
+ * { box-sizing: border-box; }
20
+
21
+ body {
22
+ margin: 0;
23
+ font-family: "Space Grotesk", "Figtree", "Montserrat", sans-serif;
24
+ background: radial-gradient(circle at 15% 10%, #193229, #0f1a16 55%);
25
+ color: var(--text);
26
+ }
27
+
28
+ header {
29
+ padding: 32px 24px 16px;
30
+ text-align: center;
31
+ }
32
+
33
+ header h1 {
34
+ margin: 0 0 8px;
35
+ font-size: 30px;
36
+ letter-spacing: 0.4px;
37
+ }
38
+
39
+ header p {
40
+ margin: 0;
41
+ color: var(--muted);
42
+ }
43
+
44
+ main {
45
+ max-width: 980px;
46
+ margin: 0 auto;
47
+ padding: 24px;
48
+ display: grid;
49
+ gap: 18px;
50
+ }
51
+
52
+ .panel {
53
+ background: var(--panel);
54
+ border: 1px solid var(--border);
55
+ border-radius: 18px;
56
+ padding: 20px;
57
+ box-shadow: 0 14px 34px rgba(0, 0, 0, 0.25);
58
+ }
59
+
60
+ label {
61
+ display: block;
62
+ font-size: 13px;
63
+ text-transform: uppercase;
64
+ letter-spacing: 1.5px;
65
+ color: var(--muted);
66
+ margin-bottom: 8px;
67
+ }
68
+
69
+ input, textarea {
70
+ width: 100%;
71
+ padding: 12px 14px;
72
+ border-radius: 12px;
73
+ border: 1px solid var(--border);
74
+ background: #0c1512;
75
+ color: var(--text);
76
+ font-size: 15px;
77
+ }
78
+
79
+ textarea { min-height: 120px; resize: vertical; }
80
+
81
+ .row { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
82
+
83
+ button {
84
+ border: none;
85
+ background: linear-gradient(120deg, var(--accent), var(--accent-2));
86
+ color: #062318;
87
+ font-weight: 700;
88
+ font-size: 15px;
89
+ padding: 12px 18px;
90
+ border-radius: 12px;
91
+ cursor: pointer;
92
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
93
+ width: 100%;
94
+ }
95
+
96
+ button:hover { transform: translateY(-1px); box-shadow: 0 10px 18px rgba(79, 209, 165, 0.2); }
97
+
98
+ pre {
99
+ white-space: pre-wrap;
100
+ word-break: break-word;
101
+ background: #0b1512;
102
+ border: 1px solid #1f352d;
103
+ border-radius: 12px;
104
+ padding: 16px;
105
+ min-height: 120px;
106
+ }
107
+
108
+ .error { color: var(--danger); font-weight: 600; }
109
+ </style>
110
+ </head>
111
+ <body>
112
+ <header>
113
+ <h1>SafeSpace API Test</h1>
114
+ <p>Quickly validate AI endpoints on your Hugging Face Space.</p>
115
+ </header>
116
+
117
+ <main>
118
+ <section class="panel">
119
+ <div class="row">
120
+ <div>
121
+ <label for="baseUrl">Base URL</label>
122
+ <input id="baseUrl" value="https://AliSakr9997-safespace.hf.space" />
123
+ </div>
124
+ <div>
125
+ <label for="userId">User ID</label>
126
+ <input id="userId" value="1" />
127
+ </div>
128
+ </div>
129
+ </section>
130
+
131
+ <section class="panel">
132
+ <label for="textInput">User Text</label>
133
+ <textarea id="textInput">I have been feeling overwhelmed at work and can't sleep.</textarea>
134
+ </section>
135
+
136
+ <section class="panel">
137
+ <label for="survey">Survey Answers (42 values, 0-4, comma-separated)</label>
138
+ <textarea id="survey">0,1,2,1,0,2,1,0,1,2,1,0,2,1,0,2,1,1,0,2,1,2,1,0,2,1,0,1,2,1,0,2,1,0,1,2,1,0,2,1,0,1</textarea>
139
+ </section>
140
+
141
+ <section class="panel">
142
+ <div class="row">
143
+ <button id="analyzeBtn">POST /v1/analysis</button>
144
+ <button id="historyBtn">GET /v1/users/{id}/analyses</button>
145
+ </div>
146
+ </section>
147
+
148
+ <section class="panel">
149
+ <label>Response</label>
150
+ <pre id="output">Waiting for request...</pre>
151
+ </section>
152
+ </main>
153
+
154
+ <script>
155
+ const output = document.getElementById('output');
156
+ const baseUrl = document.getElementById('baseUrl');
157
+ const userId = document.getElementById('userId');
158
+ const textInput = document.getElementById('textInput');
159
+ const survey = document.getElementById('survey');
160
+
161
+ function setOutput(data, isError = false) {
162
+ if (isError) {
163
+ output.innerHTML = '<span class="error">' + data + '</span>';
164
+ } else {
165
+ output.textContent = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
166
+ }
167
+ }
168
+
169
+ function parseSurvey() {
170
+ return survey.value
171
+ .split(',')
172
+ .map((v) => parseInt(v.trim(), 10))
173
+ .filter((v) => Number.isFinite(v));
174
+ }
175
+
176
+ document.getElementById('analyzeBtn').addEventListener('click', async () => {
177
+ setOutput('Sending request...');
178
+ try {
179
+ const payload = {
180
+ user_id: userId.value,
181
+ text: textInput.value,
182
+ survey_answers: parseSurvey(),
183
+ locale: 'en',
184
+ client_ts: new Date().toISOString(),
185
+ };
186
+ const res = await fetch(`${baseUrl.value}/v1/analysis`, {
187
+ method: 'POST',
188
+ headers: { 'Content-Type': 'application/json' },
189
+ body: JSON.stringify(payload),
190
+ });
191
+ const json = await res.json();
192
+ setOutput(json);
193
+ } catch (err) {
194
+ setOutput(err.message || String(err), true);
195
+ }
196
+ });
197
+
198
+ document.getElementById('historyBtn').addEventListener('click', async () => {
199
+ setOutput('Fetching history...');
200
+ try {
201
+ const res = await fetch(`${baseUrl.value}/v1/users/${userId.value}/analyses?limit=20&offset=0`);
202
+ const json = await res.json();
203
+ setOutput(json);
204
+ } catch (err) {
205
+ setOutput(err.message || String(err), true);
206
+ }
207
+ });
208
+ </script>
209
+ </body>
210
+ </html>
temp_space/main.py ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from datetime import datetime
3
+ import hashlib
4
+
5
+ import httpx
6
+ from fastapi import FastAPI, HTTPException, Depends
7
+ from fastapi.responses import HTMLResponse
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from pydantic import BaseModel, Field
10
+ from typing import Optional
11
+
12
+ from core_ai import predict_text, predict_survey, fuse_scores
13
+ from recommendations import get_recommendations
14
+
15
+ # --- DATABASE SETUP ---
16
+ from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, JSON
17
+ from sqlalchemy.orm import declarative_base, sessionmaker, Session
18
+
19
+ DATABASE_URL = os.environ.get("DATABASE_URL")
20
+ if DATABASE_URL and DATABASE_URL.startswith("postgres://"):
21
+ DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://", 1)
22
+
23
+ engine = create_engine(DATABASE_URL, connect_args={'connect_timeout': 5}) if DATABASE_URL else None
24
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) if engine else None
25
+ Base = declarative_base()
26
+
27
+
28
+ class DBUser(Base):
29
+ __tablename__ = "users"
30
+ id = Column(Integer, primary_key=True, index=True)
31
+ name = Column(String, nullable=True)
32
+ email = Column(String, unique=True, index=True)
33
+ password = Column(String)
34
+ created_at = Column(DateTime, default=datetime.utcnow)
35
+
36
+
37
+ class DBAnalysis(Base):
38
+ __tablename__ = "analyses"
39
+ id = Column(Integer, primary_key=True, index=True)
40
+ user_id = Column(Integer, index=True, nullable=True)
41
+ primary_condition = Column(String)
42
+ clinical_scoring = Column(JSON)
43
+ created_at = Column(DateTime, default=datetime.utcnow)
44
+
45
+ # --- APP SETUP ---
46
+ app = FastAPI(title="SafeSpace API", version="1.0.0")
47
+
48
+
49
+ @app.on_event("startup")
50
+ async def startup_event():
51
+ import asyncio
52
+ if engine:
53
+ try:
54
+ await asyncio.wait_for(
55
+ asyncio.to_thread(Base.metadata.create_all, bind=engine),
56
+ timeout=8.0
57
+ )
58
+ print("Database connected and tables verified.")
59
+ except asyncio.TimeoutError:
60
+ print("Database connection timed out during startup - server will start without DB verification.")
61
+ except Exception as e:
62
+ print(f"Database connection failed during startup: {e}")
63
+ print("Application startup complete.")
64
+
65
+
66
+ def get_db():
67
+ if not SessionLocal:
68
+ yield None
69
+ else:
70
+ db = SessionLocal()
71
+ try:
72
+ yield db
73
+ finally:
74
+ db.close()
75
+
76
+ # Add CORS so Flutter app can communicate with it
77
+ app.add_middleware(
78
+ CORSMiddleware,
79
+ allow_origins=["*"],
80
+ allow_credentials=True,
81
+ allow_methods=["*"],
82
+ allow_headers=["*"],
83
+ )
84
+
85
+ # --- Password Hashing ---
86
+ def hash_password(password: str) -> str:
87
+ return hashlib.sha256(password.encode()).hexdigest()
88
+
89
+ # --- DASS-42 Clinical Scoring ---
90
+ def calculate_dass_clinical_score(answers: list) -> dict:
91
+ dep_idx = [2, 4, 9, 12, 15, 16, 20, 23, 25, 30, 33, 36, 37, 41]
92
+ anx_idx = [1, 3, 6, 8, 14, 18, 19, 22, 24, 27, 29, 35, 39, 40]
93
+ str_idx = [0, 5, 7, 10, 11, 13, 17, 21, 26, 28, 31, 32, 34, 38]
94
+
95
+ dep_score = sum(answers[i] for i in dep_idx)
96
+ anx_score = sum(answers[i] for i in anx_idx)
97
+ str_score = sum(answers[i] for i in str_idx)
98
+
99
+ def get_severity(score, bounds):
100
+ if score <= bounds[0]: return "Normal"
101
+ if score <= bounds[1]: return "Mild"
102
+ if score <= bounds[2]: return "Moderate"
103
+ if score <= bounds[3]: return "Severe"
104
+ return "Extremely Severe"
105
+
106
+ return {
107
+ "depression": {"score": dep_score, "severity": get_severity(dep_score, [9, 13, 20, 27])},
108
+ "anxiety": {"score": anx_score, "severity": get_severity(anx_score, [7, 9, 14, 19])},
109
+ "stress": {"score": str_score, "severity": get_severity(str_score, [14, 18, 25, 33])}
110
+ }
111
+
112
+ # --- API MODELS ---
113
+ class AnalysisRequest(BaseModel):
114
+ user_id: str | int = Field(default=None, description="User identifier")
115
+ text: str = Field(..., min_length=1)
116
+ survey_answers: list[int] = Field(..., min_items=42, max_items=42)
117
+ locale: str = Field(default="en")
118
+ client_ts: str | None = None
119
+
120
+
121
+ class AnalyzeRequest(BaseModel):
122
+ text: str = Field(..., description="The user's response in text (Arabic/English)")
123
+ survey_answers: list[int] = Field(..., min_items=42, max_items=42, description="List of 42 integers (0-4) representing DASS-42 survey answers")
124
+ user_id: int | None = Field(default=None, description="Optional user ID to link analysis to a user")
125
+
126
+
127
+ class ChatRequest(BaseModel):
128
+ message: str
129
+ session_id: Optional[str] = "default"
130
+
131
+
132
+ class ChatResponse(BaseModel):
133
+ reply: str
134
+
135
+
136
+ class SignupRequest(BaseModel):
137
+ name: str = Field(..., min_length=1)
138
+ email: str = Field(..., min_length=5)
139
+ password: str = Field(..., min_length=4)
140
+
141
+
142
+ class LoginRequest(BaseModel):
143
+ email: str = Field(..., min_length=5)
144
+ password: str = Field(..., min_length=1)
145
+
146
+
147
+ # --- ENDPOINTS ---
148
+ @app.get("/")
149
+ def root():
150
+ return {"status": "ok", "message": "SafeSpace API"}
151
+
152
+
153
+ @app.get("/test", response_class=HTMLResponse)
154
+ def test_page():
155
+ html_path = os.path.join(os.path.dirname(__file__), "index.html")
156
+ if not os.path.exists(html_path):
157
+ raise HTTPException(status_code=404, detail="index.html not found")
158
+ with open(html_path, "r", encoding="utf-8") as f:
159
+ return f.read()
160
+
161
+
162
+ # --- AUTH ENDPOINTS ---
163
+ @app.post("/api/v1/auth/signup")
164
+ async def signup(request: SignupRequest, db: Session = Depends(get_db)):
165
+ if not db:
166
+ raise HTTPException(status_code=500, detail="Database not available")
167
+
168
+ # Check if email already exists
169
+ existing = db.query(DBUser).filter(DBUser.email == request.email).first()
170
+ if existing:
171
+ raise HTTPException(status_code=400, detail="Email already registered")
172
+
173
+ # Create new user
174
+ try:
175
+ new_user = DBUser(
176
+ name=request.name,
177
+ email=request.email,
178
+ password=hash_password(request.password),
179
+ )
180
+ db.add(new_user)
181
+ db.commit()
182
+ db.refresh(new_user)
183
+
184
+ return {
185
+ "user_id": new_user.id,
186
+ "email": new_user.email,
187
+ "name": new_user.name,
188
+ "message": "Account created successfully"
189
+ }
190
+ except Exception as e:
191
+ db.rollback()
192
+ raise HTTPException(status_code=500, detail=f"Failed to create account: {str(e)}")
193
+
194
+
195
+ @app.post("/api/v1/auth/login")
196
+ async def login(request: LoginRequest, db: Session = Depends(get_db)):
197
+ if not db:
198
+ raise HTTPException(status_code=500, detail="Database not available")
199
+
200
+ user = db.query(DBUser).filter(DBUser.email == request.email).first()
201
+ if not user:
202
+ raise HTTPException(status_code=401, detail="Email not found")
203
+
204
+ if user.password != hash_password(request.password):
205
+ # Also try plain-text match for legacy users who signed up before hashing
206
+ if user.password != request.password:
207
+ raise HTTPException(status_code=401, detail="Incorrect password")
208
+
209
+ return {
210
+ "user_id": user.id,
211
+ "email": user.email,
212
+ "name": user.name or "",
213
+ "message": "Login successful"
214
+ }
215
+
216
+
217
+ # New-style endpoint (used by index.html test page)
218
+ @app.post("/v1/analysis")
219
+ def analyze(payload: AnalysisRequest, db: Session = Depends(get_db)):
220
+ text_scores = predict_text(payload.text)
221
+ survey_scores = predict_survey(payload.survey_answers)
222
+ final_scores = fuse_scores(text_scores, survey_scores)
223
+ primary = max(final_scores, key=final_scores.get)
224
+ clinical = calculate_dass_clinical_score(payload.survey_answers)
225
+ rec = get_recommendations(primary, final_scores[primary], payload.text)
226
+
227
+ # Save to PostgreSQL if DB is connected
228
+ if db:
229
+ try:
230
+ new_analysis = DBAnalysis(
231
+ primary_condition=primary,
232
+ clinical_scoring=clinical
233
+ )
234
+ db.add(new_analysis)
235
+ db.commit()
236
+ except Exception as e:
237
+ print(f"DB save error: {e}")
238
+
239
+ return {
240
+ "analysis_id": None,
241
+ "primary": primary,
242
+ "scores": final_scores,
243
+ "severity": rec.get("severity"),
244
+ "cause": rec.get("cause"),
245
+ "recommendations": {
246
+ "tips_en": rec.get("tips_en", []),
247
+ "tips_ar": rec.get("tips_ar", []),
248
+ "resources_en": rec.get("resources_en", []),
249
+ "resources_ar": rec.get("resources_ar", []),
250
+ "referral_en": rec.get("referral_en", ""),
251
+ "referral_ar": rec.get("referral_ar", ""),
252
+ },
253
+ "suicidal_flag": rec.get("suicidal_flag", False),
254
+ "created_at": datetime.utcnow().isoformat() + "Z",
255
+ }
256
+
257
+ # Flutter-compatible endpoint (used by api_service.dart)
258
+ @app.post("/api/v1/analyze")
259
+ async def analyze_mental_health(request: AnalyzeRequest, db: Session = Depends(get_db)):
260
+ try:
261
+ text_scores = predict_text(request.text)
262
+ survey_scores = predict_survey(request.survey_answers)
263
+ final_scores = fuse_scores(text_scores, survey_scores)
264
+ primary = max(final_scores, key=final_scores.get)
265
+ clinical = calculate_dass_clinical_score(request.survey_answers)
266
+ rec = get_recommendations(primary, final_scores[primary], request.text)
267
+
268
+ # Save to PostgreSQL if DB is connected
269
+ if db:
270
+ try:
271
+ new_analysis = DBAnalysis(
272
+ user_id=request.user_id,
273
+ primary_condition=primary,
274
+ clinical_scoring=clinical
275
+ )
276
+ db.add(new_analysis)
277
+ db.commit()
278
+ except Exception as e:
279
+ print(f"DB save error: {e}")
280
+
281
+ return {
282
+ "primary_condition": primary,
283
+ "fused_scores": final_scores,
284
+ "text_scores": text_scores,
285
+ "survey_scores": survey_scores,
286
+ "clinical_scoring": clinical,
287
+ "recommendations": rec
288
+ }
289
+ except Exception as e:
290
+ raise HTTPException(status_code=500, detail=str(e))
291
+
292
+ # Flutter-compatible history endpoint
293
+ @app.get("/api/v1/analyses/history")
294
+ async def get_analyses_history(user_id: int = None, db: Session = Depends(get_db)):
295
+ try:
296
+ if not db:
297
+ return []
298
+
299
+ query = db.query(DBAnalysis)
300
+
301
+ # Filter by user_id if provided
302
+ if user_id is not None:
303
+ query = query.filter(DBAnalysis.user_id == user_id)
304
+
305
+ # Get the 10 most recent analyses, sorted by created_at ascending (oldest first for graphing)
306
+ records = query.order_by(DBAnalysis.created_at.desc()).limit(10).all()
307
+
308
+ history = []
309
+ for r in reversed(records): # Reverse so oldest is first
310
+ if r.clinical_scoring:
311
+ history.append({
312
+ "id": r.id,
313
+ "date": r.created_at.strftime("%b %d"),
314
+ "depression": r.clinical_scoring.get("depression", {}).get("score", 0),
315
+ "anxiety": r.clinical_scoring.get("anxiety", {}).get("score", 0),
316
+ "stress": r.clinical_scoring.get("stress", {}).get("score", 0),
317
+ "primary": r.primary_condition
318
+ })
319
+ return history
320
+ except Exception as e:
321
+ raise HTTPException(status_code=500, detail=str(e))
322
+
323
+ @app.post("/api/v1/chat", response_model=ChatResponse)
324
+ async def chat_with_ai(request: ChatRequest):
325
+ api_url = os.environ.get("AI_API_URL")
326
+ api_key = os.environ.get("AI_API_KEY")
327
+ chatflow_id = os.environ.get("AI_CHATFLOW_ID")
328
+
329
+ if not api_url or not api_key or not chatflow_id:
330
+ raise HTTPException(status_code=500, detail="AI API credentials are not configured in Secrets.")
331
+
332
+ endpoint = f"{api_url}/api/v1/prediction/{chatflow_id}"
333
+ headers = {"Authorization": f"Bearer {api_key}"}
334
+ payload = {"question": request.message, "overrideConfig": {"sessionId": request.session_id}}
335
+
336
+ async with httpx.AsyncClient() as client:
337
+ try:
338
+ response = await client.post(endpoint, json=payload, headers=headers, timeout=30.0)
339
+ response.raise_for_status()
340
+ data = response.json()
341
+ return ChatResponse(reply=data.get("text") or data.get("answer") or str(data))
342
+ except Exception as e:
343
+ raise HTTPException(status_code=502, detail=f"Failed to communicate with AI API: {str(e)}")
temp_space/model2.ipynb ADDED
The diff for this file is too large to render. See raw diff