3v324v23 commited on
Commit
b9497c5
ยท
1 Parent(s): c869096

Refactor tips and index layout for improved readability and user experience; add .gitignore for cache files

Browse files
Files changed (3) hide show
  1. .gitignore +1 -0
  2. app/tips.py +2 -1
  3. static/index.html +45 -59
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ *__pycache__**/
app/tips.py CHANGED
@@ -64,4 +64,5 @@ def tip_for(expected: str, heard: str | None) -> dict | None:
64
  return {"title": name, "text": text, "key": f"{expected}>{heard}"}
65
  if heard == "_":
66
  return {"title": name, "text": "ืœื ื ืฉืžืขื” ื‘ื‘ื™ืจื•ืจ.", "key": f"{expected}>_"}
67
- return {"title": name, "text": f"ื ืฉืžืขื” ื›ืžื• [{heard}].", "key": f"{expected}>{heard}"}
 
 
64
  return {"title": name, "text": text, "key": f"{expected}>{heard}"}
65
  if heard == "_":
66
  return {"title": name, "text": "ืœื ื ืฉืžืขื” ื‘ื‘ื™ืจื•ืจ.", "key": f"{expected}>_"}
67
+ text = f"ื ืฉืžืขื” ื›ืžื• {NAMES[heard]}." if heard in NAMES else "ื ืฉืžืขื” ืื—ืจืช ืžื”ืฆืคื•ื™."
68
+ return {"title": name, "text": text, "key": f"{expected}>{heard}"}
static/index.html CHANGED
@@ -7,7 +7,7 @@
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Assistant:wght@400;600;700&family=Secular+One&family=Noto+Naskh+Arabic:wght@500;700&family=Noto+Sans+Mono&display=swap">
9
  <style>
10
- /* Layout: one practice card, prompt on top, mic in the middle, per-word / per-phoneme feedback below. RTL. */
11
  :root{
12
  --bg:#EEF0E8; --surface:#FAFBF6; --ink:#1C241D; --muted:#5E685C; --line:#D5DACB;
13
  --olive:#4B6A2C; --on-olive:#FFFFFF; --olive-soft:#E3EAD6;
@@ -23,54 +23,54 @@
23
  --sumac:#E77B87; --sumac-bg:#3A1E23; --ok:#6CC08A; --ok-bg:#1C3324; --amber:#E0B45A; --amber-bg:#3A2F17;
24
  color-scheme:dark}}
25
  *{box-sizing:border-box}
 
26
  html,body{margin:0}
27
- body{background:var(--bg);color:var(--ink);font:400 17px/1.55 var(--f-body);padding:20px 16px 48px;min-height:100vh}
28
- .wrap{max-width:680px;margin:0 auto;display:flex;flex-direction:column;gap:16px}
29
- .brand{font:400 30px/1 var(--f-display)}
 
30
  .brand .ar{font-family:var(--f-ar);font-weight:700;color:var(--olive);margin-inline-start:8px}
31
  .sub{color:var(--muted);font-size:14px}
32
  .chips{display:flex;gap:6px;flex-wrap:wrap}
33
- .chip{border:1px solid var(--line);background:transparent;color:var(--ink);font:600 14px var(--f-body);padding:6px 12px;border-radius:999px;cursor:pointer}
34
  .chip[aria-pressed="true"]{background:var(--olive);border-color:var(--olive);color:var(--on-olive)}
35
- .card{background:var(--surface);border:1px solid var(--line);border-radius:16px;padding:22px 20px;display:flex;flex-direction:column;gap:18px}
36
  .meta{display:flex;justify-content:space-between;color:var(--muted);font-size:13px;letter-spacing:.04em;font-variant-numeric:tabular-nums}
37
- .prompt{font:400 clamp(26px,6vw,36px)/1.25 var(--f-display);text-wrap:balance;margin:0}
38
- .task{color:var(--muted);font-size:15px;margin:-10px 0 0}
39
- .hint{display:flex;flex-direction:column;gap:4px;padding:12px 14px;border-radius:10px;background:var(--olive-soft)}
40
- .hint .tr{font-size:20px;font-weight:600}
41
- .hint .arline{font:700 28px/1.5 var(--f-ar)}
42
  .row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
43
- .btn{font:600 15px var(--f-body);border-radius:10px;padding:9px 14px;cursor:pointer;border:1px solid var(--line);background:transparent;color:var(--ink)}
44
  .btn.primary{background:var(--olive);border-color:var(--olive);color:var(--on-olive)}
45
  .btn:disabled{opacity:.45;cursor:default}
