Update app.py
Browse files
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
|
| 16 |
|
| 17 |
@dataclass
|
| 18 |
class NWFWOExample:
|
| 19 |
-
#
|
| 20 |
-
|
| 21 |
-
#
|
| 22 |
-
|
| 23 |
-
#
|
|
|
|
|
|
|
| 24 |
explanation: Optional[str] = None
|
| 25 |
|
| 26 |
|
| 27 |
-
# ---- 2. Core tool logic:
|
| 28 |
|
| 29 |
@gr.mcp.tool(
|
| 30 |
_meta={
|
|
@@ -41,38 +51,88 @@ def check_nwfwo(
|
|
| 41 |
explanation: Optional[str] = None,
|
| 42 |
):
|
| 43 |
"""
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
|
| 57 |
-
|
|
|
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
feedback = (
|
| 64 |
-
"
|
| 65 |
-
"
|
| 66 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
result = {
|
| 69 |
-
|
| 70 |
-
"
|
| 71 |
-
"
|
| 72 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 84 |
res = check_nwfwo(
|
| 85 |
-
transliteration=
|
| 86 |
-
correct_nwfwo=
|
| 87 |
-
user_guess=
|
| 88 |
-
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("##
|
| 101 |
|
| 102 |
with gr.Row():
|
| 103 |
with gr.Column():
|
| 104 |
-
|
| 105 |
-
label="
|
| 106 |
-
value="
|
| 107 |
)
|
| 108 |
-
|
| 109 |
-
label="
|
| 110 |
-
value="
|
| 111 |
)
|
| 112 |
-
|
| 113 |
-
label="
|
| 114 |
-
value="
|
| 115 |
)
|
| 116 |
-
|
| 117 |
-
label="
|
| 118 |
-
value="
|
| 119 |
)
|
| 120 |
-
btn = gr.Button("Check")
|
| 121 |
|
| 122 |
with gr.Column():
|
| 123 |
result_box = gr.Textbox(
|
| 124 |
label="Result",
|
| 125 |
-
lines=
|
| 126 |
)
|
| 127 |
|
| 128 |
btn.click(
|
| 129 |
local_check_ui,
|
| 130 |
inputs=[
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
],
|
| 136 |
outputs=[result_box],
|
| 137 |
)
|
| 138 |
|
| 139 |
-
# ----
|
| 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
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
const
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 195 |
">
|
| 196 |
<div style="font-size:14px;color:#666;margin-bottom:4px;">
|
| 197 |
-
|
| 198 |
-
</div>
|
| 199 |
-
<div style="font-size:18px;font-weight:600;margin-bottom:12px;">
|
| 200 |
-
Transliteration
|
| 201 |
</div>
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
</div>
|
| 205 |
|
| 206 |
-
<div style="
|
| 207 |
-
<div>
|
| 208 |
-
|
| 209 |
-
|
| 210 |
</div>
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
| 214 |
</div>
|
| 215 |
</div>
|
| 216 |
|
| 217 |
-
<div style="margin-bottom:
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
| 219 |
</div>
|
| 220 |
|
| 221 |
-
<div style="
|
| 222 |
-
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
</div>
|
| 225 |
|
| 226 |
-
<div style="font-size:
|
| 227 |
-
|
| 228 |
-
|
| 229 |
</div>
|
| 230 |
</div>
|
| 231 |
`;
|
|
@@ -233,9 +355,9 @@ def nwfwo_html_card():
|
|
| 233 |
|
| 234 |
render();
|
| 235 |
|
| 236 |
-
//
|
| 237 |
window.addEventListener("openai:set_globals", (event) => {
|
| 238 |
-
if (event
|
| 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 });
|