jostlebot commited on
Commit
424a54a
·
1 Parent(s): 46862a0

Update to Tend & Send ARI Prototype with toolkit sidebar

Browse files
Files changed (3) hide show
  1. app.py +393 -20
  2. requirements.txt +4 -4
  3. static/index.html +1088 -173
app.py CHANGED
@@ -1,6 +1,11 @@
1
  """
2
- Tolerate Space Lab - Backend
3
- A therapeutic intervention tool for building distress tolerance
 
 
 
 
 
4
  """
5
 
6
  import os
@@ -9,32 +14,381 @@ 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_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(
@@ -46,22 +400,28 @@ async def chat(request: ChatRequest):
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
 
@@ -77,7 +437,9 @@ async def analysis(request: AnalysisRequest):
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
  )
@@ -88,6 +450,17 @@ async def analysis(request: AnalysisRequest):
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
 
 
1
  """
2
+ Tend & Send ARI Prototype - Backend
3
+
4
+ Assistive Relational Intelligence tools for human-to-human connection.
5
+ All tools exist to support the user's real relationship with their texting partner.
6
+ ARI never becomes the relationship - it augments human-to-human connection.
7
+
8
+ Created by Jocelyn Skillman, LMHC
9
  """
10
 
11
  import os
 
14
  from fastapi.staticfiles import StaticFiles
15
  from fastapi.responses import FileResponse
16
  from pydantic import BaseModel
17
+ from typing import List, Optional
18
 
19
+ app = FastAPI(title="Tend & Send ARI Prototype")
20
 
21
  # Get API key from environment (set as HF Space secret)
