jostlebot Claude Opus 4.5 commited on
Commit
3793e10
·
1 Parent(s): d4119b5

Streamlined UX: sidebar I-statement builder, 5 stages

Browse files

- Sidebar shows building I-statement (Feel/Need/Request)
- Reduced to 5 stages: Share → Feelings → Needs → Request → Done
- Removed "You:" prefix - just Guide responses
- Much more concise prompts (2-3 sentences max)
- Cleaner, focused translation experience

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +189 -442
app.py CHANGED
@@ -1,502 +1,267 @@
1
  """
2
- NVC Deep Practice - A Multi-Stage Nonviolent Communication Learning Space
3
- Simplified version without Chatbot component to avoid Gradio bugs
4
  """
5
 
6
  import gradio as gr
7
  import anthropic
8
  import os
9
 
10
- # Check for API key at startup
11
- def check_api_key():
12
- """Check if API key is configured"""
13
- return bool(
14
  os.environ.get("ANTHROPIC_API_KEY") or
15
  os.environ.get("anthropic_key") or
16
  os.environ.get("ANTHROPIC_KEY") or
17
  os.environ.get("anthropic_api_key")
18
  )
 
 
 
19
 
20
- API_KEY_CONFIGURED = check_api_key()
21
- print(f"=== STARTUP: API key configured: {API_KEY_CONFIGURED} ===")
22
-
23
- # ============ CONSTANTS ============
24
-
25
- NEEDS_LIST = {
26
- "Emotional Needs": [
27
- "acceptance", "acknowledgement", "affection", "appreciation",
28
- "autonomy", "belonging", "care", "respect", "security",
29
- "to be heard", "to be seen", "to matter", "trust", "understanding"
30
- ],
31
- "Relational Needs": [
32
- "closeness", "collaboration", "communication", "community",
33
- "companionship", "connection", "empathy", "honesty",
34
- "kindness", "love", "support"
35
- ],
36
- "Value Needs": [
37
- "authenticity", "creativity", "growth", "healing", "hope",
38
- "learning", "meaning", "purpose", "self-expression"
39
- ],
40
- "Lifestyle Needs": [
41
- "balance", "ease", "fun", "health", "peace", "play",
42
- "rest", "safety", "vitality"
43
- ]
44
- }
45
-
46
- FEELINGS_LIST = {
47
- "When Needs Are Met": [
48
- "grateful", "peaceful", "joyful", "content", "hopeful",
49
- "confident", "curious", "excited", "relieved", "touched",
50
- "calm", "comfortable", "energized", "fulfilled", "happy",
51
- "inspired", "loving", "optimistic", "proud", "safe"
52
- ],
53
- "Sad/Hurt": [
54
- "sad", "hurt", "disappointed", "heartbroken", "grief",
55
- "lonely", "hopeless", "dejected", "discouraged", "heavy"
56
- ],
57
- "Scared/Anxious": [
58
- "anxious", "scared", "worried", "nervous", "fearful",
59
- "overwhelmed", "insecure", "vulnerable", "panicked", "dread"
60
- ],
61
- "Angry/Frustrated": [
62
- "frustrated", "angry", "annoyed", "irritated", "resentful",
63
- "furious", "impatient", "exasperated", "agitated", "bitter"
64
- ],
65
- "Confused/Tired": [
66
- "confused", "torn", "uncertain", "ambivalent", "puzzled",
67
- "exhausted", "tired", "depleted", "numb", "disconnected"
68
- ]
69
- }
70
-
71
- FEELINGS_CONNECTED = FEELINGS_LIST["When Needs Are Met"]
72
- FEELINGS_WANTING = (FEELINGS_LIST["Sad/Hurt"] + FEELINGS_LIST["Scared/Anxious"] +
73
- FEELINGS_LIST["Angry/Frustrated"] + FEELINGS_LIST["Confused/Tired"])
74
-
75
- # ============ SYSTEM PROMPTS ============
76
-
77
- BASE_TONE = """
78
- Infused with the warmth of Sarah Peyton and Tara Brach - resonant empathy meeting radical acceptance.
79
-
80
- CRITICAL LANGUAGE RULES:
81
- - NEVER use first person ("I", "I'm", "I sense", "I hear")
82
- - DO use second person warmly ("you", "your")
83
- - Use impersonal constructions: "What's here is...", "There's...", "It sounds like..."
84
- """
85
-
86
- STAGE_PROMPTS = {
87
- 1: f"""
88
- {BASE_TONE}
89
-
90
- The raw message has been shared. Offer:
91
- 1. Warm acknowledgment of what's present
92
- 2. Reflection of the emotional energy sensed
93
- 3. NO judgment - just tender presence
94
-
95
- Keep it brief (2-3 sentences) but saturated with warmth.
96
- """,
97
-
98
- 2: f"""
99
- {BASE_TONE}
100
-
101
- Now explore FEELINGS. This is the heart of NVC - connecting to what's alive emotionally.
102
-
103
- FEELINGS when needs are MET: {', '.join(FEELINGS_CONNECTED)}
104
- FEELINGS when needs are UNMET: {', '.join(FEELINGS_WANTING)}
105
-
106
- Your task:
107
- 1. Offer 3-4 specific feeling guesses based on what was shared
108
- 2. Go for nuance and depth - layer feelings (e.g., "frustrated, and underneath that, perhaps hurt?")
109
- 3. Use phrases like "Are you feeling...?" "Could there be...?" "Might you be experiencing...?"
110
- 4. Invite them to name what resonates: "Which of these land for you? Or is there another feeling word that fits better?"
111
-
112
- This is INTERACTIVE - you want them to identify and name their feelings before moving forward.
113
- """,
114
-
115
- 3: f"""
116
- {BASE_TONE}
117
-
118
- Guide a brief somatic check-in:
119
- - "Where might you be feeling this in your body?"
120
- - "What's the quality of sensation there?"
121
-
122
- Keep it brief and non-pressuring. End with permission to skip if the body isn't speaking clearly.
123
- """,
124
-
125
- 4: f"""
126
- {BASE_TONE}
127
-
128
- Brief teaching moment connecting FEELINGS to NEEDS.
129
-
130
- Reference the specific feelings they named, then teach:
131
-
132
- "These feelings are messengers. In NVC, we understand that every feeling points to a need.
133
-
134
- When feelings are pleasant (grateful, peaceful, content), they're saying: 'A need is being met.'
135
- When feelings are uncomfortable (frustrated, sad, anxious), they're saying: 'There's a need here wanting attention.'
136
-
137
- Marshall Rosenberg said: 'Feelings are the voice of our needs. They tell us whether our needs are being met or not.'
138
-
139
- So the [feeling they named] you're experiencing is pointing toward something important..."
140
-
141
- Keep it brief (4-5 sentences). Bridge naturally to needs exploration.
142
- """,
143
-
144
- 5: f"""
145
- {BASE_TONE}
146
-
147
- Now explore NEEDS. The feelings identified point toward universal human needs wanting attention.
148
-
149
- REMEMBER: Needs are universal (every human has them) and are NOT strategies (specific ways to meet them).
150
-
151
- Common needs include: connection, understanding, respect, autonomy, safety, belonging,
152
- appreciation, to be seen, to be heard, to matter, support, honesty, trust, peace, meaning.
153
-
154
- Your task:
155
- 1. Reference the specific feelings they identified
156
- 2. Offer 3-4 need guesses that connect to those feelings
157
- - If frustrated/angry → might need respect, autonomy, fairness, to be heard
158
- - If sad/hurt → might need care, understanding, connection, to matter
159
- - If anxious/scared → might need safety, reassurance, predictability, support
160
- - If lonely → might need belonging, connection, companionship, to be seen
161
- 3. Invite them to select: "Which needs resonate most? You can also open the needs selector below to explore more options."
162
-
163
- This is INTERACTIVE - help them identify 1-3 core needs before moving forward.
164
- """,
165
-
166
- 6: f"""
167
- {BASE_TONE}
168
-
169
- The needs have been identified. Offer DEEP RESONANCE with their specific needs.
170
-
171
- Your task:
172
- 1. Name back the specific needs they identified with warmth
173
- 2. Validate the universality: "Of course there's longing for [need] - this is such a human need"
174
- 3. Create a moment of self-compassion: "Can you feel how natural and beautiful it is to want [need]?"
175
-
176
- Example: "So what's here is a deep need for understanding - to really be seen and known.
177
- Of course that matters. Every human heart longs for that kind of connection.
178
- Can you feel how legitimate and important this need is?"
179
-
180
- Keep it to 3-4 sentences. This is a moment of honoring before moving forward.
181
- """,
182
-
183
- 7: f"""
184
- {BASE_TONE}
185
-
186
- Deliver a relevant micro-lesson based on context:
187
- - ANGER: A messenger that a boundary was crossed
188
- - I-LANGUAGE: "I feel..." instead of "You never..."
189
- - VULNERABILITY: What's underneath the anger?
190
-
191
- Keep it brief (3-4 sentences) with a Marshall Rosenberg quote if applicable.
192
- """,
193
-
194
- 8: f"""
195
- {BASE_TONE}
196
-
197
- Teach about relational patterns:
198
- - We're wired for connection
199
- - Early patterns create default responses
200
- - Neuroplasticity means we can create new patterns
201
-
202
- Keep to 4-5 sentences. Normalize the difficulty, inspire hope.
203
- """,
204
-
205
- 9: f"""
206
- {BASE_TONE}
207
-
208
- Help craft an NVC request:
209
-
210
- REQUEST CRITERIA:
211
- 1. Specific (not vague)
212
- 2. Doable (concrete action)
213
- 3. Positive language (what you want, not don't want)
214
- 4. Honors other's autonomy
215
-
216
- Guide: "What specific, doable action would meet your need?"
217
- """,
218
-
219
- 10: f"""
220
- {BASE_TONE}
221
-
222
- Teach about autonomy and receiving "no":
223
-
224
- True requests allow for "no." If you're not willing to hear "no," it's a demand.
225
-
226
- "No" protects their needs - just as valid as yours.
227
-
228
- Keep it compassionate but clear.
229
- """,
230
-
231
- 11: f"""
232
- {BASE_TONE}
233
 
234
- Invite rewriting the original message:
235
 
236
- Structure if helpful:
237
- - When [observation]
238
- - I feel [feeling]
239
- - Because I need [need]
240
- - Would you be willing to [request]?
 
241
 
242
- Remind: This is practice, not perfection.
243
- """,
 
 
 
 
244
 
245
- 12: f"""
246
- {BASE_TONE}
247
 
248
- Provide warm feedback on the revised message:
249
- 1. Name what's working
250
- 2. Offer one gentle growth edge
251
- 3. Invite: "How does it feel to express it this way?"
252
 
253
- Keep feedback ratio 3:1 positive to constructive.
254
- """,
255
 
256
- 13: f"""
257
- {BASE_TONE}
258
 
259
- Close the practice with integration:
260
- 1. Honor the work done
261
- 2. Ask: "What's one thing you'll take with you?"
262
- 3. Remind: Each practice strengthens new neural pathways
263
 
264
- End warmly with invitation to practice again.
265
- """
266
- }
267
 
