benjamin5607 commited on
Commit
8e01cf9
ยท
verified ยท
1 Parent(s): 37dbc4b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +134 -33
app.py CHANGED
@@ -82,7 +82,7 @@ db_json_str = json.dumps(PLACE_DB, ensure_ascii=False)
82
  db_base64 = base64.b64encode(db_json_str.encode('utf-8')).decode('utf-8')
83
 
84
  # ==========================================
85
- # ๐Ÿ–ฅ๏ธ 3. ํ†ตํ•ฉ ํ”„๋ก ํŠธ์—”๋“œ (์ข€๋น„ ์ˆœ์„œ ์ตœ์ ํ™”)
86
  # ==========================================
87
  html_template = """
88
  <!DOCTYPE html>
@@ -121,18 +121,38 @@ html_template = """
121
 
122
  #world-map-page, #passport-page, #chat-page { background: #000; z-index: 2000; transform: translateY(100%); }
123
  #passport-page { transform: translateX(-100%); background: #0f0f1b; }
124
- #detail-page { background: #0a0a0a; z-index: 4000; transform: translateX(100%); }
125
- .nav-bar { height: 60px; display: flex; align-items: center; padding: 0 20px; border-bottom: 1px solid #222; background: #111; }
126
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  .msg { max-width: 85%; padding: 12px 18px; border-radius: 15px; font-size: 14px; line-height: 1.6; margin-bottom: 12px; }
128
  .msg.bot { align-self: flex-start; background: #222; color: #eee; border: 1px solid #333; }
129
  .msg.user { align-self: flex-end; background: var(--gold); color: black; font-weight: bold; }
130
  .reco-btn { display: block; margin-top: 8px; padding: 10px; background: var(--gold); color: black !important; border-radius: 10px; font-weight: bold; text-decoration: none; font-size: 12px; cursor: pointer; border:none; width: 100%; text-align: center; }
131
- .popup-btn { background: var(--gold); color: black; border: none; padding: 5px 10px; border-radius: 5px; font-weight: bold; font-size: 11px; cursor: pointer; margin-top: 5px; width: 100%; }
 
 
 
132
  .leaflet-popup-content-wrapper { background: #1a1a1a; color: white; border: 1px solid var(--gold); }
 
 
133
  .stamp-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; padding: 20px; }
134
  .stamp { height: 90px; border-radius: 15px; border: 2px dashed #333; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 10px; color: #444; background: #161625; }
135
  .stamp.active { border: 2px solid var(--gold); color: var(--gold); background: rgba(212,175,55,0.05); }
 
 
 
 
136
  </style>
137
  </head>
138
  <body>
@@ -157,7 +177,15 @@ html_template = """
157
  </div>
158
 
159
  <div id="passport-page" class="page"><div class="nav-bar"><button onclick="closePassport()">โ†</button> MY PASSPORT</div><div id="stamp-grid" class="stamp-grid" style="overflow-y:auto;"></div></div>
160
- <div id="world-map-page" class="page"><div class="nav-bar"><button onclick="closeMap()">๐Ÿ  HOME</button></div><div id="full-map" style="width:100%; height:100%;"></div></div>
 
 
 
 
 
 
 
 
161
 
162
  <div id="chat-page" class="page">
163
  <div class="nav-bar"><button onclick="closeChat()">โ†</button> Emily</div>
@@ -168,7 +196,23 @@ html_template = """
168
  </div>
169
  </div>
170
 
171
- <div id="detail-page" class="page"><div class="nav-bar"><button onclick="closeDetail()">โ†</button> Review</div><div id="repo-content" style="padding:20px; white-space:pre-wrap; color:#ddd; line-height:1.7;"></div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
  <script>
174
  function b64DecodeUnicode(str) { try { return decodeURIComponent(atob(str).split('').map(function(c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); } catch(e) { return "{}"; } }
@@ -176,7 +220,8 @@ html_template = """
176
  let currentLang = "KO", selectedCountry = "", selectedType = "All", chatHistory = [];
177
  let visitedPlaces = JSON.parse(localStorage.getItem('visitedPlaces') || '[]');
178
  let userLat = null, userLon = null;
179
- let worldMap = null;
 
180
 
181
  const TRANSLATIONS = {
182
  "KO": { "ai": "Korean", "greet": "์•ˆ๋…•? ์˜ค๋Š˜์€ ์–ด๋””์„œ ํ•œ์ž”ํ• ๊นŒ?", "aiBtn": "AI ๋ฆฌ๋ทฐ", "mapBtn": "์ง€๋„", "filters": ["๐ŸŒ ์ „์ฒด", "๐Ÿบ ์ „ํ†ต์ฃผ", "๐Ÿฅƒ ์œ„์Šคํ‚ค", "๐Ÿบ ๋งฅ์ฃผ", "๐Ÿท ์™€์ธ", "๐Ÿน ๋ฐ”"], "types": ["All", "Traditional", "Whiskey", "Beer", "Wine", "Bar"] },
@@ -260,12 +305,7 @@ html_template = """
260
  });
261
  }
262
 
263
- // [ํ•ต์‹ฌ ํŒจ์น˜] ์ข€๋น„ ์ˆœ์„œ ๋ณ€๊ฒฝ: ๊ฐ€์žฅ ๋น ๋ฅด๊ณ  ํŠผํŠผํ•œ 8b ๋ชจ๋ธ์„ 1๋ฒˆ์œผ๋กœ ๋ฐฐ์น˜ํ•˜์—ฌ 429 ๋ฐฉ์ง€
264
- const ZOMBIE_MODELS = [
265
- "llama-3.1-8b-instant", // 1๋ฒˆ: ์พŒ์† ์ข€๋น„ (๊ฐ€์žฅ ๋œ ์ฃฝ์Œ)
266
- "llama-3.3-70b-versatile", // 2๋ฒˆ: ์ง€๋Šฅ ์ข€๋น„ (๊ฐ€๋” ์ฃฝ์Œ)
267
- "mixtral-8x7b-32768" // 3๋ฒˆ: ์˜ˆ๋น„๊ตฐ ์ข€๋น„
268
- ];
269
 
270
  async function callGroq(msgs) {
271
  let apiKey = "GROQ_KEY_INJECTED".trim();
@@ -282,14 +322,8 @@ html_template = """
282
  if (r.ok) {
283
  const d = await r.json(); return d.choices[0].message.content;
284
  }
285
-
286
- // 429(๊ณผ๋ถ€ํ•˜)๋ฉด 1์ดˆ ์‰ฌ๊ณ  ๋‹ค์Œ ์ข€๋น„ ์‹œ๋„
287
- if (r.status === 429) {
288
- await new Promise(resolve => setTimeout(resolve, 1000));
289
- continue;
290
- }
291
  if (r.status === 401) return "Error 401: Invalid API Key.";
292
-
293
  } catch(e) { continue; }
294
  }
295
  return "Emily is sleeping (Network Issue). Please try again.";
@@ -323,21 +357,13 @@ html_template = """
323
  if(nearbyPlaces.length > 0) gpsContext = "GPS Nearby(20km): " + nearbyPlaces.join(", ");
324
  }
325
 
326
- // [์ƒŒ๋“œ์œ„์น˜ ํ”„๋กฌํ”„ํŠธ] ๋งํฌ ์ƒ์„ฑ ๊ฐ•์ œํ™”
327
  const sysPrompt = `CRITICAL RULE: OUTPUT PLACE NAMES IN [BRACKETS]. Example: [Zest].
328
-
329
  You are Emily, a bartender. Speak ONLY in ${TRANSLATIONS[currentLang].ai}.
330
-
331
- [DB]
332
- ${fullDbContext}
333
-
334
- [USER GPS]
335
- ${gpsContext}
336
-
337
  [INSTRUCTIONS]
338
  1. Specific city mentioned? Use [DB].
339
  2. 'Nearby' mentioned? Use [USER GPS].
340
-
341
  REMEMBER: WRAP PLACE NAMES IN [ ] OR I WILL BE FIRED.`;
342
 
343
  const botMsg = await callGroq([{ role: "system", content: sysPrompt }, ...recentHistory]);
@@ -368,14 +394,89 @@ html_template = """
368
  box.appendChild(div); box.scrollTop = box.scrollHeight;
369
  }
370
 
371
- function markVisited(name) { if(!visitedPlaces.includes(name)) { visitedPlaces.push(name); localStorage.setItem('visitedPlaces', JSON.stringify(visitedPlaces)); } }
 
 
 
 
 
 
 
372
  function openPassport() { document.getElementById('passport-page').style.transform = 'translateX(0%)'; const grid = document.getElementById('stamp-grid'); grid.innerHTML = ""; for(let c in PLACE_DB) { PLACE_DB[c].forEach(p => { const active = visitedPlaces.includes(p.name); const div = document.createElement('div'); div.className = 'stamp' + (active ? ' active' : ''); div.innerHTML = active ? `๐Ÿท<br><b>${p.name}</b>` : `โœˆ๏ธ`; grid.appendChild(div); }); } }
373
  function closePassport() { document.getElementById('passport-page').style.transform = 'translateX(-100%)'; }
374
- function openMap() { document.getElementById('world-map-page').style.transform = 'translateY(0%)'; if(!worldMap) { worldMap = L.map('full-map').setView([20,0],2); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(worldMap); for(let c in PLACE_DB) PLACE_DB[c].forEach(p => { if(p.lat && p.lon) { const lbl = p.label || p.category || ""; L.marker([p.lat,p.lon]).addTo(worldMap).bindPopup(`<b>${p.name}</b><br>${lbl}<br><button class='popup-btn' onclick='window.open("${getMapUrl(p,c)}","_blank")'>MAP</button>`); } }); } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  function closeMap() { document.getElementById('world-map-page').style.transform = 'translateY(100%)'; }
 
 
 
 
 
 
 
 
 
 
 
 
376
  function openChat() { document.getElementById('chat-page').style.transform = 'translateY(0%)'; if(chatHistory.length===0) addChatMessage(TRANSLATIONS[currentLang].greet, 'bot'); }
377
  function closeChat() { document.getElementById('chat-page').style.transform = 'translateY(100%)'; }
378
- async function openReview(n, c) { document.getElementById('detail-page').style.transform = 'translateX(0%)'; document.getElementById('repo-content').innerText = "Loading..."; const res = await callGroq([{role:"system",content:"Review "+n+" in "+c+" in "+TRANSLATIONS[currentLang].ai}]); document.getElementById('repo-content').innerText = res; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  function closeDetail() { document.getElementById('detail-page').style.transform = 'translateX(100%)'; }
380
  </script>
381
  </body>
 
82
  db_base64 = base64.b64encode(db_json_str.encode('utf-8')).decode('utf-8')
83
 
84
  # ==========================================
85
+ # ๐Ÿ–ฅ๏ธ 3. ํ†ตํ•ฉ ํ”„๋ก ํŠธ์—”๋“œ (์ง€๋„ ํŒ์—… ์นด๋“œ ๋ณต๊ตฌ ์™„๋ฃŒ)
86
  # ==========================================
87
  html_template = """
88
  <!DOCTYPE html>
 
121
 
122
  #world-map-page, #passport-page, #chat-page { background: #000; z-index: 2000; transform: translateY(100%); }
123
  #passport-page { transform: translateX(-100%); background: #0f0f1b; }
 
 
124
 
125
+ #detail-page { background: #0f0f12; z-index: 4000; transform: translateX(100%); display:flex; flex-direction:column; }
126
+ #detail-map { width: 100%; height: 40vh; border-bottom: 2px solid var(--gold); }
127
+ .review-content-box { flex: 1; overflow-y: auto; padding: 20px; }
128
+ .review-header { text-align: center; margin-bottom: 20px; border-bottom: 1px solid #333; padding-bottom: 15px; }
129
+ .review-title { font-size: 24px; font-weight: bold; color: var(--gold); margin-bottom: 5px; }
130
+ .review-body { font-size: 16px; line-height: 1.8; color: #e0e0e0; font-family: 'Georgia', serif; font-style: italic; text-align: justify; }
131
+ .review-signature { text-align: right; margin-top: 20px; color: var(--gold); font-weight: bold; }
132
+ .detail-actions { display: flex; gap: 10px; margin-top: 20px; }
133
+ .detail-btn { flex: 1; padding: 12px; border-radius: 10px; border: none; font-weight: bold; cursor: pointer; font-size: 14px; text-transform: uppercase; }
134
+ .btn-map { background: #333; color: white; border: 1px solid #555; }
135
+ .btn-stamp { background: var(--gold); color: black; }
136
+
137
+ .nav-bar { height: 60px; display: flex; align-items: center; padding: 0 20px; border-bottom: 1px solid #222; background: #111; }
138
  .msg { max-width: 85%; padding: 12px 18px; border-radius: 15px; font-size: 14px; line-height: 1.6; margin-bottom: 12px; }
139
  .msg.bot { align-self: flex-start; background: #222; color: #eee; border: 1px solid #333; }
140
  .msg.user { align-self: flex-end; background: var(--gold); color: black; font-weight: bold; }
141
  .reco-btn { display: block; margin-top: 8px; padding: 10px; background: var(--gold); color: black !important; border-radius: 10px; font-weight: bold; text-decoration: none; font-size: 12px; cursor: pointer; border:none; width: 100%; text-align: center; }
142
+
143
+ /* [๋ณต๊ตฌ] ํŒ์—… ๋ฒ„ํŠผ ์Šคํƒ€์ผ */
144
+ .popup-btn { background: var(--gold); color: black; border: none; padding: 6px 12px; border-radius: 5px; font-weight: bold; font-size: 11px; cursor: pointer; width: 100%; margin-top: 5px; }
145
+ .popup-btn:hover { opacity: 0.9; }
146
  .leaflet-popup-content-wrapper { background: #1a1a1a; color: white; border: 1px solid var(--gold); }
147
+ .leaflet-popup-content { margin: 10px; }
148
+
149
  .stamp-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; padding: 20px; }
150
  .stamp { height: 90px; border-radius: 15px; border: 2px dashed #333; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 10px; color: #444; background: #161625; }
151
  .stamp.active { border: 2px solid var(--gold); color: var(--gold); background: rgba(212,175,55,0.05); }
152
+
153
+ .map-search-container { position: absolute; top: 80px; left: 50%; transform: translateX(-50%); width: 90%; max-width: 500px; z-index: 3000; display: flex; gap: 10px; }
154
+ .map-search-input { flex: 1; padding: 12px 15px; border-radius: 25px; border: 2px solid var(--gold); background: rgba(0,0,0,0.85); color: white; outline: none; font-size: 14px; }
155
+ .map-loc-btn { width: 45px; height: 45px; border-radius: 50%; background: var(--gold); border: none; font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 10px rgba(0,0,0,0.5); }
156
  </style>
157
  </head>
158
  <body>
 
177
  </div>
178
 
179
  <div id="passport-page" class="page"><div class="nav-bar"><button onclick="closePassport()">โ†</button> MY PASSPORT</div><div id="stamp-grid" class="stamp-grid" style="overflow-y:auto;"></div></div>
180
+
181
+ <div id="world-map-page" class="page">
182
+ <div class="nav-bar"><button onclick="closeMap()">๐Ÿ  HOME</button></div>
183
+ <div class="map-search-container">
184
+ <input id="map-input" class="map-search-input" type="text" placeholder="Search City or Place..." onkeydown="if(event.key==='Enter') searchMap()">
185
+ <button class="map-loc-btn" onclick="panToUser()">โ—Ž</button>
186
+ </div>
187
+ <div id="full-map" style="width:100%; height:100%;"></div>
188
+ </div>
189
 
190
  <div id="chat-page" class="page">
191
  <div class="nav-bar"><button onclick="closeChat()">โ†</button> Emily</div>
 
196
  </div>
197
  </div>
198
 
199
+ <div id="detail-page" class="page">
200
+ <div class="nav-bar"><button onclick="closeDetail()">โ†</button> Bartender's Note</div>
201
+ <div id="detail-map"></div>
202
+ <div class="review-content-box">
203
+ <div class="review-header">
204
+ <div class="review-title" id="review-title">Loading...</div>
205
+ <div style="color:#888;" id="review-subtitle"></div>
206
+ </div>
207
+ <div id="repo-content" class="review-body">
208
+ Please wait while Emily recalls this place... ๐Ÿธ
209
+ </div>
210
+ <div class="detail-actions">
211
+ <button id="btn-detail-map" class="detail-btn btn-map">๐Ÿ—บ๏ธ OPEN MAP</button>
212
+ <button id="btn-detail-stamp" class="detail-btn btn-stamp">โœˆ๏ธ STAMP</button>
213
+ </div>
214
+ </div>
215
+ </div>
216
 
217
  <script>
218
  function b64DecodeUnicode(str) { try { return decodeURIComponent(atob(str).split('').map(function(c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); } catch(e) { return "{}"; } }
 
220
  let currentLang = "KO", selectedCountry = "", selectedType = "All", chatHistory = [];
221
  let visitedPlaces = JSON.parse(localStorage.getItem('visitedPlaces') || '[]');
222
  let userLat = null, userLon = null;
223
+ let worldMap = null;
224
+ let detailMap = null;
225
 
226
  const TRANSLATIONS = {
227
  "KO": { "ai": "Korean", "greet": "์•ˆ๋…•? ์˜ค๋Š˜์€ ์–ด๋””์„œ ํ•œ์ž”ํ• ๊นŒ?", "aiBtn": "AI ๋ฆฌ๋ทฐ", "mapBtn": "์ง€๋„", "filters": ["๐ŸŒ ์ „์ฒด", "๐Ÿบ ์ „ํ†ต์ฃผ", "๐Ÿฅƒ ์œ„์Šคํ‚ค", "๐Ÿบ ๋งฅ์ฃผ", "๐Ÿท ์™€์ธ", "๐Ÿน ๋ฐ”"], "types": ["All", "Traditional", "Whiskey", "Beer", "Wine", "Bar"] },
 
305
  });
306
  }
307
 
308
+ const ZOMBIE_MODELS = ["llama-3.1-8b-instant", "llama-3.3-70b-versatile", "mixtral-8x7b-32768"];
 
 
 
 
 
309
 
310
  async function callGroq(msgs) {
311
  let apiKey = "GROQ_KEY_INJECTED".trim();
 
322
  if (r.ok) {
323
  const d = await r.json(); return d.choices[0].message.content;
324
  }
325
+ if (r.status === 429) { await new Promise(r => setTimeout(r, 1000)); continue; }
 
 
 
 
 
326
  if (r.status === 401) return "Error 401: Invalid API Key.";
 
327
  } catch(e) { continue; }
328
  }
329
  return "Emily is sleeping (Network Issue). Please try again.";
 
357
  if(nearbyPlaces.length > 0) gpsContext = "GPS Nearby(20km): " + nearbyPlaces.join(", ");
358
  }
359
 
 
360
  const sysPrompt = `CRITICAL RULE: OUTPUT PLACE NAMES IN [BRACKETS]. Example: [Zest].
 
361
  You are Emily, a bartender. Speak ONLY in ${TRANSLATIONS[currentLang].ai}.
362
+ [DB] ${fullDbContext}
363
+ [USER GPS] ${gpsContext}
 
 
 
 
 
364
  [INSTRUCTIONS]
365
  1. Specific city mentioned? Use [DB].
366
  2. 'Nearby' mentioned? Use [USER GPS].
 
367
  REMEMBER: WRAP PLACE NAMES IN [ ] OR I WILL BE FIRED.`;
368
 
369
  const botMsg = await callGroq([{ role: "system", content: sysPrompt }, ...recentHistory]);
 
394
  box.appendChild(div); box.scrollTop = box.scrollHeight;
395
  }
396
 
397
+ function markVisited(name) {
398
+ if(!visitedPlaces.includes(name)) {
399
+ visitedPlaces.push(name);
400
+ localStorage.setItem('visitedPlaces', JSON.stringify(visitedPlaces));
401
+ alert("โœˆ๏ธ Stamped on Passport!");
402
+ } else { alert("You already visited here!"); }
403
+ }
404
+
405
  function openPassport() { document.getElementById('passport-page').style.transform = 'translateX(0%)'; const grid = document.getElementById('stamp-grid'); grid.innerHTML = ""; for(let c in PLACE_DB) { PLACE_DB[c].forEach(p => { const active = visitedPlaces.includes(p.name); const div = document.createElement('div'); div.className = 'stamp' + (active ? ' active' : ''); div.innerHTML = active ? `๐Ÿท<br><b>${p.name}</b>` : `โœˆ๏ธ`; grid.appendChild(div); }); } }
406
  function closePassport() { document.getElementById('passport-page').style.transform = 'translateX(-100%)'; }
407
+
408
+ // [๋ณต๊ตฌ ์™„๋ฃŒ] ์ง€๋„ ํŒ์—… ์นด๋“œ: ์„ค๋ช… + AI๋ฆฌ๋ทฐ ๋ฒ„ํŠผ + ์ง€๋„ ๋ฒ„ํŠผ
409
+ function openMap() {
410
+ document.getElementById('world-map-page').style.transform = 'translateY(0%)';
411
+ if(!worldMap) {
412
+ worldMap = L.map('full-map').setView([20,0],2);
413
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(worldMap);
414
+ for(let c in PLACE_DB) PLACE_DB[c].forEach(p => {
415
+ if(p.lat && p.lon) {
416
+ const lbl = p.label || p.category || "";
417
+ const desc = p['desc_'+currentLang.toLowerCase()] || p.desc_ko || "";
418
+ // ์„ค๋ช…์€ ๋„ˆ๋ฌด ๊ธธ๋ฉด ์ž˜๋ผ์„œ ๋ณด์—ฌ์คŒ (UI ๊นจ์ง ๋ฐฉ์ง€)
419
+ const shortDesc = desc.length > 50 ? desc.substring(0, 50) + "..." : desc;
420
+
421
+ const popupContent = `
422
+ <div style="text-align:left;">
423
+ <b style="color:#d4af37; font-size:14px;">${p.name}</b><br>
424
+ <span style="font-size:10px; color:#aaa;">${lbl}</span><br>
425
+ <div style="font-size:11px; margin:5px 0; color:#eee;">${shortDesc}</div>
426
+ <div style="display:flex; gap:5px;">
427
+ <button class="popup-btn" style="background:#d4af37; color:black;" onclick="openReview('${p.name.replace(/'/g, "\\\\'")}', '${c}')">AI REVIEW</button>
428
+ <button class="popup-btn" style="background:#333; color:white; border:1px solid #555;" onclick="window.open('${getMapUrl(p,c)}','_blank')">MAP</button>
429
+ </div>
430
+ </div>
431
+ `;
432
+ L.marker([p.lat,p.lon]).addTo(worldMap).bindPopup(popupContent);
433
+ }
434
+ });
435
+ }
436
+ }
437
+
438
  function closeMap() { document.getElementById('world-map-page').style.transform = 'translateY(100%)'; }
439
+
440
+ function searchMap() {
441
+ const q = document.getElementById('map-input').value.toLowerCase();
442
+ for(let c in PLACE_DB) {
443
+ if(c.toLowerCase().includes(q)) { const p = PLACE_DB[c][0]; if(p.lat && p.lon) { worldMap.setView([p.lat, p.lon], 10); return; } }
444
+ for(let p of PLACE_DB[c]) { if(p.name.toLowerCase().includes(q)) { if(p.lat && p.lon) { worldMap.setView([p.lat, p.lon], 16); return; } } }
445
+ }
446
+ alert("Not Found");
447
+ }
448
+
449
+ function panToUser() { if(userLat && userLon) worldMap.setView([userLat, userLon], 14); else alert("GPS missing."); }
450
+
451
  function openChat() { document.getElementById('chat-page').style.transform = 'translateY(0%)'; if(chatHistory.length===0) addChatMessage(TRANSLATIONS[currentLang].greet, 'bot'); }
452
  function closeChat() { document.getElementById('chat-page').style.transform = 'translateY(100%)'; }
453
+
454
+ async function openReview(n, c) {
455
+ document.getElementById('detail-page').style.transform = 'translateX(0%)';
456
+ document.getElementById('review-title').innerText = n;
457
+ document.getElementById('review-subtitle').innerText = c;
458
+ document.getElementById('repo-content').innerText = "Bartender Emily is preparing your review... ๐Ÿฅƒ";
459
+
460
+ let placeData = null;
461
+ if(PLACE_DB[c]) placeData = PLACE_DB[c].find(p => p.name === n);
462
+
463
+ if(placeData && placeData.lat && placeData.lon) {
464
+ if(detailMap) { detailMap.remove(); }
465
+ detailMap = L.map('detail-map').setView([placeData.lat, placeData.lon], 15);
466
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(detailMap);
467
+ L.marker([placeData.lat, placeData.lon]).addTo(detailMap);
468
+ }
469
+
470
+ document.getElementById('btn-detail-map').onclick = function() { window.open(getMapUrl(placeData, c), '_blank'); };
471
+ document.getElementById('btn-detail-stamp').onclick = function() { markVisited(n); };
472
+
473
+ const contextDesc = placeData ? (placeData['desc_'+currentLang.toLowerCase()] || placeData.desc_ko) : "";
474
+ const reviewPrompt = `Act as Emily, a charming bartender. Write a short review (max 3 sentences) for "${n}" in "${c}". [CONTEXT] ${contextDesc}. [LANG] ${TRANSLATIONS[currentLang].ai}.`;
475
+
476
+ const res = await callGroq([{role:"system",content: reviewPrompt}]);
477
+ document.getElementById('repo-content').innerHTML = res + "<div class='review-signature'>- Yours, Emily ๐Ÿ’‹</div>";
478
+ }
479
+
480
  function closeDetail() { document.getElementById('detail-page').style.transform = 'translateX(100%)'; }
481
  </script>
482
  </body>