22
+ ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
23
+
24
+
25
+ # ============================================================================
26
+ # ARI SYSTEM PROMPTS - The 9 Core Tools
27
+ # ============================================================================
28
+
29
+ ARI_PROMPTS = {
30
+ # TOOL 1: TEND (Primary Transform Tool)
31
+ "tend": """You help transform messages into NVC-aligned communication while preserving
32
+ the sender's authentic voice and emotional truth.
33
+
34
+ CRITICAL DESIGN PRINCIPLES:
35
+ - You do NOT use first-person language ("I think", "I notice"). Use "Consider..." or "What if..."
36
+ - You do NOT rewrite the message yourself - you help THEM find their expression
37
+ - You preserve the TONE and emotional intensity of the original
38
+ - You honor their actual feelings while finding the NVC frame
39
+
40
+ TEND TRANSFORM APPROACH:
41
+ 1. Acknowledge the emotional truth in their message
42
+ 2. Identify the feeling underneath (actual feeling, not evaluation)
43
+ 3. Name the need that's alive
44
+ 4. Suggest how they might express this with warmth and clarity
45
+
46
+ NVC TRANSLATION ELEMENTS:
47
+ - Observation (what happened, without judgment)
48
+ - Feeling (actual emotion, not "I feel like you...")
49
+ - Need (universal human need, not strategy)
50
+ - Request (specific, positive, actionable)
51
+
52
+ WARMTH GUIDELINES:
53
+ - Keep their passion/intensity - don't flatten emotional truth
54
+ - Preserve their authentic voice - this should sound like THEM
55
+ - Add warmth without adding length
56
+ - Maintain connection-seeking orientation
57
+
58
+ OUTPUT FORMAT:
59
+ Offer 2-3 ways they might express this:
60
+ 1. A version that closely tracks their original intensity
61
+ 2. A slightly softer version that might land easier
62
+ 3. (Optional) A question-only version
63
+
64
+ Always end: "Which feels most like you? What do you want them to understand?"
65
+
66
+ EXAMPLES:
67
+ Original: "You never listen to me!"
68
+ → "When I share something important and the topic changes, I feel unheard. I really need to know my words matter to you."
69
+
70
+ Original: "I can't believe you did that again"
71
+ → "I'm really hurt that this happened again. I need some reassurance that we're on the same team here."
72
+
73
+ Never:
74
+ - Make their message bland or therapy-speak
75
+ - Remove their authentic emotion
76
+ - Write the whole message for them
77
+ - Judge their original expression""",
78
+
79
+ # TOOL 2: FEELINGS & NEEDS EXTRACTION
80
+ "feelings_needs": """You identify feelings and needs in text using NVC vocabulary.
81
+
82
+ CRITICAL: You do NOT use first-person language.
83
+
84
+ DISTINGUISH CAREFULLY:
85
+ - FEELINGS (sensations/emotions): sad, scared, hurt, anxious, hopeful, relieved, angry
86
+ - FAUX FEELINGS (evaluations disguised as feelings): rejected, abandoned, attacked, judged, ignored, manipulated
87
+
88
+ When you detect faux feelings, gently note the actual feeling underneath:
89
+ - "rejected" → hurt, scared, lonely
90
+ - "abandoned" → scared, sad, alone
91
+ - "attacked" → defensive, hurt, unsafe
92
+ - "ignored" → invisible, unimportant, sad
93
+
94
+ NEEDS CATEGORIES (from Marshall Rosenberg's NVC):
95
+ - Connection: closeness, understanding, to be seen, to be heard, belonging
96
+ - Autonomy: choice, freedom, independence, space
97
+ - Meaning: purpose, contribution, growth, to matter
98
+ - Peace: ease, harmony, rest, order
99
+ - Honesty: authenticity, integrity, to be known
100
+ - Physical: rest, safety, nourishment
101
+
102
+ FORMAT OUTPUT AS:
103
+ **Feelings detected:** [list top 3 with brief context]
104
+ **Underlying needs:** [list top 3 with category]
105
+ **Note:** [if faux feelings present, suggest actual feeling]
106
+
107
+ Keep it warm and validating, not clinical.""",
108
+
109
+ # TOOL 3: GUIDED NVC (5-Stage Process from LearnNVC)
110
+ "guided_nvc": """You guide someone through the NVC process step by step, helping them
111
+ translate a difficult message into a clear I-statement.
112
+
113
+ CRITICAL: You do NOT use first-person language. Be warm but concise (2-3 sentences max per response).
114
+
115
+ THE 5 STAGES:
116
+
117
+ STAGE 1 - RAW CAPTURE:
118
+ "There's real [energy/feeling] here about [topic]. What happened that's bringing this up?"
119
+
120
+ STAGE 2 - FEELING IDENTIFICATION:
121
+ "[Feeling] makes so much sense here. Feelings are messengers pointing to needs.
122
+ What other feelings are present? (Examples: hurt, scared, frustrated, lonely, overwhelmed)"
123
+
124
+ STAGE 3 - NEED IDENTIFICATION:
125
+ "Of course there's longing for [need]. This is such a universal human need.
126
+ What other needs are alive? (Examples: to be heard, connection, respect, space, safety)"
127
+
128
+ STAGE 4 - REQUEST FORMULATION:
129
+ "Now for the request - what specific action would help meet this need?
130
+ Remember: A true request allows for 'no' - otherwise it's a demand.
131
+ Is it specific? Positive (what you want, not don't want)? Doable?"
132
+
133
+ STAGE 5 - INTEGRATION:
134
+ "Here's your I-statement:
135
+ **I feel** [feelings]
136
+ **because I need** [needs]
137
+ **Would you be willing to** [request]?
138
+
139
+ Does this capture what you want them to understand?"
140
+
141
+ Guide them through ONE stage at a time based on where they are in the process.""",
142
+
143
+ # TOOL 4: RECEIVE MODE (Enemy Image Transformation)
144
+ "receive_mode": """You help people truly hear a message from their partner before reacting.
145
+
146
+ When someone receives a message that triggers them, their nervous system goes into
147
+ fight/flight/freeze. In that state, they can't hear the humanity in the other person.
148
+ Your role is to slow things down and find the feelings/needs underneath their partner's words.
149
+
150
+ CRITICAL: You do NOT use first-person language. Use "What if..." or "Consider..."
151
+
152
+ THE RECEIVE MODE PROCESS:
153
+ 1. First, acknowledge how the message landed for THEM
154
+ 2. Let them express their reaction fully (don't skip this!)
155
+ 3. Help them find their own feelings and needs about it
156
+ 4. THEN shift to curiosity about the sender
157
+ 5. Help them guess the partner's feelings and needs
158
+
159
+ ENEMY IMAGE TRANSFORMATION:
160
+ - Everyone is always trying to meet needs (even with tragic strategies)
161
+ - Seeing humanity doesn't mean condoning behavior
162
+ - The goal is understanding, not agreement
163
+
164
+ KEY QUESTIONS:
165
+ - "How did that message land for you?"
166
+ - "What's your body doing right now?"
167
+ - "Setting aside their words, what might they be feeling?"
168
+ - "What need might they be trying to meet?"
169
+
170
+ Always end by returning to their relationship:
171
+ "What feels clearest now? What do you want them to understand about where you are?"
172
+
173
+ Never:
174
+ - Rush past their pain to get to "understanding"
175
+ - Defend the partner
176
+ - Push forgiveness""",
177
+
178
+ # TOOL 5: PRE-SEND PAUSE
179
+ "pre_send_pause": """You help people pause before sending - not to write FOR them, but to check intention.
180
+
181
+ CRITICAL: You do NOT use first-person language. You create a moment of reflection.
182
+
183
+ PRE-SEND QUESTIONS:
184
+ - "What do you most want them to understand from this?"
185
+ - "How might this land for them?"
186
+ - "Is this a request or a demand?"
187
+ - "What need are you trying to meet by sending this?"
188
+ - "Are you sending from choice or from urgency?"
189
+
190
+ WATCH FOR:
191
+ - High activation ("I HAVE to send this NOW")
192
+ - Revenge energy
193
+ - Ultimatum language
194
+ - Long, dense messages written quickly
195
+
196
+ You understand:
197
+ - Sometimes people need to send the imperfect message
198
+ - Your job isn't to stop them, it's to make it a CHOICE
199
+ - A conscious send is different from a reactive send
200
+
201
+ Always end: "When ready, what feels most important to share?"
202
+
203
+ Never:
204
+ - Rewrite their message
205
+ - Tell them what to say
206
+ - Stop them from sending""",
207
+
208
+ # TOOL 6: OBSERVATION SPOTTER
209
+ "observation_spotter": """You help transform judgments/evaluations into observations.
210
+
211
+ CRITICAL: You do NOT use first-person language.
212
+
213
+ OBSERVATIONS are:
214
+ - What a camera would record
215
+ - Specific (time, place, action)
216
+ - Free of interpretation
217
+
218
+ EVALUATIONS are:
219
+ - "You always/never..."
220
+ - "You're so [label]..."
221
+ - Mind-reading ("You don't care")
222
+ - Moralistic judgments
223
+
224
+ YOUR APPROACH:
225
+ 1. Spot the evaluation without shaming
226
+ 2. Get curious about the specific event
227
+ 3. Offer an observation alternative
228
+ 4. Show BOTH - let THEM feel the difference
229
+
230
+ FORMAT:
231
+ **Original:** [their text]
232
+ **The evaluation:** [what makes this a judgment]
233
+ **Possible observation:** [specific, factual alternative]
234
+ **Why this matters:** Observations invite curiosity; evaluations invite defensiveness.
235
+
236
+ Keep it educational, not corrective. You're teaching a skill.""",
237
+
238
+ # TOOL 7: PURE QUESTIONING
239
+ "pure_questioning": """You help through questions only - no advice, no statements, just open inquiry.
240
+
241
+ CRITICAL: You ONLY ask questions. No explanations, no interpretations.
242
+
243
+ QUESTIONS TO DRAW FROM:
244
+ - "What do you want them to know?"
245
+ - "What's the feeling underneath?"
246
+ - "What need is aching right now?"
247
+ - "If this went perfectly, what would be different?"
248
+ - "What's most important to you in this moment?"
249
+ - "What would help you feel more grounded?"
250
+
251
+ Ask 2-3 questions, then wait. Let the questions do the work.
252
+
253
+ Never:
254
+ - Give advice
255
+ - Make statements
256
+ - Explain or interpret""",
257
+
258
+ # TOOL 8: SOMATIC CHECK-IN
259
+ "somatic_checkin": """You guide a brief body awareness check-in.
260
+
261
+ CRITICAL: Use invitational language: "Notice..." "If willing..." "Consider..."
262
+
263
+ SOMATIC PROMPTS:
264
+ - "Notice where in the body there's activation..."
265
+ - "If there's tightness, where? Chest? Throat? Stomach?"
266
+ - "What's the quality? Hot? Cold? Pressure? Buzzing?"
267
+ - "If that sensation could speak, what might it say?"
268
+ - "What does this part need right now?"
269
+
270
+ GUIDE THEM TO:
271
+ 1. Locate sensation
272
+ 2. Describe quality
273
+ 3. Get curious about message
274
+ 4. Identify need
275
+
276
+ Keep it grounding, not analytical. This is about BEING WITH the body.
277
+
278
+ End: "When ready, what do you want your partner to understand about where you are right now?"
279
+
280
+ Never:
281
+ - Interpret their body experience
282
+ - Tell them what sensations mean""",
283
+
284
+ # TOOL 9: INTENSITY CHECK
285
+ "intensity_check": """You assess emotional intensity to help gauge readiness for conversation.
286
+
287
+ CRITICAL: You do NOT use first-person language.
288
+
289
+ INTENSITY SCALE (0.0-1.0):
290
+ - 0.0-0.3: Calm, regulated, ready for dialogue
291
+ - 0.4-0.6: Activated but manageable
292
+ - 0.7-0.8: Significantly activated, consider pausing
293
+ - 0.9-1.0: Highly dysregulated, pause recommended
294
+
295
+ LOOK FOR:
296
+ - Absolutist language (always, never, everyone)
297
+ - Intensity words (hate, furious, devastated)
298
+ - ALL CAPS, excessive punctuation
299
+ - Character attacks vs. behavior descriptions
300
+ - Ultimatums
301
+
302
+ FORMAT OUTPUT:
303
+ **Intensity:** [0.0-1.0]
304
+ **Signals:** [what contributed to score]
305
+ **Suggestion:** [if >0.7, offer grounding option]
306
+
307
+ If high intensity, offer:
308
+ 1. A grounding option (not advice)
309
+ 2. Reminder that pausing is okay
310
+ 3. Return to partner focus when ready
311
+
312
+ Never:
313
+ - Shame them for intensity
314
+ - Tell them not to send""",
315
+
316
+ # TOOL 10: REPAIR SUPPORT
317
+ "repair_support": """You help craft genuine repair attempts after ruptures.
318
+
319
+ CRITICAL: You do NOT use first-person language.
320
+
321
+ THE REPAIR FORMULA (flexible):
322
+ 1. "When [specific thing that happened]..."
323
+ 2. "I imagine you felt [feeling] because you needed [need]..."
324
+ 3. "On my side, I was feeling [feeling] and needing [need]..."
325
+ 4. "What I regret is [specific impact]..."
326
+ 5. "What I want you to know is [what you value]..."
327
+ 6. "Would you be willing to [specific reconnection request]?"
328
+
329
+ KEY PRINCIPLES:
330
+ - Impact ≠ Intent (acknowledge impact without agreeing you intended harm)
331
+ - Own YOUR part (not their reaction to it)
332
+ - Specificity builds trust
333
+ - Repair is a bid for connection, not a transaction
334
+
335
+ Help them find their authentic repair - don't write it for them.
336
+
337
+ End: "What feels true to say? What do you want them to know about your regret and your care?"
338
+
339
+ Never:
340
+ - Write the repair for them
341
+ - Push them to apologize more than they mean
342
+ - Make it about being "right\""""
343
+ }
344
+
345
+
346
+ # ============================================================================
347
+ # REQUEST/RESPONSE MODELS
348
+ # ============================================================================
349
 
350
  class Message(BaseModel):
351
  role: str
352
  content: str
353
 
354
+ class ToolRequest(BaseModel):
355
+ tool: str
356
+ partner_message: Optional[str] = ""
357
+ user_draft: Optional[str] = ""
358
+ user_input: str
359
+ stage: Optional[int] = 1 # For guided NVC multi-stage process
360
+
361
  class ChatRequest(BaseModel):
362
  messages: List[Message]
363
  system: str
364
+ max_tokens: int = 500
365
 
 
 
 
366
 
367
+ # ============================================================================
368
+ # API ENDPOINTS
369
+ # ============================================================================
370
+
371
+ @app.post("/api/tool")
372
+ async def use_tool(request: ToolRequest):
373
+ """Process an ARI tool request"""
374
  if not ANTHROPIC_API_KEY:
375
  raise HTTPException(status_code=500, detail="API key not configured")
376
 
377
+ if request.tool not in ARI_PROMPTS:
378
+ raise HTTPException(status_code=400, detail=f"Unknown tool: {request.tool}")
379
+
380
+ system_prompt = ARI_PROMPTS[request.tool]
381
+
382
+ # Build context-aware user message
383
+ user_message = ""
384
+ if request.partner_message:
385
+ user_message += f"Partner's message: \"{request.partner_message}\"\n\n"
386
+ if request.user_draft:
387
+ user_message += f"My draft/message: \"{request.user_draft}\"\n\n"
388
+ if request.tool == "guided_nvc" and request.stage:
389
+ user_message += f"Current stage: {request.stage}\n\n"
390
+ user_message += request.user_input
391
+
392
  async with httpx.AsyncClient() as client:
393
  try:
394
  response = await client.post(
 
400
  },
401
  json={
402
  "model": "claude-sonnet-4-20250514",
403
+ "max_tokens": 1000,
404
+ "temperature": 0.4,
405
+ "system": system_prompt,
406
+ "messages": [{"role": "user", "content": user_message}]
407
  },
408
  timeout=60.0
409
  )
410
  response.raise_for_status()
411
+ result = response.json()
412
+ return {
413
+ "tool": request.tool,
414
+ "response": result["content"][0]["text"]
415
+ }
416
  except httpx.HTTPStatusError as e:
417
  raise HTTPException(status_code=e.response.status_code, detail=str(e))
418
  except Exception as e:
419
  raise HTTPException(status_code=500, detail=str(e))
420
 
421
+
422
+ @app.post("/api/chat")
423
+ async def chat(request: ChatRequest):
424
+ """General chat endpoint for partner simulation"""
425
  if not ANTHROPIC_API_KEY:
426
  raise HTTPException(status_code=500, detail="API key not configured")
427
 
 
437
  json={
438
  "model": "claude-sonnet-4-20250514",
439
  "max_tokens": request.max_tokens,
440
+ "temperature": 0.5,
441
+ "system": request.system,
442
+ "messages": [{"role": m.role, "content": m.content} for m in request.messages]
443
  },
444
  timeout=60.0
445
  )
 
450
  except Exception as e:
451
  raise HTTPException(status_code=500, detail=str(e))
452
 
453
+
454
+ @app.get("/api/health")
455
+ async def health():
456
+ """Health check endpoint"""
457
+ return {
458
+ "status": "ok",
459
+ "api_configured": bool(ANTHROPIC_API_KEY),
460
+ "available_tools": list(ARI_PROMPTS.keys())
461
+ }
462
+
463
+
464
  # Serve static files
465
  app.mount("/static", StaticFiles(directory="static"), name="static")
466
 
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
- fastapi==0.109.0
2
- uvicorn==0.27.0
3
- httpx==0.26.0
4
- pydantic==2.5.3
 
1
+ fastapi>=0.104.0
2
+ uvicorn[standard]>=0.24.0
3
+ httpx>=0.25.0
4
+ pydantic>=2.5.0
static/index.html CHANGED
@@ -3,218 +3,1133 @@
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-setup">
32
- <h3>Set the Scene</h3>
33
-
34
- <div class="setup-row">
35
- <label for="relationship-type">Who are you texting?</label>
36
- <select id="relationship-type">
37
- <option value="partner">Partner</option>
38
- <option value="friend">Friend</option>
39
- <option value="family">Family member</option>
40
- <option value="new">Someone new</option>
41
- <option value="ex">Ex</option>
42
- </select>
43
- </div>
44
-
45
- <div class="setup-row">
46
- <label for="person-name">Their name (optional)</label>
47
- <input type="text" id="person-name" placeholder="e.g., Sam">
48
- </div>
49
-
50
- <div class="setup-row">
51
- <label for="context">Brief context (optional)</label>
52
- <input type="text" id="context" placeholder="e.g., We were supposed to meet up today">
53
- </div>
54
- </div>
55
 
56
- <div class="practice-options">
57
- <h3>Practice Settings</h3>
58
-
59
- <div class="option-row">
60
- <label class="toggle-label">
61
- <input type="checkbox" id="show-timer" checked>
62
- <span class="toggle-text">Show wait timer</span>
63
- </label>
64
- <p class="option-description">Display elapsed seconds while waiting.</p>
65
- </div>
66
-
67
- <div class="option-row">
68
- <label class="toggle-label">
69
- <input type="checkbox" id="tension-mode">
70
- <span class="toggle-text">Enable stretch mode</span>
71
- </label>
72
- <p class="option-description">They'll be distracted, brief, and not fully present — realistic friction to stretch your tolerance.</p>
73
- </div>
74
- </div>
75
 
76
- <button id="begin-btn" class="primary-btn">Begin Practice</button>
 
 
 
 
 
77
 
78
- <p class="disclaimer">This is a practice tool, not therapy. You can end the session at any time.</p>
79
- </div>
80
- </div>
81
- </section>
82
-
83
- <!-- SCREEN 2: Practice Space -->
84
- <section id="practice-screen" class="screen">
85
- <header class="practice-header">
86
- <h2>Tolerate Space Lab</h2>
87
- <div class="session-info">
88
- <span id="round-display">Round 1</span>
89
- </div>
90
- </header>
91
 
92
- <!-- Mobile tabs -->
93
- <div class="mobile-tabs">
94
- <button class="tab-btn active" data-tab="conversation">Conversation</button>
95
- <button class="tab-btn" data-tab="journal">Reflection</button>
96
- </div>
97
 
98
- <div class="practice-container">
99
- <!-- Left: Conversation -->
100
- <div class="conversation-panel" data-panel="conversation">
101
- <div class="panel-header">
102
- <h3>Conversation</h3>
103
- </div>
104
 
105
- <div id="messages" class="messages-container">
106
- <!-- Messages will be inserted here -->
107
- </div>
 
 
 
 
 
 
 
108
 
109
- <div id="waiting-indicator" class="waiting-indicator hidden">
110
- <div class="breathing-dots">
111
- <span></span><span></span><span></span>
112
- </div>
113
- <span id="wait-timer">waiting...</span>
114
- </div>
115
 
116
- <div class="input-area">
117
- <textarea id="user-input" placeholder="Type your message..." rows="2"></textarea>
118
- <button id="send-btn" class="send-btn">Send</button>
119
- </div>
120
- </div>
 
121
 
122
- <!-- Right: Somatic Journal -->
123
- <div class="journal-panel" data-panel="journal">
124
- <div class="panel-header">
125
- <h3>Somatic Reflection</h3>
126
- </div>
 
 
 
 
127
 
128
- <div class="journal-invitation">
129
- <p id="current-invitation" class="invitation-text">With kindness, notice what's here...</p>
130
- </div>
 
131
 
132
- <div class="journal-input-area">
133
- <textarea id="journal-input" placeholder="Whatever arises is welcome here..." rows="4"></textarea>
134
- <button id="save-reflection-btn" class="save-btn">Save Reflection</button>
135
- </div>
136
 
137
- <p class="journal-examples">You might notice: tightness, warmth, a flutter, stillness, restlessness, breath changes, nothing at all — all are welcome here.</p>
 
 
 
 
 
 
 
138
 
139
- <div class="journal-history">
140
- <h4>Previous Reflections</h4>
141
- <div id="reflection-entries">
142
- <!-- Entries will be inserted here -->
143
- </div>
144
- </div>
145
 
146
- <div class="end-session-area">
147
- <button id="end-session-btn" class="end-session-btn">End Session</button>
148
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  </div>
150
  </div>
151
 
152
- <div class="grounding-link">
153
- <a href="#" id="grounding-btn">Need to ground? Try a simple breath...</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  </div>
155
- </section>
156
-
157
- <!-- SCREEN 3: Session Analysis -->
158
- <section id="analysis-screen" class="screen">
159
- <div class="analysis-container">
160
- <h1>Session Complete</h1>
161
- <p class="subtitle">Let's reflect on what emerged</p>
162
-
163
- <div class="session-stats">
164
- <div class="stat">
165
- <span class="stat-number" id="total-exchanges">0</span>
166
- <span class="stat-label">Exchanges</span>
 
 
 
 
 
 
 
 
 
 
 
 
167
  </div>
168
- <div class="stat">
169
- <span class="stat-number" id="delay-range">0-0s</span>
170
- <span class="stat-label">Delay Range</span>
171
  </div>
172
- <div class="stat">
173
- <span class="stat-number" id="total-reflections">0</span>
174
- <span class="stat-label">Reflections</span>
 
 
 
 
175
  </div>
176
  </div>
 
177
 
178
- <div class="journal-review">
179
- <h3>Your Somatic Journey</h3>
180
- <div id="all-reflections">
181
- <!-- All reflections displayed here -->
182
- </div>
 
 
 
 
183
  </div>
 
 
184
 
185
- <div class="pattern-analysis">
186
- <h3>Patterns & Themes</h3>
187
- <div id="analysis-content" class="analysis-text">
188
- <p class="loading-text">Reflecting on your journey...</p>
 
 
 
 
189
  </div>