46
  :focus-visible{outline:3px solid var(--olive);outline-offset:2px}
47
- .mic-zone{display:flex;flex-direction:column;align-items:center;gap:10px;padding:6px 0}
48
- .mic{width:92px;height:92px;border-radius:50%;border:0;background:var(--olive);color:var(--on-olive);cursor:pointer;display:grid;place-items:center;position:relative}
49
- .mic svg{width:38px;height:38px}
50
  .mic.live{background:var(--sumac)}
51
- .mic.live::after{content:"";position:absolute;inset:-8px;border-radius:50%;border:3px solid var(--sumac);opacity:.5;animation:pulse 1.2s ease-out infinite}
52
  .mic:disabled{opacity:.5;cursor:progress}
53
  @keyframes pulse{from{transform:scale(.9);opacity:.6}to{transform:scale(1.15);opacity:0}}
54
  @media (prefers-reduced-motion:reduce){.mic.live::after{animation:none}}
55
- .status{color:var(--muted);font-size:14px;min-height:1.5em;text-align:center}
56
- .result{display:flex;flex-direction:column;gap:16px;border-top:1px solid var(--line);padding-top:18px}
57
- .verdict{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
58
- .score{font:400 34px/1 var(--f-display);font-variant-numeric:tabular-nums}
59
  .pill{font:700 13px var(--f-body);padding:4px 10px;border-radius:999px}
60
  .good{background:var(--ok-bg);color:var(--ok)} .mid{background:var(--amber-bg);color:var(--amber)} .bad{background:var(--sumac-bg);color:var(--sumac)}
61
- .label{font-size:12px;font-weight:700;letter-spacing:.08em;color:var(--muted);margin-bottom:6px}
62
- .words{display:flex;flex-wrap:wrap;gap:14px 18px;direction:rtl}
63
- .word{display:flex;flex-direction:column;align-items:center;gap:6px}
64
- .word .w{font:700 30px/1.4 var(--f-ar)}
65
  .word .w.bad{background:none;color:var(--sumac)} .word .w.mid{background:none;color:var(--amber)} .word .w.good{background:none;color:var(--ink)}
66
- .phs{display:flex;gap:3px;direction:ltr;flex-wrap:wrap;justify-content:center}
67
- .ph{font:500 14px/1 var(--f-ipa);padding:5px 6px;border-radius:6px;min-width:22px;text-align:center;font-variant-numeric:tabular-nums}
68
- .heard{font:500 15px/1.6 var(--f-ipa);direction:ltr;text-align:left;overflow-x:auto;color:var(--muted)}
69
- .tips{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
70
- .tips li{padding:10px 12px;border-radius:10px;border:1px solid var(--line);display:flex;flex-direction:column;gap:2px}
71
  .tips .what{font-weight:700}
72
- .tips .what .ar{font-family:var(--f-ar);font-size:20px}
73
- .note{font-size:13px;color:var(--muted);max-width:65ch}
74
  .warn{background:var(--amber-bg);color:var(--amber);border-radius:10px;padding:10px 12px;font-size:14px}
75
  </style>
76
  </head>
@@ -96,35 +96,27 @@ body{background:var(--bg);color:var(--ink);font:400 17px/1.55 var(--f-body);padd
96
  <div class="status" id="status">ืœื—ืฆื•, ื“ื‘ืจื•, ื•ืœื—ืฆื• ืฉื•ื‘ ืœืกื™ื•ื</div>
97
  </div>
98
 
99
- <div class="row">
100
- <button class="btn" id="hintBtn">ืจืžื–</button>
101
- <button class="btn" id="answerBtn">ื”ืฆื’ ืชืฉื•ื‘ื”</button>
102
- <button class="btn" id="speakBtn">๐Ÿ”Š ื”ืฉืžืข</button>
103
- <button class="btn" id="playMine" disabled>โ–ถ ื”ื”ืงืœื˜ื” ืฉืœื™</button>
104
- <span style="flex:1"></span>
105
- <button class="btn primary" id="nextBtn">ื”ืžืฉืคื˜ ื”ื‘ื โ†</button>
106
- </div>
107
-
108
  <div class="hint" id="hint" hidden>
109
  <div class="tr" id="hintTr"></div>
110
  <div class="arline" id="hintAr" lang="ar" hidden></div>
111
  </div>
112
 
113
  <section class="result" id="result" hidden>
114
- <div class="verdict"><span class="score" id="score"></span><span class="pill" id="pill"></span></div>
115
- <div>
116
- <div class="label">ืฆื™ื•ืŸ ืœื›ืœ ืžื™ืœื” ื•ืœื›ืœ ื”ื’ื”</div>
117
- <div class="words" id="words"></div>
118
- </div>
119
  <ul class="tips" id="tips"></ul>
120
- <div>
121
- <div class="label">ื”ื”ื’ืื™ื ืฉื”ืžื•ื“ืœ ื–ื™ื”ื”</div>
122
- <div class="heard" id="heard"></div>
123
- </div>
124
  </section>
 
 
 
 
 
 
 
 
 
125
  </main>
126
 
127
- <p class="note">ื”ื ื™ืชื•ื— ื ืขืฉื” ื‘ืจืžืช ื”ื”ื’ื”: ืžื•ื“ืœ ื–ื™ื”ื•ื™ ื”ื’ืื™ื ืžื™ื™ืฉืจ ืืช ื”ื”ืงืœื˜ื” ืžื•ืœ ืจืฆืฃ ื”ื”ื’ืื™ื ื”ืฆืคื•ื™ ื•ื‘ื•ื“ืง ื›ืžื” ื‘ื˜ื•ื— ื”ื•ื ื‘ื›ืœ ื”ื’ื”, ื•ืื™ื–ื” ื”ื’ื” ืื—ืจ ื ืฉืžืข ื‘ืžืงื•ืžื•. ื”ื”ืฉืžืขื” ืžืฉืชืžืฉืช ื‘ืงื•ืœ ืขืจื‘ื™ ืฉืœ ื”ืžื›ืฉื™ืจ, ืฉื‘ื“ืจืš ื›ืœืœ ืื™ื ื• ื‘ืžื‘ื˜ื ืคืœืกื˜ื™ื ื™.</p>
128
  </div>
129
 
130
  <script>
@@ -133,7 +125,6 @@ const store={get(k,d){try{const v=localStorage.getItem(k);return v?JSON.parse(v)
133
  let CATS=[], PHRASES=[], cat=store.get("ihki.cat","meet"), best=store.get("ihki.best2",{}), order=[], pos=0, cur=null;
134
  let myAudioUrl=null;
135
 
136
- const VOWELS=new Set(["a","i","u","e","o","aห","iห","uห","eห","oห"]);
137
  const band=s=>s>=75?"good":s>=50?"mid":"bad";
138
 
139
  function shuffle(a){for(let i=a.length-1;i>0;i--){const j=Math.random()*(i+1)|0;[a[i],a[j]]=[a[j],a[i]]}return a}
@@ -170,13 +161,8 @@ function render(r){
170
  for(const w of r.words){
171
  const box=document.createElement("div"); box.className="word";
172
  const ar=document.createElement("div"); ar.className="w "+band(w.score); ar.lang="ar"; ar.textContent=w.ar;
173
- const phs=document.createElement("div"); phs.className="phs";
174
- for(const p of w.phonemes){
175
- const c=document.createElement("span"); c.className="ph "+band(p.score); c.textContent=p.ipa;
176
- c.title=`${p.ipa}: ${Math.round(p.score)}`+(p.heard?` (ื ืฉืžืข: ${p.heard})`:"");
177
- phs.appendChild(c);
178
- }
179
- box.append(ar,phs); words.appendChild(box);
180
  }
181
 
182
  const tips=$("tips"); tips.innerHTML="";
@@ -197,8 +183,8 @@ function render(r){
197
  const li=document.createElement("li"); li.innerHTML='<span class="what">ื›ืœ ื”ืขื™ืฆื•ืจื™ื ื ืฉืžืขื• ื ื›ื•ืŸ</span><span>ื ืกื• ืœื•ืžืจ ืืช ื”ืžืฉืคื˜ ื‘ืงืฆื‘ ื“ื™ื‘ื•ืจ ื˜ื‘ืขื™.</span>';
198
  tips.appendChild(li);
199
  }
200
- $("heard").textContent="/"+r.heard_ipa+"/";
201
  $("result").hidden=false;
 
202
  if(best[cur.id]==null||s>best[cur.id]){best[cur.id]=s; store.set("ihki.best2",best);}
203
  }
204
 
 
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Assistant:wght@400;600;700&family=Secular+One&family=Noto+Naskh+Arabic:wght@500;700&family=Noto+Sans+Mono&display=swap">
9
  <style>
10
+ /* Layout: one compact practice card that fits the first screen: prompt, mic, hint, per-word feedback, actions. RTL. */
11
  :root{
12
  --bg:#EEF0E8; --surface:#FAFBF6; --ink:#1C241D; --muted:#5E685C; --line:#D5DACB;
13
  --olive:#4B6A2C; --on-olive:#FFFFFF; --olive-soft:#E3EAD6;
 
23
  --sumac:#E77B87; --sumac-bg:#3A1E23; --ok:#6CC08A; --ok-bg:#1C3324; --amber:#E0B45A; --amber-bg:#3A2F17;
24
  color-scheme:dark}}
25
  *{box-sizing:border-box}
26
+ [hidden]{display:none!important}
27
  html,body{margin:0}
28
+ body{background:var(--bg);color:var(--ink);font:400 17px/1.55 var(--f-body);padding:10px 16px 24px;min-height:100vh}
29
+ .wrap{max-width:680px;margin:0 auto;display:flex;flex-direction:column;gap:10px}
30
+ header{display:flex;align-items:baseline;gap:4px 12px;flex-wrap:wrap}
31
+ .brand{font:400 24px/1 var(--f-display)}
32
  .brand .ar{font-family:var(--f-ar);font-weight:700;color:var(--olive);margin-inline-start:8px}
33
  .sub{color:var(--muted);font-size:14px}
34
  .chips{display:flex;gap:6px;flex-wrap:wrap}
35
+ .chip{border:1px solid var(--line);background:transparent;color:var(--ink);font:600 14px var(--f-body);padding:4px 12px;border-radius:999px;cursor:pointer}
36
  .chip[aria-pressed="true"]{background:var(--olive);border-color:var(--olive);color:var(--on-olive)}
37
+ .card{background:var(--surface);border:1px solid var(--line);border-radius:16px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}
38
  .meta{display:flex;justify-content:space-between;color:var(--muted);font-size:13px;letter-spacing:.04em;font-variant-numeric:tabular-nums}
39
+ .prompt{font:400 clamp(22px,5vw,30px)/1.2 var(--f-display);text-wrap:balance;margin:0}
40
+ .task{color:var(--muted);font-size:14px;margin:-6px 0 0}
41
+ .hint{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 16px;padding:6px 12px;border-radius:10px;background:var(--olive-soft)}
42
+ .hint .tr{font-size:18px;font-weight:600}
43
+ .hint .arline{font:700 24px/1.4 var(--f-ar)}
44
  .row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
45
+ .btn{font:600 15px var(--f-body);border-radius:10px;padding:6px 12px;cursor:pointer;border:1px solid var(--line);background:transparent;color:var(--ink)}
46
  .btn.primary{background:var(--olive);border-color:var(--olive);color:var(--on-olive)}
47
  .btn:disabled{opacity:.45;cursor:default}
48
  :focus-visible{outline:3px solid var(--olive);outline-offset:2px}
49
+ .mic-zone{display:flex;align-items:center;gap:14px}
50
+ .mic{flex:none;width:60px;height:60px;border-radius:50%;border:0;background:var(--olive);color:var(--on-olive);cursor:pointer;display:grid;place-items:center;position:relative}
51
+ .mic svg{width:26px;height:26px}
52
  .mic.live{background:var(--sumac)}
53
+ .mic.live::after{content:"";position:absolute;inset:-6px;border-radius:50%;border:3px solid var(--sumac);opacity:.5;animation:pulse 1.2s ease-out infinite}
54
  .mic:disabled{opacity:.5;cursor:progress}
55
  @keyframes pulse{from{transform:scale(.9);opacity:.6}to{transform:scale(1.15);opacity:0}}
56
  @media (prefers-reduced-motion:reduce){.mic.live::after{animation:none}}
57
+ .status{color:var(--muted);font-size:14px}
58
+ .result{display:flex;flex-direction:column;gap:8px;border-top:1px solid var(--line);padding-top:10px}
59
+ .verdict{display:flex;align-items:center;gap:8px 14px;flex-wrap:wrap}
60
+ .score{font:400 28px/1 var(--f-display);font-variant-numeric:tabular-nums}
61
  .pill{font:700 13px var(--f-body);padding:4px 10px;border-radius:999px}
62
  .good{background:var(--ok-bg);color:var(--ok)} .mid{background:var(--amber-bg);color:var(--amber)} .bad{background:var(--sumac-bg);color:var(--sumac)}
63
+ .words{display:flex;flex-wrap:wrap;gap:4px 14px;direction:rtl}
64
+ .word{display:flex;flex-direction:column;align-items:center}
65
+ .word .w{font:700 26px/1.3 var(--f-ar)}
66
+ .word .n{font-size:12px;line-height:1;color:var(--muted);font-variant-numeric:tabular-nums}
67
  .word .w.bad{background:none;color:var(--sumac)} .word .w.mid{background:none;color:var(--amber)} .word .w.good{background:none;color:var(--ink)}
68
+ .tips{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}
69
+ .tips li{padding:6px 10px;border-radius:10px;border:1px solid var(--line);font-size:15px;line-height:1.4}
70
+ .tips .what::after{content:" ยท "}
 
 
71
  .tips .what{font-weight:700}
72
+ .tips .what .ar{font-family:var(--f-ar);font-size:18px}
73
+ .note{font-size:12px;margin:0;color:var(--muted);max-width:65ch}
74
  .warn{background:var(--amber-bg);color:var(--amber);border-radius:10px;padding:10px 12px;font-size:14px}
75
  </style>
76
  </head>
 
96
  <div class="status" id="status">ืœื—ืฆื•, ื“ื‘ืจื•, ื•ืœื—ืฆื• ืฉื•ื‘ ืœืกื™ื•ื</div>
97
  </div>
98
 
 
 
 
 
 
 
 
 
 
99
  <div class="hint" id="hint" hidden>
100
  <div class="tr" id="hintTr"></div>
101
  <div class="arline" id="hintAr" lang="ar" hidden></div>
102
  </div>
103
 
104
  <section class="result" id="result" hidden>
105
+ <div class="verdict"><span class="score" id="score"></span><span class="pill" id="pill"></span><div class="words" id="words"></div></div>
 
 
 
 
106
  <ul class="tips" id="tips"></ul>
 
 
 
 
107
  </section>
108
+
109
+ <div class="row">
110
+ <button class="btn" id="hintBtn">ืจืžื–</button>
111
+ <button class="btn" id="answerBtn">ื”ืฆื’ ืชืฉื•ื‘ื”</button>
112
+ <button class="btn" id="speakBtn">๐Ÿ”Š ื”ืฉืžืข</button>
113
+ <button class="btn" id="playMine" disabled>โ–ถ ื”ื”ืงืœื˜ื” ืฉืœื™</button>
114
+ <span style="flex:1"></span>
115
+ <button class="btn primary" id="nextBtn">ื”ืžืฉืคื˜ ื”ื‘ื โ†</button>
116
+ </div>
117
  </main>
118
 
119
+ <p class="note">ื”ื ื™ืชื•ื— ื ืขืฉื” ื‘ืจืžืช ื”ื”ื’ื”. ื”ื”ืฉืžืขื” ืžืฉืชืžืฉืช ื‘ืงื•ืœ ืขืจื‘ื™ ืฉืœ ื”ืžื›ืฉื™ืจ, ืฉื‘ื“ืจืš ื›ืœืœ ืื™ื ื• ื‘ืžื‘ื˜ื ืคืœืกื˜ื™ื ื™.</p>
120
  </div>
121
 
122
  <script>
 
125
  let CATS=[], PHRASES=[], cat=store.get("ihki.cat","meet"), best=store.get("ihki.best2",{}), order=[], pos=0, cur=null;
126
  let myAudioUrl=null;
127
 
 
128
  const band=s=>s>=75?"good":s>=50?"mid":"bad";
129
 
130
  function shuffle(a){for(let i=a.length-1;i>0;i--){const j=Math.random()*(i+1)|0;[a[i],a[j]]=[a[j],a[i]]}return a}
 
161
  for(const w of r.words){
162
  const box=document.createElement("div"); box.className="word";
163
  const ar=document.createElement("div"); ar.className="w "+band(w.score); ar.lang="ar"; ar.textContent=w.ar;
164
+ const n=document.createElement("div"); n.className="n"; n.textContent=Math.round(w.score);
165
+ box.append(ar,n); words.appendChild(box);
 
 
 
 
 
166
  }
167
 
168
  const tips=$("tips"); tips.innerHTML="";
 
183
  const li=document.createElement("li"); li.innerHTML='<span class="what">ื›ืœ ื”ืขื™ืฆื•ืจื™ื ื ืฉืžืขื• ื ื›ื•ืŸ</span><span>ื ืกื• ืœื•ืžืจ ืืช ื”ืžืฉืคื˜ ื‘ืงืฆื‘ ื“ื™ื‘ื•ืจ ื˜ื‘ืขื™.</span>';
184
  tips.appendChild(li);
185
  }
 
186
  $("result").hidden=false;
187
+ $("result").scrollIntoView({block:"nearest",behavior:"smooth"});
188
  if(best[cur.id]==null||s>best[cur.id]){best[cur.id]=s; store.set("ihki.best2",best);}
189
  }
190