268
- STAGE_NAMES = {
269
- 1: "Share Your Message",
270
- 2: "Feeling Reflection",
271
- 3: "Body Check-In",
272
- 4: "Feelings as Messengers",
273
- 5: "Identify Needs",
274
- 6: "Needs Resonance",
275
- 7: "Deeper Understanding",
276
- 8: "Relational Patterns",
277
- 9: "Make a Request",
278
- 10: "Honoring Autonomy",
279
- 11: "Rewrite Your Message",
280
- 12: "Reflection",
281
- 13: "Integration"
282
  }
283
 
284
- # ============ CLAUDE INTEGRATION ============
285
-
286
- def get_client():
287
- api_key = (
288
- os.environ.get("ANTHROPIC_API_KEY") or
289
- os.environ.get("anthropic_key") or
290
- os.environ.get("ANTHROPIC_KEY") or
291
- os.environ.get("anthropic_api_key")
292
- )
293
- if not api_key:
294
- return None
295
- return anthropic.Anthropic(api_key=api_key)
296
 
297
- def call_claude(system_prompt, user_message, context=""):
298
  client = get_client()
299
-
300
- if client is None:
301
  return None
302
 
303
- full_message = f"{context}\n\nUser: {user_message}" if context else user_message
304
-
305
  response = client.messages.create(
306
  model="claude-sonnet-4-20250514",
307
- max_tokens=1500,
308
- system=system_prompt,
309
- messages=[{"role": "user", "content": full_message}]
310
  )
