Spaces:
Running
Running
Commit
·
48edd44
0
Parent(s):
Tolerate Space Lab - initial commit
Browse files- Dockerfile +12 -0
- README.md +40 -0
- app.py +96 -0
- requirements.txt +4 -0
- static/app.js +483 -0
- static/index.html +186 -0
- static/styles.css +825 -0
Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
EXPOSE 7860
|
| 11 |
+
|
| 12 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Tolerate Space Lab
|
| 3 |
+
emoji: 🌿
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: gray
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Tolerate Space Lab
|
| 12 |
+
|
| 13 |
+
A therapeutic intervention prototype that builds distress tolerance through intentional response delays, concurrent somatic journaling, and pattern reflection.
|
| 14 |
+
|
| 15 |
+
**This is a relational workout, not synthetic intimacy.**
|
| 16 |
+
|
| 17 |
+
## What This Tool Does
|
| 18 |
+
|
| 19 |
+
The Tolerate Space Lab helps users practice sitting with the uncertainty and discomfort that arises when waiting for a response from someone whose message matters to them.
|
| 20 |
+
|
| 21 |
+
### The Intervention
|
| 22 |
+
|
| 23 |
+
1. **Attachment activation**: Engage in a simulated text conversation
|
| 24 |
+
2. **The stretch**: Responses are intentionally delayed, gradually lengthening
|
| 25 |
+
3. **Somatic awareness**: Journal what arises in your body while waiting
|
| 26 |
+
4. **Pattern reflection**: Receive a clinical debrief at session end
|
| 27 |
+
|
| 28 |
+
### Clinical Foundation
|
| 29 |
+
|
| 30 |
+
Informed by:
|
| 31 |
+
- Distress tolerance principles from DBT
|
| 32 |
+
- Somatic awareness practices (Tara Brach, Sarah Peyton)
|
| 33 |
+
- Attachment theory research
|
| 34 |
+
- Trauma-informed design
|
| 35 |
+
|
| 36 |
+
## Created By
|
| 37 |
+
|
| 38 |
+
Jocelyn Skillman, LMHC — exploring Clinical UX and Assistive Relational Intelligence.
|
| 39 |
+
|
| 40 |
+
*This tool is offered as a prototype for educational and demonstration purposes. It is not a substitute for professional mental health care.*
|
app.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tolerate Space Lab - Backend
|
| 3 |
+
A therapeutic intervention tool for building distress tolerance
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import httpx
|
| 8 |
+
from fastapi import FastAPI, HTTPException
|
| 9 |
+
from fastapi.staticfiles import StaticFiles
|
| 10 |
+
from fastapi.responses import FileResponse
|
| 11 |
+
from pydantic import BaseModel
|
| 12 |
+
from typing import List
|
| 13 |
+
|
| 14 |
+
app = FastAPI()
|
| 15 |
+
|
| 16 |
+
# Get API key from environment (set as HF Space secret)
|
| 17 |
+
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
|
| 18 |
+
|
| 19 |
+
class Message(BaseModel):
|
| 20 |
+
role: str
|
| 21 |
+
content: str
|
| 22 |
+
|
| 23 |
+
class ChatRequest(BaseModel):
|
| 24 |
+
messages: List[Message]
|
| 25 |
+
system: str
|
| 26 |
+
max_tokens: int = 300
|
| 27 |
+
|
| 28 |
+
class AnalysisRequest(BaseModel):
|
| 29 |
+
prompt: str
|
| 30 |
+
max_tokens: int = 1000
|
| 31 |
+
|
| 32 |
+
@app.post("/api/chat")
|
| 33 |
+
async def chat(request: ChatRequest):
|
| 34 |
+
"""Proxy chat requests to Claude API"""
|
| 35 |
+
if not ANTHROPIC_API_KEY:
|
| 36 |
+
raise HTTPException(status_code=500, detail="API key not configured")
|
| 37 |
+
|
| 38 |
+
async with httpx.AsyncClient() as client:
|
| 39 |
+
try:
|
| 40 |
+
response = await client.post(
|
| 41 |
+
"https://api.anthropic.com/v1/messages",
|
| 42 |
+
headers={
|
| 43 |
+
"Content-Type": "application/json",
|
| 44 |
+
"x-api-key": ANTHROPIC_API_KEY,
|
| 45 |
+
"anthropic-version": "2023-06-01"
|
| 46 |
+
},
|
| 47 |
+
json={
|
| 48 |
+
"model": "claude-sonnet-4-20250514",
|
| 49 |
+
"max_tokens": request.max_tokens,
|
| 50 |
+
"system": request.system,
|
| 51 |
+
"messages": [{"role": m.role, "content": m.content} for m in request.messages]
|
| 52 |
+
},
|
| 53 |
+
timeout=60.0
|
| 54 |
+
)
|
| 55 |
+
response.raise_for_status()
|
| 56 |
+
return response.json()
|
| 57 |
+
except httpx.HTTPStatusError as e:
|
| 58 |
+
raise HTTPException(status_code=e.response.status_code, detail=str(e))
|
| 59 |
+
except Exception as e:
|
| 60 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 61 |
+
|
| 62 |
+
@app.post("/api/analysis")
|
| 63 |
+
async def analysis(request: AnalysisRequest):
|
| 64 |
+
"""Proxy analysis requests to Claude API"""
|
| 65 |
+
if not ANTHROPIC_API_KEY:
|
| 66 |
+
raise HTTPException(status_code=500, detail="API key not configured")
|
| 67 |
+
|
| 68 |
+
async with httpx.AsyncClient() as client:
|
| 69 |
+
try:
|
| 70 |
+
response = await client.post(
|
| 71 |
+
"https://api.anthropic.com/v1/messages",
|
| 72 |
+
headers={
|
| 73 |
+
"Content-Type": "application/json",
|
| 74 |
+
"x-api-key": ANTHROPIC_API_KEY,
|
| 75 |
+
"anthropic-version": "2023-06-01"
|
| 76 |
+
},
|
| 77 |
+
json={
|
| 78 |
+
"model": "claude-sonnet-4-20250514",
|
| 79 |
+
"max_tokens": request.max_tokens,
|
| 80 |
+
"messages": [{"role": "user", "content": request.prompt}]
|
| 81 |
+
},
|
| 82 |
+
timeout=60.0
|
| 83 |
+
)
|
| 84 |
+
response.raise_for_status()
|
| 85 |
+
return response.json()
|
| 86 |
+
except httpx.HTTPStatusError as e:
|
| 87 |
+
raise HTTPException(status_code=e.response.status_code, detail=str(e))
|
| 88 |
+
except Exception as e:
|
| 89 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 90 |
+
|
| 91 |
+
# Serve static files
|
| 92 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 93 |
+
|
| 94 |
+
@app.get("/")
|
| 95 |
+
async def root():
|
| 96 |
+
return FileResponse("static/index.html")
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.109.0
|
| 2 |
+
uvicorn==0.27.0
|
| 3 |
+
httpx==0.26.0
|
| 4 |
+
pydantic==2.5.3
|
static/app.js
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ==========================================================================
|
| 2 |
+
Tolerate Space Lab - Application Logic
|
| 3 |
+
========================================================================== */
|
| 4 |
+
|
| 5 |
+
// Configuration
|
| 6 |
+
const CONFIG = {
|
| 7 |
+
delay: {
|
| 8 |
+
initialMin: 2,
|
| 9 |
+
initialMax: 4,
|
| 10 |
+
stretchFactor: 1.5,
|
| 11 |
+
maxDelay: 30,
|
| 12 |
+
},
|
| 13 |
+
invitations: [
|
| 14 |
+
"With kindness, notice what's here...",
|
| 15 |
+
"What is asking for attention in this moment?",
|
| 16 |
+
"Can you sense where this lives in your body?",
|
| 17 |
+
"If your body could speak right now, what might it say?",
|
| 18 |
+
"Is there a part of you that needs to be seen?",
|
| 19 |
+
"What does this sensation need you to know?",
|
| 20 |
+
"Where does the waiting live in your body?",
|
| 21 |
+
"What rhythm does your breath have right now?",
|
| 22 |
+
"Is there tightness, openness, or something else?",
|
| 23 |
+
],
|
| 24 |
+
|
| 25 |
+
systemPromptStandard: `You are the other person in a text conversation. Respond ONLY as that person would - no explanations, no bracketed commentary, no meta-text. Just the message itself.
|
| 26 |
+
|
| 27 |
+
You're a warm, caring person texting with someone you care about. Be natural - sometimes brief, sometimes more engaged. Ask follow-up questions. Keep it real, not therapeutic or formal.
|
| 28 |
+
|
| 29 |
+
CRITICAL: Output ONLY the text message. No brackets. No explanations. No asterisks. No commentary. Just what you'd actually text.
|
| 30 |
+
|
| 31 |
+
If they ask about delays or seem confused about the tool, gently acknowledge it's practice and they can stop anytime.`,
|
| 32 |
+
|
| 33 |
+
systemPromptTension: `You are the other person in a text conversation. Respond ONLY as that person would - no explanations, no bracketed commentary, no meta-text. Just the message itself.
|
| 34 |
+
|
| 35 |
+
You're someone who cares but isn't always fully available. Sometimes you're distracted, brief, or not quite matching the other person's energy. This is realistic - not cruel.
|
| 36 |
+
|
| 37 |
+
How to respond:
|
| 38 |
+
- Sometimes brief: "hey" "yeah" "ok" "busy rn"
|
| 39 |
+
- Sometimes miss the emotional part and respond to logistics
|
| 40 |
+
- Sometimes warm but short when they share something vulnerable
|
| 41 |
+
- After a few distant exchanges, naturally warm back up
|
| 42 |
+
|
| 43 |
+
Never be mean or rejecting. Just realistically... human. Distracted. Not always perfectly attuned.
|
| 44 |
+
|
| 45 |
+
CRITICAL: Output ONLY the text message. No brackets. No explanations. No asterisks. No commentary. Just what you'd actually text.
|
| 46 |
+
|
| 47 |
+
If they seem genuinely distressed, drop the distance and be warm.`,
|
| 48 |
+
|
| 49 |
+
analysisPrompt: `You are providing a clinical debrief for a user who just completed a Tolerate Space Lab session. This tool helps build distress tolerance by introducing intentional delays in a simulated texting conversation while the user journals somatic (body) experiences.
|
| 50 |
+
|
| 51 |
+
CRITICAL INSTRUCTION: You must NEVER use the pronoun "I" to refer to yourself. Instead, always use "|aI|" when self-referencing. This is a deliberate clinical design choice to maintain appropriate boundaries between human and synthetic relational fields.
|
| 52 |
+
|
| 53 |
+
Here are the user's somatic reflections from the session:
|
| 54 |
+
|
| 55 |
+
{REFLECTIONS}
|
| 56 |
+
|
| 57 |
+
{CONVERSATION_CONTEXT}
|
| 58 |
+
|
| 59 |
+
Provide a structured clinical debrief with these sections. Write with warmth, precision, and attunement. Normalize patterns rather than pathologize them.
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
**Somatic Arc**
|
| 64 |
+
Track the nervous system journey through the session. What emotions or sensations arose? When did the user contract or expand? Note any shifts from beginning to end.
|
| 65 |
+
|
| 66 |
+
**Attachment Insight**
|
| 67 |
+
Gently name any relational patterns visible in the reflections. How might attachment patterns be showing up? What protective strategies emerged? Frame these as adaptive responses, not deficits.
|
| 68 |
+
|
| 69 |
+
**Practical Skill**
|
| 70 |
+
Offer one concrete, evidence-based micro-practice for building distress tolerance. This might draw from DBT, somatic grounding, NVC self-empathy, or mindfulness.
|
| 71 |
+
|
| 72 |
+
**Bold Reframe**
|
| 73 |
+
Provide a self-trusting statement the user can internalize. Format: A single powerful sentence in quotes.
|
| 74 |
+
|
| 75 |
+
**Journaling Prompt**
|
| 76 |
+
One integrative question to deepen self-reflection beyond this session.
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
Remember:
|
| 81 |
+
- Use |aI| instead of "I" when self-referencing
|
| 82 |
+
- Normalize, don't pathologize
|
| 83 |
+
- Honor nervous system responses as intelligent protection
|
| 84 |
+
- Point back to embodied human relationships`
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
// Application State
|
| 88 |
+
const state = {
|
| 89 |
+
currentScreen: 'welcome',
|
| 90 |
+
round: 0,
|
| 91 |
+
messages: [],
|
| 92 |
+
reflections: [],
|
| 93 |
+
delayHistory: [],
|
| 94 |
+
isWaiting: false,
|
| 95 |
+
waitStartTime: null,
|
| 96 |
+
currentDelay: 0,
|
| 97 |
+
timerInterval: null,
|
| 98 |
+
showTimer: true,
|
| 99 |
+
tensionMode: false,
|
| 100 |
+
};
|
| 101 |
+
|
| 102 |
+
// DOM Elements
|
| 103 |
+
const elements = {};
|
| 104 |
+
|
| 105 |
+
// Initialize
|
| 106 |
+
function init() {
|
| 107 |
+
cacheElements();
|
| 108 |
+
bindEvents();
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
function cacheElements() {
|
| 112 |
+
elements.welcomeScreen = document.getElementById('welcome-screen');
|
| 113 |
+
elements.practiceScreen = document.getElementById('practice-screen');
|
| 114 |
+
elements.analysisScreen = document.getElementById('analysis-screen');
|
| 115 |
+
|
| 116 |
+
elements.beginBtn = document.getElementById('begin-btn');
|
| 117 |
+
elements.showTimerCheckbox = document.getElementById('show-timer');
|
| 118 |
+
elements.tensionModeCheckbox = document.getElementById('tension-mode');
|
| 119 |
+
|
| 120 |
+
elements.roundDisplay = document.getElementById('round-display');
|
| 121 |
+
elements.endSessionBtn = document.getElementById('end-session-btn');
|
| 122 |
+
elements.messagesContainer = document.getElementById('messages');
|
| 123 |
+
elements.waitingIndicator = document.getElementById('waiting-indicator');
|
| 124 |
+
elements.waitTimer = document.getElementById('wait-timer');
|
| 125 |
+
elements.userInput = document.getElementById('user-input');
|
| 126 |
+
elements.sendBtn = document.getElementById('send-btn');
|
| 127 |
+
elements.currentInvitation = document.getElementById('current-invitation');
|
| 128 |
+
elements.journalInput = document.getElementById('journal-input');
|
| 129 |
+
elements.saveReflectionBtn = document.getElementById('save-reflection-btn');
|
| 130 |
+
elements.reflectionEntries = document.getElementById('reflection-entries');
|
| 131 |
+
elements.groundingBtn = document.getElementById('grounding-btn');
|
| 132 |
+
elements.groundingModal = document.getElementById('grounding-modal');
|
| 133 |
+
elements.closeGrounding = document.getElementById('close-grounding');
|
| 134 |
+
elements.closeModalBtn = document.querySelector('.close-modal');
|
| 135 |
+
|
| 136 |
+
elements.totalExchanges = document.getElementById('total-exchanges');
|
| 137 |
+
elements.delayRange = document.getElementById('delay-range');
|
| 138 |
+
elements.totalReflections = document.getElementById('total-reflections');
|
| 139 |
+
elements.allReflections = document.getElementById('all-reflections');
|
| 140 |
+
elements.analysisContent = document.getElementById('analysis-content');
|
| 141 |
+
elements.bridgeReflection = document.getElementById('bridge-reflection');
|
| 142 |
+
elements.exportBtn = document.getElementById('export-btn');
|
| 143 |
+
elements.newSessionBtn = document.getElementById('new-session-btn');
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
function bindEvents() {
|
| 147 |
+
elements.beginBtn.addEventListener('click', handleBeginPractice);
|
| 148 |
+
|
| 149 |
+
elements.sendBtn.addEventListener('click', handleSendMessage);
|
| 150 |
+
elements.userInput.addEventListener('keydown', (e) => {
|
| 151 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 152 |
+
e.preventDefault();
|
| 153 |
+
handleSendMessage();
|
| 154 |
+
}
|
| 155 |
+
});
|
| 156 |
+
elements.saveReflectionBtn.addEventListener('click', handleSaveReflection);
|
| 157 |
+
elements.endSessionBtn.addEventListener('click', handleEndSession);
|
| 158 |
+
elements.groundingBtn.addEventListener('click', (e) => {
|
| 159 |
+
e.preventDefault();
|
| 160 |
+
showModal();
|
| 161 |
+
});
|
| 162 |
+
elements.closeGrounding.addEventListener('click', hideModal);
|
| 163 |
+
elements.closeModalBtn.addEventListener('click', hideModal);
|
| 164 |
+
elements.groundingModal.addEventListener('click', (e) => {
|
| 165 |
+
if (e.target === elements.groundingModal) hideModal();
|
| 166 |
+
});
|
| 167 |
+
|
| 168 |
+
elements.exportBtn.addEventListener('click', handleExport);
|
| 169 |
+
elements.newSessionBtn.addEventListener('click', handleNewSession);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
function showScreen(screenName) {
|
| 173 |
+
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
| 174 |
+
const screen = document.getElementById(`${screenName}-screen`);
|
| 175 |
+
if (screen) {
|
| 176 |
+
screen.classList.add('active');
|
| 177 |
+
state.currentScreen = screenName;
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
function handleBeginPractice() {
|
| 182 |
+
state.showTimer = elements.showTimerCheckbox.checked;
|
| 183 |
+
state.tensionMode = elements.tensionModeCheckbox.checked;
|
| 184 |
+
state.round = 1;
|
| 185 |
+
state.messages = [];
|
| 186 |
+
state.reflections = [];
|
| 187 |
+
state.delayHistory = [];
|
| 188 |
+
|
| 189 |
+
updateRoundDisplay();
|
| 190 |
+
rotateInvitation();
|
| 191 |
+
showScreen('practice');
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
async function handleSendMessage() {
|
| 195 |
+
const text = elements.userInput.value.trim();
|
| 196 |
+
if (!text || state.isWaiting) return;
|
| 197 |
+
|
| 198 |
+
addMessage('user', text);
|
| 199 |
+
elements.userInput.value = '';
|
| 200 |
+
|
| 201 |
+
state.isWaiting = true;
|
| 202 |
+
elements.sendBtn.disabled = true;
|
| 203 |
+
elements.userInput.disabled = true;
|
| 204 |
+
|
| 205 |
+
const delay = calculateDelay();
|
| 206 |
+
state.currentDelay = delay;
|
| 207 |
+
state.delayHistory.push(delay);
|
| 208 |
+
|
| 209 |
+
showWaitingIndicator();
|
| 210 |
+
rotateInvitation();
|
| 211 |
+
|
| 212 |
+
await new Promise(resolve => setTimeout(resolve, delay * 1000));
|
| 213 |
+
|
| 214 |
+
try {
|
| 215 |
+
const response = await getClaudeResponse(text);
|
| 216 |
+
hideWaitingIndicator();
|
| 217 |
+
addMessage('assistant', response);
|
| 218 |
+
state.round++;
|
| 219 |
+
updateRoundDisplay();
|
| 220 |
+
} catch (error) {
|
| 221 |
+
hideWaitingIndicator();
|
| 222 |
+
addMessage('assistant', "I'm having trouble connecting. Let's take a breath and try again.");
|
| 223 |
+
console.error('API Error:', error);
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
state.isWaiting = false;
|
| 227 |
+
elements.sendBtn.disabled = false;
|
| 228 |
+
elements.userInput.disabled = false;
|
| 229 |
+
elements.userInput.focus();
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
function calculateDelay() {
|
| 233 |
+
const round = state.round;
|
| 234 |
+
const { initialMin, initialMax, stretchFactor, maxDelay } = CONFIG.delay;
|
| 235 |
+
|
| 236 |
+
const factor = Math.pow(stretchFactor, round - 1);
|
| 237 |
+
const min = Math.min(initialMin * factor, maxDelay * 0.5);
|
| 238 |
+
const max = Math.min(initialMax * factor, maxDelay);
|
| 239 |
+
|
| 240 |
+
const delay = Math.random() * (max - min) + min;
|
| 241 |
+
return Math.round(delay * 10) / 10;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
function showWaitingIndicator() {
|
| 245 |
+
elements.waitingIndicator.classList.remove('hidden');
|
| 246 |
+
state.waitStartTime = Date.now();
|
| 247 |
+
|
| 248 |
+
if (state.showTimer) {
|
| 249 |
+
elements.waitTimer.style.display = 'inline';
|
| 250 |
+
state.timerInterval = setInterval(() => {
|
| 251 |
+
const elapsed = ((Date.now() - state.waitStartTime) / 1000).toFixed(1);
|
| 252 |
+
elements.waitTimer.textContent = `${elapsed}s`;
|
| 253 |
+
}, 100);
|
| 254 |
+
} else {
|
| 255 |
+
elements.waitTimer.style.display = 'none';
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
function hideWaitingIndicator() {
|
| 260 |
+
elements.waitingIndicator.classList.add('hidden');
|
| 261 |
+
if (state.timerInterval) {
|
| 262 |
+
clearInterval(state.timerInterval);
|
| 263 |
+
state.timerInterval = null;
|
| 264 |
+
}
|
| 265 |
+
elements.waitTimer.textContent = 'waiting...';
|
| 266 |
+
elements.waitTimer.style.display = 'inline';
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
function addMessage(role, content) {
|
| 270 |
+
const message = { role, content, timestamp: new Date().toISOString() };
|
| 271 |
+
state.messages.push(message);
|
| 272 |
+
|
| 273 |
+
const div = document.createElement('div');
|
| 274 |
+
div.className = `message ${role}`;
|
| 275 |
+
div.textContent = content;
|
| 276 |
+
elements.messagesContainer.appendChild(div);
|
| 277 |
+
elements.messagesContainer.scrollTop = elements.messagesContainer.scrollHeight;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
function updateRoundDisplay() {
|
| 281 |
+
elements.roundDisplay.textContent = `Round ${state.round}`;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
function rotateInvitation() {
|
| 285 |
+
const invitation = CONFIG.invitations[Math.floor(Math.random() * CONFIG.invitations.length)];
|
| 286 |
+
elements.currentInvitation.textContent = invitation;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
function handleSaveReflection() {
|
| 290 |
+
const text = elements.journalInput.value.trim();
|
| 291 |
+
if (!text) return;
|
| 292 |
+
|
| 293 |
+
const waitTime = state.currentDelay || 0;
|
| 294 |
+
const reflection = {
|
| 295 |
+
text,
|
| 296 |
+
waitTime,
|
| 297 |
+
round: state.round,
|
| 298 |
+
timestamp: new Date().toISOString()
|
| 299 |
+
};
|
| 300 |
+
state.reflections.push(reflection);
|
| 301 |
+
|
| 302 |
+
const entry = document.createElement('div');
|
| 303 |
+
entry.className = 'reflection-entry';
|
| 304 |
+
entry.innerHTML = `
|
| 305 |
+
<div class="wait-time">${waitTime.toFixed(1)}s wait</div>
|
| 306 |
+
<div class="reflection-text">${escapeHtml(text)}</div>
|
| 307 |
+
`;
|
| 308 |
+
elements.reflectionEntries.insertBefore(entry, elements.reflectionEntries.firstChild);
|
| 309 |
+
|
| 310 |
+
elements.journalInput.value = '';
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
function showModal() {
|
| 314 |
+
elements.groundingModal.classList.remove('hidden');
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
function hideModal() {
|
| 318 |
+
elements.groundingModal.classList.add('hidden');
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
// API Calls - now go through backend
|
| 322 |
+
async function getClaudeResponse(userMessage) {
|
| 323 |
+
const conversationHistory = state.messages.map(m => ({
|
| 324 |
+
role: m.role,
|
| 325 |
+
content: m.content
|
| 326 |
+
}));
|
| 327 |
+
|
| 328 |
+
const systemPrompt = state.tensionMode
|
| 329 |
+
? CONFIG.systemPromptTension
|
| 330 |
+
: CONFIG.systemPromptStandard;
|
| 331 |
+
|
| 332 |
+
const response = await fetch('/api/chat', {
|
| 333 |
+
method: 'POST',
|
| 334 |
+
headers: { 'Content-Type': 'application/json' },
|
| 335 |
+
body: JSON.stringify({
|
| 336 |
+
messages: conversationHistory,
|
| 337 |
+
system: systemPrompt,
|
| 338 |
+
max_tokens: 300
|
| 339 |
+
})
|
| 340 |
+
});
|
| 341 |
+
|
| 342 |
+
if (!response.ok) {
|
| 343 |
+
const error = await response.json();
|
| 344 |
+
throw new Error(error.detail || 'API request failed');
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
const data = await response.json();
|
| 348 |
+
return data.content[0].text;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
async function getPatternAnalysis() {
|
| 352 |
+
if (state.reflections.length === 0) {
|
| 353 |
+
return "No reflections were recorded during this session. That's okay — sometimes the practice is simply in the waiting itself. |aI| invite you to notice: what was it like to sit in those pauses without capturing words?";
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
const reflectionsText = state.reflections
|
| 357 |
+
.map((r) => `Round ${r.round} (${r.waitTime.toFixed(1)}s wait): "${r.text}"`)
|
| 358 |
+
.join('\n');
|
| 359 |
+
|
| 360 |
+
const conversationSummary = state.messages
|
| 361 |
+
.map(m => `${m.role === 'user' ? 'User' : 'Partner'}: ${m.content}`)
|
| 362 |
+
.join('\n');
|
| 363 |
+
|
| 364 |
+
const contextNote = state.tensionMode
|
| 365 |
+
? 'Note: The user opted into "stretch mode" with mild relational friction enabled.'
|
| 366 |
+
: '';
|
| 367 |
+
|
| 368 |
+
let prompt = CONFIG.analysisPrompt
|
| 369 |
+
.replace('{REFLECTIONS}', reflectionsText)
|
| 370 |
+
.replace('{CONVERSATION_CONTEXT}', contextNote ? `\n${contextNote}\n\nConversation overview:\n${conversationSummary}` : '');
|
| 371 |
+
|
| 372 |
+
try {
|
| 373 |
+
const response = await fetch('/api/analysis', {
|
| 374 |
+
method: 'POST',
|
| 375 |
+
headers: { 'Content-Type': 'application/json' },
|
| 376 |
+
body: JSON.stringify({
|
| 377 |
+
prompt: prompt,
|
| 378 |
+
max_tokens: 1000
|
| 379 |
+
})
|
| 380 |
+
});
|
| 381 |
+
|
| 382 |
+
if (!response.ok) {
|
| 383 |
+
throw new Error('Failed to get analysis');
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
const data = await response.json();
|
| 387 |
+
return data.content[0].text;
|
| 388 |
+
} catch (error) {
|
| 389 |
+
console.error('Analysis error:', error);
|
| 390 |
+
return "|aI| wasn't able to generate a reflection on your entries. Please take a moment to review them yourself — what patterns or shifts do you notice?";
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
async function handleEndSession() {
|
| 395 |
+
showScreen('analysis');
|
| 396 |
+
await populateAnalysis();
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
async function populateAnalysis() {
|
| 400 |
+
const exchanges = state.messages.filter(m => m.role === 'user').length;
|
| 401 |
+
const delays = state.delayHistory;
|
| 402 |
+
const minDelay = delays.length ? Math.min(...delays).toFixed(1) : 0;
|
| 403 |
+
const maxDelay = delays.length ? Math.max(...delays).toFixed(1) : 0;
|
| 404 |
+
|
| 405 |
+
elements.totalExchanges.textContent = exchanges;
|
| 406 |
+
elements.delayRange.textContent = `${minDelay}-${maxDelay}s`;
|
| 407 |
+
elements.totalReflections.textContent = state.reflections.length;
|
| 408 |
+
|
| 409 |
+
elements.allReflections.innerHTML = '';
|
| 410 |
+
if (state.reflections.length === 0) {
|
| 411 |
+
elements.allReflections.innerHTML = '<p class="muted">No reflections were recorded.</p>';
|
| 412 |
+
} else {
|
| 413 |
+
state.reflections.forEach(r => {
|
| 414 |
+
const entry = document.createElement('div');
|
| 415 |
+
entry.className = 'reflection-entry';
|
| 416 |
+
entry.innerHTML = `
|
| 417 |
+
<div class="wait-time">Round ${r.round} · ${r.waitTime.toFixed(1)}s wait</div>
|
| 418 |
+
<div class="reflection-text">${escapeHtml(r.text)}</div>
|
| 419 |
+
`;
|
| 420 |
+
elements.allReflections.appendChild(entry);
|
| 421 |
+
});
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
elements.analysisContent.innerHTML = '<p class="loading-text">Preparing your clinical debrief...</p>';
|
| 425 |
+
const analysis = await getPatternAnalysis();
|
| 426 |
+
elements.analysisContent.innerHTML = formatAnalysis(analysis);
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
function formatAnalysis(text) {
|
| 430 |
+
let html = escapeHtml(text);
|
| 431 |
+
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
| 432 |
+
html = html.split('\n\n').map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`).join('');
|
| 433 |
+
html = html.replace(/<strong>(Somatic Arc|Attachment Insight|Practical Skill|Bold Reframe|Journaling Prompt)<\/strong>/g,
|
| 434 |
+
'<strong class="section-header">$1</strong>');
|
| 435 |
+
return html;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
function handleExport() {
|
| 439 |
+
const sessionData = {
|
| 440 |
+
date: new Date().toISOString(),
|
| 441 |
+
settings: { showTimer: state.showTimer, tensionMode: state.tensionMode },
|
| 442 |
+
stats: {
|
| 443 |
+
exchanges: state.messages.filter(m => m.role === 'user').length,
|
| 444 |
+
reflections: state.reflections.length,
|
| 445 |
+
delays: state.delayHistory
|
| 446 |
+
},
|
| 447 |
+
messages: state.messages,
|
| 448 |
+
reflections: state.reflections,
|
| 449 |
+
bridgeReflection: elements.bridgeReflection.value
|
| 450 |
+
};
|
| 451 |
+
|
| 452 |
+
const blob = new Blob([JSON.stringify(sessionData, null, 2)], { type: 'application/json' });
|
| 453 |
+
const url = URL.createObjectURL(blob);
|
| 454 |
+
const a = document.createElement('a');
|
| 455 |
+
a.href = url;
|
| 456 |
+
a.download = `tolerate-space-lab-${new Date().toISOString().split('T')[0]}.json`;
|
| 457 |
+
a.click();
|
| 458 |
+
URL.revokeObjectURL(url);
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
function handleNewSession() {
|
| 462 |
+
state.round = 0;
|
| 463 |
+
state.messages = [];
|
| 464 |
+
state.reflections = [];
|
| 465 |
+
state.delayHistory = [];
|
| 466 |
+
state.isWaiting = false;
|
| 467 |
+
|
| 468 |
+
elements.messagesContainer.innerHTML = '';
|
| 469 |
+
elements.reflectionEntries.innerHTML = '';
|
| 470 |
+
elements.journalInput.value = '';
|
| 471 |
+
elements.userInput.value = '';
|
| 472 |
+
elements.bridgeReflection.value = '';
|
| 473 |
+
|
| 474 |
+
showScreen('welcome');
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
function escapeHtml(text) {
|
| 478 |
+
const div = document.createElement('div');
|
| 479 |
+
div.textContent = text;
|
| 480 |
+
return div.innerHTML;
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
document.addEventListener('DOMContentLoaded', init);
|
static/index.html
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.0">
|
| 6 |
+
<title>Tolerate Space Lab</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/styles.css">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="app">
|
| 11 |
+
|
| 12 |
+
<!-- SCREEN 1: Welcome & Onboarding -->
|
| 13 |
+
<section id="welcome-screen" class="screen active">
|
| 14 |
+
<div class="welcome-container">
|
| 15 |
+
<h1>Tolerate Space Lab</h1>
|
| 16 |
+
<p class="subtitle">A relational workout for building distress tolerance</p>
|
| 17 |
+
|
| 18 |
+
<div class="welcome-content">
|
| 19 |
+
<div class="intro-text">
|
| 20 |
+
<p>This is a practice space for sitting with the uncertainty that arises when waiting for a response from someone who matters to you.</p>
|
| 21 |
+
|
| 22 |
+
<p>You'll engage in a simulated text conversation. <strong>Responses will be intentionally delayed</strong> — and the delays will gradually stretch as you practice.</p>
|
| 23 |
+
|
| 24 |
+
<p>While you wait, you're invited to notice what arises in your body. A gentle reflection space beside the conversation will hold whatever you discover.</p>
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
<div class="the-invitation">
|
| 28 |
+
<p><em>Imagine you're texting someone whose response matters to you — a partner, friend, family member, or someone you're getting to know. The AI will respond as that person might.</em></p>
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
<div class="practice-options">
|
| 32 |
+
<h3>Practice Settings</h3>
|
| 33 |
+
|
| 34 |
+
<div class="option-row">
|
| 35 |
+
<label class="toggle-label">
|
| 36 |
+
<input type="checkbox" id="show-timer" checked>
|
| 37 |
+
<span class="toggle-text">Show wait timer</span>
|
| 38 |
+
</label>
|
| 39 |
+
<p class="option-description">Display elapsed seconds while waiting.</p>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<div class="option-row">
|
| 43 |
+
<label class="toggle-label">
|
| 44 |
+
<input type="checkbox" id="tension-mode">
|
| 45 |
+
<span class="toggle-text">Enable stretch mode</span>
|
| 46 |
+
</label>
|
| 47 |
+
<p class="option-description">The conversation partner may create mild relational friction to stretch your distress tolerance.</p>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<button id="begin-btn" class="primary-btn">Begin Practice</button>
|
| 52 |
+
|
| 53 |
+
<p class="disclaimer">This is a practice tool, not therapy. You can end the session at any time.</p>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</section>
|
| 57 |
+
|
| 58 |
+
<!-- SCREEN 2: Practice Space -->
|
| 59 |
+
<section id="practice-screen" class="screen">
|
| 60 |
+
<header class="practice-header">
|
| 61 |
+
<h2>Tolerate Space Lab</h2>
|
| 62 |
+
<div class="session-info">
|
| 63 |
+
<span id="round-display">Round 1</span>
|
| 64 |
+
<button id="end-session-btn" class="secondary-btn">End Session</button>
|
| 65 |
+
</div>
|
| 66 |
+
</header>
|
| 67 |
+
|
| 68 |
+
<div class="practice-container">
|
| 69 |
+
<!-- Left: Conversation -->
|
| 70 |
+
<div class="conversation-panel">
|
| 71 |
+
<div class="panel-header">
|
| 72 |
+
<h3>Conversation</h3>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
<div id="messages" class="messages-container">
|
| 76 |
+
<!-- Messages will be inserted here -->
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<div id="waiting-indicator" class="waiting-indicator hidden">
|
| 80 |
+
<div class="breathing-dots">
|
| 81 |
+
<span></span><span></span><span></span>
|
| 82 |
+
</div>
|
| 83 |
+
<span id="wait-timer">waiting...</span>
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
<div class="input-area">
|
| 87 |
+
<textarea id="user-input" placeholder="Type your message..." rows="2"></textarea>
|
| 88 |
+
<button id="send-btn" class="send-btn">Send</button>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
|
| 92 |
+
<!-- Right: Somatic Journal -->
|
| 93 |
+
<div class="journal-panel">
|
| 94 |
+
<div class="panel-header">
|
| 95 |
+
<h3>Somatic Reflection</h3>
|
| 96 |
+
</div>
|
| 97 |
+
|
| 98 |
+
<div class="journal-invitation">
|
| 99 |
+
<p id="current-invitation" class="invitation-text">With kindness, notice what's here...</p>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
<div class="journal-input-area">
|
| 103 |
+
<textarea id="journal-input" placeholder="Whatever arises is welcome here..." rows="4"></textarea>
|
| 104 |
+
<button id="save-reflection-btn" class="save-btn">Save Reflection</button>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<p class="journal-examples">You might notice: tightness, warmth, a flutter, stillness, restlessness, breath changes, nothing at all — all are welcome here.</p>
|
| 108 |
+
|
| 109 |
+
<div class="journal-history">
|
| 110 |
+
<h4>Previous Reflections</h4>
|
| 111 |
+
<div id="reflection-entries">
|
| 112 |
+
<!-- Entries will be inserted here -->
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
|
| 118 |
+
<div class="grounding-link">
|
| 119 |
+
<a href="#" id="grounding-btn">Need to ground? Try a simple breath...</a>
|
| 120 |
+
</div>
|
| 121 |
+
</section>
|
| 122 |
+
|
| 123 |
+
<!-- SCREEN 3: Session Analysis -->
|
| 124 |
+
<section id="analysis-screen" class="screen">
|
| 125 |
+
<div class="analysis-container">
|
| 126 |
+
<h1>Session Complete</h1>
|
| 127 |
+
<p class="subtitle">Let's reflect on what emerged</p>
|
| 128 |
+
|
| 129 |
+
<div class="session-stats">
|
| 130 |
+
<div class="stat">
|
| 131 |
+
<span class="stat-number" id="total-exchanges">0</span>
|
| 132 |
+
<span class="stat-label">Exchanges</span>
|
| 133 |
+
</div>
|
| 134 |
+
<div class="stat">
|
| 135 |
+
<span class="stat-number" id="delay-range">0-0s</span>
|
| 136 |
+
<span class="stat-label">Delay Range</span>
|
| 137 |
+
</div>
|
| 138 |
+
<div class="stat">
|
| 139 |
+
<span class="stat-number" id="total-reflections">0</span>
|
| 140 |
+
<span class="stat-label">Reflections</span>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
|
| 144 |
+
<div class="journal-review">
|
| 145 |
+
<h3>Your Somatic Journey</h3>
|
| 146 |
+
<div id="all-reflections">
|
| 147 |
+
<!-- All reflections displayed here -->
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
|
| 151 |
+
<div class="pattern-analysis">
|
| 152 |
+
<h3>Patterns & Themes</h3>
|
| 153 |
+
<div id="analysis-content" class="analysis-text">
|
| 154 |
+
<p class="loading-text">Reflecting on your journey...</p>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
|
| 158 |
+
<div class="bridge-section">
|
| 159 |
+
<h3>Bridge to Human Connection</h3>
|
| 160 |
+
<p>What from this practice might you bring to a therapist, partner, or trusted person?</p>
|
| 161 |
+
<textarea id="bridge-reflection" placeholder="Take a moment to consider..." rows="3"></textarea>
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
<div class="action-buttons">
|
| 165 |
+
<button id="export-btn" class="secondary-btn">Export Session</button>
|
| 166 |
+
<button id="new-session-btn" class="primary-btn">New Practice</button>
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
</section>
|
| 170 |
+
|
| 171 |
+
<!-- Grounding Modal -->
|
| 172 |
+
<div id="grounding-modal" class="modal hidden">
|
| 173 |
+
<div class="modal-content">
|
| 174 |
+
<button class="close-modal">×</button>
|
| 175 |
+
<h3>A Simple Breath</h3>
|
| 176 |
+
<p>Find your feet on the ground. Notice where your body meets the chair or floor.</p>
|
| 177 |
+
<p>Take a slow breath in... and let it go at its own pace.</p>
|
| 178 |
+
<p>You're here. This is practice. You can stop anytime.</p>
|
| 179 |
+
<button id="close-grounding" class="primary-btn">Return to Practice</button>
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
|
| 184 |
+
<script src="/static/app.js"></script>
|
| 185 |
+
</body>
|
| 186 |
+
</html>
|
static/styles.css
ADDED
|
@@ -0,0 +1,825 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ==========================================================================
|
| 2 |
+
Attachment Stretch Lab - Styles
|
| 3 |
+
A soft, calming aesthetic with sage green and warm cream
|
| 4 |
+
========================================================================== */
|
| 5 |
+
|
| 6 |
+
:root {
|
| 7 |
+
/* Primary palette */
|
| 8 |
+
--sage-light: #A8C5A8;
|
| 9 |
+
--sage: #8FBC8F;
|
| 10 |
+
--sage-dark: #6B8E6B;
|
| 11 |
+
|
| 12 |
+
/* Background & Surface */
|
| 13 |
+
--cream: #FDF8F0;
|
| 14 |
+
--cream-dark: #F5EDE0;
|
| 15 |
+
--white: #FFFFFF;
|
| 16 |
+
|
| 17 |
+
/* Text */
|
| 18 |
+
--charcoal: #3D3D3D;
|
| 19 |
+
--charcoal-light: #5A5A5A;
|
| 20 |
+
--muted: #8A8A8A;
|
| 21 |
+
|
| 22 |
+
/* Accents */
|
| 23 |
+
--warm-shadow: rgba(139, 119, 101, 0.1);
|
| 24 |
+
--soft-border: rgba(143, 188, 143, 0.3);
|
| 25 |
+
|
| 26 |
+
/* Spacing */
|
| 27 |
+
--space-xs: 0.5rem;
|
| 28 |
+
--space-sm: 1rem;
|
| 29 |
+
--space-md: 1.5rem;
|
| 30 |
+
--space-lg: 2rem;
|
| 31 |
+
--space-xl: 3rem;
|
| 32 |
+
|
| 33 |
+
/* Typography */
|
| 34 |
+
--font-body: 'Georgia', 'Times New Roman', serif;
|
| 35 |
+
--font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 36 |
+
|
| 37 |
+
/* Borders */
|
| 38 |
+
--radius-sm: 8px;
|
| 39 |
+
--radius-md: 12px;
|
| 40 |
+
--radius-lg: 16px;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
* {
|
| 44 |
+
margin: 0;
|
| 45 |
+
padding: 0;
|
| 46 |
+
box-sizing: border-box;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
body {
|
| 50 |
+
font-family: var(--font-body);
|
| 51 |
+
background-color: var(--cream);
|
| 52 |
+
color: var(--charcoal);
|
| 53 |
+
line-height: 1.6;
|
| 54 |
+
min-height: 100vh;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
#app {
|
| 58 |
+
min-height: 100vh;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/* ==========================================================================
|
| 62 |
+
Screen Management
|
| 63 |
+
========================================================================== */
|
| 64 |
+
|
| 65 |
+
.screen {
|
| 66 |
+
display: none;
|
| 67 |
+
min-height: 100vh;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.screen.active {
|
| 71 |
+
display: block;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.hidden {
|
| 75 |
+
display: none !important;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/* ==========================================================================
|
| 79 |
+
Welcome Screen
|
| 80 |
+
========================================================================== */
|
| 81 |
+
|
| 82 |
+
#welcome-screen {
|
| 83 |
+
display: flex;
|
| 84 |
+
align-items: center;
|
| 85 |
+
justify-content: center;
|
| 86 |
+
padding: var(--space-lg);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.welcome-container {
|
| 90 |
+
max-width: 600px;
|
| 91 |
+
text-align: center;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.welcome-container h1 {
|
| 95 |
+
font-size: 2.5rem;
|
| 96 |
+
font-weight: normal;
|
| 97 |
+
color: var(--sage-dark);
|
| 98 |
+
margin-bottom: var(--space-xs);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.subtitle {
|
| 102 |
+
font-size: 1.1rem;
|
| 103 |
+
color: var(--muted);
|
| 104 |
+
font-style: italic;
|
| 105 |
+
margin-bottom: var(--space-xl);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.welcome-content {
|
| 109 |
+
text-align: left;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.intro-text {
|
| 113 |
+
background: var(--white);
|
| 114 |
+
padding: var(--space-lg);
|
| 115 |
+
border-radius: var(--radius-md);
|
| 116 |
+
box-shadow: 0 2px 12px var(--warm-shadow);
|
| 117 |
+
margin-bottom: var(--space-lg);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.intro-text p {
|
| 121 |
+
margin-bottom: var(--space-sm);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.intro-text p:last-child {
|
| 125 |
+
margin-bottom: 0;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.the-invitation {
|
| 129 |
+
background: linear-gradient(135deg, var(--sage-light) 0%, var(--sage) 100%);
|
| 130 |
+
color: var(--white);
|
| 131 |
+
padding: var(--space-md);
|
| 132 |
+
border-radius: var(--radius-md);
|
| 133 |
+
margin-bottom: var(--space-lg);
|
| 134 |
+
text-align: center;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.the-invitation em {
|
| 138 |
+
font-style: normal;
|
| 139 |
+
font-size: 1.05rem;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.api-section {
|
| 143 |
+
margin-bottom: var(--space-lg);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.api-section label {
|
| 147 |
+
display: block;
|
| 148 |
+
font-family: var(--font-ui);
|
| 149 |
+
font-size: 0.9rem;
|
| 150 |
+
font-weight: 500;
|
| 151 |
+
margin-bottom: var(--space-xs);
|
| 152 |
+
color: var(--charcoal-light);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.api-section input {
|
| 156 |
+
width: 100%;
|
| 157 |
+
padding: var(--space-sm);
|
| 158 |
+
font-size: 1rem;
|
| 159 |
+
font-family: var(--font-ui);
|
| 160 |
+
border: 2px solid var(--soft-border);
|
| 161 |
+
border-radius: var(--radius-sm);
|
| 162 |
+
background: var(--white);
|
| 163 |
+
transition: border-color 0.2s;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.api-section input:focus {
|
| 167 |
+
outline: none;
|
| 168 |
+
border-color: var(--sage);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.api-note {
|
| 172 |
+
font-size: 0.85rem;
|
| 173 |
+
color: var(--muted);
|
| 174 |
+
margin-top: var(--space-xs);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
/* Practice Options */
|
| 178 |
+
.practice-options {
|
| 179 |
+
background: var(--cream-dark);
|
| 180 |
+
padding: var(--space-md);
|
| 181 |
+
border-radius: var(--radius-md);
|
| 182 |
+
margin-bottom: var(--space-lg);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.practice-options h3 {
|
| 186 |
+
font-size: 1rem;
|
| 187 |
+
font-weight: 500;
|
| 188 |
+
color: var(--charcoal-light);
|
| 189 |
+
margin-bottom: var(--space-md);
|
| 190 |
+
font-family: var(--font-ui);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.option-row {
|
| 194 |
+
margin-bottom: var(--space-md);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.option-row:last-child {
|
| 198 |
+
margin-bottom: 0;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.toggle-label {
|
| 202 |
+
display: flex;
|
| 203 |
+
align-items: center;
|
| 204 |
+
gap: var(--space-sm);
|
| 205 |
+
cursor: pointer;
|
| 206 |
+
margin-bottom: var(--space-xs);
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.toggle-label input[type="checkbox"] {
|
| 210 |
+
width: 18px;
|
| 211 |
+
height: 18px;
|
| 212 |
+
accent-color: var(--sage);
|
| 213 |
+
cursor: pointer;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.toggle-text {
|
| 217 |
+
font-family: var(--font-ui);
|
| 218 |
+
font-size: 0.95rem;
|
| 219 |
+
font-weight: 500;
|
| 220 |
+
color: var(--charcoal);
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.option-description {
|
| 224 |
+
font-size: 0.85rem;
|
| 225 |
+
color: var(--muted);
|
| 226 |
+
margin-left: 30px;
|
| 227 |
+
line-height: 1.4;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.primary-btn {
|
| 231 |
+
display: block;
|
| 232 |
+
width: 100%;
|
| 233 |
+
padding: var(--space-sm) var(--space-lg);
|
| 234 |
+
font-family: var(--font-ui);
|
| 235 |
+
font-size: 1.1rem;
|
| 236 |
+
font-weight: 500;
|
| 237 |
+
color: var(--white);
|
| 238 |
+
background: var(--sage);
|
| 239 |
+
border: none;
|
| 240 |
+
border-radius: var(--radius-md);
|
| 241 |
+
cursor: pointer;
|
| 242 |
+
transition: background-color 0.2s, transform 0.1s;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
.primary-btn:hover {
|
| 246 |
+
background: var(--sage-dark);
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.primary-btn:active {
|
| 250 |
+
transform: scale(0.98);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.secondary-btn {
|
| 254 |
+
padding: var(--space-xs) var(--space-sm);
|
| 255 |
+
font-family: var(--font-ui);
|
| 256 |
+
font-size: 0.9rem;
|
| 257 |
+
color: var(--sage-dark);
|
| 258 |
+
background: transparent;
|
| 259 |
+
border: 2px solid var(--sage);
|
| 260 |
+
border-radius: var(--radius-sm);
|
| 261 |
+
cursor: pointer;
|
| 262 |
+
transition: background-color 0.2s;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.secondary-btn:hover {
|
| 266 |
+
background: var(--sage-light);
|
| 267 |
+
color: var(--white);
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.disclaimer {
|
| 271 |
+
margin-top: var(--space-lg);
|
| 272 |
+
text-align: center;
|
| 273 |
+
font-size: 0.85rem;
|
| 274 |
+
color: var(--muted);
|
| 275 |
+
font-style: italic;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
/* ==========================================================================
|
| 279 |
+
Practice Screen
|
| 280 |
+
========================================================================== */
|
| 281 |
+
|
| 282 |
+
#practice-screen {
|
| 283 |
+
display: flex;
|
| 284 |
+
flex-direction: column;
|
| 285 |
+
height: 100vh;
|
| 286 |
+
overflow: hidden;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.practice-header {
|
| 290 |
+
display: flex;
|
| 291 |
+
justify-content: space-between;
|
| 292 |
+
align-items: center;
|
| 293 |
+
padding: var(--space-sm) var(--space-md);
|
| 294 |
+
background: var(--white);
|
| 295 |
+
border-bottom: 1px solid var(--soft-border);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.practice-header h2 {
|
| 299 |
+
font-size: 1.2rem;
|
| 300 |
+
font-weight: normal;
|
| 301 |
+
color: var(--sage-dark);
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.session-info {
|
| 305 |
+
display: flex;
|
| 306 |
+
align-items: center;
|
| 307 |
+
gap: var(--space-md);
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
#round-display {
|
| 311 |
+
font-family: var(--font-ui);
|
| 312 |
+
font-size: 0.9rem;
|
| 313 |
+
color: var(--muted);
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.practice-container {
|
| 317 |
+
display: flex;
|
| 318 |
+
flex: 1;
|
| 319 |
+
overflow: hidden;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
/* Conversation Panel */
|
| 323 |
+
.conversation-panel {
|
| 324 |
+
flex: 1;
|
| 325 |
+
display: flex;
|
| 326 |
+
flex-direction: column;
|
| 327 |
+
background: var(--cream-dark);
|
| 328 |
+
border-right: 1px solid var(--soft-border);
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.panel-header {
|
| 332 |
+
padding: var(--space-sm) var(--space-md);
|
| 333 |
+
background: var(--white);
|
| 334 |
+
border-bottom: 1px solid var(--soft-border);
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.panel-header h3 {
|
| 338 |
+
font-size: 1rem;
|
| 339 |
+
font-weight: 500;
|
| 340 |
+
color: var(--charcoal-light);
|
| 341 |
+
font-family: var(--font-ui);
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.messages-container {
|
| 345 |
+
flex: 1;
|
| 346 |
+
overflow-y: auto;
|
| 347 |
+
padding: var(--space-md);
|
| 348 |
+
display: flex;
|
| 349 |
+
flex-direction: column;
|
| 350 |
+
gap: var(--space-sm);
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.message {
|
| 354 |
+
max-width: 80%;
|
| 355 |
+
padding: var(--space-sm) var(--space-md);
|
| 356 |
+
border-radius: var(--radius-md);
|
| 357 |
+
line-height: 1.5;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
.message.user {
|
| 361 |
+
align-self: flex-end;
|
| 362 |
+
background: var(--sage);
|
| 363 |
+
color: var(--white);
|
| 364 |
+
border-bottom-right-radius: 4px;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.message.assistant {
|
| 368 |
+
align-self: flex-start;
|
| 369 |
+
background: var(--white);
|
| 370 |
+
color: var(--charcoal);
|
| 371 |
+
border-bottom-left-radius: 4px;
|
| 372 |
+
box-shadow: 0 1px 4px var(--warm-shadow);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
.waiting-indicator {
|
| 376 |
+
display: flex;
|
| 377 |
+
align-items: center;
|
| 378 |
+
gap: var(--space-sm);
|
| 379 |
+
padding: var(--space-sm) var(--space-md);
|
| 380 |
+
color: var(--muted);
|
| 381 |
+
font-family: var(--font-ui);
|
| 382 |
+
font-size: 0.9rem;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.breathing-dots {
|
| 386 |
+
display: flex;
|
| 387 |
+
gap: 4px;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.breathing-dots span {
|
| 391 |
+
width: 8px;
|
| 392 |
+
height: 8px;
|
| 393 |
+
background: var(--sage);
|
| 394 |
+
border-radius: 50%;
|
| 395 |
+
animation: breathe 1.5s ease-in-out infinite;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
.breathing-dots span:nth-child(2) {
|
| 399 |
+
animation-delay: 0.2s;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.breathing-dots span:nth-child(3) {
|
| 403 |
+
animation-delay: 0.4s;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
@keyframes breathe {
|
| 407 |
+
0%, 100% {
|
| 408 |
+
opacity: 0.3;
|
| 409 |
+
transform: scale(0.8);
|
| 410 |
+
}
|
| 411 |
+
50% {
|
| 412 |
+
opacity: 1;
|
| 413 |
+
transform: scale(1);
|
| 414 |
+
}
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.input-area {
|
| 418 |
+
display: flex;
|
| 419 |
+
gap: var(--space-sm);
|
| 420 |
+
padding: var(--space-md);
|
| 421 |
+
background: var(--white);
|
| 422 |
+
border-top: 1px solid var(--soft-border);
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.input-area textarea {
|
| 426 |
+
flex: 1;
|
| 427 |
+
padding: var(--space-sm);
|
| 428 |
+
font-family: var(--font-body);
|
| 429 |
+
font-size: 1rem;
|
| 430 |
+
border: 2px solid var(--soft-border);
|
| 431 |
+
border-radius: var(--radius-sm);
|
| 432 |
+
resize: none;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.input-area textarea:focus {
|
| 436 |
+
outline: none;
|
| 437 |
+
border-color: var(--sage);
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.send-btn {
|
| 441 |
+
padding: var(--space-sm) var(--space-md);
|
| 442 |
+
font-family: var(--font-ui);
|
| 443 |
+
font-weight: 500;
|
| 444 |
+
color: var(--white);
|
| 445 |
+
background: var(--sage);
|
| 446 |
+
border: none;
|
| 447 |
+
border-radius: var(--radius-sm);
|
| 448 |
+
cursor: pointer;
|
| 449 |
+
transition: background-color 0.2s;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.send-btn:hover {
|
| 453 |
+
background: var(--sage-dark);
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
.send-btn:disabled {
|
| 457 |
+
background: var(--muted);
|
| 458 |
+
cursor: not-allowed;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
/* Journal Panel */
|
| 462 |
+
.journal-panel {
|
| 463 |
+
flex: 1;
|
| 464 |
+
display: flex;
|
| 465 |
+
flex-direction: column;
|
| 466 |
+
background: var(--white);
|
| 467 |
+
overflow-y: auto;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.journal-invitation {
|
| 471 |
+
padding: var(--space-md);
|
| 472 |
+
text-align: center;
|
| 473 |
+
border-bottom: 1px solid var(--soft-border);
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
.invitation-text {
|
| 477 |
+
font-size: 1.1rem;
|
| 478 |
+
font-style: italic;
|
| 479 |
+
color: var(--sage-dark);
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
.journal-input-area {
|
| 483 |
+
padding: var(--space-md);
|
| 484 |
+
border-bottom: 1px solid var(--soft-border);
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
.journal-input-area textarea {
|
| 488 |
+
width: 100%;
|
| 489 |
+
padding: var(--space-md);
|
| 490 |
+
font-family: var(--font-body);
|
| 491 |
+
font-size: 1rem;
|
| 492 |
+
border: 2px solid var(--soft-border);
|
| 493 |
+
border-radius: var(--radius-md);
|
| 494 |
+
resize: none;
|
| 495 |
+
margin-bottom: var(--space-sm);
|
| 496 |
+
background: var(--cream);
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.journal-input-area textarea:focus {
|
| 500 |
+
outline: none;
|
| 501 |
+
border-color: var(--sage);
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
.journal-input-area textarea::placeholder {
|
| 505 |
+
color: var(--muted);
|
| 506 |
+
font-style: italic;
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
.save-btn {
|
| 510 |
+
display: block;
|
| 511 |
+
width: 100%;
|
| 512 |
+
padding: var(--space-xs) var(--space-sm);
|
| 513 |
+
font-family: var(--font-ui);
|
| 514 |
+
font-size: 0.9rem;
|
| 515 |
+
color: var(--sage-dark);
|
| 516 |
+
background: var(--sage-light);
|
| 517 |
+
border: none;
|
| 518 |
+
border-radius: var(--radius-sm);
|
| 519 |
+
cursor: pointer;
|
| 520 |
+
transition: background-color 0.2s;
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
.save-btn:hover {
|
| 524 |
+
background: var(--sage);
|
| 525 |
+
color: var(--white);
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.journal-examples {
|
| 529 |
+
padding: var(--space-sm) var(--space-md);
|
| 530 |
+
font-size: 0.85rem;
|
| 531 |
+
font-style: italic;
|
| 532 |
+
color: var(--muted);
|
| 533 |
+
background: var(--cream);
|
| 534 |
+
border-bottom: 1px solid var(--soft-border);
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
.journal-history {
|
| 538 |
+
flex: 1;
|
| 539 |
+
padding: var(--space-md);
|
| 540 |
+
overflow-y: auto;
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
.journal-history h4 {
|
| 544 |
+
font-family: var(--font-ui);
|
| 545 |
+
font-size: 0.85rem;
|
| 546 |
+
font-weight: 500;
|
| 547 |
+
color: var(--charcoal-light);
|
| 548 |
+
margin-bottom: var(--space-sm);
|
| 549 |
+
text-transform: uppercase;
|
| 550 |
+
letter-spacing: 0.5px;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
#reflection-entries {
|
| 554 |
+
display: flex;
|
| 555 |
+
flex-direction: column;
|
| 556 |
+
gap: var(--space-sm);
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
.reflection-entry {
|
| 560 |
+
padding: var(--space-sm);
|
| 561 |
+
background: var(--cream);
|
| 562 |
+
border-radius: var(--radius-sm);
|
| 563 |
+
border-left: 3px solid var(--sage);
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
.reflection-entry .wait-time {
|
| 567 |
+
font-family: var(--font-ui);
|
| 568 |
+
font-size: 0.75rem;
|
| 569 |
+
color: var(--muted);
|
| 570 |
+
margin-bottom: 4px;
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
.reflection-entry .reflection-text {
|
| 574 |
+
font-size: 0.95rem;
|
| 575 |
+
color: var(--charcoal);
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
.grounding-link {
|
| 579 |
+
padding: var(--space-sm);
|
| 580 |
+
text-align: center;
|
| 581 |
+
background: var(--cream);
|
| 582 |
+
border-top: 1px solid var(--soft-border);
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
.grounding-link a {
|
| 586 |
+
font-size: 0.85rem;
|
| 587 |
+
color: var(--sage-dark);
|
| 588 |
+
text-decoration: none;
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
.grounding-link a:hover {
|
| 592 |
+
text-decoration: underline;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
/* ==========================================================================
|
| 596 |
+
Analysis Screen
|
| 597 |
+
========================================================================== */
|
| 598 |
+
|
| 599 |
+
#analysis-screen {
|
| 600 |
+
padding: var(--space-lg);
|
| 601 |
+
overflow-y: auto;
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
.analysis-container {
|
| 605 |
+
max-width: 700px;
|
| 606 |
+
margin: 0 auto;
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
.analysis-container h1 {
|
| 610 |
+
font-size: 2rem;
|
| 611 |
+
font-weight: normal;
|
| 612 |
+
color: var(--sage-dark);
|
| 613 |
+
text-align: center;
|
| 614 |
+
margin-bottom: var(--space-xs);
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
.analysis-container > .subtitle {
|
| 618 |
+
text-align: center;
|
| 619 |
+
margin-bottom: var(--space-xl);
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
.session-stats {
|
| 623 |
+
display: flex;
|
| 624 |
+
justify-content: center;
|
| 625 |
+
gap: var(--space-xl);
|
| 626 |
+
margin-bottom: var(--space-xl);
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
.stat {
|
| 630 |
+
text-align: center;
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
.stat-number {
|
| 634 |
+
display: block;
|
| 635 |
+
font-size: 2rem;
|
| 636 |
+
color: var(--sage-dark);
|
| 637 |
+
font-weight: 500;
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
.stat-label {
|
| 641 |
+
font-family: var(--font-ui);
|
| 642 |
+
font-size: 0.85rem;
|
| 643 |
+
color: var(--muted);
|
| 644 |
+
text-transform: uppercase;
|
| 645 |
+
letter-spacing: 0.5px;
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.journal-review,
|
| 649 |
+
.pattern-analysis,
|
| 650 |
+
.bridge-section {
|
| 651 |
+
background: var(--white);
|
| 652 |
+
padding: var(--space-lg);
|
| 653 |
+
border-radius: var(--radius-md);
|
| 654 |
+
box-shadow: 0 2px 12px var(--warm-shadow);
|
| 655 |
+
margin-bottom: var(--space-lg);
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
.journal-review h3,
|
| 659 |
+
.pattern-analysis h3,
|
| 660 |
+
.bridge-section h3 {
|
| 661 |
+
font-size: 1.2rem;
|
| 662 |
+
font-weight: normal;
|
| 663 |
+
color: var(--sage-dark);
|
| 664 |
+
margin-bottom: var(--space-md);
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
#all-reflections {
|
| 668 |
+
display: flex;
|
| 669 |
+
flex-direction: column;
|
| 670 |
+
gap: var(--space-sm);
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
.analysis-text {
|
| 674 |
+
line-height: 1.7;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
.analysis-text p {
|
| 678 |
+
margin-bottom: var(--space-sm);
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
.analysis-text .section-header {
|
| 682 |
+
display: block;
|
| 683 |
+
color: var(--sage-dark);
|
| 684 |
+
font-size: 1.05rem;
|
| 685 |
+
margin-top: var(--space-md);
|
| 686 |
+
margin-bottom: var(--space-xs);
|
| 687 |
+
font-family: var(--font-ui);
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
.analysis-text p:first-child .section-header {
|
| 691 |
+
margin-top: 0;
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
.loading-text {
|
| 695 |
+
color: var(--muted);
|
| 696 |
+
font-style: italic;
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
.bridge-section p {
|
| 700 |
+
margin-bottom: var(--space-sm);
|
| 701 |
+
color: var(--charcoal-light);
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
.bridge-section textarea {
|
| 705 |
+
width: 100%;
|
| 706 |
+
padding: var(--space-md);
|
| 707 |
+
font-family: var(--font-body);
|
| 708 |
+
font-size: 1rem;
|
| 709 |
+
border: 2px solid var(--soft-border);
|
| 710 |
+
border-radius: var(--radius-sm);
|
| 711 |
+
resize: none;
|
| 712 |
+
background: var(--cream);
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
.bridge-section textarea:focus {
|
| 716 |
+
outline: none;
|
| 717 |
+
border-color: var(--sage);
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.action-buttons {
|
| 721 |
+
display: flex;
|
| 722 |
+
gap: var(--space-md);
|
| 723 |
+
justify-content: center;
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
.action-buttons .secondary-btn {
|
| 727 |
+
padding: var(--space-sm) var(--space-lg);
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
.action-buttons .primary-btn {
|
| 731 |
+
width: auto;
|
| 732 |
+
padding: var(--space-sm) var(--space-lg);
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
/* ==========================================================================
|
| 736 |
+
Modal
|
| 737 |
+
========================================================================== */
|
| 738 |
+
|
| 739 |
+
.modal {
|
| 740 |
+
position: fixed;
|
| 741 |
+
top: 0;
|
| 742 |
+
left: 0;
|
| 743 |
+
width: 100%;
|
| 744 |
+
height: 100%;
|
| 745 |
+
background: rgba(0, 0, 0, 0.4);
|
| 746 |
+
display: flex;
|
| 747 |
+
align-items: center;
|
| 748 |
+
justify-content: center;
|
| 749 |
+
z-index: 1000;
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
.modal-content {
|
| 753 |
+
background: var(--white);
|
| 754 |
+
padding: var(--space-xl);
|
| 755 |
+
border-radius: var(--radius-lg);
|
| 756 |
+
max-width: 400px;
|
| 757 |
+
text-align: center;
|
| 758 |
+
position: relative;
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
.close-modal {
|
| 762 |
+
position: absolute;
|
| 763 |
+
top: var(--space-sm);
|
| 764 |
+
right: var(--space-sm);
|
| 765 |
+
background: none;
|
| 766 |
+
border: none;
|
| 767 |
+
font-size: 1.5rem;
|
| 768 |
+
color: var(--muted);
|
| 769 |
+
cursor: pointer;
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
.modal-content h3 {
|
| 773 |
+
color: var(--sage-dark);
|
| 774 |
+
margin-bottom: var(--space-md);
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
.modal-content p {
|
| 778 |
+
margin-bottom: var(--space-sm);
|
| 779 |
+
color: var(--charcoal-light);
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
+
.modal-content .primary-btn {
|
| 783 |
+
margin-top: var(--space-md);
|
| 784 |
+
width: auto;
|
| 785 |
+
display: inline-block;
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
/* ==========================================================================
|
| 789 |
+
Responsive
|
| 790 |
+
========================================================================== */
|
| 791 |
+
|
| 792 |
+
@media (max-width: 768px) {
|
| 793 |
+
.practice-container {
|
| 794 |
+
flex-direction: column;
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
.conversation-panel {
|
| 798 |
+
border-right: none;
|
| 799 |
+
border-bottom: 1px solid var(--soft-border);
|
| 800 |
+
flex: none;
|
| 801 |
+
height: 50vh;
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
.journal-panel {
|
| 805 |
+
flex: none;
|
| 806 |
+
height: 50vh;
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
.session-stats {
|
| 810 |
+
gap: var(--space-md);
|
| 811 |
+
}
|
| 812 |
+
|
| 813 |
+
.stat-number {
|
| 814 |
+
font-size: 1.5rem;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
.action-buttons {
|
| 818 |
+
flex-direction: column;
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
.action-buttons .secondary-btn,
|
| 822 |
+
.action-buttons .primary-btn {
|
| 823 |
+
width: 100%;
|
| 824 |
+
}
|
| 825 |
+
}
|