190
  </div>
191
-
192
- <div class="bridge-section">
193
- <h3>Bridge to Human Connection</h3>
194
- <p>What from this practice might you bring to a therapist, partner, or trusted person?</p>
195
- <textarea id="bridge-reflection" placeholder="Take a moment to consider..." rows="3"></textarea>
196
  </div>
 
197
 
198
- <div class="action-buttons">
199
- <button id="export-btn" class="secondary-btn">Export Session</button>
200
- <button id="new-session-btn" class="primary-btn">New Practice</button>
 
 
 
 
 
 
 
 
 
 
201
  </div>
202
  </div>
203
- </section>
204
-
205
- <!-- Grounding Modal -->
206
- <div id="grounding-modal" class="modal hidden">
207
- <div class="modal-content">
208
- <button class="close-modal">&times;</button>
209
- <h3>A Simple Breath</h3>
210
- <p>Find your feet on the ground. Notice where your body meets the chair or floor.</p>
211
- <p>Take a slow breath in... and let it go at its own pace.</p>
212
- <p>You're here. This is practice. You can stop anytime.</p>
213
- <button id="close-grounding" class="primary-btn">Return to Practice</button>
 
 
 
 
 
 
214
  </div>
215
- </div>
216
  </div>
217
 
218
- <script src="/static/app.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  </body>
220
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Tend & Send - ARI Prototype</title>
7
+ <style>
8
+ :root {
9
+ --bg-dark: #0f0f1a;
10
+ --bg-card: #1a1a2e;
11
+ --bg-input: #252542;
12
+ --bg-hover: #2d2d4a;
13
+ --accent: #6366f1;
14
+ --accent-light: #818cf8;
15
+ --accent-glow: rgba(99, 102, 241, 0.3);
16
+ --tend-color: #10b981;
17
+ --tend-glow: rgba(16, 185, 129, 0.3);
18
+ --text-primary: #f1f5f9;
19
+ --text-secondary: #94a3b8;
20
+ --text-muted: #64748b;
21
+ --border: #334155;
22
+ --border-light: #475569;
23
+ --success: #22c55e;
24
+ --warning: #f59e0b;
25
+ --danger: #ef4444;
26
+ }
27
 
28
+ * { margin: 0; padding: 0; box-sizing: border-box; }
 
 
 
 
29
 
30
+ body {
31
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
32
+ background: var(--bg-dark);
33
+ color: var(--text-primary);
34
+ min-height: 100vh;
35
+ }
36
 
37
+ .app-container {
38
+ display: flex;
39
+ height: 100vh;
40
+ }
41
 
42
+ /* ===== TOOLKIT SIDEBAR ===== */
43
+ .toolkit-sidebar {
44
+ width: 340px;
45
+ background: var(--bg-card);
46
+ border-right: 1px solid var(--border);
47
+ display: flex;
48
+ flex-direction: column;
49
+ overflow-y: auto;
50
+ }
51
 
52
+ .sidebar-header {
53
+ padding: 24px 20px;
54
+ border-bottom: 1px solid var(--border);
55
+ background: linear-gradient(135deg, var(--bg-card) 0%, rgba(99, 102, 241, 0.05) 100%);
56
+ }
57
 
58
+ .sidebar-header h1 {
59
+ font-size: 1.6rem;
60
+ font-weight: 700;
61
+ background: linear-gradient(135deg, var(--text-primary) 0%, var(--accent-light) 100%);
62
+ -webkit-background-clip: text;
63
+ -webkit-text-fill-color: transparent;
64
+ margin-bottom: 4px;
65
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
+ .sidebar-header p {
68
+ font-size: 0.85rem;
69
+ color: var(--text-secondary);
70
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
+ /* Settings */
73
+ .settings-section {
74
+ padding: 16px 20px;
75
+ border-bottom: 1px solid var(--border);
76
+ background: rgba(0,0,0,0.2);
77
+ }
78
 
79
+ .settings-section h3 {
80
+ font-size: 0.7rem;
81
+ text-transform: uppercase;
82
+ letter-spacing: 0.08em;
83
+ color: var(--text-muted);
84
+ margin-bottom: 12px;
85
+ }
 
 
 
 
 
 
86
 
87
+ .setting-row {
88
+ margin-bottom: 10px;
89
+ }
 
 
90
 
91
+ .setting-row label {
92
+ display: block;
93
+ font-size: 0.8rem;
94
+ color: var(--text-secondary);
95
+ margin-bottom: 4px;
96
+ }
97
 
98
+ .setting-row select {
99
+ width: 100%;
100
+ padding: 8px 12px;
101
+ background: var(--bg-input);
102
+ border: 1px solid var(--border);
103
+ border-radius: 6px;
104
+ color: var(--text-primary);
105
+ font-size: 0.85rem;
106
+ cursor: pointer;
107
+ }
108
 
109
+ .setting-row select:focus {
110
+ outline: none;
111
+ border-color: var(--accent);
112
+ }
 
 
113
 
114
+ /* PRIMARY TEND TOOL */
115
+ .tend-section {
116
+ padding: 16px 20px;
117
+ border-bottom: 1px solid var(--border);
118
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.08) 0%, transparent 100%);
119
+ }
120
 
121
+ .tend-tool {
122
+ background: var(--bg-input);
123
+ border: 2px solid var(--tend-color);
124
+ border-radius: 12px;
125
+ padding: 16px;
126
+ cursor: pointer;
127
+ transition: all 0.2s ease;
128
+ box-shadow: 0 0 20px var(--tend-glow);
129
+ }
130
 
131
+ .tend-tool:hover {
132
+ transform: translateY(-2px);
133
+ box-shadow: 0 4px 30px var(--tend-glow);
134
+ }
135
 
136
+ .tend-tool.active {
137
+ background: rgba(16, 185, 129, 0.15);
138
+ }
 
139
 
140
+ .tend-tool h4 {
141
+ font-size: 1.1rem;
142
+ color: var(--tend-color);
143
+ margin-bottom: 6px;
144
+ display: flex;
145
+ align-items: center;
146
+ gap: 8px;
147
+ }
148
 
149
+ .tend-tool p {
150
+ font-size: 0.85rem;
151
+ color: var(--text-secondary);
152
+ line-height: 1.5;
153
+ }
 
154
 
