Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
[
|
| 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) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|