KwabsHug commited on
Commit
9b6c9af
·
verified ·
1 Parent(s): 3591ed6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +222 -100
app.py CHANGED
@@ -4,27 +4,37 @@ NWFWO Practice – Gradio MCP app for ChatGPT
4
  This file does two things:
5
  1) Runs a normal Gradio web app (for debugging in your browser)
6
  2) Exposes an MCP server + HTML UI card for ChatGPT Apps
 
 
 
 
 
 
 
 
7
  """
8
 
9
  from dataclasses import dataclass
10
- from typing import Optional
11
 
12
  import gradio as gr
13
 
14
 
15
- # ---- 1. Data model for the “game state” ----
16
 
17
  @dataclass
18
  class NWFWOExample:
19
- # A short scenario or sentence in the foreign language (transliterated)
20
- transliteration: str
21
- # The correct NWFWO answer (what the learner should guess)
22
- correct_nwfwo: str
23
- # Optional: explanation to show after checking
 
 
24
  explanation: Optional[str] = None
25
 
26
 
27
- # ---- 2. Core tool logic: check the learner’s guess ----
28
 
29
  @gr.mcp.tool(
30
  _meta={
@@ -41,38 +51,88 @@ def check_nwfwo(
41
  explanation: Optional[str] = None,
42
  ):
43
  """
44
- Simple checker for NWFWO practice.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- Arguments (ChatGPT will send these as JSON):
47
- - transliteration: the foreign sentence/word in transliteration
48
- - correct_nwfwo: the correct NWFWO answer
49
- - user_guess: what the learner typed or selected
50
- - explanation: optional teacher explanation
51
 