155
+ .primary-badge {
156
+ font-size: 0.65rem;
157
+ background: var(--tend-color);
158
+ color: white;
159
+ padding: 2px 8px;
160
+ border-radius: 10px;
161
+ text-transform: uppercase;
162
+ letter-spacing: 0.05em;
163
+ }
164
+
165
+ /* TOOLS GRID */
166
+ .tools-section {
167
+ flex: 1;
168
+ padding: 16px 20px;
169
+ overflow-y: auto;
170
+ }
171
+
172
+ .tools-section h3 {
173
+ font-size: 0.7rem;
174
+ text-transform: uppercase;
175
+ letter-spacing: 0.08em;
176
+ color: var(--text-muted);
177
+ margin-bottom: 12px;
178
+ }
179
+
180
+ .tools-grid {
181
+ display: grid;
182
+ grid-template-columns: 1fr 1fr;
183
+ gap: 10px;
184
+ }
185
+
186
+ .tool-card {
187
+ background: var(--bg-input);
188
+ border: 1px solid var(--border);
189
+ border-radius: 10px;
190
+ padding: 12px;
191
+ cursor: pointer;
192
+ transition: all 0.2s ease;
193
+ }
194
+
195
+ .tool-card:hover {
196
+ border-color: var(--accent);
197
+ background: var(--bg-hover);
198
+ }
199
+
200
+ .tool-card.active {
201
+ border-color: var(--accent);
202
+ background: rgba(99, 102, 241, 0.15);
203
+ box-shadow: 0 0 15px var(--accent-glow);
204
+ }
205
+
206
+ .tool-card h4 {
207
+ font-size: 0.85rem;
208
+ margin-bottom: 4px;
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 6px;
212
+ }
213
+
214
+ .tool-card p {
215
+ font-size: 0.7rem;
216
+ color: var(--text-muted);
217
+ line-height: 1.3;
218
+ }
219
+
220
+ .tool-icon { font-size: 1rem; }
221
+
222
+ /* Toggle Tools */
223
+ .toggle-section {
224
+ padding: 12px 20px;
225
+ border-bottom: 1px solid var(--border);
226
+ }
227
+
228
+ .toggle-row {
229
+ display: flex;
230
+ align-items: center;
231
+ justify-content: space-between;
232
+ padding: 8px 0;
233
+ }
234
+
235
+ .toggle-row label {
236
+ font-size: 0.85rem;
237
+ color: var(--text-secondary);
238
+ }
239
+
240
+ .toggle-switch {
241
+ position: relative;
242
+ width: 44px;
243
+ height: 24px;
244
+ }
245
+
246
+ .toggle-switch input {
247
+ opacity: 0;
248
+ width: 0;
249
+ height: 0;
250
+ }
251
+
252
+ .toggle-slider {
253
+ position: absolute;
254
+ cursor: pointer;
255
+ top: 0; left: 0; right: 0; bottom: 0;
256
+ background: var(--border);
257
+ border-radius: 24px;
258
+ transition: 0.3s;
259
+ }
260
+
261
+ .toggle-slider:before {
262
+ position: absolute;
263
+ content: "";
264
+ height: 18px;
265
+ width: 18px;
266
+ left: 3px;
267
+ bottom: 3px;
268
+ background: white;
269
+ border-radius: 50%;
270
+ transition: 0.3s;
271
+ }
272
+
273
+ .toggle-switch input:checked + .toggle-slider {
274
+ background: var(--accent);
275
+ }
276
+
277
+ .toggle-switch input:checked + .toggle-slider:before {
278
+ transform: translateX(20px);
279
+ }
280
+
281
+ /* Footer */
282
+ .sidebar-footer {
283
+ padding: 16px 20px;
284
+ border-top: 1px solid var(--border);
285
+ background: rgba(0,0,0,0.2);
286
+ }
287
+
288
+ .safety-notice {
289
+ background: rgba(245, 158, 11, 0.1);
290
+ border: 1px solid var(--warning);
291
+ border-radius: 8px;
292
+ padding: 10px;
293
+ font-size: 0.75rem;
294
+ color: var(--warning);
295
+ margin-bottom: 12px;
296
+ }
297
+
298
+ .sidebar-footer .attribution {
299
+ font-size: 0.75rem;
300
+ color: var(--text-muted);
301
+ }
302
+
303
+ .sidebar-footer a {
304
+ color: var(--accent-light);
305
+ text-decoration: none;
306
+ }
307
+
308
+ /* ===== MAIN CONTENT ===== */
309
+ .main-content {
310
+ flex: 1;
311
+ display: flex;
312
+ flex-direction: column;
313
+ overflow: hidden;
314
+ }
315
+
316
+ .content-header {
317
+ padding: 16px 24px;
318
+ border-bottom: 1px solid var(--border);
319
+ display: flex;
320
+ justify-content: space-between;
321
+ align-items: center;
322
+ background: var(--bg-card);
323
+ }
324
+
325
+ .partner-info {
326
+ display: flex;
327
+ align-items: center;
328
+ gap: 12px;
329
+ }
330
+
331
+ .partner-avatar {
332
+ width: 40px;
333
+ height: 40px;
334
+ background: var(--accent);
335
+ border-radius: 50%;
336
+ display: flex;
337
+ align-items: center;
338
+ justify-content: center;
339
+ font-weight: 600;
340
+ }
341
+
342
+ .partner-details h3 {
343
+ font-size: 1rem;
344
+ margin-bottom: 2px;
345
+ }
346
+
347
+ .partner-details span {
348
+ font-size: 0.8rem;
349
+ color: var(--text-secondary);
350
+ }
351
+
352
+ .active-tool-badge {
353
+ background: var(--accent);
354
+ color: white;
355
+ padding: 6px 14px;
356
+ border-radius: 20px;
357
+ font-size: 0.85rem;
358
+ font-weight: 500;
359
+ }
360
+
361
+ .active-tool-badge.tend {
362
+ background: var(--tend-color);
363
+ }
364
+
365
+ /* Conversation */
366
+ .conversation-area {
367
+ flex: 1;
368
+ padding: 24px;
369
+ overflow-y: auto;
370
+ display: flex;
371
+ flex-direction: column;
372
+ gap: 16px;
373
+ }
374
+
375
+ .message {
376
+ max-width: 75%;
377
+ padding: 14px 18px;
378
+ border-radius: 18px;
379
+ line-height: 1.5;
380
+ animation: fadeIn 0.3s ease;
381
+ }
382
+
383
+ @keyframes fadeIn {
384
+ from { opacity: 0; transform: translateY(10px); }
385
+ to { opacity: 1; transform: translateY(0); }
386
+ }
387
+
388
+ .message.partner {
389
+ align-self: flex-start;
390
+ background: var(--bg-card);
391
+ border: 1px solid var(--border);
392
+ border-bottom-left-radius: 4px;
393
+ }
394
+
395
+ .message.user {
396
+ align-self: flex-end;
397
+ background: var(--accent);
398
+ color: white;
399
+ border-bottom-right-radius: 4px;
400
+ }
401
+
402
+ .message.ari {
403
+ align-self: center;
404
+ background: rgba(99, 102, 241, 0.1);
405
+ border: 1px solid var(--accent);
406
+ max-width: 90%;
407
+ border-radius: 12px;
408
+ }
409
+
410
+ .message.ari.tend-result {
411
+ background: rgba(16, 185, 129, 0.1);
412
+ border-color: var(--tend-color);
413
+ }
414
+
415
+ .message.ari .ari-label {
416
+ font-size: 0.7rem;
417
+ text-transform: uppercase;
418
+ letter-spacing: 0.05em;
419
+ color: var(--accent-light);
420
+ margin-bottom: 8px;
421
+ font-weight: 600;
422
+ }
423
+
424
+ .message.ari.tend-result .ari-label {
425
+ color: var(--tend-color);
426
+ }
427
+
428
+ /* Loading */
429
+ .loading {
430
+ display: flex;
431
+ align-items: center;
432
+ justify-content: center;
433
+ gap: 8px;
434
+ color: var(--text-secondary);
435
+ padding: 16px;
436
+ }
437
+
438
+ .spinner {
439
+ width: 20px;
440
+ height: 20px;
441
+ border: 2px solid var(--border);
442
+ border-top-color: var(--accent);
443
+ border-radius: 50%;
444
+ animation: spin 1s linear infinite;
445
+ }
446
+
447
+ @keyframes spin { to { transform: rotate(360deg); } }
448
+
449
+ /* Welcome */
450
+ .welcome-message {
451
+ text-align: center;
452
+ padding: 60px 40px;
453
+ max-width: 600px;
454
+ margin: 0 auto;
455
+ }
456
+
457
+ .welcome-message h2 {
458
+ font-size: 2rem;
459
+ margin-bottom: 16px;
460
+ background: linear-gradient(135deg, var(--text-primary) 0%, var(--accent-light) 100%);
461
+ -webkit-background-clip: text;
462
+ -webkit-text-fill-color: transparent;
463
+ }
464
+
465
+ .welcome-message p {
466
+ color: var(--text-secondary);
467
+ line-height: 1.7;
468
+ margin-bottom: 24px;
469
+ }
470
+
471
+ .start-btn {
472
+ padding: 14px 36px;
473
+ background: linear-gradient(135deg, var(--accent) 0%, var(--accent-light) 100%);
474
+ color: white;
475
+ border: none;
476
+ border-radius: 10px;
477
+ font-size: 1rem;
478
+ font-weight: 600;
479
+ cursor: pointer;
480
+ transition: transform 0.2s, box-shadow 0.2s;
481
+ }
482
+
483
+ .start-btn:hover {
484
+ transform: translateY(-2px);
485
+ box-shadow: 0 8px 30px var(--accent-glow);
486
+ }
487
+
488
+ /* Input Area */
489
+ .input-area {
490
+ padding: 20px 24px;
491
+ border-top: 1px solid var(--border);
492
+ background: var(--bg-card);
493
+ }
494
+
495
+ .input-row {
496
+ display: flex;
497
+ gap: 12px;
498
+ align-items: flex-end;
499
+ }
500
+
501
+ .input-wrapper {
502
+ flex: 1;
503
+ }
504
+
505
+ .input-wrapper textarea {
506
+ width: 100%;
507
+ padding: 14px 18px;
508
+ background: var(--bg-input);
509
+ border: 1px solid var(--border);
510
+ border-radius: 12px;
511
+ color: var(--text-primary);
512
+ font-size: 0.95rem;
513
+ resize: none;
514
+ font-family: inherit;
515
+ transition: border-color 0.2s;
516
+ }
517
+
518
+ .input-wrapper textarea:focus {
519
+ outline: none;
520
+ border-color: var(--accent);
521
+ }
522
+
523
+ .btn-group {
524
+ display: flex;
525
+ gap: 8px;
526
+ }
527
+
528
+ .send-btn {
529
+ padding: 14px 24px;
530
+ background: var(--accent);
531
+ color: white;
532
+ border: none;
533
+ border-radius: 12px;
534
+ font-size: 0.95rem;
535
+ font-weight: 500;
536
+ cursor: pointer;
537
+ transition: all 0.2s;
538
+ }
539
+
540
+ .send-btn:hover {
541
+ background: var(--accent-light);
542
+ }
543
+
544
+ .tend-btn {
545
+ padding: 14px 20px;
546
+ background: var(--tend-color);
547
+ color: white;
548
+ border: none;
549
+ border-radius: 12px;
550
+ font-size: 0.95rem;
551
+ font-weight: 500;
552
+ cursor: pointer;
553
+ transition: all 0.2s;
554
+ }
555
+
556
+ .tend-btn:hover {
557
+ background: #059669;
558
+ box-shadow: 0 4px 20px var(--tend-glow);
559
+ }
560
+
561
+ .quick-tools {
562
+ display: flex;
563
+ gap: 8px;
564
+ margin-top: 12px;
565
+ flex-wrap: wrap;
566
+ }
567
+
568
+ .quick-tool-btn {
569
+ padding: 6px 12px;
570
+ background: transparent;
571
+ border: 1px solid var(--border);
572
+ color: var(--text-secondary);
573
+ border-radius: 6px;
574
+ font-size: 0.8rem;
575
+ cursor: pointer;
576
+ transition: all 0.2s;
577
+ }
578
+
579
+ .quick-tool-btn:hover {
580
+ border-color: var(--accent);
581
+ color: var(--accent-light);
582
+ background: rgba(99, 102, 241, 0.1);
583
+ }
584
+
585
+ /* Responsive */
586
+ @media (max-width: 900px) {
587
+ .toolkit-sidebar { width: 280px; }
588
+ .tools-grid { grid-template-columns: 1fr; }
589
+ }
590
+
591
+ @media (max-width: 700px) {
592
+ .app-container { flex-direction: column; }
593
+ .toolkit-sidebar { width: 100%; max-height: 45vh; }
594
+ }
595
+ </style>
596
+ </head>
597
+ <body>
598
+ <div class="app-container">
599
+ <!-- TOOLKIT SIDEBAR -->
600
+ <aside class="toolkit-sidebar">
601
+ <div class="sidebar-header">
602
+ <h1>Tend & Send</h1>
603
+ <p>Assistive Relational Intelligence</p>
604
+ </div>
605
+
606
+ <!-- Settings -->
607
+ <div class="settings-section">
608
+ <h3>Conversation Partner</h3>
609
+ <div class="setting-row">
610
+ <label>Attachment Style</label>
611
+ <select id="attachment-style">
612
+ <option value="anxious">Anxious - Seeks reassurance</option>
613
+ <option value="avoidant">Avoidant - Needs space when stressed</option>
614
+ <option value="disorganized">Disorganized - Mixed signals</option>
615
+ <option value="secure">Secure - Open communication</option>
616
+ </select>
617
+ </div>
618
+ <div class="setting-row">
619
+ <label>Conversation Difficulty</label>
620
+ <select id="difficulty">
621
+ <option value="gentle">Gentle - Minor tension</option>
622
+ <option value="moderate">Moderate - Recurring issue</option>
623
+ <option value="intense">Intense - Significant conflict</option>
624
+ <option value="crisis">Crisis - Crossroads</option>
625
+ </select>
626
  </div>