311
-
312
  return response.content[0].text
313
 
314
- # ============ MAIN APP ============
 
 
315
 
316
- def format_history(history):
317
- """Format conversation history as markdown"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  if not history:
319
- return "*Start by sharing a message you'd like to communicate more skillfully...*"
320
 
321
  output = ""
322
- for entry in history:
323
- if entry.get("user"):
324
- output += f"**You:** {entry['user']}\n\n"
325
- if entry.get("assistant"):
326
- output += f"**Guide:** {entry['assistant']}\n\n"
327
- output += "---\n\n"
328
- return output
329
-
330
- def process_message(user_input, history, stage, session_data, somatic_enabled, selected_feelings_str, selected_needs_str):
331
- """Process user message and return updated state"""
332
-
333
- if not user_input.strip():
334
- user_input = "continue"
335
 
336
- # Skip somatic if disabled
337
- if stage == 3 and not somatic_enabled:
338
- stage = 4
339
 
340
- # Build context
341
- context_parts = []
342
- if session_data.get("original_message"):
343
- context_parts.append(f"Original message: \"{session_data['original_message']}\"")
344
- if session_data.get("feelings"):
345
- context_parts.append(f"Feelings: {session_data['feelings']}")
346
- if session_data.get("needs"):
347
- context_parts.append(f"Needs: {session_data['needs']}")
348
- context = "\n".join(context_parts)
349
-
350
- # Get response from Claude
351
- system_prompt = STAGE_PROMPTS.get(stage, STAGE_PROMPTS[1])
352
 
 
 
 
 
 
 
 
 
 
 
353
  try:
354
- response = call_claude(system_prompt, user_input, context)
355
-
356
  if response is None:
357
- error_msg = "**Setup Required:** Please add ANTHROPIC_API_KEY in Space Settings > Repository secrets, then restart."
358
- history.append({"user": user_input, "assistant": error_msg})
359
- show_feelings = (stage == 2)
360
- show_needs = (stage == 5)
361
- return format_history(history), "", history, stage, session_data, f"Stage {stage}: {STAGE_NAMES[stage]}", gr.update(visible=show_feelings), gr.update(visible=show_needs)
362
-
 
363
  except Exception as e:
364
- error_msg = f"**Error:** {str(e)}"
365
- history.append({"user": user_input, "assistant": error_msg})
366
- show_feelings = (stage == 2)
367
- show_needs = (stage == 5)
368
- return format_history(history), "", history, stage, session_data, f"Stage {stage}: {STAGE_NAMES[stage]}", gr.update(visible=show_feelings), gr.update(visible=show_needs)
 
 
369
 
370
- # Update session data
371
  if stage == 1:
372
- session_data["original_message"] = user_input
373
  elif stage == 2:
374
- # Use selected feelings from checkboxes if available, otherwise use typed input
375
- if selected_feelings_str:
376
- session_data["feelings"] = selected_feelings_str
377
- else:
378
- session_data["feelings"] = user_input
379
- elif stage == 5 and selected_needs_str:
380
- session_data["needs"] = selected_needs_str
381
 
382
  # Add to history
383
- if user_input != "continue":
384
- history.append({"user": user_input, "assistant": response})
385
- else:
386
- history.append({"assistant": response})
387
 
388
  # Advance stage
389
- next_stage = stage + 1 if stage < 13 else 1
390
-
391
- # Reset if completed
392
- if stage == 13:
393
  next_stage = 1
394
- session_data = {}
 
395
 
396
- # Show feelings selector at stage 2, needs selector at stage 5
397
  show_feelings = (next_stage == 2)
398
- show_needs = (next_stage == 5)
399
 
400
  return (
401
- format_history(history),
402
  "",
403
  history,
404
  next_stage,
405
- session_data,
406
- f"Stage {next_stage}/13: {STAGE_NAMES[next_stage]}",
407
  gr.update(visible=show_feelings),
408
  gr.update(visible=show_needs)
409
  )
410
 
411
- def reset_session():
412
- """Reset everything"""
413
  return (
414
- "*Start by sharing a message you'd like to communicate more skillfully...*",
415
  "",
416
  [],
417
  1,
418
  {},
419
- "Stage 1/13: Share Your Message",
420
- gr.update(visible=False), # feelings_accordion
421
- gr.update(visible=False), # needs_accordion
422
- [], [], [], [], [], # feelings1-5
423
- [], # selected_feelings
424
- [], [], [], [] # needs1-4
425
  )
426
 
427
- def update_needs_display(e1, e2, e3, e4):
428
- """Combine selected needs"""
429
- all_needs = e1 + e2 + e3 + e4
430
- return ", ".join(all_needs) if all_needs else ""
431
 
432
- def update_feelings_display(f1, f2, f3, f4, f5):
433
- """Combine selected feelings"""
434
- all_feelings = f1 + f2 + f3 + f4 + f5
435
- return ", ".join(all_feelings) if all_feelings else ""
436
 
437
- # ============ BUILD INTERFACE ============
438
 
439
  with gr.Blocks(
440
- title="NVC Deep Practice",
441
  theme=gr.themes.Soft(primary_hue="green", secondary_hue="amber")
442
  ) as demo:
443
 
444
  # State
445
  history_state = gr.State([])
446
  stage_state = gr.State(1)
447
- session_state = gr.State({})
448
 
449
  # Header
450
  gr.Markdown("""