52
- Returns: a dict that ChatGPT AND the UI card can use.
53
- """
54
- norm_correct = correct_nwfwo.strip().lower()
55
- norm_guess = user_guess.strip().lower()
56
 
57
- is_correct = norm_guess == norm_correct
 
58
 
59
- # Build friendly feedback
60
- if is_correct:
61
- feedback = "✅ Correct! Your NWFWO matches the target."
62
- else:
 
 
 
 
 
 
 
 
63
  feedback = (
64
- " Not quite. Compare your NWFWO with the correct one "
65
- "and think about which sounds or letters changed."
66
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
  result = {
69
- "transliteration": transliteration,
70
- "correct_nwfwo": correct_nwfwo,
71
- "user_guess": user_guess,
72
- "is_correct": is_correct,
 
 
 
 
 
 
 
 
 
 
73
  "feedback": feedback,
74
- "explanation": explanation
75
- or "This NWFWO shows how the foreign writing maps to the native word.",
76
  }
77
 
78
  return result
@@ -80,63 +140,70 @@ def check_nwfwo(
80
 
81
  # ---- 3. Normal Gradio UI (handy while you’re developing) ----
82
 
83
- def local_check_ui(transliteration, correct_nwfwo, user_guess, explanation):
84
  res = check_nwfwo(
85
- transliteration=transliteration,
86
- correct_nwfwo=correct_nwfwo,
87
- user_guess=user_guess,
88
- explanation=explanation,
89
- )
90
- return (
91
- f"Transliteration: {res['transliteration']}\n"
92
- f"Your NWFWO: {res['user_guess']}\n"
93
- f"Correct NWFWO: {res['correct_nwfwo']}\n\n"
94
- f"{res['feedback']}\n\n"
95
- f"Explanation: {res['explanation']}"
96
  )
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
  with gr.Blocks() as demo:
100
- gr.Markdown("## NWFWO Practice – Local Debug UI")
101
 
102
  with gr.Row():
103
  with gr.Column():
104
- transliteration_box = gr.Textbox(
105
- label="Transliteration (foreign text written in your script)",
106
- value="salaam",
107
  )
108
- correct_nwfwo_box = gr.Textbox(
109
- label="Correct NWFWO",
110
- value="salaam",
111
  )
112
- user_guess_box = gr.Textbox(
113
- label="Your guess NWFWO",
114
- value="salam",
115
  )
116
- explanation_box = gr.Textbox(
117
- label="Explanation (optional)",
118
- value="This example shows how long vs short vowels work.",
119
  )
120
- btn = gr.Button("Check")
121
 
122
  with gr.Column():
123
  result_box = gr.Textbox(
124
  label="Result",
125
- lines=8,
126
  )
127
 
128
  btn.click(
129
  local_check_ui,
130
  inputs=[
131
- transliteration_box,
132
- correct_nwfwo_box,
133
- user_guess_box,
134
- explanation_box,
135
  ],
136
  outputs=[result_box],
137
  )
138
 
139
- # ---- (NEW) wire the MCP resource to a Gradio event ----
140
  # This makes Gradio actually register the MCP resource.
141
  widget_preview = gr.Code(
142
  label="NWFWO widget HTML (for MCP)",
@@ -170,62 +237,117 @@ def nwfwo_html_card():
170
  const input = (window.openai && window.openai.toolInput) || {};
171
  const output = (window.openai && window.openai.toolOutput) || {};
172
 
173
- const transliteration = input.transliteration || output.transliteration || "salaam";
174
- const userGuess = input.user_guess || output.user_guess || "salam";
175
- const correctNwfwo = input.correct_nwfwo || output.correct_nwfwo || "salaam";
176
-
177
- const isCorrect = output.is_correct;
178
- const feedback = output.feedback || "";
179
- const explanation = output.explanation || "";
180
-
181
- let badge = "";
182
- if (typeof isCorrect === "boolean") {
183
- badge = isCorrect
184
- ? '<span style="padding:4px 8px;border-radius:999px;background:#d4edda;">Correct</span>'
185
- : '<span style="padding:4px 8px;border-radius:999px;background:#f8d7da;">Try again</span>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  }
187
 
 
 
 
 
188
  root.innerHTML = `
189
  <div style="
190
  border-radius:16px;
191
  border:1px solid #ddd;
192
  padding:16px;
193
  font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
194
- max-width: 500px;
195
  ">
196
  <div style="font-size:14px;color:#666;margin-bottom:4px;">
197
- NWFWO Practice
198
- </div>
199
- <div style="font-size:18px;font-weight:600;margin-bottom:12px;">
200
- Transliteration
201
  </div>
202
- <div style="padding:8px 12px;border-radius:12px;background:#f5f5f5;margin-bottom:12px;">
203
- ${transliteration}
 
 
 
 
204
  </div>
205
 
206
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
207
- <div>
208
- <div style="font-size:14px;color:#666;">Your NWFWO</div>
209
- <div style="font-size:16px;">${userGuess}</div>
210
  </div>
211
- <div>
212
- <div style="font-size:14px;color:#666;">Target NWFWO</div>
213
- <div style="font-size:16px;font-weight:600;">${correctNwfwo}</div>
 
 
 
214
  </div>
215
  </div>
216
 
217
- <div style="margin-bottom:8px;">
218
- ${badge}
 
 
 
219
  </div>
220
 
221
- <div style="font-size:14px;margin-top:8px;border-top:1px solid #eee;padding-top:8px;">
222
- <div style="font-weight:600;margin-bottom:4px;">Feedback</div>
223
- <div>${feedback}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  </div>
225
 
226
- <div style="font-size:13px;color:#555;margin-top:8px;">
227
- <div style="font-weight:600;margin-bottom:4px;">Explanation</div>
228
- <div>${explanation}</div>
229
  </div>
230
  </div>
231
  `;
@@ -233,9 +355,9 @@ def nwfwo_html_card():
233
 
234
  render();
235
 
236
- // Good practice: re-render when ChatGPT updates tool globals
237
  window.addEventListener("openai:set_globals", (event) => {
238
- if (event.detail && event.detail.globals && event.detail.globals.toolOutput) {
239
  render();
240
  }
241
  }, { passive: true });
 
4
  This file does two things:
5
  1) Runs a normal Gradio web app (for debugging in your browser)
6
  2) Exposes an MCP server + HTML UI card for ChatGPT Apps
7
+
8
+ MVP behaviour:
9
+ - ChatGPT passes in:
10
+ - transliteration: foreign sentence (clue)
11
+ - correct_nwfwo: natural translation in native language (clue)
12
+ - explanation: literal native sentence that follows the foreign info flow (correct order)
13
+ - user_guess: learner's attempt at ordering the native words to match that flow
14
+ - This tool compares the word order and returns feedback + data for the widget.
15
  """
