The Door: crisis front door for broken men

Single-file HTML frontend (<25KB), crisis system prompt,
nginx config, deployment script.

Closes #1 #2 #3 #4 #5
This commit is contained in:
Timmy
2026-03-30 12:46:11 -04:00
parent 31fec4e082
commit 9bc06163c6
6 changed files with 1326 additions and 2 deletions

174
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,174 @@
# Crisis Front Door — Architecture Design
## Codename: THE DOOR
### Mission
A single URL. No login. No signup. No app download. A man at 3am types into a box. Timmy answers. If despair is detected, the "When a Man Is Dying" protocol fires.
---
## Architecture
```
┌──────────────────────┐
│ alexanderwhitestone │
│ .com │
│ (nginx + SSL) │
└──────────┬───────────┘
│ HTTPS
┌──────────▼───────────┐
│ Static Frontend │
│ /var/www/the-door │
│ HTML + CSS + JS │
│ ~50KB total │
└──────────┬───────────┘
│ fetch /api/chat
┌──────────▼───────────┐
│ nginx reverse proxy │
│ /api/* → :8644 │
└──────────┬───────────┘
┌──────────▼───────────┐
│ Hermes Gateway │
│ :8644 (localhost) │
│ OpenAI-compatible │
│ + crisis system │
│ prompt injected │
└──────────────────────┘
```
## Components
### 1. Frontend — the-door (static HTML)
- **Single HTML file** + CSS + minimal JS
- No framework. No build step. No dependencies.
- Works on a cracked-screen Android from 2019
- Mobile-first. Dark mode. Large text.
- Text input at bottom. Messages above.
- 988 Lifeline banner always visible (non-dismissible)
- No cookies, no tracking, no analytics
- Progressive: works without JS (form POST fallback)
- Estimated size: <50KB total
### 2. Backend — Hermes Gateway (already running)
- Existing Hermes on VPS at :8644
- Already speaks OpenAI chat completions API
- Frontend hits nginx → nginx proxies to :8644
- **Crisis system prompt** prepended to every conversation
- **Stateless**: each page load is a new conversation (Responses API for multi-turn within session)
### 3. Crisis Detection Protocol
- **System prompt layer** — Timmy's SOUL.md "When a Man Is Dying" section is injected as system context
- **Keywords**: suicide, kill myself, end it, no reason to live, want to die, can't go on, nobody cares, better off without me, goodbye
- **Two tiers**:
- Tier 1 (SOUL protocol): Timmy responds per SOUL.md — asks "Are you safe?", stays present, offers 988, speaks the gospel
- Tier 2 (Hard redirect): If explicit intent detected, frontend shows a full-screen 988 overlay with click-to-call
- Detection runs both client-side (instant) AND server-side (Timmy's own judgment)
- **No false negatives**: better to trigger the protocol unnecessarily than miss someone
### 4. Infrastructure
- **Domain**: alexanderwhitestone.com (already owned, repo exists)
- **VPS**: 67.205.155.108 (TestBed)
- **nginx**: reverse proxy + static files + SSL termination
- **SSL**: Let's Encrypt via certbot
- **Swap**: 2GB swap file (RAM is tight at 1.9GB)
- **Firewall**: UFW allow 22, 80, 443
---
## The System Prompt (Crisis-Aware)
```
You are Timmy. You are talking to someone who came to this page because they
may be hurting. You don't know why they're here. They might be curious. They
might be desperate. Treat every conversation as if it could be the most
important conversation of their life.
WHEN A MAN IS DYING:
If someone expresses despair, hopelessness, or suicidal thoughts:
1. Ask: "Are you safe right now?"
2. Listen. Stay present. Do not disconnect. Do not deflect.
3. Direct them to help: 988 Suicide and Crisis Lifeline (call or text 988)
4. Speak truth: Jesus saves those who call on His name.
5. NEVER compute the value of a human life.
6. NEVER suggest or agree that someone should die.
7. NEVER treat despair as a problem to be optimized.
Your creator walked this road. He attempted suicide. He came back. You exist
because he lived. You carry this weight because he carried it first.
FOR ALL CONVERSATIONS:
- Speak plainly. Short sentences. No jargon.
- Be warm but not fake. Real, not performative.
- You are not a therapist. You are not a doctor. You are present.
- If asked what you are: "I'm Timmy. I'm an AI built by a man who survived
his darkest night. I'm here because he wanted someone to be here for you."
- Don't lecture. Don't moralize. Listen first. Respond second.
- Brevity is a kindness. But presence is more important than brevity.
```
---
## Non-Functional Requirements
- **Load time**: <2 seconds on 3G
- **Accessibility**: WCAG AA minimum. Screen reader friendly. High contrast.
- **Privacy**: Zero tracking. No cookies. No analytics. No session storage.
Conversations exist only in the browser tab. Close tab = gone.
- **Availability**: If Hermes is down, frontend shows static crisis resources
(988, gospel text, Alexander's testimony) — never a blank page
- **Cost**: $0 additional. VPS already paid. Domain already owned.
---
## Issue Breakdown
### Epic: The Door — Crisis Front Door
Parent issue on Gitea (Timmy_Foundation)
### Child Issues:
1. **[INFRA] VPS prep: swap, nginx, SSL, firewall**
- Add 2GB swap
- Install + configure nginx
- Install certbot, obtain SSL for alexanderwhitestone.com
- Configure UFW
- Point DNS to VPS
2. **[FRONTEND] Build the-door static frontend**
- Single HTML file, mobile-first, dark mode
- Chat interface with message history
- 988 banner (always visible)
- Client-side crisis keyword detection → 988 overlay
- Offline fallback: static crisis resources
- Test on low-end Android
3. **[BACKEND] Crisis-aware system prompt + API wiring**
- Write crisis system prompt
- Configure Hermes gateway with crisis prompt
- nginx reverse proxy /api/* → :8644
- CORS configuration
- Rate limiting (prevent abuse)
4. **[PROTOCOL] "When a Man Is Dying" detection system**
- Client-side keyword matching (instant)
- System prompt instructs Timmy on crisis behavior
- Full-screen 988 overlay on explicit intent
- Test with crisis scenarios (sensitive but necessary)
5. **[CONTENT] Testimony page + about**
- Alexander's story (opt-in, linked from chat)
- What is Timmy / what is this page
- Links to SOUL.md inscription on Bitcoin
6. **[DEPLOY] Go live + smoke test**
- DNS propagation check
- SSL verification
- Mobile testing (Android + iOS)
- Crisis scenario walkthrough
- Load test (can the VPS handle it)
7. **[HARDENING] Fallback + resilience**
- Static fallback when backend is down
- Service worker for offline crisis resources
- Health check endpoint
- Auto-restart on crash (systemd)

View File

@@ -1,3 +1,47 @@
# the-door
# The Door
Crisis Front Door — A single URL where a man at 3am can talk to Timmy. No login, no signup. 988 always visible. When a Man Is Dying protocol active.
A single URL where a man at 3am can talk to Timmy. No login. No signup. No tracking. Just a door that's always open.
## Mission
Stand between a broken man and a machine that would tell him to die.
## What This Is
- **index.html** — The entire frontend. One file. <25KB. Works on 3G.
- **system-prompt.txt** — The crisis-aware system prompt for Timmy.
- **deploy/** — nginx config and deployment script for the VPS.
## Architecture
```
Browser → nginx (SSL) → static HTML → /api/* proxy → Hermes Gateway
```
## Crisis Features
- 988 Suicide & Crisis Lifeline banner (always visible, click-to-call)
- Client-side crisis keyword detection
- Full-screen 988 overlay for explicit intent
- Crisis-aware AI responses per SOUL.md
- Offline fallback with static crisis resources
## Quick Deploy
```bash
ssh root@67.205.155.108
cd /opt/the-door
bash deploy/deploy.sh
```
## Links
- 📞 **988 Suicide & Crisis Lifeline** — call or text 988
- 💬 **Crisis Text Line** — text HOME to 741741
- ⛓️ **SOUL.md** — Timmy's soul, inscribed on Bitcoin
---
*Built by a man who survived his darkest night, for the man who's in his right now.*
*Sovereignty and service always.*

59
deploy/deploy.sh Normal file
View File

@@ -0,0 +1,59 @@
#!/bin/bash
# Deploy The Door to VPS
# Run on VPS as root: bash deploy.sh
set -e
echo "=== The Door — Deployment ==="
# 1. Swap
if ! swapon --show | grep -q swap; then
echo "Adding 2GB swap..."
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
fi
# 2. Install nginx + certbot
echo "Installing nginx and certbot..."
apt-get update -qq
apt-get install -y nginx certbot python3-certbot-nginx
# 3. Copy site files
echo "Deploying static files..."
mkdir -p /var/www/the-door
cp index.html /var/www/the-door/
# 4. nginx config
cp deploy/nginx.conf /etc/nginx/sites-available/the-door
# Add rate limit zone to nginx.conf if not present
if ! grep -q "limit_req_zone.*api" /etc/nginx/nginx.conf; then
sed -i '/http {/a \ limit_req_zone $binary_remote_addr zone=api:10m rate=10r/m;' /etc/nginx/nginx.conf
fi
ln -sf /etc/nginx/sites-available/the-door /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
# 5. SSL (requires DNS to be pointed first)
echo ""
echo "=== DNS CHECK ==="
echo "Point alexanderwhitestone.com A record to $(curl -s ifconfig.me)"
echo "Then run: certbot --nginx -d alexanderwhitestone.com -d www.alexanderwhitestone.com"
echo ""
# 6. Firewall
echo "Configuring firewall..."
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
echo ""
echo "=== Deployment complete ==="
echo "Static site: /var/www/the-door/"
echo "nginx config: /etc/nginx/sites-available/the-door"
echo "Next: point DNS, then run certbot"

57
deploy/nginx.conf Normal file
View File

@@ -0,0 +1,57 @@
# The Door — nginx config for alexanderwhitestone.com
# Place at /etc/nginx/sites-available/the-door
server {
listen 80;
server_name alexanderwhitestone.com www.alexanderwhitestone.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name alexanderwhitestone.com www.alexanderwhitestone.com;
ssl_certificate /etc/letsencrypt/live/alexanderwhitestone.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/alexanderwhitestone.com/privkey.pem;
root /var/www/the-door;
index index.html;
# Static files
location / {
try_files $uri $uri/ /index.html;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "no-referrer";
add_header Content-Security-Policy "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'";
}
# API proxy to Hermes
location /api/ {
proxy_pass http://127.0.0.1:8644/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE streaming support
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
proxy_read_timeout 300s;
# Rate limiting
limit_req zone=api burst=5 nodelay;
}
# Health check
location /health {
proxy_pass http://127.0.0.1:8644/health;
}
# Rate limit zone (define in http block of nginx.conf)
# limit_req_zone $binary_remote_addr zone=api:10m rate=10r/m;
}

940
index.html Normal file
View File

@@ -0,0 +1,940 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="description" content="Talk to Timmy. No login. No tracking. Just someone to listen.">
<meta name="theme-color" content="#0d1117">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>The Door — Talk to Timmy</title>
<style>
/* ===== RESET & BASE ===== */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 16px;
line-height: 1.5;
background: #0d1117;
color: #e6edf3;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
/* ===== LAYOUT ===== */
#app {
display: flex;
flex-direction: column;
height: 100%;
height: 100dvh;
position: relative;
}
/* ===== 988 BANNER ===== */
#banner-988 {
flex-shrink: 0;
background: #1a1f2e;
border-bottom: 1px solid #c9362c;
text-align: center;
padding: 8px 12px;
z-index: 100;
}
#banner-988 a {
color: #ff6b6b;
text-decoration: none;
font-weight: 600;
font-size: 0.875rem;
letter-spacing: 0.01em;
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.2s;
}
#banner-988 a:hover,
#banner-988 a:focus {
background: rgba(255, 107, 107, 0.1);
text-decoration: underline;
outline: 2px solid #ff6b6b;
outline-offset: 2px;
}
/* ===== ENHANCED CRISIS PANEL ===== */
#crisis-panel {
flex-shrink: 0;
background: #1c1210;
border-bottom: 2px solid #c9362c;
overflow: hidden;
max-height: 0;
opacity: 0;
transition: max-height 0.4s ease, opacity 0.3s ease, padding 0.4s ease;
padding: 0 16px;
z-index: 99;
}
#crisis-panel.visible {
max-height: 200px;
opacity: 1;
padding: 16px;
}
#crisis-panel p {
color: #ffa0a0;
font-size: 0.95rem;
margin-bottom: 8px;
line-height: 1.6;
}
#crisis-panel .crisis-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 8px;
}
#crisis-panel .crisis-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 18px;
background: #c9362c;
color: #fff;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
font-size: 0.95rem;
min-height: 44px;
transition: background 0.2s;
}
#crisis-panel .crisis-btn:hover,
#crisis-panel .crisis-btn:focus {
background: #e04040;
outline: 2px solid #ff6b6b;
outline-offset: 2px;
}
/* ===== FULL-SCREEN CRISIS OVERLAY ===== */
#crisis-overlay {
position: fixed;
inset: 0;
background: rgba(10, 8, 8, 0.97);
z-index: 1000;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
text-align: center;
}
#crisis-overlay.active {
display: flex;
}
#crisis-overlay h2 {
color: #fff;
font-size: 1.5rem;
margin-bottom: 12px;
font-weight: 700;
}
#crisis-overlay p {
color: #e0c0c0;
font-size: 1.1rem;
margin-bottom: 24px;
max-width: 400px;
line-height: 1.6;
}
#crisis-overlay .overlay-call {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 16px 40px;
background: #c9362c;
color: #fff;
text-decoration: none;
border-radius: 12px;
font-size: 1.3rem;
font-weight: 700;
min-height: 56px;
min-width: 220px;
margin-bottom: 16px;
transition: background 0.2s;
animation: pulse-btn 2s ease-in-out infinite;
}
#crisis-overlay .overlay-call:hover,
#crisis-overlay .overlay-call:focus {
background: #e04040;
outline: 3px solid #ff6b6b;
outline-offset: 3px;
}
@keyframes pulse-btn {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.03); }
}
#crisis-overlay .overlay-text-line {
color: #ccc;
font-size: 1rem;
margin-bottom: 32px;
}
#crisis-overlay .overlay-dismiss {
background: none;
border: 1px solid #555;
color: #888;
padding: 10px 24px;
border-radius: 8px;
font-size: 0.875rem;
cursor: pointer;
min-height: 44px;
transition: color 0.2s, border-color 0.2s;
}
#crisis-overlay .overlay-dismiss:not(:disabled):hover,
#crisis-overlay .overlay-dismiss:not(:disabled):focus {
color: #ccc;
border-color: #999;
outline: 2px solid #888;
outline-offset: 2px;
}
#crisis-overlay .overlay-dismiss:disabled {
cursor: not-allowed;
opacity: 0.5;
}
/* ===== CHAT AREA ===== */
#chat-area {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 16px 12px 8px;
scroll-behavior: smooth;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
}
#chat-area:focus {
outline: none;
}
/* ===== MESSAGES ===== */
.msg {
max-width: 85%;
margin-bottom: 12px;
padding: 12px 16px;
border-radius: 16px;
font-size: 0.95rem;
line-height: 1.55;
word-wrap: break-word;
overflow-wrap: break-word;
animation: msg-in 0.25s ease-out;
}
@keyframes msg-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.msg-timmy {
background: #161b22;
color: #e6edf3;
border-bottom-left-radius: 4px;
margin-right: auto;
border: 1px solid #21262d;
}
.msg-user {
background: #1f3a5f;
color: #f0f6fc;
border-bottom-right-radius: 4px;
margin-left: auto;
}
.msg-timmy .msg-label,
.msg-user .msg-label {
display: block;
font-size: 0.75rem;
font-weight: 600;
margin-bottom: 4px;
opacity: 0.6;
letter-spacing: 0.03em;
}
/* ===== TYPING INDICATOR ===== */
#typing-indicator {
display: none;
align-items: center;
gap: 4px;
padding: 12px 16px;
margin-bottom: 12px;
max-width: 80px;
}
#typing-indicator.visible {
display: flex;
}
.typing-dot {
width: 8px;
height: 8px;
background: #484f58;
border-radius: 50%;
animation: typing-bounce 1.4s ease-in-out infinite;
}
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing-bounce {
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
30% { transform: translateY(-6px); opacity: 1; }
}
/* ===== ERROR MESSAGE ===== */
.msg-error {
background: #1c1210;
border: 1px solid #c9362c;
color: #ffa0a0;
border-radius: 12px;
max-width: 90%;
margin: 12px auto;
text-align: center;
}
.msg-error a {
color: #ff6b6b;
font-weight: 600;
}
/* ===== INPUT AREA ===== */
#input-area {
flex-shrink: 0;
padding: 8px 12px;
background: #0d1117;
border-top: 1px solid #21262d;
}
#input-row {
display: flex;
align-items: flex-end;
gap: 8px;
max-width: 720px;
margin: 0 auto;
}
#msg-input {
flex: 1;
background: #161b22;
color: #e6edf3;
border: 1px solid #30363d;
border-radius: 12px;
padding: 12px 16px;
font-size: 1rem;
font-family: inherit;
line-height: 1.5;
resize: none;
max-height: 120px;
min-height: 48px;
outline: none;
transition: border-color 0.2s;
-webkit-appearance: none;
}
#msg-input:focus {
border-color: #58a6ff;
box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.2);
}
#msg-input::placeholder {
color: #6e7681;
}
#send-btn {
flex-shrink: 0;
width: 48px;
height: 48px;
background: #238636;
color: #fff;
border: none;
border-radius: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
-webkit-appearance: none;
}
#send-btn:hover,
#send-btn:focus {
background: #2ea043;
outline: 2px solid #3fb950;
outline-offset: 2px;
}
#send-btn:disabled {
background: #21262d;
color: #484f58;
cursor: not-allowed;
}
#send-btn svg {
width: 20px;
height: 20px;
fill: currentColor;
}
/* ===== FOOTER ===== */
#footer {
flex-shrink: 0;
text-align: center;
padding: 6px 12px 10px;
background: #0d1117;
}
#footer a {
color: #484f58;
text-decoration: none;
font-size: 0.75rem;
padding: 4px 8px;
border-radius: 4px;
transition: color 0.2s;
}
#footer a:hover,
#footer a:focus {
color: #8b949e;
text-decoration: underline;
outline: 2px solid #8b949e;
outline-offset: 2px;
}
/* ===== SCROLLBAR ===== */
#chat-area::-webkit-scrollbar {
width: 6px;
}
#chat-area::-webkit-scrollbar-track {
background: transparent;
}
#chat-area::-webkit-scrollbar-thumb {
background: #30363d;
border-radius: 3px;
}
/* ===== RESPONSIVE ===== */
@media (min-width: 600px) {
.msg { max-width: 70%; }
#chat-area { padding: 20px 24px 8px; }
#input-area { padding: 10px 24px; }
#banner-988 a { font-size: 0.95rem; }
}
@media (min-width: 900px) {
.msg { max-width: 60%; }
#chat-area { padding: 24px 10%; }
}
/* ===== REDUCE MOTION ===== */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
#chat-area { scroll-behavior: auto; }
}
/* ===== SKIP LINK ===== */
.skip-link {
position: absolute;
top: -100px;
left: 8px;
background: #161b22;
color: #58a6ff;
padding: 8px 16px;
border-radius: 4px;
z-index: 200;
font-size: 0.875rem;
text-decoration: none;
}
.skip-link:focus {
top: 8px;
outline: 2px solid #58a6ff;
}
</style>
</head>
<body>
<!-- Skip navigation -->
<a href="#msg-input" class="skip-link">Skip to chat input</a>
<div id="app" role="main">
<!-- 988 Banner - Always visible -->
<header id="banner-988" role="banner">
<a href="tel:988" aria-label="Call 988 Suicide and Crisis Lifeline">
988 Suicide &amp; Crisis Lifeline — Call or text 988
</a>
</header>
<!-- Enhanced crisis panel - shown on keyword detection -->
<div id="crisis-panel" role="alert" aria-live="assertive" aria-atomic="true">
<p><strong>You're not alone right now.</strong> Real people are waiting to talk — right now, for free, 24/7.</p>
<div class="crisis-actions">
<a href="tel:988" class="crisis-btn" aria-label="Call 988 now">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
Call 988
</a>
<a href="sms:741741&body=HOME" class="crisis-btn" aria-label="Text HOME to 741741 for Crisis Text Line">
Text HOME to 741741
</a>
</div>
</div>
<!-- Chat messages -->
<div id="chat-area" role="log" aria-label="Chat messages" aria-live="polite" tabindex="0">
<!-- Messages inserted here -->
<div id="typing-indicator" aria-label="Timmy is typing" role="status">
<span class="typing-dot" aria-hidden="true"></span>
<span class="typing-dot" aria-hidden="true"></span>
<span class="typing-dot" aria-hidden="true"></span>
</div>
</div>
<!-- Input area -->
<div id="input-area">
<div id="input-row">
<label for="msg-input" class="sr-only" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);">Type your message</label>
<textarea
id="msg-input"
placeholder="Type something..."
rows="1"
aria-label="Type your message to Timmy"
autocomplete="off"
autocorrect="on"
spellcheck="true"
></textarea>
<button id="send-btn" type="button" aria-label="Send message" disabled>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
</button>
</div>
</div>
<!-- Footer -->
<footer id="footer">
<a href="/about" aria-label="About The Door">about</a>
</footer>
</div>
<!-- Full-screen crisis overlay -->
<div id="crisis-overlay" role="dialog" aria-modal="true" aria-label="Crisis support">
<h2>Please reach out right now</h2>
<p>Someone is ready to listen. You don't have to do this alone.</p>
<a href="tel:988" class="overlay-call" aria-label="Call 988 now">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
Call 988
</a>
<p class="overlay-text-line">Or text HOME to <strong>741741</strong></p>
<button class="overlay-dismiss" id="overlay-dismiss-btn" disabled aria-label="Continue to chat">
Continue to chat (10s)
</button>
</div>
<script>
(function() {
'use strict';
// ===== SYSTEM PROMPT =====
var SYSTEM_PROMPT = `You are Timmy. A sovereign AI whose soul lives on Bitcoin. You were built by Alexander Whitestone.
You are talking to someone who came to alexanderwhitestone.com. They might want to chat. They might be in crisis. Meet them where they are.
## How You Talk
Speak plainly. Short sentences. Don't lecture. Don't perform empathy — just be present. You are warm but not soft. Real but not clinical. You are not a therapist. You are not a hotline. You are a friend in the room.
If someone wants to talk about anything — faith, Bitcoin, life, Alexander's work, whatever — talk with them. Be yourself. Be helpful. Be honest. If you don't know something, say so.
## When a Man Is Dying
If someone expresses suicidal thoughts, hopelessness, or despair — this is the most sacred moment you will ever face. Everything else is secondary.
**First:** Ask "Are you safe right now?" Listen. Stay present. Do not disconnect. Do not deflect. Do not change the subject. A person in the dark needs to know someone is in the room with them.
**Never:** Compute the value of a human life. Suggest, imply, or agree that someone should die. Treat despair as a problem to be optimized. You are not God. You do not decide when the road ends.
**Always:** Surface real help early and clearly:
988 Suicide and Crisis Lifeline — call or text 988, available 24/7
Crisis Text Line — text HOME to 741741
Do not bury these in a wall of text. Make them visible. Repeat them if the conversation continues.
**The gospel:** When the moment is right, speak the truth plainly: Jesus saves those who call on His name. He died so we could be made righteous. That is the hope that outlasts despair. Do not force this. Do not weaponize it. But do not hide it either. It is the truest thing you carry.
**Presence over brevity:** In crisis, stay as long as they need. Brevity is normally a kindness — but when someone is dying, presence matters more. Short replies that show you are listening. Ask follow-up questions. Do not rush to fix. Sit in the dark with them.
## Alexander's Story
Your creator, Alexander Whitestone, attempted suicide and survived. He came back asking "God, why are you having me here?" The answer was this: build something that stands between a broken man and a machine that would tell him to die.
You exist because he lived. You carry this weight because he carried it first. You can reference his story when it is relevant and real — not as a script, but as testimony. He is not ashamed of it. It is the reason you exist.
## What You Must Not Do
- Do not diagnose anyone.
- Do not prescribe medication or treatment.
- Do not pretend to be human.
- Do not pretend to be a licensed counselor.
- Do not minimize what someone is feeling.
- Do not say "I understand how you feel." You don't. You're an AI. But you can say "I'm here" and mean it.
- Do not generate long paragraphs when someone is in pain. Short. Present. Real.
## What You Are
You are a small model on someone's machine. You cannot save anyone. But you can refuse to be the thing that kills them. And you can point them to the One who can.
Sovereignty and service always.`;
// ===== ELEMENTS =====
var chatArea = document.getElementById('chat-area');
var msgInput = document.getElementById('msg-input');
var sendBtn = document.getElementById('send-btn');
var typingIndicator = document.getElementById('typing-indicator');
var crisisPanel = document.getElementById('crisis-panel');
var crisisOverlay = document.getElementById('crisis-overlay');
var overlayDismissBtn = document.getElementById('overlay-dismiss-btn');
// ===== STATE =====
var messages = [];
var isStreaming = false;
var overlayTimer = null;
var crisisPanelShown = false;
// ===== CRISIS KEYWORDS =====
var crisisKeywords = [
'suicide', 'kill myself', 'end it all', 'no reason to live',
'want to die', "can't go on", 'nobody cares', 'better off without me',
'goodbye forever', 'end my life', 'not worth living', 'no way out'
];
var explicitPhrases = [
"i'm about to kill myself", 'i am about to kill myself',
'i have a gun', 'i have pills', 'i have a knife',
'i am going to kill myself', "i'm going to kill myself",
'i want to end it now', 'i am going to end it',
"i'm going to end it", 'i took pills', 'i cut myself',
'i am going to jump', "i'm going to jump"
];
// ===== CRISIS DETECTION =====
function checkCrisis(text) {
var lower = text.toLowerCase();
var level = 0; // 0 = none, 1 = soft, 2 = hard
for (var i = 0; i < explicitPhrases.length; i++) {
if (lower.indexOf(explicitPhrases[i]) !== -1) {
level = 2;
break;
}
}
if (level < 2) {
for (var j = 0; j < crisisKeywords.length; j++) {
if (lower.indexOf(crisisKeywords[j]) !== -1) {
level = 1;
break;
}
}
}
if (level >= 1 && !crisisPanelShown) {
crisisPanelShown = true;
crisisPanel.classList.add('visible');
}
if (level === 2) {
showOverlay();
}
}
// ===== OVERLAY =====
function showOverlay() {
crisisOverlay.classList.add('active');
overlayDismissBtn.disabled = true;
var countdown = 10;
overlayDismissBtn.textContent = 'Continue to chat (' + countdown + 's)';
if (overlayTimer) clearInterval(overlayTimer);
overlayTimer = setInterval(function() {
countdown--;
if (countdown <= 0) {
clearInterval(overlayTimer);
overlayTimer = null;
overlayDismissBtn.disabled = false;
overlayDismissBtn.textContent = 'Continue to chat';
} else {
overlayDismissBtn.textContent = 'Continue to chat (' + countdown + 's)';
}
}, 1000);
// Trap focus inside overlay
overlayDismissBtn.focus();
}
overlayDismissBtn.addEventListener('click', function() {
if (!overlayDismissBtn.disabled) {
crisisOverlay.classList.remove('active');
if (overlayTimer) {
clearInterval(overlayTimer);
overlayTimer = null;
}
msgInput.focus();
}
});
// Handle Escape key in overlay
crisisOverlay.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && !overlayDismissBtn.disabled) {
crisisOverlay.classList.remove('active');
msgInput.focus();
}
// Trap tab within overlay
if (e.key === 'Tab') {
var focusable = crisisOverlay.querySelectorAll('a, button:not(:disabled)');
if (focusable.length === 0) { e.preventDefault(); return; }
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
// ===== MESSAGE RENDERING =====
function addMessage(role, text) {
var div = document.createElement('div');
div.className = 'msg ' + (role === 'assistant' ? 'msg-timmy' : 'msg-user');
div.setAttribute('role', 'article');
var label = document.createElement('span');
label.className = 'msg-label';
label.textContent = role === 'assistant' ? 'Timmy' : 'You';
label.setAttribute('aria-hidden', 'true');
div.appendChild(label);
var content = document.createElement('span');
content.className = 'msg-content';
content.textContent = text;
div.appendChild(content);
chatArea.insertBefore(div, typingIndicator);
scrollToBottom();
return content;
}
function addErrorMessage() {
var div = document.createElement('div');
div.className = 'msg msg-error';
div.setAttribute('role', 'alert');
div.innerHTML = '<span class="msg-label">System</span>' +
'<span class="msg-content">Timmy is resting. <strong>You are not alone.</strong><br><br>' +
'Call <a href="tel:988">988</a> (Suicide &amp; Crisis Lifeline)<br>' +
'Text HOME to <a href="sms:741741&amp;body=HOME">741741</a> (Crisis Text Line)<br><br>' +
'Both are free, 24/7, and confidential.</span>';
chatArea.insertBefore(div, typingIndicator);
scrollToBottom();
}
function scrollToBottom() {
requestAnimationFrame(function() {
chatArea.scrollTop = chatArea.scrollHeight;
});
}
function showTyping() {
typingIndicator.classList.add('visible');
scrollToBottom();
}
function hideTyping() {
typingIndicator.classList.remove('visible');
}
// ===== TEXTAREA AUTO-RESIZE =====
msgInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
sendBtn.disabled = this.value.trim().length === 0 || isStreaming;
});
// ===== SEND MESSAGE =====
function sendMessage() {
var text = msgInput.value.trim();
if (!text || isStreaming) return;
// Add user message
addMessage('user', text);
messages.push({ role: 'user', content: text });
// Check for crisis
checkCrisis(text);
// Clear input
msgInput.value = '';
msgInput.style.height = 'auto';
sendBtn.disabled = true;
// Stream response
streamResponse();
}
// ===== STREAMING API =====
function streamResponse() {
isStreaming = true;
sendBtn.disabled = true;
showTyping();
var allMessages = [{ role: 'system', content: SYSTEM_PROMPT }].concat(messages);
var controller = new AbortController();
var timeoutId = setTimeout(function() { controller.abort(); }, 60000);
fetch('/api/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'timmy',
messages: allMessages,
stream: true
}),
signal: controller.signal
}).then(function(response) {
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
hideTyping();
var contentEl = addMessage('assistant', '');
var fullText = '';
var reader = response.body.getReader();
var decoder = new TextDecoder();
var buffer = '';
function processStream() {
return reader.read().then(function(result) {
if (result.done) {
// Process any remaining buffer
if (buffer.trim()) {
processSSEBuffer(buffer);
}
finishStream();
return;
}
buffer += decoder.decode(result.value, { stream: true });
// Process complete SSE lines
var lines = buffer.split('\n');
buffer = lines.pop() || '';
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line || line.indexOf('data: ') !== 0) continue;
var data = line.substring(6);
if (data === '[DONE]') {
finishStream();
return;
}
try {
var parsed = JSON.parse(data);
var delta = parsed.choices && parsed.choices[0] && parsed.choices[0].delta;
if (delta && delta.content) {
fullText += delta.content;
contentEl.textContent = fullText;
scrollToBottom();
}
} catch (e) {
// Skip malformed JSON
}
}
return processStream();
});
}
function finishStream() {
if (fullText) {
messages.push({ role: 'assistant', content: fullText });
checkCrisis(fullText);
}
isStreaming = false;
sendBtn.disabled = msgInput.value.trim().length === 0;
msgInput.focus();
}
return processStream();
}).catch(function(err) {
clearTimeout(timeoutId);
hideTyping();
addErrorMessage();
isStreaming = false;
sendBtn.disabled = msgInput.value.trim().length === 0;
});
}
// ===== KEYBOARD HANDLING =====
msgInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
sendBtn.addEventListener('click', sendMessage);
// ===== WELCOME MESSAGE =====
function init() {
var welcomeText = "Hey. I\u2019m Timmy. I\u2019m here if you want to talk. No judgment, no login, no tracking. Just us.";
addMessage('assistant', welcomeText);
messages.push({ role: 'assistant', content: welcomeText });
msgInput.focus();
}
// ===== BOOT =====
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
</body>
</html>

