jostlebot commited on
Commit
48edd44
·
0 Parent(s):

Tolerate Space Lab - initial commit

Browse files
Files changed (7) hide show
  1. Dockerfile +12 -0
  2. README.md +40 -0
  3. app.py +96 -0
  4. requirements.txt +4 -0
  5. static/app.js +483 -0
  6. static/index.html +186 -0
  7. 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">&times;</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
+ }