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