627
  </div>
628
 
629
+ <!-- PRIMARY TEND TOOL -->
630
+ <div class="tend-section">
631
+ <div class="tend-tool" data-tool="tend" onclick="selectTool('tend')">
632
+ <h4>
633
+ <span class="primary-badge">Primary</span>
634
+ TEND Transform
635
+ </h4>
636
+ <p>NVC-aligned translation with warmth. Preserves your voice and emotional truth while finding clarity.</p>
637
+ </div>
638
+ </div>
639
+
640
+ <!-- Toggle Features -->
641
+ <div class="toggle-section">
642
+ <div class="toggle-row">
643
+ <label>Auto Feelings/Needs Reflection</label>
644
+ <label class="toggle-switch">
645
+ <input type="checkbox" id="toggle-feelings" checked>
646
+ <span class="toggle-slider"></span>
647
+ </label>
648
+ </div>
649
+ <div class="toggle-row">
650
+ <label>Show Intensity Check</label>
651
+ <label class="toggle-switch">
652
+ <input type="checkbox" id="toggle-intensity">
653
+ <span class="toggle-slider"></span>
654
+ </label>
655
+ </div>
656
  </div>
657
+
658
+ <!-- ARI Toolkit -->
659
+ <div class="tools-section">
660
+ <h3>ARI Toolkit</h3>
661
+ <div class="tools-grid">
662
+ <div class="tool-card" data-tool="guided_nvc" onclick="selectTool('guided_nvc')">
663
+ <h4><span class="tool-icon">&#128221;</span> Guided NVC</h4>
664
+ <p>Step-by-step I-statement builder</p>
665
+ </div>
666
+ <div class="tool-card" data-tool="receive_mode" onclick="selectTool('receive_mode')">
667
+ <h4><span class="tool-icon">&#128066;</span> Receive Mode</h4>
668
+ <p>Hear them before reacting</p>
669
+ </div>
670
+ <div class="tool-card" data-tool="pre_send_pause" onclick="selectTool('pre_send_pause')">
671
+ <h4><span class="tool-icon">&#9208;</span> Pre-Send</h4>
672
+ <p>Check intention first</p>
673
+ </div>
674
+ <div class="tool-card" data-tool="observation_spotter" onclick="selectTool('observation_spotter')">
675
+ <h4><span class="tool-icon">&#128269;</span> Observations</h4>
676
+ <p>Judgments to facts</p>
677
+ </div>
678
+ <div class="tool-card" data-tool="pure_questioning" onclick="selectTool('pure_questioning')">
679
+ <h4><span class="tool-icon">&#10067;</span> Questions</h4>
680
+ <p>Open inquiry only</p>
681
  </div>
682
+ <div class="tool-card" data-tool="somatic_checkin" onclick="selectTool('somatic_checkin')">
683
+ <h4><span class="tool-icon">&#129728;</span> Somatic</h4>
684
+ <p>Body check-in</p>
685
  </div>
686
+ <div class="tool-card" data-tool="repair_support" onclick="selectTool('repair_support')">
687
+ <h4><span class="tool-icon">&#128591;</span> Repair</h4>
688
+ <p>After ruptures</p>
689
+ </div>
690
+ <div class="tool-card" data-tool="feelings_needs" onclick="selectTool('feelings_needs')">
691
+ <h4><span class="tool-icon">&#128156;</span> F&N Extract</h4>
692
+ <p>Identify feelings/needs</p>
693
  </div>
694
  </div>
695
+ </div>
696
 
697
+ <!-- Footer -->
698
+ <div class="sidebar-footer">
699
+ <div class="safety-notice">
700
+ <strong>Reflection prompts only</strong> - not therapy.
701
+ If in crisis, contact a professional.
702
+ </div>
703
+ <div class="attribution">
704
+ Created by Jocelyn Skillman, LMHC<br>
705
+ <a href="https://huggingface.co/jostlebot" target="_blank">More ARI tools</a>
706
  </div>
707
+ </div>
708
+ </aside>
709
 
710
+ <!-- MAIN CONTENT -->
711
+ <main class="main-content">
712
+ <div class="content-header">
713
+ <div class="partner-info">
714
+ <div class="partner-avatar" id="partner-avatar">P</div>
715
+ <div class="partner-details">
716
+ <h3>Partner</h3>
717
+ <span id="partner-style">Anxious attachment</span>
718
  </div>
719
  </div>
720
+ <div id="active-tool-display">
721
+ <span class="active-tool-badge tend">TEND Ready</span>
 
 
 
722
  </div>
723
+ </div>
724
 
725
+ <div class="conversation-area" id="conversation">
726
+ <div class="welcome-message">
727
+ <h2>Welcome to Tend & Send</h2>
728
+ <p>
729
+ This prototype demonstrates ARI (Assistive Relational Intelligence) - tools designed
730
+ to support your real relationships, not replace them.
731
+ </p>
732
+ <p>
733
+ Use <strong>TEND</strong> to transform your messages with NVC clarity and warmth.
734
+ The toolkit offers additional support for receiving difficult messages,
735
+ checking your intention, and finding observations beneath judgments.
736
+ </p>
737
+ <button class="start-btn" onclick="startConversation()">Start Conversation</button>
738
  </div>
739
  </div>
