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

Mobile-friendly: single column, ultra-concise

Browse files

- Single column layout (stacks well on mobile)
- 1-2 sentence responses only (max 150 tokens)
- Simpler feelings/needs categories (4 each)
- Translation builds at bottom as you go
- Cleaner, faster UX

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

Files changed (1) hide show
  1. app.py +116 -215
app.py CHANGED
@@ -1,13 +1,12 @@
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 = (
@@ -16,299 +15,201 @@ def get_client():
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
-
268
- with gr.Row():
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__":
 
1
  """
2
+ NVC Translation Practice - Transform messages with compassion
 
3
  """
4
 
5
  import gradio as gr
6
  import anthropic
7
  import os
8
 
9
+ # ============ SETUP ============
10
 
11
  def get_client():
12
  api_key = (
 
15
  os.environ.get("ANTHROPIC_KEY") or
16
  os.environ.get("anthropic_api_key")
17
  )
18
+ return anthropic.Anthropic(api_key=api_key) if api_key else None
 
 
19
 
20
  API_KEY_CONFIGURED = bool(get_client())
21
 
22
+ # ============ DATA ============
23
 
24
  FEELINGS = {
25
+ "Sad": ["sad", "hurt", "disappointed", "lonely"],
26
+ "Anxious": ["anxious", "worried", "overwhelmed", "scared"],
27
+ "Frustrated": ["frustrated", "angry", "annoyed", "irritated"],
28
+ "Tired": ["exhausted", "confused", "numb", "depleted"]
29
  }
30
 
31
  NEEDS = {
32
+ "Connection": ["understanding", "to be heard", "belonging", "closeness"],
33
+ "Autonomy": ["autonomy", "choice", "freedom", "space"],
34
+ "Security": ["safety", "stability", "trust", "support"],
35
+ "Meaning": ["purpose", "respect", "growth", "authenticity"]
36
  }
37
 
38
+ # ============ PROMPTS (ultra-concise) ============
39
 
40
+ BASE = "Warm guide. 1-2 sentences only. Use 'you' not 'I'."
41
 
42
  PROMPTS = {
43
+ 1: f"{BASE} Brief acknowledgment of what was shared.",
44
+ 2: f"{BASE} Name 2 feeling guesses. Say: 'Select below or type your own.'",
45
+ 3: f"{BASE} Connect feelings to 2 possible needs. Say: 'Choose below.'",
46
+ 4: f"{BASE} Ask: 'What specific action would help?' Keep it short.",
47
+ 5: f"{BASE} One sentence celebrating their work. Invite another round."
 
 
 
 
48
  }
49
 
50
+ # ============ FUNCTIONS ============
51
 
52
  def call_claude(prompt, message, context=""):
53
  client = get_client()
54
  if not client:
55
  return None
56
+ msg = f"{context}\n{message}" if context else message
57
+ resp = client.messages.create(
 
58
  model="claude-sonnet-4-20250514",
59
+ max_tokens=150,
60
  system=prompt,
61
+ messages=[{"role": "user", "content": msg}]
62
  )
63
+ return resp.content[0].text
64
 
65
  def build_statement(data):
66
+ """Build I-statement for display"""
67
+ lines = []
68
 
69
  if data.get("original"):
70
+ orig = data['original'][:60] + "..." if len(data.get('original','')) > 60 else data['original']
71
+ lines.append(f"*\"{orig}\"*")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
+ lines.append("")
74
+ lines.append("**I feel** " + (data.get("feelings") or "___"))
75
+ lines.append("**because I need** " + (data.get("needs") or "___"))
76
+ lines.append("**Would you** " + (data.get("request") or "___") + "?")
77
 
78
+ return "\n\n".join(lines)
 
 
 
79
 
80
+ def process(user_input, stage, data, feelings_str, needs_str):
81
+ """Process input and advance"""
 
 
 
 
 
 
82
 
83
  if not user_input.strip() and stage > 1:
84
  user_input = "continue"
85
 
86
+ # Context for Claude
87
  context = ""
88
  if data.get("original"):
89
+ context = f"Original: {data['original']}"
90
  if data.get("feelings"):
91
+ context += f"\nFeelings: {data['feelings']}"
92
  if data.get("needs"):
93
+ context += f"\nNeeds: {data['needs']}"
94
 
95
+ # Get response
96
  try:
97
  response = call_claude(PROMPTS.get(stage, PROMPTS[1]), user_input, context)
98
+ if not response:
99
+ return ("*Add ANTHROPIC_API_KEY in Settings*", "", stage, data,
100
+ build_statement(data), gr.update(visible=False), gr.update(visible=False))
101
+ except:
102
+ return ("*Error - try again*", "", stage, data,
103
+ build_statement(data), gr.update(visible=False), gr.update(visible=False))
104
+
105
+ # Update data
 
 
 
 
 
 
 
 
 
 
106
  if stage == 1:
107
  data["original"] = user_input
108
  elif stage == 2:
109
+ data["feelings"] = feelings_str or user_input
110
  elif stage == 3:
111
+ data["needs"] = needs_str or user_input
112
  elif stage == 4:
113
  data["request"] = user_input
 
 
 
 
 
114
 
115
+ # Next stage
116
+ next_stage = stage + 1 if stage < 5 else 1
117
  if stage == 5:
 
 
118
  data = {}
 
 
 
 
 
119
 
120
  return (
121
+ response,
122
  "",
 
123
  next_stage,
124
  data,
125
  build_statement(data),
126
+ gr.update(visible=(next_stage == 2)),
127
+ gr.update(visible=(next_stage == 3))
128
  )
129
 
130
  def reset():
131
+ return ("*What message do you want to transform?*", "", 1, {},
132
+ build_statement({}), gr.update(visible=False), gr.update(visible=False),
133
+ [], [], [], [], [], [], [], [])
 
 
 
 
 
 
 
 
 
 
 
134
 
135
+ def combine_feelings(a, b, c, d):
136
+ return ", ".join(a + b + c + d)
137
 
138
+ def combine_needs(a, b, c, d):
139
+ return ", ".join(a + b + c + d)
140
 
141
  # ============ UI ============
142
 
143
+ css = """
144
+ .container { max-width: 800px; margin: auto; }
145
+ .statement-box { background: #f8f9fa; padding: 1rem; border-radius: 8px; }
146
+ """
147
+
148
+ with gr.Blocks(title="NVC Practice", theme=gr.themes.Soft(), css=css) as demo:
149
 
 
 
150
  stage_state = gr.State(1)
151
  data_state = gr.State({})
152
 
153
+ gr.Markdown("## NVC Translation Practice")
 
 
 
 
154
 
155
  if not API_KEY_CONFIGURED:
156
+ gr.Markdown("*Add ANTHROPIC_API_KEY in Settings > Secrets*")
157
+
158
+ # Guide response
159
+ guide = gr.Markdown("*What message do you want to transform?*")
160
+
161
+ # Feelings (hidden until stage 2)
162
+ with gr.Accordion("Select feelings", open=True, visible=False) as feelings_box:
163
+ with gr.Row():
164
+ f1 = gr.CheckboxGroup(FEELINGS["Sad"], label="Sad", scale=1)
165
+ f2 = gr.CheckboxGroup(FEELINGS["Anxious"], label="Anxious", scale=1)
166
+ with gr.Row():
167
+ f3 = gr.CheckboxGroup(FEELINGS["Frustrated"], label="Frustrated", scale=1)
168
+ f4 = gr.CheckboxGroup(FEELINGS["Tired"], label="Tired", scale=1)
169
+ feelings_txt = gr.Textbox(label="Selected", interactive=False)
170
+
171
+ # Needs (hidden until stage 3)
172
+ with gr.Accordion("Select needs", open=True, visible=False) as needs_box:
173
+ with gr.Row():
174
+ n1 = gr.CheckboxGroup(NEEDS["Connection"], label="Connection", scale=1)
175
+ n2 = gr.CheckboxGroup(NEEDS["Autonomy"], label="Autonomy", scale=1)
176
+ with gr.Row():
177
+ n3 = gr.CheckboxGroup(NEEDS["Security"], label="Security", scale=1)
178
+ n4 = gr.CheckboxGroup(NEEDS["Meaning"], label="Meaning", scale=1)
179
+ needs_txt = gr.Textbox(label="Selected", interactive=False)
180
+
181
+ # Input
182
+ user_input = gr.Textbox(placeholder="Type here...", lines=2, show_label=False)
183
 
184
  with gr.Row():
185
+ submit = gr.Button("Continue", variant="primary", scale=2)
186
+ reset_btn = gr.Button("Reset", scale=1)
187
+
188
+ # Building statement
189
+ gr.Markdown("---")
190
+ gr.Markdown("**Your translation:**")
191
+ statement = gr.Markdown(build_statement({}), elem_classes="statement-box")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
 
193
+ # Wiring
194
+ for cb in [f1, f2, f3, f4]:
195
+ cb.change(combine_feelings, [f1, f2, f3, f4], feelings_txt)
196
  for cb in [n1, n2, n3, n4]:
197
+ cb.change(combine_needs, [n1, n2, n3, n4], needs_txt)
198
 
199
+ submit.click(
 
200
  process,
201
+ [user_input, stage_state, data_state, feelings_txt, needs_txt],
202
+ [guide, user_input, stage_state, data_state, statement, feelings_box, needs_box]
203
  )
 
204
  user_input.submit(
205
  process,
206
+ [user_input, stage_state, data_state, feelings_txt, needs_txt],
207
+ [guide, user_input, stage_state, data_state, statement, feelings_box, needs_box]
208
  )
 
 
209
  reset_btn.click(
210
+ reset, [],
211
+ [guide, user_input, stage_state, data_state, statement, feelings_box, needs_box,
212
+ f1, f2, f3, f4, n1, n2, n3, n4]
 
213
  )
214
 
215
  if __name__ == "__main__":