16
 
17
  from dataclasses import dataclass
18
+ from typing import Optional, List
19
 
20
  import gradio as gr
21
 
22
 
23
+ # ---- 1. Data model (kept for future extension, not essential for MVP) ----
24
 
25
  @dataclass
26
  class NWFWOExample:
27
+ # Foreign sentence (text or transliteration)
28
+ foreign_original: str
29
+ # Natural translation in the native language
30
+ native_natural: str
31
+ # Literal native sentence that follows the foreign word order
32
+ native_literal_from_foreign: str
33
+ # Optional explanation / note
34
  explanation: Optional[str] = None
35
 
36
 
37
+ # ---- 2. Core tool logic: word-order practice ----
38
 
39
  @gr.mcp.tool(
40
  _meta={
 
51
  explanation: Optional[str] = None,
52
  ):
53
  """
54
+ Word-order practice for listening support.
55
+
56
+ Parameters (as ChatGPT should use them):
57
+
58
+ - transliteration:
59
+ foreign sentence (A1), shown as a clue.
60
+ - correct_nwfwo:
61
+ natural translation in the learner's native language (B1), shown as a clue.
62
+ - explanation:
63
+ literal native sentence that follows the foreign word order (A2).
64
+ This is the CORRECT target order the learner should try to match.
65
+ - user_guess:
66
+ learner's attempt at ordering the native words to match that foreign flow.
67
+
68
+ Behaviour:
69
+ - Split the correct literal sentence and user guess into tokens.
70
+ - Compare position-by-position.
71
+ - Return counts and per-position matches plus the sentences for display.
72
+ """
73
 
74
+ # Rename for clarity
75
+ foreign_original = transliteration or ""
76
+ native_natural = correct_nwfwo or ""
77
+ native_literal_from_foreign = explanation or ""
78
+ user_order_native_literal = user_guess or ""
79
 
80
+ # Tokenise (simple space split MVP ChatGPT should format inputs cleanly)
81
+ def tokenize(s: str) -> List[str]:
82
+ # Minimal normalisation: strip and split on whitespace
83
+ return [t for t in s.strip().split() if t]
84
 
85
+ correct_tokens = tokenize(native_literal_from_foreign)
86
+ user_tokens = tokenize(user_order_native_literal)
87
 
88
+ max_len = max(len(correct_tokens), len(user_tokens)) if correct_tokens or user_tokens else 0
89
+
90
+ matches_by_index: List[bool] = []
91
+ for i in range(max_len):
92
+ c = correct_tokens[i] if i < len(correct_tokens) else None
93
+ u = user_tokens[i] if i < len(user_tokens) else None
94
+ matches_by_index.append(c is not None and u is not None and c.lower() == u.lower())
95
+
96
+ num_correct = sum(1 for m in matches_by_index if m)
97
+ total_positions = len(correct_tokens)
98
+
99
+ if total_positions == 0:
100
  feedback = (
101
+ "I couldn't see any words in the 'correct literal' sentence. "
102
+ "Make sure explanation contains the literal native sentence to follow."
103
  )
104
+ else:
105
+ if num_correct == total_positions:
106
+ feedback = (
107
+ f"✅ Great! All {total_positions} positions match the foreign information flow."
108
+ )
109
+ elif num_correct == 0:
110
+ feedback = (
111
+ "❌ None of the positions match the foreign flow yet. "
112
+ "Try lining up the verb and time/place words with how they appear in the foreign sentence."
113
+ )
114
+ else:
115
+ feedback = (
116
+ f"⚠️ You matched {num_correct} out of {total_positions} positions. "
117
+ "Look at which words moved and how that changes the flow."
118
+ )
119
 
120
  result = {
121
+ # Original parameters (so existing callers still see them)
122
+ "transliteration": foreign_original,
123
+ "correct_nwfwo": native_natural,
124
+ "user_guess": user_order_native_literal,
125
+ "explanation": native_literal_from_foreign,
126
+ # Semantic fields
127
+ "foreign_original": foreign_original,
128
+ "native_natural": native_natural,
129
+ "native_literal_from_foreign": native_literal_from_foreign,
130
+ "correct_tokens": correct_tokens,
131
+ "user_tokens": user_tokens,
132
+ "matches_by_index": matches_by_index,
133
+ "num_correct": num_correct,
134
+ "total_positions": total_positions,
135
  "feedback": feedback,
 
 
136
  }
137
 
138
  return result
 
140
 
141
  # ---- 3. Normal Gradio UI (handy while you’re developing) ----
142
 
143
+ def local_check_ui(foreign_sentence, native_natural, user_order, native_literal):
144
  res = check_nwfwo(
145
+ transliteration=foreign_sentence,
146
+ correct_nwfwo=native_natural,
147
+ user_guess=user_order,
148
+ explanation=native_literal,
 
 
 
 
 
 
 
149
  )
150
 
151
+ # Simple text summary for the debug UI
152
+ lines = [
153
+ f"Foreign sentence: {res['foreign_original']}",
154
+ f"Translation (clue): {res['native_natural']}",
155
+ "",
156
+ f"Correct foreign flow in native words: {res['native_literal_from_foreign']}",
157
+ f"Your order: {' '.join(res['user_tokens'])}",
158
+ "",
159
+ f"Positions correct: {res['num_correct']} / {res['total_positions']}",
160
+ "",
161
+ f"Feedback: {res['feedback']}",
162
+ ]
163
+ return "\n".join(lines)
164
+
165
 
166
  with gr.Blocks() as demo:
167
+ gr.Markdown("## Word Order Practice – Local Debug UI")
168
 
169
  with gr.Row():
170
  with gr.Column():
171
+ foreign_box = gr.Textbox(
172
+ label="Foreign sentence (clue)",
173
+ value="Tomorrow together rice want-to-eat?",
174
  )
175
+ native_natural_box = gr.Textbox(
176
+ label="Natural translation in your language (clue)",
177
+ value="Do you want to eat together tomorrow?",
178
  )
179
+ native_literal_box = gr.Textbox(
180
+ label="Correct literal native sentence (foreign info flow)",
181
+ value="Tomorrow together rice want-to-eat?",
182
  )
183
+ user_order_box = gr.Textbox(
184
+ label="Your ordered native sentence (try to follow foreign flow)",
185
+ value="Tomorrow rice want-to-eat together?",
186
  )
187
+ btn = gr.Button("Check order")
188
 
189
  with gr.Column():
190
  result_box = gr.Textbox(
191
  label="Result",
192
+ lines=10,
193
  )
194
 
195
  btn.click(
196
  local_check_ui,
197
  inputs=[
198
+ foreign_box,
199
+ native_natural_box,
200
+ user_order_box,
201
+ native_literal_box,
202
  ],
203
  outputs=[result_box],
204
  )
205
 
206
+ # ---- wire the MCP resource to a Gradio event ----
207
  # This makes Gradio actually register the MCP resource.
208
  widget_preview = gr.Code(
209
  label="NWFWO widget HTML (for MCP)",
 
237
  const input = (window.openai && window.openai.toolInput) || {};
238
  const output = (window.openai && window.openai.toolOutput) || {};
239
 
240
+ const foreignSentence = input.transliteration
241
+ || output.foreign_original
242
+ || output.transliteration
243
+ || "Foreign sentence goes here";
244
+
245
+ const nativeNatural = input.correct_nwfwo
246
+ || output.native_natural
247
+ || "Natural translation goes here";
248
+
249
+ const nativeLiteral = output.native_literal_from_foreign
250
+ || input.explanation
251
+ || output.explanation
252
+ || "Literal native sentence (foreign flow)";
253
+
254
+ const userOrder = output.user_tokens && output.user_tokens.length
255
+ ? output.user_tokens.join(" ")
256
+ : (input.user_guess || output.user_guess || "");
257
+
258
+ const correctTokens = output.correct_tokens || [];
259
+ const userTokens = output.user_tokens || [];
260
+ const matches = output.matches_by_index || [];
261
+ const numCorrect = output.num_correct ?? 0;
262
+ const total = output.total_positions ?? correctTokens.length;
263
+
264
+ const rows = [];
265
+ const maxLen = Math.max(correctTokens.length, userTokens.length);
266
+ for (let i = 0; i < maxLen; i++) {
267
+ const c = correctTokens[i] ?? "";
268
+ const u = userTokens[i] ?? "";
269
+ const m = matches[i];
270
+
271
+ rows.push(`
272
+ <tr>
273
+ <td style="padding:4px 8px; font-size:13px;">${i + 1}</td>
274
+ <td style="padding:4px 8px; font-size:13px;">${c}</td>
275
+ <td style="padding:4px 8px; font-size:13px;">${u}</td>
276
+ <td style="padding:4px 8px; font-size:13px;">
277
+ ${m === true ? "✅" : (u ? "❌" : "")}
278
+ </td>
279
+ </tr>
280
+ `);
281
  }
282
 
283
+ const summaryText = total > 0
284
+ ? `${numCorrect} / ${total} positions match the foreign information flow.`
285
+ : "No positions to compare yet – check the literal sentence input.";
286
+
287
  root.innerHTML = `
288
  <div style="
289
  border-radius:16px;
290
  border:1px solid #ddd;
291
  padding:16px;
292
  font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
293
+ max-width: 640px;
294
  ">
295
  <div style="font-size:14px;color:#666;margin-bottom:4px;">
296
+ Word Order Practice – follow the foreign information flow
 
 
 
297
  </div>
298
+
299
+ <div style="margin-bottom:12px;">
300
+ <div style="font-size:13px;color:#666;">Foreign sentence</div>
301
+ <div style="font-size:17px;font-weight:600;">
302
+ ${foreignSentence}
303
+ </div>
304
  </div>
305
 
306
+ <div style="margin-bottom:12px;">
307
+ <div style="font-size:13px;color:#666;">Translation (clue)</div>
308
+ <div style="font-size:15px;">
309
+ ${nativeNatural}
310
  </div>
311
+ </div>
312
+
313
+ <div style="margin-bottom:12px;">
314
+ <div style="font-size:13px;color:#666;">Foreign flow in your language (correct literal order)</div>
315
+ <div style="padding:8px 12px;border-radius:12px;background:#f5f5f5;font-size:14px;">
316
+ ${nativeLiteral}
317
  </div>
318
  </div>
319
 
320
+ <div style="margin-bottom:12px;">
321
+ <div style="font-size:13px;color:#666;">Your order</div>
322
+ <div style="padding:8px 12px;border-radius:12px;background:#fafafa;font-size:14px;">
323
+ ${userOrder || "<span style='color:#999;'>No attempt recorded.</span>"}
324
+ </div>
325
  </div>
326
 
327
+ <div style="margin-bottom:8px;font-size:13px;font-weight:600;">
328
+ Position-by-position comparison
329
+ </div>
330
+ <table style="border-collapse:collapse;width:100%;font-size:13px;">
331
+ <thead>
332
+ <tr>
333
+ <th style="text-align:left;padding:4px 8px;">#</th>
334
+ <th style="text-align:left;padding:4px 8px;">Correct word</th>
335
+ <th style="text-align:left;padding:4px 8px;">Your word</th>
336
+ <th style="text-align:left;padding:4px 8px;">Match</th>
337
+ </tr>
338
+ </thead>
339
+ <tbody>
340
+ ${rows.join("")}
341
+ </tbody>
342
+ </table>
343
+
344
+ <div style="margin-top:8px;font-size:13px;">
345
+ ${summaryText}
346
  </div>
347
 
348
+ <div style="margin-top:4px;font-size:12px;color:#777;">
349
+ Tip: keep the foreign sentence and the translation in mind, and try to place time, place,
350
+ and verb in the same order as they appear in the foreign sentence.
351
  </div>
352
  </div>
353
  `;
 
355
 
356
  render();
357
 
358
+ // Re-render when ChatGPT updates tool globals
359
  window.addEventListener("openai:set_globals", (event) => {
360
+ if (event?.detail?.globals?.toolOutput) {
361
  render();
362
  }
363
  }, { passive: true });