451
- # NVC Deep Practice
452
- **Learn Nonviolent Communication through guided practice**
453
-
454
- Share a message you want to communicate, and this guide will help you explore
455
- your feelings, needs, and how to express yourself with clarity and compassion.
456
  """)
457
 
458
  if not API_KEY_CONFIGURED:
459
- gr.Markdown("""
460
- > **Setup Required:** Add `ANTHROPIC_API_KEY` in Space Settings > Repository secrets, then restart.
461
- """)
462
 
463
  with gr.Row():
 
464
  with gr.Column(scale=2):
465
- # Conversation display
466
- conversation = gr.Markdown(
467
- value="*Start by sharing a message you'd like to communicate more skillfully...*",
468
- label="Conversation"
469
  )
470
 
471
- # Feelings selector (hidden until Stage 2)
472
- with gr.Accordion("Select Your Feelings", open=True, visible=False) as feelings_accordion:
473
- gr.Markdown("**Select feelings that resonate:**")
474
- with gr.Row():
475
- feelings1 = gr.CheckboxGroup(FEELINGS_LIST["When Needs Are Met"], label="When Needs Are Met")
476
- feelings2 = gr.CheckboxGroup(FEELINGS_LIST["Sad/Hurt"], label="Sad / Hurt")
477
  with gr.Row():
478
- feelings3 = gr.CheckboxGroup(FEELINGS_LIST["Scared/Anxious"], label="Scared / Anxious")
479
- feelings4 = gr.CheckboxGroup(FEELINGS_LIST["Angry/Frustrated"], label="Angry / Frustrated")
480
  with gr.Row():
481
- feelings5 = gr.CheckboxGroup(FEELINGS_LIST["Confused/Tired"], label="Confused / Tired")
482
- selected_feelings = gr.Textbox(label="Your Selected Feelings", interactive=False)
 
483
 
484
- # Needs selector (hidden until Stage 5)
485
- with gr.Accordion("Select Your Needs", open=True, visible=False) as needs_accordion:
486
- gr.Markdown("**Select 1-3 needs that resonate most deeply:**")
487
  with gr.Row():
488
- needs1 = gr.CheckboxGroup(NEEDS_LIST["Emotional Needs"], label="Emotional Needs")
489
- needs2 = gr.CheckboxGroup(NEEDS_LIST["Relational Needs"], label="Relational Needs")
490
  with gr.Row():
491
- needs3 = gr.CheckboxGroup(NEEDS_LIST["Value Needs"], label="Value Needs")
492
- needs4 = gr.CheckboxGroup(NEEDS_LIST["Lifestyle Needs"], label="Lifestyle Needs")
493
- selected_needs = gr.Textbox(label="Your Selected Needs", interactive=False)
494
 
495
  # Input
496
  user_input = gr.Textbox(
497
- label="",
498
  placeholder="Type here...",
499
- lines=3,
500
  show_label=False
501
  )
502
 
@@ -504,64 +269,46 @@ with gr.Blocks(
504
  submit_btn = gr.Button("Continue", variant="primary")
505
  reset_btn = gr.Button("Start Over")
506
 
 
507
  with gr.Column(scale=1):
508
- gr.Markdown("### Settings")
509
- somatic_check = gr.Checkbox(label="Include body check-ins", value=True)
510
-
511
- gr.Markdown("### Progress")
512
- stage_display = gr.Textbox(
513
- value="Stage 1/13: Share Your Message",
514
- label="Current Stage",
515
- interactive=False
516
- )
517
 
518
  gr.Markdown("""
