Spaces:
Running
Running
Download server.js from Samfredoly/TOUSE: direct link, hf CLI and curl.
- Browser
- Download file 11.2 kB
-
https://huggingface.co/spaces/Samfredoly/TOUSE/resolve/main/server.js
- Command line
-
hf download hf://spaces/Samfredoly/TOUSE/server.js
-
curl -L -o server.js https://huggingface.co/spaces/Samfredoly/TOUSE/resolve/main/server.js
11.2 kB
| import http from 'node:http'; | |
| import fs from 'node:fs'; | |
| import path from 'node:path'; | |
| import crypto from 'node:crypto'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { createServer as createViteServer } from 'vite'; | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| const port = Number(process.env.PORT || 3000); | |
| const isProduction = process.env.NODE_ENV === 'production'; | |
| const model = 'cohere/north-mini-code:free'; | |
| const storageDir = process.env.DATA_DIR || (fs.existsSync('/data') ? '/data' : path.join(__dirname, 'data')); | |
| const submissionsFile = path.join(storageDir, 'submissions.json'); | |
| function loadSubmissions() { | |
| try { | |
| const parsed = JSON.parse(fs.readFileSync(submissionsFile, 'utf8')); | |
| return Array.isArray(parsed) ? parsed : []; | |
| } catch { | |
| return []; | |
| } | |
| } | |
| let submissions = loadSubmissions(); | |
| function persistSubmissions() { | |
| fs.mkdirSync(storageDir, { recursive: true }); | |
| const tempFile = `${submissionsFile}.tmp`; | |
| fs.writeFileSync(tempFile, JSON.stringify(submissions, null, 2)); | |
| fs.renameSync(tempFile, submissionsFile); | |
| } | |
| const fallbackReading = { | |
| score: 58, | |
| label: 'offline reflection — not a model read', | |
| read: 'The requested model could not be reached, so this is only a rough local reflection. There may be care here, but a conversation still has to do the work that a score cannot.', | |
| careSignal: 'unverified until it shows up in action', | |
| confidence: 'low', | |
| source: 'offline-fallback', | |
| }; | |
| function sendJson(res, status, body) { | |
| res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' }); | |
| res.end(JSON.stringify(body)); | |
| } | |
| function cleanString(value, max = 800) { | |
| return typeof value === 'string' ? value.trim().slice(0, max) : ''; | |
| } | |
| function parseModelJson(content) { | |
| const stripped = content.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim(); | |
| const start = stripped.indexOf('{'); | |
| const end = stripped.lastIndexOf('}'); | |
| if (start === -1 || end === -1) throw new Error('Model response was not JSON'); | |
| return JSON.parse(stripped.slice(start, end + 1)); | |
| } | |
| function normalizeReading(raw) { | |
| const score = Math.max(0, Math.min(100, Math.round(Number(raw.score) || fallbackReading.score))); | |
| return { | |
| score, | |
| label: cleanString(raw.label, 80) || fallbackReading.label, | |
| read: cleanString(raw.read, 320) || fallbackReading.read, | |
| careSignal: cleanString(raw.careSignal, 100) || fallbackReading.careSignal, | |
| confidence: cleanString(raw.confidence, 30) || fallbackReading.confidence, | |
| source: raw.source === 'offline-fallback' ? 'offline-fallback' : 'model', | |
| }; | |
| } | |
| function localReflection(payload) { | |
| const text = `${cleanString(payload.apology, 700)} ${Array.isArray(payload.promises) ? payload.promises.join(' ') : ''}`.toLowerCase(); | |
| let score = 46; | |
| if (text.length > 160) score += 8; | |
| if (/(hurt|impact|carried|understand|listen|need|trust|show up|ordinary|defend)/.test(text)) score += 12; | |
| if (/(miss you|lonely|i need you|come back|my pain|my heart)/.test(text)) score -= 5; | |
| if (/(sorry|change|respect|pace|choice|choose)/.test(text)) score += 6; | |
| return { ...fallbackReading, score: Math.max(35, Math.min(74, score)) }; | |
| } | |
| function findSubmission(id) { | |
| return submissions.find((submission) => submission.id === id); | |
| } | |
| function latestSubmission() { | |
| return [...submissions].sort((a, b) => b.createdAt - a.createdAt)[0]; | |
| } | |
| function passwordMatches(supplied) { | |
| const expected = process.env.OWNER_VIEW_PASSWORD || ''; | |
| const value = cleanString(supplied, 200); | |
| return Boolean(expected && value && Buffer.byteLength(expected) === Buffer.byteLength(value) | |
| && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(value))); | |
| } | |
| async function rateSincerity(payload) { | |
| const apiKey = process.env.OPENROUTER_API_KEY; | |
| if (!apiKey) return localReflection(payload); | |
| const system = `You are a careful relationship reflection assistant. You cannot detect lies or know intent; never claim certainty. Given an apology, promises, consent statement, and name, produce an imperfect impression of how specific, accountable, and other-aware the answer sounds. Do not reward romantic intensity alone. Penalize vague promises, self-pity, pressure, blame shifting, or centering only the apologizer's relief. Reward naming impact, concrete behavior change, patience, and respect for the other person's choice. Return ONLY valid JSON with these keys: score (integer 0-100), label (short lowercase phrase), read (2-3 warm but direct sentences), careSignal (short phrase), confidence (one of low, medium, high). Use a nuanced score; do not give 90+ unless the text is unusually specific and accountable.`; | |
| const user = JSON.stringify({ | |
| name: cleanString(payload.name, 80), | |
| promises: Array.isArray(payload.promises) ? payload.promises.slice(0, 3).map((item) => cleanString(item, 140)) : [], | |
| apology: cleanString(payload.apology, 700), | |
| consent: Boolean(payload.consent), | |
| }); | |
| const response = await fetch('https://openrouter.ai/api/v1/chat/completions', { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `Bearer ${apiKey}`, | |
| 'Content-Type': 'application/json', | |
| 'HTTP-Referer': 'https://secondchance.manus.app', | |
| 'X-Title': 'Second Chance sincerity reflection', | |
| }, | |
| body: JSON.stringify({ | |
| model, | |
| temperature: 0.45, | |
| max_tokens: 260, | |
| messages: [{ role: 'system', content: system }, { role: 'user', content: user }], | |
| }), | |
| }); | |
| if (!response.ok) throw new Error(`Model request failed (${response.status})`); | |
| const data = await response.json(); | |
| const content = data?.choices?.[0]?.message?.content; | |
| if (typeof content !== 'string') throw new Error('Model response was empty'); | |
| return normalizeReading({ ...parseModelJson(content), source: 'model' }); | |
| } | |
| async function readBody(req) { | |
| let body = ''; | |
| for await (const chunk of req) { | |
| body += chunk; | |
| if (body.length > 12000) throw new Error('Request too large'); | |
| } | |
| return JSON.parse(body || '{}'); | |
| } | |
| async function handleApi(req, res) { | |
| if (req.method === 'GET' && req.url === '/api/latest-submission') { | |
| const latest = latestSubmission(); | |
| sendJson(res, 200, latest ? { available: true, submissionId: latest.id } : { available: false }); | |
| return true; | |
| } | |
| if (req.method === 'POST' && req.url === '/api/owner/submissions') { | |
| try { | |
| const payload = await readBody(req); | |
| if (!passwordMatches(payload.password)) { | |
| sendJson(res, 401, { error: 'That key does not open this room.' }); | |
| return true; | |
| } | |
| sendJson(res, 200, { submissions: [...submissions].sort((a, b) => b.createdAt - a.createdAt) }); | |
| } catch (error) { | |
| console.error('[owner-submissions]', error.message); | |
| sendJson(res, 400, { error: 'The private key could not be checked.' }); | |
| } | |
| return true; | |
| } | |
| if (req.method === 'GET' && req.url.startsWith('/api/owner/submissions/')) { | |
| const id = cleanString(req.url.split('/').pop(), 100); | |
| const submission = findSubmission(id); | |
| if (!submission) { | |
| sendJson(res, 404, { error: 'Submission not found.' }); | |
| return true; | |
| } | |
| sendJson(res, 200, { submission }); | |
| return true; | |
| } | |
| if (req.method === 'POST' && req.url === '/api/owner-unlock') { | |
| try { | |
| const payload = await readBody(req); | |
| const submission = findSubmission(cleanString(payload.submissionId, 100)) || latestSubmission(); | |
| if (!passwordMatches(payload.password) || !submission) { | |
| sendJson(res, 401, { error: 'That key does not open this room.' }); | |
| return true; | |
| } | |
| sendJson(res, 200, { | |
| ok: true, | |
| submission: { | |
| name: submission.name, | |
| promises: submission.promises, | |
| apology: submission.apology, | |
| reading: submission.reading, | |
| }, | |
| }); | |
| return true; | |
| } catch (error) { | |
| console.error('[owner-unlock]', error.message); | |
| sendJson(res, 400, { error: 'The private key could not be checked.' }); | |
| return true; | |
| } | |
| } | |
| if (req.method !== 'POST' || req.url !== '/api/sincerity') return false; | |
| try { | |
| const payload = await readBody(req); | |
| if (!cleanString(payload.name, 80) || !cleanString(payload.apology, 700) || !Array.isArray(payload.promises) || payload.promises.length < 3 || payload.consent !== true) { | |
| sendJson(res, 400, { error: 'Complete the name, three promises, hard part, and consent first.' }); | |
| return true; | |
| } | |
| const normalizedPayload = { | |
| name: cleanString(payload.name, 80), | |
| promises: payload.promises.slice(0, 3).map((item) => cleanString(item, 140)), | |
| apology: cleanString(payload.apology, 700), | |
| consent: true, | |
| }; | |
| let reading; | |
| try { | |
| reading = await rateSincerity(normalizedPayload); | |
| } catch (error) { | |
| console.error('[sincerity]', error.message); | |
| reading = localReflection(normalizedPayload); | |
| } | |
| const submissionId = crypto.randomUUID(); | |
| submissions.push({ | |
| id: submissionId, | |
| ...normalizedPayload, | |
| reading, | |
| createdAt: Date.now(), | |
| viewed: false, | |
| decision: 'pending', | |
| updatedAt: Date.now(), | |
| }); | |
| persistSubmissions(); | |
| sendJson(res, 200, { sealed: true, submissionId }); | |
| } catch (error) { | |
| console.error('[sincerity]', error.message); | |
| sendJson(res, 200, localReflection({})); | |
| } | |
| return true; | |
| } | |
| function serveStatic(req, res) { | |
| const requestPath = new URL(req.url, 'http://localhost').pathname; | |
| const relative = requestPath === '/' ? 'index.html' : requestPath === '/owner' ? 'owner.html' : requestPath.replace(/^\//, ''); | |
| const filePath = path.resolve(__dirname, isProduction ? 'dist' : 'public', relative); | |
| const basePath = path.resolve(__dirname, isProduction ? 'dist' : 'public'); | |
| if (!filePath.startsWith(basePath)) { | |
| res.writeHead(403); res.end('Forbidden'); return; | |
| } | |
| if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { | |
| const ext = path.extname(filePath); | |
| const types = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png', '.json': 'application/json', '.mp3': 'audio/mpeg' }; | |
| res.writeHead(200, { 'Content-Type': types[ext] || 'application/octet-stream' }); | |
| fs.createReadStream(filePath).pipe(res); | |
| } else { | |
| res.writeHead(404, { 'Content-Type': 'text/plain' }); | |
| res.end('Not found'); | |
| } | |
| } | |
| const server = http.createServer(async (req, res) => { | |
| if (await handleApi(req, res)) return; | |
| if (!isProduction) { | |
| const vite = server.vite; | |
| vite.middlewares(req, res, () => serveStatic(req, res)); | |
| } else { | |
| serveStatic(req, res); | |
| } | |
| }); | |
| if (!isProduction) { | |
| server.vite = await createViteServer({ | |
| server: { middlewareMode: true, hmr: false }, | |
| appType: 'spa', | |
| plugins: [{ | |
| name: 'remove-preview-hmr-client', | |
| transformIndexHtml: { | |
| order: 'post', | |
| handler: (html) => html.replace(/\s*<script type="module" src="\/@vite\/client"><\/script>/g, ''), | |
| }, | |
| }], | |
| }); | |
| } | |
| server.listen(port, '0.0.0.0', () => console.log(`Second Chance listening on 0.0.0.0:${port} (${isProduction ? 'production' : 'development'})`)); | |