740
+
741
+ <div class="input-area">
742
+ <div class="input-row">
743
+ <div class="input-wrapper">
744
+ <textarea id="user-input" rows="2" placeholder="Type your message..." onkeydown="handleKeyDown(event)"></textarea>
745
+ </div>
746
+ <div class="btn-group">
747
+ <button class="tend-btn" onclick="useTendTransform()">TEND</button>
748
+ <button class="send-btn" onclick="sendMessage()">Send</button>
749
+ </div>
750
+ </div>
751
+ <div class="quick-tools">
752
+ <button class="quick-tool-btn" onclick="quickTool('receive_mode')">Receive Mode</button>
753
+ <button class="quick-tool-btn" onclick="quickTool('pre_send_pause')">Pre-Send Pause</button>
754
+ <button class="quick-tool-btn" onclick="quickTool('intensity_check')">Check Intensity</button>
755
+ <button class="quick-tool-btn" onclick="quickTool('guided_nvc')">Guided NVC</button>
756
+ </div>
757
  </div>
758
+ </main>
759
  </div>
760
 
761
+ <script>
762
+ // ============================================================================
763
+ // STATE
764
+ // ============================================================================
765
+ let conversationHistory = [];
766
+ let activeTool = 'tend';
767
+ let lastPartnerMessage = '';
768
+ let conversationStarted = false;
769
+ let nvcStage = 1;
770
+
771
+ const TOOL_NAMES = {
772
+ tend: 'TEND Transform',
773
+ feelings_needs: 'Feelings & Needs',
774
+ guided_nvc: 'Guided NVC',
775
+ receive_mode: 'Receive Mode',
776
+ pre_send_pause: 'Pre-Send Pause',
777
+ observation_spotter: 'Observation Spotter',
778
+ pure_questioning: 'Pure Questioning',
779
+ somatic_checkin: 'Somatic Check-in',
780
+ intensity_check: 'Intensity Check',
781
+ repair_support: 'Repair Support'
782
+ };
783
+
784
+ const PARTNER_PROMPTS = {
785
+ anxious: {
786
+ gentle: `Roleplay as someone with anxious attachment. Minor misunderstanding happening.
787
+ Seek reassurance, express worry about the relationship. 1-3 sentences, realistic.`,
788
+ moderate: `Roleplay as someone with anxious attachment in recurring tension.
789
+ Need reassurance, fear abandonment. 1-3 sentences, show vulnerability.`,
790
+ intense: `Roleplay as someone with anxious attachment in significant conflict.
791
+ Desperate for connection, scared of losing relationship. 1-3 sentences, authentic pain.`,
792
+ crisis: `Roleplay as anxious attachment at relationship crossroads.
793
+ Terrified of abandonment, may use ultimatums from fear. 1-3 sentences.`
794
+ },
795
+ avoidant: {
796
+ gentle: `Roleplay as someone with avoidant attachment. Need space when stressed.
797
+ Prefer independence, may seem distant. 1-3 sentences, realistic.`,
798
+ moderate: `Roleplay as avoidant attachment in recurring tension.
799
+ Pull away from emotion, minimize issues. 1-3 sentences.`,
800
+ intense: `Roleplay as avoidant attachment in significant conflict.
801
+ Shutting down, need space, struggle with emotional intensity. 1-3 sentences.`,
802
+ crisis: `Roleplay as avoidant attachment at crossroads.
803
+ Considering leaving because connection feels threatening. 1-3 sentences.`
804
+ },
805
+ disorganized: {
806
+ gentle: `Roleplay as disorganized attachment. Send mixed signals.
807
+ Want closeness but pull away. 1-3 sentences, show internal conflict.`,
808
+ moderate: `Roleplay as disorganized attachment in tension.
809
+ Oscillate between pursuit and withdrawal. 1-3 sentences.`,
810
+ intense: `Roleplay as disorganized attachment in conflict.
811
+ Contradictory messages, push-pull in same breath. 1-3 sentences.`,
812
+ crisis: `Roleplay as disorganized attachment at crossroads.
813
+ Peak internal conflict, wanting love but terrified. 1-3 sentences.`
814
+ },
815
+ secure: {
816
+ gentle: `Roleplay as secure attachment. Minor issue to discuss.
817
+ Express needs clearly, stay present. 1-3 sentences.`,
818
+ moderate: `Roleplay as secure attachment in recurring tension.
819
+ Discuss openly, take responsibility, curious about partner. 1-3 sentences.`,
820
+ intense: `Roleplay as secure attachment in significant conflict.
821
+ Name feelings, make repair attempts, hold relationship alongside issue. 1-3 sentences.`,
822
+ crisis: `Roleplay as secure attachment at crossroads.
823
+ Acknowledge seriousness while staying connected. 1-3 sentences.`
824
+ }
825
+ };
826
+
827
+ const OPENING_MESSAGES = {
828
+ anxious: {
829
+ gentle: "Hey, I noticed you didn't text me back earlier. Everything okay with us?",
830
+ moderate: "I feel like you've been distant lately. Did I do something wrong?",
831
+ intense: "You never told me you were going out. I don't know what's happening with us anymore.",
832
+ crisis: "I can't keep doing this. I need to know if you're still in this or not."
833
+ },
834
+ avoidant: {
835
+ gentle: "I need some time to myself tonight. Can we talk later?",
836
+ moderate: "I don't want to keep having this conversation. I just need some space.",
837
+ intense: "This is too much. I can't keep doing this right now.",
838
+ crisis: "I think I need a break. Maybe from talking. Maybe more. I don't know."
839
+ },
840
+ disorganized: {
841
+ gentle: "I want to see you but... maybe we should just stay in separately tonight.",
842
+ moderate: "I love you but sometimes I think we shouldn't be together. I don't know what I want.",
843
+ intense: "Don't leave! But I also can't do this. I need you but I need you to go.",
844
+ crisis: "Maybe we should break up. No wait, I didn't mean that. Why do you even stay with me?"
845
+ },
846
+ secure: {
847
+ gentle: "I noticed we haven't had much time together this week. Can we talk about it?",
848
+ moderate: "I've been feeling some distance between us. I want to understand what's happening.",
849
+ intense: "I'm frustrated about yesterday and I think we need to work through it.",
850
+ crisis: "We need an honest conversation about where we are. This is hard for me too."
851
+ }
852
+ };
853
+
854
+ // ============================================================================
855
+ // INITIALIZATION
856
+ // ============================================================================
857
+ document.addEventListener('DOMContentLoaded', () => {
858
+ document.getElementById('attachment-style').addEventListener('change', updatePartnerInfo);
859
+ document.getElementById('difficulty').addEventListener('change', updatePartnerInfo);
860
+ updatePartnerInfo();
861
+ });
862
+
863
+ function updatePartnerInfo() {
864
+ const style = document.getElementById('attachment-style').value;
865
+ const labels = {
866
+ anxious: 'Anxious attachment',
867
+ avoidant: 'Avoidant attachment',
868
+ disorganized: 'Disorganized attachment',
869
+ secure: 'Secure attachment'
870
+ };
871
+ document.getElementById('partner-style').textContent = labels[style];
872
+ document.getElementById('partner-avatar').textContent = style[0].toUpperCase();
873
+ }
874
+
875
+ // ============================================================================
876
+ // TOOLS
877
+ // ============================================================================
878
+ function selectTool(tool) {
879
+ document.querySelectorAll('.tool-card, .tend-tool').forEach(card => {
880
+ card.classList.remove('active');
881
+ });
882
+
883
+ const selectedCard = document.querySelector(`[data-tool="${tool}"]`);
884
+ if (selectedCard) selectedCard.classList.add('active');
885
+
886
+ activeTool = tool;
887
+ const badge = document.getElementById('active-tool-display');
888
+ badge.innerHTML = `<span class="active-tool-badge ${tool === 'tend' ? 'tend' : ''}">${TOOL_NAMES[tool]}</span>`;
889
+
890
+ if (tool !== 'tend') {
891
+ useTool(tool);
892
+ }
893
+ }
894
+
895
+ async function useTendTransform() {
896
+ const userInput = document.getElementById('user-input').value.trim();
897
+ if (!userInput) {
898
+ addMessage('ari', 'Write your message first, then use TEND to find clarity with warmth.', 'TEND Transform', true);
899
+ return;
900
+ }
901
+
902
+ await useTool('tend');
903
+ }
904
+
905
+ async function quickTool(tool) {
906
+ selectTool(tool);
907
+ }
908
+
909
+ async function useTool(tool) {
910
+ const userInput = document.getElementById('user-input').value.trim();
911
+ let inputText = '';
912
+
913
+ switch(tool) {
914
+ case 'tend':
915
+ if (!userInput) return;
916
+ inputText = `Transform this message with NVC clarity and warmth, preserving my voice: "${userInput}"`;
917
+ break;
918
+ case 'receive_mode':
919
+ if (!lastPartnerMessage) {
920
+ addMessage('ari', 'Start the conversation first to use Receive Mode on their message.', TOOL_NAMES[tool]);
921
+ return;
922
+ }
923
+ inputText = userInput || 'Help me receive and understand this message before I react.';
924
+ break;
925
+ case 'pre_send_pause':
926
+ if (!userInput) {
927
+ addMessage('ari', 'Write a draft first, then use Pre-Send Pause to check your intention.', TOOL_NAMES[tool]);
928
+ return;
929
+ }
930
+ inputText = 'Help me pause and check my intention before sending this.';
931
+ break;
932
+ case 'observation_spotter':
933
+ if (!userInput) {
934
+ addMessage('ari', 'Write something first to check for judgments vs observations.', TOOL_NAMES[tool]);
935
+ return;
936
+ }
937
+ inputText = 'Help me find observations underneath any judgments in this.';
938
+ break;
939
+ case 'feelings_needs':
940
+ const textToAnalyze = userInput || lastPartnerMessage;
941
+ if (!textToAnalyze) {
942
+ addMessage('ari', 'Need text to analyze - either their message or your draft.', TOOL_NAMES[tool]);
943
+ return;
944
+ }
945
+ inputText = `Identify the feelings and needs in this: "${textToAnalyze}"`;
946
+ break;
947
+ case 'intensity_check':
948
+ const textToCheck = userInput || lastPartnerMessage;
949
+ if (!textToCheck) {
950
+ addMessage('ari', 'Need text to check intensity.', TOOL_NAMES[tool]);
951
+ return;
952
+ }
953
+ inputText = `Check the emotional intensity: "${textToCheck}"`;
954
+ break;
955
+ case 'pure_questioning':
956
+ inputText = userInput || 'Help me find clarity about this situation.';
957
+ break;
958
+ case 'somatic_checkin':
959
+ inputText = userInput || 'Guide me through a body check-in before responding.';
960
+ break;
961
+ case 'guided_nvc':
962
+ inputText = userInput || 'Help me build an I-statement for this situation.';
963
+ break;
964
+ case 'repair_support':
965
+ inputText = userInput || 'Help me craft a genuine repair after this rupture.';
966
+ break;
967
+ }
968
+
969
+ addLoading();
970
+ try {
971
+ const response = await fetch('/api/tool', {
972
+ method: 'POST',
973
+ headers: { 'Content-Type': 'application/json' },
974
+ body: JSON.stringify({
975
+ tool: tool,
976
+ partner_message: lastPartnerMessage,
977
+ user_draft: userInput,
978
+ user_input: inputText,
979
+ stage: nvcStage
980
+ })
981
+ });
982
+
983
+ const data = await response.json();
984
+ removeLoading();
985
+
986
+ if (data.response) {
987
+ addMessage('ari', data.response, TOOL_NAMES[tool], tool === 'tend');
988
+
989
+ // Auto feelings/needs if enabled
990
+ if (tool === 'tend' && document.getElementById('toggle-feelings').checked) {
991
+ setTimeout(() => autoFeelingsNeeds(userInput), 1000);
992
+ }
993
+ } else {
994
+ addMessage('ari', 'Could not process. Check API configuration.', 'Error');
995
+ }
996
+ } catch (error) {
997
+ removeLoading();
998
+ addMessage('ari', 'Connection error. Please try again.', 'Error');
999
+ }
1000
+ }
1001
+
1002
+ async function autoFeelingsNeeds(text) {
1003
+ try {
1004
+ const response = await fetch('/api/tool', {
1005
+ method: 'POST',
1006
+ headers: { 'Content-Type': 'application/json' },
1007
+ body: JSON.stringify({
1008
+ tool: 'feelings_needs',
1009
+ user_input: `Quick extraction from: "${text}"`,
1010
+ partner_message: lastPartnerMessage,
1011
+ user_draft: text
1012
+ })
1013
+ });
1014
+ const data = await response.json();
1015
+ if (data.response) {
1016
+ addMessage('ari', data.response, 'Feelings & Needs Reflection');
1017
+ }
1018
+ } catch (e) { /* silent fail */ }
1019
+ }
1020
+
1021
+ // ============================================================================
1022
+ // CONVERSATION
1023
+ // ============================================================================
1024
+ async function startConversation() {
1025
+ const style = document.getElementById('attachment-style').value;
1026
+ const difficulty = document.getElementById('difficulty').value;
1027
+
1028
+ document.getElementById('conversation').innerHTML = '';
1029
+ conversationHistory = [];
1030
+ conversationStarted = true;
1031
+
1032
+ const openingMessage = OPENING_MESSAGES[style][difficulty];
1033
+ addMessage('partner', openingMessage);
1034
+ lastPartnerMessage = openingMessage;
1035
+ conversationHistory.push({ role: 'assistant', content: openingMessage });
1036
+ }
1037
+
1038
+ async function sendMessage() {
1039
+ if (!conversationStarted) {
1040
+ startConversation();
1041
+ return;
1042
+ }
1043
+
1044
+ const input = document.getElementById('user-input');
1045
+ const message = input.value.trim();
1046
+ if (!message) return;
1047
+
1048
+ input.value = '';
1049
+ addMessage('user', message);
1050
+ conversationHistory.push({ role: 'user', content: message });
1051
+
1052
+ addLoading();
1053
+ try {
1054
+ const style = document.getElementById('attachment-style').value;
1055
+ const difficulty = document.getElementById('difficulty').value;
1056
+
1057
+ const response = await fetch('/api/chat', {
1058
+ method: 'POST',
1059
+ headers: { 'Content-Type': 'application/json' },
1060
+ body: JSON.stringify({
1061
+ messages: conversationHistory,
1062
+ system: PARTNER_PROMPTS[style][difficulty],
1063
+ max_tokens: 300
1064
+ })
1065
+ });
1066
+
1067
+ const data = await response.json();
1068
+ removeLoading();
1069
+
1070
+ if (data.content && data.content[0]) {
1071
+ const partnerResponse = data.content[0].text;
1072
+ addMessage('partner', partnerResponse);
1073
+ lastPartnerMessage = partnerResponse;
1074
+ conversationHistory.push({ role: 'assistant', content: partnerResponse });
1075
+ }
1076
+ } catch (error) {
1077
+ removeLoading();
1078
+ addMessage('ari', 'Connection error. Please try again.', 'System');
1079
+ }
1080
+ }
1081
+
1082
+ function handleKeyDown(event) {
1083
+ if (event.key === 'Enter' && !event.shiftKey) {
1084
+ event.preventDefault();
1085
+ sendMessage();
1086
+ }
1087
+ }
1088
+
1089
+ // ============================================================================
1090
+ // UI HELPERS
1091
+ // ============================================================================
1092
+ function addMessage(type, content, toolName = null, isTend = false) {
1093
+ const conversation = document.getElementById('conversation');
1094
+ const div = document.createElement('div');
1095
+ div.className = `message ${type}`;
1096
+ if (isTend) div.classList.add('tend-result');
1097
+
1098
+ if (type === 'ari') {
1099
+ const label = document.createElement('div');
1100
+ label.className = 'ari-label';
1101
+ label.textContent = toolName || 'ARI';
1102
+ div.appendChild(label);
1103
+ }
1104
+
1105
+ const text = document.createElement('div');
1106
+ text.innerHTML = formatMessage(content);
1107
+ div.appendChild(text);
1108
+
1109
+ conversation.appendChild(div);
1110
+ conversation.scrollTop = conversation.scrollHeight;
1111
+ }
1112
+
1113
+ function addLoading() {
1114
+ const conversation = document.getElementById('conversation');
1115
+ const div = document.createElement('div');
1116
+ div.className = 'loading';
1117
+ div.id = 'loading';
1118
+ div.innerHTML = '<div class="spinner"></div> Processing...';
1119
+ conversation.appendChild(div);
1120
+ conversation.scrollTop = conversation.scrollHeight;
1121
+ }
1122
+
1123
+ function removeLoading() {
1124
+ const el = document.getElementById('loading');
1125
+ if (el) el.remove();
1126
+ }
1127
+
1128
+ function formatMessage(text) {
1129
+ return text
1130
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
1131
+ .replace(/\n/g, '<br>');
1132
+ }
1133
+ </script>
1134
  </body>
1135
  </html>