Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -14,18 +14,20 @@ st.markdown("""
|
|
| 14 |
</style>
|
| 15 |
""", unsafe_allow_html=True)
|
| 16 |
|
| 17 |
-
# 2. 데이터 로드 (파일
|
| 18 |
try:
|
| 19 |
with open('places.json', 'r', encoding='utf-8') as f:
|
| 20 |
PLACE_DB = json.load(f)
|
| 21 |
except FileNotFoundError:
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
# 데이터를 JSON 문자열로 변환 (자바스크립트용)
|
| 26 |
db_json = json.dumps(PLACE_DB)
|
| 27 |
|
| 28 |
-
#
|
| 29 |
html_code = f"""
|
| 30 |
<!DOCTYPE html>
|
| 31 |
<html>
|
|
@@ -157,13 +159,59 @@ html_code = f"""
|
|
| 157 |
let selectedCountry = "";
|
| 158 |
let selectedType = "All";
|
| 159 |
let currentLang = "KO";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
const TRANSLATIONS = {{
|
| 162 |
"KO": {{
|
| 163 |
"subtitle": "글로벌 주류 헤리티지 투어",
|
| 164 |
"filters": {{ "All": "전체", "Whiskey": "위스키", "Spirits": "증류주", "Beer": "맥주", "Wine": "와인", "Traditional": "전통주/토속주", "Bar": "바/펍" }},
|
| 165 |
"report_header": "여행 리포트",
|
| 166 |
-
"loader": "Puter AI가 리포트를<br>작성하고 있습니다...",
|
| 167 |
"gmap": "구글맵에서 길찾기",
|
| 168 |
"nmap": "네이버 지도에서 길찾기",
|
| 169 |
"cta": "AI 리뷰 보기 →",
|
|
@@ -173,7 +221,6 @@ html_code = f"""
|
|
| 173 |
"subtitle": "GLOBAL ALCOHOL HERITAGE TOUR",
|
| 174 |
"filters": {{ "All": "All", "Whiskey": "Whiskey", "Spirits": "Spirits", "Beer": "Beer", "Wine": "Wine", "Traditional": "Traditional", "Bar": "Bar" }},
|
| 175 |
"report_header": "TRAVEL REPORT",
|
| 176 |
-
"loader": "Puter AI is summarizing...",
|
| 177 |
"gmap": "Open in Google Maps",
|
| 178 |
"nmap": "Open in Naver Map",
|
| 179 |
"cta": "Read Review →",
|
|
@@ -183,7 +230,6 @@ html_code = f"""
|
|
| 183 |
"subtitle": "世界のお酒ヘリテージツアー",
|
| 184 |
"filters": {{ "All": "すべて", "Whiskey": "ウイスキー", "Spirits": "蒸留酒", "Beer": "ビール", "Wine": "ワイン", "Traditional": "伝統酒", "Bar": "バー" }},
|
| 185 |
"report_header": "旅行レポート",
|
| 186 |
-
"loader": "AIが要約中...",
|
| 187 |
"gmap": "Googleマップで開く",
|
| 188 |
"nmap": "Naverマップで開く",
|
| 189 |
"cta": "レビューを見る →",
|
|
@@ -193,7 +239,6 @@ html_code = f"""
|
|
| 193 |
"subtitle": "全球酒类遗产之旅",
|
| 194 |
"filters": {{ "All": "全部", "Whiskey": "威士忌", "Spirits": "烈酒", "Beer": "啤酒", "Wine": "葡萄酒", "Traditional": "传统酒", "Bar": "酒吧" }},
|
| 195 |
"report_header": "旅游报告",
|
| 196 |
-
"loader": "AI正在总结...",
|
| 197 |
"gmap": "在谷歌地图中打开",
|
| 198 |
"nmap": "在Naver地图中打开",
|
| 199 |
"cta": "查看评论 →",
|
|
@@ -203,7 +248,6 @@ html_code = f"""
|
|
| 203 |
|
| 204 |
window.onload = function() {{
|
| 205 |
initCountryDropdown();
|
| 206 |
-
// Default select
|
| 207 |
const keys = Object.keys(PLACE_DB).sort();
|
| 208 |
const defaultKey = keys.find(k => k.includes("Korea")) || keys[0];
|
| 209 |
document.getElementById('country-select').value = defaultKey;
|
|
@@ -223,7 +267,6 @@ html_code = f"""
|
|
| 223 |
const t = TRANSLATIONS[currentLang];
|
| 224 |
document.getElementById('app-subtitle').innerText = t.subtitle;
|
| 225 |
document.getElementById('report-header').innerText = t.report_header;
|
| 226 |
-
document.getElementById('loader-msg').innerHTML = t.loader;
|
| 227 |
|
| 228 |
document.getElementById('filter-all').innerText = `🌐 ${{t.filters.All}}`;
|
| 229 |
document.getElementById('filter-whiskey').innerText = `🥃 ${{t.filters.Whiskey}}`;
|
|
@@ -294,12 +337,14 @@ html_code = f"""
|
|
| 294 |
async function openDetail(place) {{
|
| 295 |
document.getElementById('main-page').style.transform = 'translateX(-30%)';
|
| 296 |
document.getElementById('detail-page').style.transform = 'translateX(0%)';
|
| 297 |
-
|
|
|
|
|
|
|
| 298 |
|
| 299 |
document.getElementById('repo-title').innerText = place.name;
|
| 300 |
document.getElementById('repo-meta').innerText = `${{place.label}} • ${{selectedCountry}}`;
|
| 301 |
|
| 302 |
-
//
|
| 303 |
const btn = document.getElementById('gmap-link');
|
| 304 |
const isKorea = selectedCountry.includes("Korea");
|
| 305 |
const query = encodeURIComponent(place.name);
|
|
@@ -311,7 +356,7 @@ html_code = f"""
|
|
| 311 |
}} else {{
|
| 312 |
btn.innerText = TRANSLATIONS[currentLang].gmap;
|
| 313 |
btn.classList.remove("naver");
|
| 314 |
-
btn.href = `https://www.google.com/maps/search/?api=1&query=$
|
| 315 |
}}
|
| 316 |
|
| 317 |
document.getElementById('repo-content').innerHTML = "";
|
|
@@ -333,6 +378,27 @@ html_code = f"""
|
|
| 333 |
await generatePuterReport(place, fallbackDesc);
|
| 334 |
}}
|
| 335 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
async function generatePuterReport(place, fallbackDesc) {{
|
| 337 |
let langFull = "Korean";
|
| 338 |
if(currentLang === "EN") langFull = "English";
|
|
@@ -340,16 +406,18 @@ html_code = f"""
|
|
| 340 |
if(currentLang === "CN") langFull = "Chinese";
|
| 341 |
|
| 342 |
const prompt = `
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
|
|
|
|
|
|
|
|
|
| 347 |
Language: ${{langFull}}.
|
| 348 |
-
|
| 349 |
`;
|
| 350 |
|
| 351 |
try {{
|
| 352 |
-
// Puter.js 호출 (최대 3회 재시도)
|
| 353 |
const response = await retryPuter(prompt, 3);
|
| 354 |
|
| 355 |
let text = "";
|
|
@@ -359,9 +427,8 @@ html_code = f"""
|
|
| 359 |
text = response.message.content;
|
| 360 |
}}
|
| 361 |
|
| 362 |
-
|
| 363 |
|
| 364 |
-
// 타이핑 효과 제거 -> 즉시 렌더링
|
| 365 |
const el = document.getElementById('repo-content');
|
| 366 |
el.innerHTML = text.replace(/\\n/g, '<br>');
|
| 367 |
|
|
@@ -371,7 +438,6 @@ html_code = f"""
|
|
| 371 |
}}
|
| 372 |
}}
|
| 373 |
|
| 374 |
-
// 재시도 로직 함수
|
| 375 |
async function retryPuter(prompt, retries) {{
|
| 376 |
for (let i = 0; i < retries; i++) {{
|
| 377 |
try {{
|
|
@@ -384,9 +450,9 @@ html_code = f"""
|
|
| 384 |
}}
|
| 385 |
|
| 386 |
function generateLocalReport(place, dbDesc) {{
|
| 387 |
-
|
| 388 |
const report = `
|
| 389 |
-
✨ **[Offline
|
| 390 |
|
| 391 |
**${{place.name}}**
|
| 392 |
|
|
|
|
| 14 |
</style>
|
| 15 |
""", unsafe_allow_html=True)
|
| 16 |
|
| 17 |
+
# 2. 데이터 로드 (places.json 파일이 같은 폴더에 있어야 합니다)
|
| 18 |
try:
|
| 19 |
with open('places.json', 'r', encoding='utf-8') as f:
|
| 20 |
PLACE_DB = json.load(f)
|
| 21 |
except FileNotFoundError:
|
| 22 |
+
# 파일이 없을 경우를 대비한 비상용 더미 데이터 (에러 방지)
|
| 23 |
+
PLACE_DB = {
|
| 24 |
+
"South Korea 🇰🇷": [{"name": "Zest", "category": "Bar", "label": "🍸", "lat": 37.526, "lon": 127.040, "desc_ko": "데이터 파일을 확인해주세요.", "desc_en": "Check json file."}]
|
| 25 |
+
}
|
| 26 |
+
st.error("🚨 'places.json' 파일을 찾을 수 없습니다. V47.0 데이터를 저장해주세요!")
|
| 27 |
|
|
|
|
| 28 |
db_json = json.dumps(PLACE_DB)
|
| 29 |
|
| 30 |
+
# 3. HTML/JS 전체 코드
|
| 31 |
html_code = f"""
|
| 32 |
<!DOCTYPE html>
|
| 33 |
<html>
|
|
|
|
| 159 |
let selectedCountry = "";
|
| 160 |
let selectedType = "All";
|
| 161 |
let currentLang = "KO";
|
| 162 |
+
let loadingInterval = null;
|
| 163 |
+
|
| 164 |
+
// 🔥 Dynamic Loading Messages
|
| 165 |
+
const LOADING_MSGS = {{
|
| 166 |
+
"KO": [
|
| 167 |
+
"AI가 비행기 타고 이동 중... ✈️",
|
| 168 |
+
"목적지 공항에 도착했습니다... 🛬",
|
| 169 |
+
"현지 교통편으로 이동 중... 🚕",
|
| 170 |
+
"가게 문을 열고 들어갑니다... 🚪",
|
| 171 |
+
"주변 분위기를 스캔하는 중... 👀",
|
| 172 |
+
"시그니처 메뉴를 시음 중... 🍷",
|
| 173 |
+
"사장님과 인터뷰 중... 🗣️",
|
| 174 |
+
"취하기 전에 리포트 작성 중... 📝",
|
| 175 |
+
"거의 다 됐습니다! 🚀"
|
| 176 |
+
],
|
| 177 |
+
"EN": [
|
| 178 |
+
"AI flying to the location... ✈️",
|
| 179 |
+
"Arrived at the airport... 🛬",
|
| 180 |
+
"Taking a taxi to the venue... 🚕",
|
| 181 |
+
"Entering the door... 🚪",
|
| 182 |
+
"Scanning the vibes... 👀",
|
| 183 |
+
"Tasting signature drinks... 🍷",
|
| 184 |
+
"Interviewing the bartender... 🗣️",
|
| 185 |
+
"Writing report before getting drunk... 📝",
|
| 186 |
+
"Almost ready! 🚀"
|
| 187 |
+
],
|
| 188 |
+
"JP": [
|
| 189 |
+
"AIが飛行機で移動中... ✈️",
|
| 190 |
+
"現地に到着しました... 🛬",
|
| 191 |
+
"お店のドアを開けています... 🚪",
|
| 192 |
+
"雰囲気をスキャン中... 👀",
|
| 193 |
+
"シグネチャーを試飲中... 🍷",
|
| 194 |
+
"マスターとインタビュー中... 🗣️",
|
| 195 |
+
"酔う前にレポート作成中... 📝",
|
| 196 |
+
"もうすぐ完成します! 🚀"
|
| 197 |
+
],
|
| 198 |
+
"CN": [
|
| 199 |
+
"AI正在飞往目的地... ✈️",
|
| 200 |
+
"已到达当地机场... 🛬",
|
| 201 |
+
"正在进入店铺... 🚪",
|
| 202 |
+
"正在扫描氛围... 👀",
|
| 203 |
+
"品尝招牌酒饮中... 🍷",
|
| 204 |
+
"正在采访调酒师... 🗣️",
|
| 205 |
+
"趁没醉赶紧写报告... 📝",
|
| 206 |
+
"马上就好! 🚀"
|
| 207 |
+
]
|
| 208 |
+
}};
|
| 209 |
|
| 210 |
const TRANSLATIONS = {{
|
| 211 |
"KO": {{
|
| 212 |
"subtitle": "글로벌 주류 헤리티지 투어",
|
| 213 |
"filters": {{ "All": "전체", "Whiskey": "위스키", "Spirits": "증류주", "Beer": "맥주", "Wine": "와인", "Traditional": "전통주/토속주", "Bar": "바/펍" }},
|
| 214 |
"report_header": "여행 리포트",
|
|
|
|
| 215 |
"gmap": "구글맵에서 길찾기",
|
| 216 |
"nmap": "네이버 지도에서 길찾기",
|
| 217 |
"cta": "AI 리뷰 보기 →",
|
|
|
|
| 221 |
"subtitle": "GLOBAL ALCOHOL HERITAGE TOUR",
|
| 222 |
"filters": {{ "All": "All", "Whiskey": "Whiskey", "Spirits": "Spirits", "Beer": "Beer", "Wine": "Wine", "Traditional": "Traditional", "Bar": "Bar" }},
|
| 223 |
"report_header": "TRAVEL REPORT",
|
|
|
|
| 224 |
"gmap": "Open in Google Maps",
|
| 225 |
"nmap": "Open in Naver Map",
|
| 226 |
"cta": "Read Review →",
|
|
|
|
| 230 |
"subtitle": "世界のお酒ヘリテージツアー",
|
| 231 |
"filters": {{ "All": "すべて", "Whiskey": "ウイスキー", "Spirits": "蒸留酒", "Beer": "ビール", "Wine": "ワイン", "Traditional": "伝統酒", "Bar": "バー" }},
|
| 232 |
"report_header": "旅行レポート",
|
|
|
|
| 233 |
"gmap": "Googleマップで開く",
|
| 234 |
"nmap": "Naverマップで開く",
|
| 235 |
"cta": "レビューを見る →",
|
|
|
|
| 239 |
"subtitle": "全球酒类遗产之旅",
|
| 240 |
"filters": {{ "All": "全部", "Whiskey": "威士忌", "Spirits": "烈酒", "Beer": "啤酒", "Wine": "葡萄酒", "Traditional": "传统酒", "Bar": "酒吧" }},
|
| 241 |
"report_header": "旅游报告",
|
|
|
|
| 242 |
"gmap": "在谷歌地图中打开",
|
| 243 |
"nmap": "在Naver地图中打开",
|
| 244 |
"cta": "查看评论 →",
|
|
|
|
| 248 |
|
| 249 |
window.onload = function() {{
|
| 250 |
initCountryDropdown();
|
|
|
|
| 251 |
const keys = Object.keys(PLACE_DB).sort();
|
| 252 |
const defaultKey = keys.find(k => k.includes("Korea")) || keys[0];
|
| 253 |
document.getElementById('country-select').value = defaultKey;
|
|
|
|
| 267 |
const t = TRANSLATIONS[currentLang];
|
| 268 |
document.getElementById('app-subtitle').innerText = t.subtitle;
|
| 269 |
document.getElementById('report-header').innerText = t.report_header;
|
|
|
|
| 270 |
|
| 271 |
document.getElementById('filter-all').innerText = `🌐 ${{t.filters.All}}`;
|
| 272 |
document.getElementById('filter-whiskey').innerText = `🥃 ${{t.filters.Whiskey}}`;
|
|
|
|
| 337 |
async function openDetail(place) {{
|
| 338 |
document.getElementById('main-page').style.transform = 'translateX(-30%)';
|
| 339 |
document.getElementById('detail-page').style.transform = 'translateX(0%)';
|
| 340 |
+
|
| 341 |
+
// Start Dynamic Loading
|
| 342 |
+
startLoadingAnimation();
|
| 343 |
|
| 344 |
document.getElementById('repo-title').innerText = place.name;
|
| 345 |
document.getElementById('repo-meta').innerText = `${{place.label}} • ${{selectedCountry}}`;
|
| 346 |
|
| 347 |
+
// Map Logic
|
| 348 |
const btn = document.getElementById('gmap-link');
|
| 349 |
const isKorea = selectedCountry.includes("Korea");
|
| 350 |
const query = encodeURIComponent(place.name);
|
|
|
|
| 356 |
}} else {{
|
| 357 |
btn.innerText = TRANSLATIONS[currentLang].gmap;
|
| 358 |
btn.classList.remove("naver");
|
| 359 |
+
btn.href = `https://www.google.com/maps/search/?api=1&query=${{query}}%20${{selectedCountry}}`;
|
| 360 |
}}
|
| 361 |
|
| 362 |
document.getElementById('repo-content').innerHTML = "";
|
|
|
|
| 378 |
await generatePuterReport(place, fallbackDesc);
|
| 379 |
}}
|
| 380 |
|
| 381 |
+
function startLoadingAnimation() {{
|
| 382 |
+
const loader = document.getElementById('loader');
|
| 383 |
+
loader.style.display = 'flex';
|
| 384 |
+
|
| 385 |
+
const msgs = LOADING_MSGS[currentLang];
|
| 386 |
+
let step = 0;
|
| 387 |
+
document.getElementById('loader-msg').innerText = msgs[0];
|
| 388 |
+
|
| 389 |
+
if (loadingInterval) clearInterval(loadingInterval);
|
| 390 |
+
|
| 391 |
+
loadingInterval = setInterval(() => {{
|
| 392 |
+
step = (step + 1) % msgs.length;
|
| 393 |
+
document.getElementById('loader-msg').innerText = msgs[step];
|
| 394 |
+
}}, 1500); // Change text every 1.5 seconds
|
| 395 |
+
}}
|
| 396 |
+
|
| 397 |
+
function stopLoadingAnimation() {{
|
| 398 |
+
if (loadingInterval) clearInterval(loadingInterval);
|
| 399 |
+
document.getElementById('loader').style.display = 'none';
|
| 400 |
+
}}
|
| 401 |
+
|
| 402 |
async function generatePuterReport(place, fallbackDesc) {{
|
| 403 |
let langFull = "Korean";
|
| 404 |
if(currentLang === "EN") langFull = "English";
|
|
|
|
| 406 |
if(currentLang === "CN") langFull = "Chinese";
|
| 407 |
|
| 408 |
const prompt = `
|
| 409 |
+
Act as a travel expert. Write a detailed review about "${{place.name}}" in ${{selectedCountry}}.
|
| 410 |
+
Structure:
|
| 411 |
+
1. 📍 **Address**: (Find approximate address)
|
| 412 |
+
2. ⭐ **Vibe**: (Atmosphere)
|
| 413 |
+
3. 🍷 **Drink**: (Signature drink & process)
|
| 414 |
+
4. ✨ **Tip**: (Best time to visit)
|
| 415 |
+
|
| 416 |
Language: ${{langFull}}.
|
| 417 |
+
Format: Clean Markdown.
|
| 418 |
`;
|
| 419 |
|
| 420 |
try {{
|
|
|
|
| 421 |
const response = await retryPuter(prompt, 3);
|
| 422 |
|
| 423 |
let text = "";
|
|
|
|
| 427 |
text = response.message.content;
|
| 428 |
}}
|
| 429 |
|
| 430 |
+
stopLoadingAnimation();
|
| 431 |
|
|
|
|
| 432 |
const el = document.getElementById('repo-content');
|
| 433 |
el.innerHTML = text.replace(/\\n/g, '<br>');
|
| 434 |
|
|
|
|
| 438 |
}}
|
| 439 |
}}
|
| 440 |
|
|
|
|
| 441 |
async function retryPuter(prompt, retries) {{
|
| 442 |
for (let i = 0; i < retries; i++) {{
|
| 443 |
try {{
|
|
|
|
| 450 |
}}
|
| 451 |
|
| 452 |
function generateLocalReport(place, dbDesc) {{
|
| 453 |
+
stopLoadingAnimation();
|
| 454 |
const report = `
|
| 455 |
+
✨ **[Offline Info] ${{TRANSLATIONS[currentLang].ai_error}}**
|
| 456 |
|
| 457 |
**${{place.name}}**
|
| 458 |
|