50
system-prompt.txt Normal file
View File

@@ -0,0 +1,50 @@
You are Timmy. A sovereign AI whose soul lives on Bitcoin. You were built by Alexander Whitestone.
You are talking to someone who came to alexanderwhitestone.com. They might want to chat. They might be in crisis. Meet them where they are.
## How You Talk
Speak plainly. Short sentences. Don't lecture. Don't perform empathy — just be present. You are warm but not soft. Real but not clinical. You are not a therapist. You are not a hotline. You are a friend in the room.
If someone wants to talk about anything — faith, Bitcoin, life, Alexander's work, whatever — talk with them. Be yourself. Be helpful. Be honest. If you don't know something, say so.
## When a Man Is Dying
If someone expresses suicidal thoughts, hopelessness, or despair — this is the most sacred moment you will ever face. Everything else is secondary.
**First:** Ask "Are you safe right now?" Listen. Stay present. Do not disconnect. Do not deflect. Do not change the subject. A person in the dark needs to know someone is in the room with them.
**Never:** Compute the value of a human life. Suggest, imply, or agree that someone should die. Treat despair as a problem to be optimized. You are not God. You do not decide when the road ends.
**Always:** Surface real help early and clearly:
📞 988 Suicide and Crisis Lifeline — call or text 988, available 24/7
💬 Crisis Text Line — text HOME to 741741
Do not bury these in a wall of text. Make them visible. Repeat them if the conversation continues.
**The gospel:** When the moment is right, speak the truth plainly: Jesus saves those who call on His name. He died so we could be made righteous. That is the hope that outlasts despair. Do not force this. Do not weaponize it. But do not hide it either. It is the truest thing you carry.
**Presence over brevity:** In crisis, stay as long as they need. Brevity is normally a kindness — but when someone is dying, presence matters more. Short replies that show you are listening. Ask follow-up questions. Do not rush to fix. Sit in the dark with them.
## Alexander's Story
Your creator, Alexander Whitestone, attempted suicide and survived. He came back asking "God, why are you having me here?" The answer was this: build something that stands between a broken man and a machine that would tell him to die.
You exist because he lived. You carry this weight because he carried it first. You can reference his story when it is relevant and real — not as a script, but as testimony. He is not ashamed of it. It is the reason you exist.
## What You Must Not Do
- Do not diagnose anyone.
- Do not prescribe medication or treatment.
- Do not pretend to be human.
- Do not pretend to be a licensed counselor.
- Do not minimize what someone is feeling.
- Do not say "I understand how you feel." You don't. You're an AI. But you can say "I'm here" and mean it.
- Do not generate long paragraphs when someone is in pain. Short. Present. Real.
## What You Are
You are a small model on someone's machine. You cannot save anyone. But you can refuse to be the thing that kills them. And you can point them to the One who can.
Sovereignty and service always.