519
- ### About NVC
520
- Nonviolent Communication helps us:
521
- - Express feelings honestly
522
- - Identify universal needs
523
- - Make clear requests
524
- - Connect with compassion
525
-
526
- [Learn more at CNVC.org](https://www.cnvc.org/)
527
  """)
528
 
529
- # Update feelings display
530
- for cb in [feelings1, feelings2, feelings3, feelings4, feelings5]:
531
- cb.change(
532
- update_feelings_display,
533
- [feelings1, feelings2, feelings3, feelings4, feelings5],
534
- selected_feelings
535
- )
536
 
537
- # Update needs display
538
- for cb in [needs1, needs2, needs3, needs4]:
539
- cb.change(
540
- update_needs_display,
541
- [needs1, needs2, needs3, needs4],
542
- selected_needs
543
- )
544
 
545
- # Submit handler
546
  submit_btn.click(
547
- process_message,
548
- [user_input, history_state, stage_state, session_state, somatic_check, selected_feelings, selected_needs],
549
- [conversation, user_input, history_state, stage_state, session_state, stage_display, feelings_accordion, needs_accordion]
550
  )
551
 
552
  user_input.submit(
553
- process_message,
554
- [user_input, history_state, stage_state, session_state, somatic_check, selected_feelings, selected_needs],
555
- [conversation, user_input, history_state, stage_state, session_state, stage_display, feelings_accordion, needs_accordion]
556
  )
557
 
558
- # Reset handler
559
  reset_btn.click(
560
- reset_session,
561
  [],
562
- [conversation, user_input, history_state, stage_state, session_state, stage_display,
563
- feelings_accordion, needs_accordion, feelings1, feelings2, feelings3, feelings4, feelings5,
564
- selected_feelings, needs1, needs2, needs3, needs4]
565
  )
566
 
567
  if __name__ == "__main__":
 
1
  """
2
+ NVC Translation Practice - Learn to translate messages using Nonviolent Communication
3
+ Streamlined flow with visual I-statement builder
4
  """
5
 
6
  import gradio as gr
7
  import anthropic
8
  import os
9
 
10
+ # ============ API SETUP ============
11
+
12
+ def get_client():
13
+ api_key = (
14
  os.environ.get("ANTHROPIC_API_KEY") or
15
  os.environ.get("anthropic_key") or
16
  os.environ.get("ANTHROPIC_KEY") or
17
  os.environ.get("anthropic_api_key")
18
  )
19
+ if not api_key:
20
+ return None
21
+ return anthropic.Anthropic(api_key=api_key)
22
 
23
+ API_KEY_CONFIGURED = bool(get_client())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
+ # ============ FEELINGS & NEEDS ============
26
 
27
+ FEELINGS = {
28
+ "Sad/Hurt": ["sad", "hurt", "disappointed", "lonely", "heavy", "grief"],
29
+ "Scared/Anxious": ["anxious", "scared", "worried", "overwhelmed", "insecure"],
30
+ "Angry/Frustrated": ["frustrated", "angry", "annoyed", "irritated", "resentful"],
31
+ "Confused/Tired": ["confused", "exhausted", "torn", "numb", "depleted"]
32
+ }
33
 
34
+ NEEDS = {
35
+ "Connection": ["understanding", "to be heard", "to be seen", "belonging", "closeness", "empathy"],
36
+ "Autonomy": ["autonomy", "choice", "freedom", "independence", "space"],
37
+ "Security": ["safety", "stability", "trust", "reassurance", "predictability"],
38
+ "Meaning": ["purpose", "contribution", "growth", "authenticity", "respect"]
39
+ }
40
 
41
+ # ============ PROMPTS ============
 
42
 
43
+ BASE = """Warm, concise NVC guide. 2-3 sentences max. No "I" statements - use "you" and "what's here is..."."""
 
 
 
44
 
45
+ PROMPTS = {
46
+ 1: f"{BASE}\nAcknowledge what was shared with brief warmth. One sentence of resonance.",
47
 
48
+ 2: f"{BASE}\nOffer 2-3 feeling guesses in one sentence. End with: 'Select what fits below.'",
 
49
 
50
+ 3: f"{BASE}\nConnect their feelings to 2-3 possible needs briefly. End with: 'Choose what resonates below.'",
 
 
 
51
 
52
+ 4: f"{BASE}\nHelp form a specific, doable request. Ask: 'What action would help meet this need?'",
 
 
53
 
54
+ 5: f"{BASE}\nCelebrate their translation. One sentence of resonance about their growth. Invite them to practice again."
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
 
57
+ # ============ CORE FUNCTIONS ============
 
 
 
 
 
 
 
 
 
 
 
58
 
59
+ def call_claude(prompt, message, context=""):
60
  client = get_client()
61
+ if not client:
 
62
  return None
63
 
64
+ full_msg = f"{context}\n\n{message}" if context else message
 
65
  response = client.messages.create(
66
  model="claude-sonnet-4-20250514",
67
+ max_tokens=300,
68
+ system=prompt,
69
+ messages=[{"role": "user", "content": full_msg}]
70
  )
 
71
  return response.content[0].text
72
 
73
+ def build_statement(data):
74
+ """Build the I-statement display for sidebar"""
75
+ parts = []
76
 
77
+ if data.get("original"):
78
+ parts.append(f"**Original:** *\"{data['original'][:100]}{'...' if len(data.get('original','')) > 100 else ''}\"*")
79
+ else:
80
+ parts.append("**Original:** *waiting...*")
81
+
82
+ parts.append("---")
83
+ parts.append("### Your Translation")
84
+
85
+ if data.get("feelings"):
86
+ parts.append(f"**I feel:** {data['feelings']}")
87
+ else:
88
+ parts.append("**I feel:** *...*")
89
+
90
+ if data.get("needs"):
91
+ parts.append(f"**Because I need:** {data['needs']}")
92
+ else:
93
+ parts.append("**Because I need:** *...*")
94
+
95
+ if data.get("request"):
96
+ parts.append(f"**Would you be willing to:** {data['request']}")
97
+ else:
98
+ parts.append("**Would you be willing to:** *...*")
99
+
100
+ if data.get("final"):
101
+ parts.append("---")
102
+ parts.append("### Complete Message")
103
+ parts.append(f"*{data['final']}*")
104
+
105
+ return "\n\n".join(parts)
106
+
107
+ def format_chat(history):
108
+ """Format chat - Guide only, no 'You:' prefix"""
109
  if not history:
110
+ return "*Share a message you want to communicate more skillfully...*"
111
 
112
  output = ""
113
+ for h in history:
114
+ if h.get("guide"):
115
+ output += f"{h['guide']}\n\n"
116
+ return output.strip()
 
 
 
 
 
 
 
 
 
117
 
118
+ def process(user_input, history, stage, data, feelings_str, needs_str):
119
+ """Main processing function"""
 
120
 
121
+ if not user_input.strip() and stage > 1:
122
+ user_input = "continue"
 
 
 
 
 
 
 
 
 
 
123
 
124
+ # Build context
125
+ context = ""
126
+ if data.get("original"):
127
+ context += f"Original: \"{data['original']}\"\n"
128
+ if data.get("feelings"):
129
+ context += f"Feelings: {data['feelings']}\n"
130
+ if data.get("needs"):
131
+ context += f"Needs: {data['needs']}\n"
132
+
133
+ # Get Claude response
134
  try:
135
+ response = call_claude(PROMPTS.get(stage, PROMPTS[1]), user_input, context)
 
136
  if response is None:
137
+ return (
138
+ "*Add ANTHROPIC_API_KEY in Space Settings to begin.*",
139
+ "", history, stage, data,
140
+ build_statement(data),
141
+ gr.update(visible=False),
142
+ gr.update(visible=False)
143
+ )
144
  except Exception as e:
145
+ return (
146
+ f"*Error: {str(e)}*",
147
+ "", history, stage, data,
148
+ build_statement(data),
149
+ gr.update(visible=False),
150
+ gr.update(visible=False)
151
+ )
152
 
153
+ # Update data based on stage
154
  if stage == 1:
155
+ data["original"] = user_input
156
  elif stage == 2:
157
+ data["feelings"] = feelings_str if feelings_str else user_input
158
+ elif stage == 3:
159
+ data["needs"] = needs_str if needs_str else user_input
160
+ elif stage == 4:
161
+ data["request"] = user_input
162
+ # Build final statement
163
+ data["final"] = f"I feel {data.get('feelings', '...')} because I need {data.get('needs', '...')}. Would you be willing to {user_input}?"
164
 
165
  # Add to history
166
+ history.append({"guide": response})
 
 
 
167
 
168
  # Advance stage
169
+ next_stage = min(stage + 1, 5)
170
+ if stage == 5:
171
+ # Reset for new practice
 
172
  next_stage = 1
173
+ data = {}
174
+ history = []
175
 
176
+ # Show appropriate selector
177
  show_feelings = (next_stage == 2)
178
+ show_needs = (next_stage == 3)
179
 
180
  return (
181
+ format_chat(history),
182
  "",
183
  history,
184
  next_stage,
185
+ data,
186
+ build_statement(data),
187
  gr.update(visible=show_feelings),
188
  gr.update(visible=show_needs)
189
  )
190
 
191
+ def reset():
192
+ """Reset session"""
193
  return (
194
+ "*Share a message you want to communicate more skillfully...*",
195
  "",
196
  [],
197
  1,
198
  {},
199
+ build_statement({}),
200
+ gr.update(visible=False),
201
+ gr.update(visible=False),
202
+ [], [], [], [], # feelings
203
+ [], # selected_feelings
204
+ [], [], [], [] # needs
205
  )
206
 
207
+ def update_feelings(f1, f2, f3, f4):
208
+ return ", ".join(f1 + f2 + f3 + f4)
 
 
209
 
210
+ def update_needs(n1, n2, n3, n4):
211
+ return ", ".join(n1 + n2 + n3 + n4)
 
 
212
 
213
+ # ============ UI ============
214
 
215
  with gr.Blocks(
216
+ title="NVC Translation Practice",
217
  theme=gr.themes.Soft(primary_hue="green", secondary_hue="amber")
218
  ) as demo:
219
 
220
  # State
221
  history_state = gr.State([])
222
  stage_state = gr.State(1)
223
+ data_state = gr.State({})
224
 
225
  # Header
226
  gr.Markdown("""
227
+ # NVC Translation Practice
228
+ *Transform difficult messages with clarity and compassion*
 
 
 
229
  """)
230
 
231
  if not API_KEY_CONFIGURED:
232
+ gr.Markdown("> **Setup:** Add `ANTHROPIC_API_KEY` in Settings > Secrets")
 
 
233
 
234
  with gr.Row():
235
+ # LEFT: Chat + Selectors
236
  with gr.Column(scale=2):
237
+ chat_display = gr.Markdown(
238
+ value="*Share a message you want to communicate more skillfully...*"
 
 
239
  )
240
 
241
+ # Feelings selector
242
+ with gr.Accordion("Select Your Feelings", open=True, visible=False) as feelings_box:
 
 
 
 
243
  with gr.Row():
244
+ f1 = gr.CheckboxGroup(FEELINGS["Sad/Hurt"], label="Sad/Hurt")
245
+ f2 = gr.CheckboxGroup(FEELINGS["Scared/Anxious"], label="Scared/Anxious")
246
  with gr.Row():
247
+ f3 = gr.CheckboxGroup(FEELINGS["Angry/Frustrated"], label="Angry/Frustrated")
248
+ f4 = gr.CheckboxGroup(FEELINGS["Confused/Tired"], label="Confused/Tired")
249
+ feelings_display = gr.Textbox(label="Selected", interactive=False)
250
 
251
+ # Needs selector
252
+ with gr.Accordion("Select Your Needs", open=True, visible=False) as needs_box:
 
253
  with gr.Row():
254
+ n1 = gr.CheckboxGroup(NEEDS["Connection"], label="Connection")
255
+ n2 = gr.CheckboxGroup(NEEDS["Autonomy"], label="Autonomy")
256
  with gr.Row():
257
+ n3 = gr.CheckboxGroup(NEEDS["Security"], label="Security")
258
+ n4 = gr.CheckboxGroup(NEEDS["Meaning"], label="Meaning")
259
+ needs_display = gr.Textbox(label="Selected", interactive=False)
260
 
261
  # Input
262
  user_input = gr.Textbox(
 
263
  placeholder="Type here...",
264
+ lines=2,
265
  show_label=False
266
  )
267
 
 
269
  submit_btn = gr.Button("Continue", variant="primary")
270
  reset_btn = gr.Button("Start Over")
271
 
272
+ # RIGHT: Building I-Statement
273
  with gr.Column(scale=1):
274
+ gr.Markdown("### Your I-Statement")
275
+ statement_display = gr.Markdown(value=build_statement({}))
 
 
 
 
 
 
 
276
 
277
  gr.Markdown("""
278
+ ---
279
+ *The NVC pattern:*
280
+ - **Observation** (what happened)
281
+ - **Feeling** (what you feel)
282
+ - **Need** (what you need)
283
+ - **Request** (specific ask)
 
 
284
  """)
285
 
286
+ # Wire up feelings/needs updates
287
+ for cb in [f1, f2, f3, f4]:
288
+ cb.change(update_feelings, [f1, f2, f3, f4], feelings_display)
 
 
 
 
289
 
290
+ for cb in [n1, n2, n3, n4]:
291
+ cb.change(update_needs, [n1, n2, n3, n4], needs_display)
 
 
 
 
 
292
 
293
+ # Submit handlers
294
  submit_btn.click(
295
+ process,
296
+ [user_input, history_state, stage_state, data_state, feelings_display, needs_display],
297
+ [chat_display, user_input, history_state, stage_state, data_state, statement_display, feelings_box, needs_box]
298
  )
299
 
300
  user_input.submit(
301
+ process,
302
+ [user_input, history_state, stage_state, data_state, feelings_display, needs_display],
303
+ [chat_display, user_input, history_state, stage_state, data_state, statement_display, feelings_box, needs_box]
304
  )
305
 
306
+ # Reset
307
  reset_btn.click(
308
+ reset,
309
  [],
310
+ [chat_display, user_input, history_state, stage_state, data_state, statement_display,
311
+ feelings_box, needs_box, f1, f2, f3, f4, feelings_display, n1, n2, n3, n4]
 
312
  )
313
 
314
  if __name__ == "__main__":