bep40 commited on
Commit
edb362b
·
verified ·
1 Parent(s): 767f8ac

Update AI frontend to send opinion text and label URL posts as opinion posts

Browse files
Files changed (1) hide show
  1. static/app_v2.js +16 -223
static/app_v2.js CHANGED
@@ -70,7 +70,7 @@ function _fetchWithTimeout(url, ms){
70
  });
71
  }
72
 
73
- // ===== rewriteUrl: tạo bài rewrite slide AI từ URL bài viết =====
74
  // Gọi /api/url_wall (backend thực tế), lấy post rồi đẩy lên Tường AI và mở slide viewer.
75
  async function rewriteUrl(){
76
  const inp = document.getElementById('url-input');
@@ -79,14 +79,15 @@ async function rewriteUrl(){
79
  if(!/^https?:\/\//i.test(url)){ alert('URL cần bắt đầu bằng http:// hoặc https://'); return; }
80
  let btn = null;
81
  try { btn = inp && inp.parentElement ? inp.parentElement.querySelector('button') : null; } catch(e){}
82
- const orig = btn ? btn.textContent : 'Rewrite';
83
- if(btn){ btn.disabled = true; btn.textContent = '⏳ Đang tạo...'; }
84
- toast('⏳ Đang tạo bài rewrite slide AI...');
85
  try {
 
86
  const r = await fetch('/api/url_wall', {
87
  method:'POST',
88
  headers:{'Content-Type':'application/json'},
89
- body: JSON.stringify({url: url})
90
  });
91
  const j = await r.json();
92
  if(!r.ok || j.error) throw new Error(j.error || 'Lỗi tạo bài');
@@ -94,7 +95,7 @@ async function rewriteUrl(){
94
  if(typeof prependWallPost === 'function'){ prependWallPost(j.post); }
95
  else if(Array.isArray(_wallPosts)){ _wallPosts.unshift(j.post); }
96
  if(inp) inp.value = '';
97
- toast('✅ Đã tạo bài rewrite slide AI!');
98
  const idx = (_wallPosts || []).indexOf(j.post);
99
  if(j.post.slides && j.post.slides.length){ readSlidePost(idx >= 0 ? idx : 0); }
100
  else if(typeof readWallPost === 'function'){ readWallPost(idx >= 0 ? idx : 0); }
@@ -148,7 +149,7 @@ async function loadHome(){
148
 
149
  homeEl.innerHTML =
150
  '<div id="home-featured-area"></div>'
151
- +'<div class="ai-compose"><div class="ai-compose-title">🤖 AI viết bài</div><div class="ai-compose-row"><input id="topic-input" placeholder="Nhập chủ đề..."><button onclick="searchTopic()">Tìm nguồn</button></div><div class="ai-compose-row"><input id="url-input" placeholder="Dán URL bài viết..."><button class="secondary" onclick="rewriteUrl()">Rewrite</button></div><div class="ai-compose-row" style="margin-top:8px"><textarea id="opinion-input" placeholder="💭 Nhập quan điểm nhân của bạn..." rows="2" style="width:100%;padding:8px;border-radius:8px;border:1px solid #333;background:#222;color:#eee;font-size:12px;resize:vertical"></textarea></div><div class="ai-compose-row"><button onclick="openPersonalPostPreview()">📝 Xem trước & Đăng bài quan điểm</button></div><div id="hot-topics" class="hot-topic-row"></div></div>'
152
  +'<div id="ai-wall-under-compose"></div>'
153
  +'<div id="hashtag-box"></div>'
154
  +'<div class="ls-section"><div class="ls-header"><h3>⚽ Livescore</h3></div><div class="ls-tabs"><span class="ls-tab active" data-tab="updates7d" onclick="loadLivescore(\'updates7d\')">📋 Cập nhật</span><span class="ls-tab" data-tab="live" onclick="loadLivescore(\'live\')">🔴 Live</span><span class="ls-tab" data-tab="today" onclick="loadLivescore(\'today\')">📅 Hôm nay</span><span class="ls-tab" data-tab="incoming" onclick="loadLivescore(\'incoming\')">⏰ Sắp tới</span><span class="ls-tab" data-tab="results" onclick="loadLivescore(\'results\')">✅ Kết quả</span><span class="ls-tab" data-tab="bxh_nha" onclick="loadLivescore(\'bxh_nha\')">🏆 NHA</span><span class="ls-tab" data-tab="bxh_laliga" onclick="loadLivescore(\'bxh_laliga\')">🏆 La Liga</span></div><div class="ls-content" id="ls-content"><div class="loading">Đang tải...</div></div></div>'
@@ -217,7 +218,7 @@ function _renderWallIn(afterEl){
217
 
218
  function _renderHLIn(afterEl){
219
  if(!_hlLeagueData||Object.keys(_hlLeagueData).length===0||!afterEl) return;
220
- const HL_CONFIG={"world-cup":{name:"World Cup 2026",emoji:"🌍"},"premier-league":{name:"Premier League",emoji:"🏴"},"champions-league":{name:"Champions League",emoji:"⭐"},"la-liga":{name:"La Liga",emoji:"🇪🇸"},"serie-a":{name:"Serie A",emoji:"🇮🇹"},"bundesliga":{name:"Bundesliga",emoji:"🇩🇪"},"friendly":{name:"Giao hữu",emoji:"🤝"}};
221
  for(const[key,cfg] of Object.entries(HL_CONFIG)){
222
  const vids=_hlLeagueData[key];
223
  if(!vids||!vids.length) continue;
@@ -244,7 +245,7 @@ function makeWallItem(p,i){
244
  });
245
  var emotOpts = '';
246
  EMOTION_LIST.forEach(function(e){
247
- emotOpts += '<option value="'+e.id+'"'+(e.id===_ttsSelections[selKey].emotion?' selected':'')+'>'+e.label+'</option>';
248
  });
249
  var spd = p.short_speed || '1.2';
250
  var voiceBar = '<div class="wall-tts-bar" style="margin:6px 0 4px;display:grid;grid-template-columns:1fr auto auto;gap:3px">'
@@ -386,7 +387,7 @@ document.addEventListener('click',function(e){
386
  function detectLanguage(text){
387
  if(!text) return 'vi';
388
  var t=text.toLowerCase(), chars=new Set(t);
389
- var vnChars='đăâêôơưàảãạáằẳẵặắầẩẫậấèẻẽẹéềễểệếìỉĩịíòỏõọóồổỗộốờởỡợớùủũụúừửữựứỳỷỹỵý';
390
  var vnCount=0; for(var c of vnChars){if(chars.has(c)) vnCount++;}
391
  if(vnCount>=2) return 'vi';
392
  if(chars.has('ñ')||chars.has('¿')||chars.has('¡')) return 'es';
@@ -496,9 +497,9 @@ let _currentMatchUrl=null;
496
  let _htPage=0,_htTopic='';
497
  async function loadHotTopics(){const j=await fetch('/api/hot_topics').then(r=>r.json()).catch(()=>({topics:[]}));const el=document.getElementById('hot-topics');if(!el)return;el.innerHTML=(j.topics||[]).slice(0,18).map(t=>{const topicText=t.topic||t.label.replace(/^#/,'');return`<button class="hot-chip" onclick="searchTopic('${topicText.replace(/'/g,"\\'")}')">${esc(t.label)}</button>`;}).join('');}
498
  function searchTopic(topic){if(!topic){topic=document.getElementById('topic-input')?.value.trim();if(!topic){alert('Nhập chủ đề');return;}}document.getElementById('topic-input').value='';_htTopic=topic;_htPage=0;showHashtagSources(topic,0);}
499
- async function showHashtagSources(topic,page){const box=document.getElementById('hashtag-box');if(!box)return;if(page===0)box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div class="hashtag-loading"><div class="hashtag-spinner"></div>Đang tìm...</div></div>`;try{const r=await fetch(`/api/hashtag/sources?topic=${encodeURIComponent(topic)}&page=${page}`);const j=await r.json();const sources=j.sources||[];if(!sources.length&&page===0){box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div style="color:#888;padding:8px">Không tìm được bài viết liên quan</div></div>`;return;}let h='';if(page===0)h=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)} <span style="font-size:10px;color:#888">(${j.total} bài từ 8 nguồn)</span></h3><div id="ht-list">`;sources.forEach((s,i)=>{const idx=page*8+i;h+=`<div class="hashtag-src-item" onclick="readArticle('${esc(s.url)}')"><div class="hashtag-src-img" id="ht-img-${idx}"></div><div class="hashtag-src-text"><div class="hashtag-src-title">${esc(s.title)}</div><div class="hashtag-src-via">${esc(s.via||'')}</div></div></div>`;});if(page===0){h+=`</div><button class="hashtag-rewrite-btn" onclick="rewriteHashtag('${esc(topic).replace(/'/g,"\\'")}')">🤖 Rewrite AI tổng hợp & đăng tường</button>`;if(j.has_more)h+=`<button class="hashtag-load-more" id="ht-more" onclick="loadMoreHashtag()">Tải thêm ▼</button>`;h+=`</div>`;box.innerHTML=h;}else{document.getElementById('ht-list')?.insertAdjacentHTML('beforeend',h);const btn=document.getElementById('ht-more');if(btn){if(!j.has_more)btn.remove();else{btn.disabled=false;btn.textContent='Tải thêm ▼';}}}sources.forEach((s,i)=>{const idx=page*8+i;if(!s.url)return;const ctrl=new AbortController();setTimeout(()=>ctrl.abort(),4000);fetch('/api/article?url='+encodeURIComponent(s.url),{signal:ctrl.signal}).then(r=>r.json()).then(d=>{if(d&&(d.og_image||d.img)){const el=document.getElementById('ht-img-'+idx);if(el)el.innerHTML=`<img src="${_proxyImg(esc(d.og_image||d.img))}" onerror="this.style.display='none'">`;}}).catch(()=>{});});}catch(e){box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div style="color:#e74c3c;padding:8px">Lỗi: ${esc(e.message)}</div></div>`;}}
500
  function loadMoreHashtag(){_htPage++;const btn=document.getElementById('ht-more');if(btn){btn.disabled=true;btn.textContent='Đang tải...';}showHashtagSources(_htTopic,_htPage);}
501
- async function rewriteHashtag(topic){const btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang tổng hợp...';}try{const r=await fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic})});const j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');toast('✅ Đã đăng Tường AI!');if(btn)btn.textContent='✅ Đăng thành công!';if(j.post)prependWallPost(j.post);}catch(e){toast('❌ '+e.message);if(btn){btn.disabled=false;btn.textContent='🤖 Rewrite AI';}}}
502
  async function loadLivescore(tab){document.querySelectorAll('.ls-tab').forEach(t=>t.classList.remove('active'));document.querySelector(`.ls-tab[data-tab="${tab}"]`)?.classList.add('active');const el=document.getElementById('ls-content');if(!el)return;el.innerHTML='<div class="loading">Đang tải...</div>';let ep='/api/livescore/'+tab;if(tab.startsWith('bxh_'))ep='/api/livescore/standings/'+tab.replace('bxh_','');try{const r=await fetch(ep);const d=await r.json();el.innerHTML=d.html&&d.html.length>50?d.html:'<div class="loading">Không có dữ liệu</div>';bindMatchClicks(el);}catch(e){el.innerHTML='<div class="loading">Lỗi</div>';}}
503
  function bindMatchClicks(el){el.querySelectorAll('.match-detail').forEach(md=>{md.style.cursor='pointer';md.addEventListener('click',function(e){const statusA=this.querySelector('.status a');const teamA=this.querySelector('.teams a[href*="/tran-dau/"]');const a = statusA || teamA;if(a){e.preventDefault();e.stopPropagation();const href=a.getAttribute('href')||'';const m=href.match(/\/tran-dau\/(\d+)\//);if(m){const fullUrl=href.startsWith('http')?href:'https://bongda.com.vn'+href;openMatch(m[1],fullUrl);}}});});el.querySelectorAll('a').forEach(a=>{a.addEventListener('click',e=>{e.preventDefault();e.stopPropagation()});});}
504
  function openMatch(id,url){if(!id)return;_currentEventId=id;if(url)_currentMatchUrl=url;document.getElementById('match-overlay').classList.add('active');document.body.style.overflow='hidden';loadMatchTab('detail')}
@@ -529,8 +530,7 @@ function toast(msg){let t=document.getElementById('progress-toast');if(t){t.text
529
  var ta=document.createElement('textarea');
530
  ta.value=shareUrl;ta.style.position='fixed';ta.style.left='-9999px';ta.style.top='-9999px';ta.style.opacity='0';
531
  document.body.appendChild(ta);ta.select();ta.setSelectionRange(0,99999);
532
- if(document.execCommand('copy')){toast('📋 Đã sao chép link!');}
533
- else{prompt('📋 Sao chép link:', shareUrl);}
534
  document.body.removeChild(ta);
535
  }catch(e){prompt('📋 Sao chép link:', shareUrl);}
536
  try{if(navigator.share)navigator.share({title:title||'',url:shareUrl}).catch(function(){});}catch(e){}
@@ -560,213 +560,6 @@ function fmtNum(n){if(!n)return'0';if(n>=1000000)return(n/1000000).toFixed(1)+'M
560
  async function loadCounters(videoIds){for(let i=0;i<videoIds.length;i++){const id=videoIds[i];if(!id)continue;const j=await getInteractions(id);const vc=document.getElementById('vc-'+i);if(vc)vc.textContent=fmtNum(j.views);const lc=document.getElementById('lc-'+i);if(lc)lc.textContent=fmtNum(j.likes);const cc=document.getElementById('cc-'+i);if(cc)cc.textContent=fmtNum(j.comments);}}
561
  async function toggleComments(videoId,idx){const panel=document.getElementById('cmt-inline-'+idx);if(!panel)return;if(panel.style.display!=='none'){panel.style.display='none';/* Restore snap alignment */var sl=panel.closest('.tiktok-slide');if(sl)sl.style.scrollSnapAlign='start';return;}panel.style.display='block';panel.innerHTML='<div style="padding:8px;color:#888;font-size:11px">Đang tải...</div>';const cmts=await getComments(videoId);renderInlineComments(panel,videoId,idx,cmts);/* Remove snap so feed can scroll to show comment input */var sl=panel.closest('.tiktok-slide');if(sl)sl.style.scrollSnapAlign='';setTimeout(function(){var feed=document.getElementById('tiktok-feed');if(feed){var slidePos=sl?sl.offsetTop:0;feed.scrollTop=slidePos+200;}/* Focus input */var inp=document.getElementById('cmt-input-'+idx);if(inp)inp.focus();},300);}
562
  function renderInlineComments(panel,videoId,idx,cmts){let h='<div class="inline-cmt-header"><span>💬 Bình luận</span><button onclick="toggleComments(\''+esc(videoId)+'\','+idx+')">✕</button></div><div class="inline-cmt-list">';if(cmts.length){cmts.slice(-30).forEach(c=>{h+=`<div class="inline-cmt-item"><span class="inline-cmt-time">${c.time||''}</span>${esc(c.text)}</div>`;});}else{h+='<div style="color:#777;font-size:11px;padding:4px">Chưa có bình luận</div>';}h+=`</div><div class="inline-cmt-input"><input id="cmt-input-${idx}" placeholder="Viết bình luận..." onkeydown="if(event.key==='Enter')submitInlineCmt('${esc(videoId)}',${idx})"><button onclick="submitInlineCmt('${esc(videoId)}',${idx})">Gửi</button></div>`;panel.innerHTML=h;const list=panel.querySelector('.inline-cmt-list');if(list)list.scrollTop=list.scrollHeight;}
563
- async function submitInlineCmt(videoId,idx){const inp=document.getElementById('cmt-input-'+idx);if(!inp)return;const text=inp.value.trim();if(!text)return;inp.value='';inp.disabled=true;const cmts=await postComment(videoId,text);inp.disabled=false;const panel=document.getElementById('cmt-inline-'+idx);if(panel)renderInlineComments(panel,videoId,idx,cmts);const cc=document.getElementById('cc-'+idx);if(cc)cc.textContent=fmtNum(cmts.length);/* Keep focus on input after submit */var inp2=document.getElementById('cmt-input-'+idx);if(inp2)inp2.focus();}
564
  function toggle169View(videoId,iconId){const slide=videoId?document.querySelector(`.tiktok-slide[data-vid="${videoId}"]`):null;if(slide){slide.classList.toggle('ratio-wide');const iconEl=iconId?document.getElementById(iconId):null;if(iconEl)iconEl.textContent=slide.classList.contains('ratio-wide')?'📺':'🖥️';else{const btn=slide.querySelector('.tiktok-right-btn .icon');if(btn)btn.textContent=slide.classList.contains('ratio-wide')?'📺':'🖥️';}return}document.querySelectorAll('.tiktok-slide.ratio-wide').forEach(s=>s.classList.remove('ratio-wide'));document.querySelectorAll('.tiktok-slide').forEach(s=>s.classList.add('ratio-wide'));document.querySelectorAll('.tiktok-right-btn .icon').forEach(b=>{if(b.textContent==='🖥️')b.textContent='📺';})}
565
- function initTikTokFeed(){const feed=document.getElementById('tiktok-feed');if(!feed)return;const slides=feed.querySelectorAll('.tiktok-slide');let cur=-1;function act(i){if(i===cur)return;slides.forEach((sl,idx)=>{const v=sl.querySelector('video');const fr=sl.querySelector('iframe');if(idx===i){if(v&&v.dataset.hls&&!v._hls&&typeof Hls!=='undefined'&&Hls.isSupported()){const hls=new Hls();hls.loadSource(v.dataset.hls);hls.attachMedia(v);hls.on(Hls.Events.MANIFEST_PARSED,()=>v.play().catch(()=>{}));v._hls=hls}else if(v)v.play().catch(()=>{});if(fr&&!fr.src&&fr.dataset.ytSrc)fr.src=fr.dataset.ytSrc;const vid=sl.dataset.vid;if(vid&&!sl._viewed){sl._viewed=true;doInteract(vid,'view').then(j=>{if(j){const vc=document.getElementById('vc-'+idx);if(vc)vc.textContent=fmtNum(j.views);}});}}else{if(v){v.pause();if(v._hls){v._hls.destroy();v._hls=null}}if(fr&&fr.src)fr.src=''}});cur=i}let sT;feed.addEventListener('scroll',()=>{clearTimeout(sT);sT=setTimeout(()=>{const rect=feed.getBoundingClientRect(),ctr=rect.top+rect.height/2;let best=-1,bestD=1e9;slides.forEach((sl,i)=>{const d=Math.abs(sl.getBoundingClientRect().top+sl.getBoundingClientRect().height/2-ctr);if(d<bestD){bestD=d;best=i}});if(best>=0)act(best)},150)});setTimeout(()=>act(0),400);slides.forEach(sl=>{const v=sl.querySelector('video');if(v)v.addEventListener('click',e=>{e.preventDefault();v.paused?v.play().catch(()=>{}):v.pause()})});const ids=[...slides].map(sl=>sl.dataset.vid||'');loadCounters(ids)}
566
- async function openHighlightFeed(league,idx,forceUrl){showView('view-tiktok');const el=document.getElementById('view-tiktok');el.innerHTML='<div class="loading">Đang tải...</div>';let articles=(_hlLeagueData||{})[league]||[];if(!articles.length){try{articles=await fetch('/api/highlights/'+league).then(r=>r.json())}catch(e){articles=[]}}if(!articles.length){el.innerHTML='<div class="loading">Không có video</div>';return}const vids=[];const results=await Promise.all(articles.map(async(a,i)=>{try{const r=await fetch('/api/video_url?url='+encodeURIComponent(a.link)+'&img='+encodeURIComponent(a.img||''));const v=await r.json();if(v&&v.src){return{_idx:i,title:a.title||v.title||'',link:a.link||'',img:a.img||v.poster||'',src:v.src,type:v.type||'',poster:v.poster||a.img||''}}return null}catch(e){}return null}));results.forEach(r=>{if(r)vids.push(r)});vids.sort((a,b)=>a._idx-b._idx);if(!vids.length){el.innerHTML='<div class="loading">Không tìm thấy video</div>';return}let ti=vids.findIndex(v=>v._idx===idx);if(ti<0)ti=0;const ordered=ti>0?[...vids.slice(ti),...vids.slice(0,ti)]:vids;let h=`<button class="back-btn" onclick="switchCat('home')">← Highlight</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">`;ordered.forEach((v,i)=>{const isYT=v.type==='youtube',isHLS=!isYT&&v.src?.includes('.m3u8'),poster=v.poster?` poster="${esc(v.poster)}"`:'';const vtag=isYT?`<iframe data-yt-src="${v.src}" allowfullscreen allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture"></iframe>`:isHLS?`<video playsinline preload="none"${poster} data-hls="${v.src}" loop controls></video>`:`<video playsinline preload="none"${poster} loop controls><source src="${v.src}" type="video/mp4"></video>`;const videoId='hl-'+league+'-'+v._idx;h+=buildTikTokSlide({vtag,title:v.title,badge:'HL',badgeClass:'badge-fpt',videoId,idx:i,total:ordered.length,shareUrl:v.link||'',postId:'',extraBtn:`<button class="tiktok-right-btn" onclick="event.stopPropagation();openHighlightFeed('${league}',${(i+1)%ordered.length})"><div class="icon">⏭️</div></button>`})});h+='</div></div>';el.innerHTML=h;setTimeout(()=>initTikTokFeed(),200);}
567
- async function openYTShortsFeed(idx){showView('view-tiktok');const el=document.getElementById('view-tiktok');el.innerHTML='<div class="loading">Đã xóa Shorts Dân trí/SKĐS</div>';}
568
- async function openShortAIFeed(idx){showView('view-tiktok');const el=document.getElementById('view-tiktok');if(!_wallPosts||!_wallPosts.length){el.innerHTML='<div class="loading">Không có Short AI</div>';return}const aiPosts=_wallPosts.filter(p=>p.video);if(!aiPosts.length||idx>=aiPosts.length){el.innerHTML='<div class="loading">Không có Short AI</div>';return}const ordered=aiPosts.slice(idx).concat(aiPosts.slice(0,idx));let h=`<button class="back-btn" onclick="switchCat('home')">← Tường AI</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">`;ordered.forEach((p,i)=>{const baseIdx=_wallPosts.indexOf(p);const vtag=p.video?`<video playsinline preload="none" src="${esc(p.video)}" loop controls></video>`:'';h+=buildTikTokSlide({vtag,title:p.title,badge:'Short AI',badgeClass:'badge-ai',videoId:p.id||'ai-'+i,idx:i,total:ordered.length,shareUrl:p.video||'',postId:p.id||'',extraBtn:`<button class="tiktok-right-btn" onclick="event.stopPropagation();shareVideoToApps('${esc(p.video)}','${esc(p.title)}')"><div class="icon">📥</div></button>`})});h+='</div></div>';el.innerHTML=h;setTimeout(()=>initTikTokFeed(),200);}
569
- function readWallPost(idx){const p=_wallPosts&&_wallPosts[idx];if(!p)return;if(p.slides&&p.slides.length){readSlidePost(idx);return}readArticle(p.url||'','','',p.title,p.text);}
570
- /** Show rewrite slide viewer - vertical slides with text+image */
571
- function readSlidePost(idx){const p=_wallPosts[idx];if(!p||!p.slides)return;showView('view-article');const el=document.getElementById('view-article');let h=`<button class="back-btn" onclick="switchCat('home')">← Tường AI</button><div class="slide-viewer" style="padding:12px;max-width:600px;margin:0 auto">`;p.slides.forEach((s,i)=>{h+=`<div class="slide-card" style="background:#1a1a1a;border:1px solid #2a2a2a;border-radius:12px;padding:16px;margin-bottom:12px"><div class="slide-num" style="color:#5cb87a;font-size:12px;font-weight:700;margin-bottom:6px">Slide ${s.index||i+1}/${p.slides.length}</div>${s.image?`<img src="${_proxyImg(s.image)}" style="width:100%;max-height:300px;object-fit:cover;border-radius:8px;margin-bottom:8px" loading="lazy" onerror="this.style.display=\'none\'">`:''}<p style="color:#ddd;font-size:14px;line-height:1.6;margin:0">${esc(s.text)}</p></div>`;});h+=`</div><div class="article-actions" style="padding:12px;text-align:center"><button onclick="doShare('${esc(p.title)}','${esc(p.url||'')}','${esc(p.img||'')}','${esc(p.id||'')}')" style="background:#2d8659;border:0;color:#fff;padding:8px 16px;border-radius:10px;cursor:pointer;font-size:12px">📤 Chia sẻ bài viết này</button></div>`;el.innerHTML=h;}
572
- function readNewsTab(tab){loadNewsTab();}
573
- function loadNewsTab(){const el=document.getElementById('view-cat');if(!el)return;el.innerHTML='<div class="loading">Đang tải tin tức...</div>';fetch('/api/homepage').then(r=>r.json()).then(articles=>{if(!articles||!articles.length){el.innerHTML='<div class="loading">Không có tin</div>';return}let h='<div class="grid">';articles.forEach(a=>{const src=a.source||'vne';const badge=a.group||a.source||'';h+=`<div class="card" onclick="readArticle('${esc(a.link)}')"><div class="card-img">${a.img?`<img src="${_proxyImg(a.img)}" loading="lazy" onerror="this.style.display=\'none\'">`:''}</div><div class="card-body"><span class="badge badge-${src}">${esc(badge)}</span><div class="card-title">${esc(a.title)}</div></div></div>`;});h+='</div>';el.innerHTML=h;}).catch(()=>{el.innerHTML='<div class="loading">Lỗi tải</div>';});}
574
-
575
- function readArticle(url,title,img,presetTitle,presetText){showView('view-article');const el=document.getElementById('view-article');el.innerHTML='<div class="loading">Đang tải...</div><button class="back-btn" onclick="switchCat(\'home\')">← Quay lại</button>';if(presetTitle){el.innerHTML=`<button class="back-btn" onclick="switchCat('home')">← Quay lại</button><div class="article-view"><h1 class="article-title">${esc(presetTitle)}</h1>${presetText?`<div class="article-summary" style="white-space:pre-wrap">${esc(presetText)}</div>`:''}</div><div class="article-actions"><button onclick="doShare('${esc(presetTitle)}','${esc(url||'')}','')">📤 Chia sẻ</button></div>`;return;}if(!url)return;fetch('/api/article?url='+encodeURIComponent(url)).then(r=>r.json()).then(d=>{let h=`<button class="back-btn" onclick="switchCat('home')">← Quay lại</button><div class="article-view">`;if(d.title)h+=`<h1 class="article-title">${esc(d.title)}</h1>`;if(d.summary)h+=`<div class="article-summary">${esc(d.summary)}</div>`;if(d.body)d.body.forEach(b=>{if(b.type==='p')h+=`<p class="article-p">${esc(b.text)}</p>`;else if(b.type==='heading')h+=`<h2 class="article-h2">${esc(b.text)}</h2>`;else if(b.type==='img'&&b.src)h+=`<img class="article-img" src="${_proxyImg(b.src)}" loading="lazy" onerror="this.style.display=\'none\'">`;});h+=`</div><div class="article-actions"><button onclick="doShare('${esc(d.title||'')}','${esc(url)}','${esc(d.og_image||'')}')">📤 Chia sẻ</button><button class="primary" onclick="rewriteSlide('${esc(url)}')">🤖 Slide Rewrite AI</button></div>`;el.innerHTML=h;}).catch(()=>{el.innerHTML=`<button class="back-btn" onclick="switchCat('home')">← Quay lại</button><div class="article-view"><p>Không thể tải bài viết</p></div>`;});}
576
- async function rewriteSlide(url){if(!url)return;const btn=document.querySelector('.article-actions .primary')||event?.target;if(btn){btn.disabled=true;btn.textContent='⏳ Đang tạo slides...';}toast('⏳ Đang tạo slide rewrite...');try{const r=await fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})});const j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');toast('✅ Đã tạo slide! Đang tạo Short AI...');if(btn)btn.textContent='✅ Đang tạo Short AI...';if(j.post){j.post.slides=j.slides||[];prependWallPost(j.post);}// Show slides immediately if available
577
- if(j.slides && j.slides.length){showView('view-article');const el=document.getElementById('view-article');let h=`<button class="back-btn" onclick="switchCat('home')">← Tường AI</button><div class="slide-viewer" style="padding:12px;max-width:600px;margin:0 auto">`;j.slides.forEach(s=>{h+=`<div class="slide-card" style="background:#1a1a1a;border:1px solid #2a2a2a;border-radius:12px;padding:16px;margin-bottom:12px"><div class="slide-num" style="color:#5cb87a;font-size:12px;font-weight:700;margin-bottom:6px">Slide ${s.index}/${j.slides.length}</div>${s.image?`<img src="${s.image.startsWith('/api/')?s.image:'/api/proxy/img?url='+encodeURIComponent(s.image)}" style="width:100%;max-height:300px;object-fit:cover;border-radius:8px;margin-bottom:8px" loading="lazy" onerror="this.style.display='none'">`:''}<p style="color:#ddd;font-size:14px;line-height:1.6;margin:0">${esc(s.text)}</p></div>`;});h+=`</div><div style="text-align:center;padding:12px"><div id="short-ai-status" style="color:#888;font-size:12px">⏳ Đang tạo video Short AI...</div></div>`;el.innerHTML=h;}
578
- const postId=j.post&&j.post.id;if(postId){setTimeout(async()=>{const sr=await fetch('/api/ai/short/'+encodeURIComponent(postId),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({voice:'vi-VN-HoaiMyNeural',emotion:'neutral',speed:1.2})});const sj=await sr.json();if(sr.ok&&sj.video&&typeof _wallPosts!=='undefined'){const p=_wallPosts.find(x=>String(x.id)===String(postId));if(p){p.video=sj.video;const itemId='wall-item-'+postId;const el2=document.getElementById(itemId);if(el2){const idx=_wallPosts.indexOf(p);el2.insertAdjacentHTML('afterend',makeWallItem(p,idx));el2.remove();}}toast('✅ Short AI đã sẵn sàng!');const statusEl=document.getElementById('short-ai-status');if(statusEl)statusEl.innerHTML='✅ Short AI đẵn sàng! <button class="primary" onclick="openShortAIFeed(0)" style="background:#2d8659;border:0;color:#fff;padding:6px 12px;border-radius:10px;font-size:11px;cursor:pointer;margin-left:8px">▶ Xem ngay</button>';}},500);}if(btn)btn.textContent='✅ Hoàn tất';}catch(e){toast('❌ '+e.message);if(btn){btn.disabled=false;btn.textContent='🤖 Slide Rewrite AI';}}}
579
-
580
- function downloadVideo(url, title){
581
- var a = document.createElement('a');
582
- a.href = url;
583
- a.download = (title||'video').toString().replace(/[^a-zA-Z0-9_\-\p{L}]/gu,'_').substring(0,60)+'.mp4';
584
- a.target = '_blank';
585
- a.rel = 'noopener';
586
- a.style.display = 'none';
587
- document.body.appendChild(a);
588
- a.click();
589
- document.body.removeChild(a);
590
- toast('📥 Đang tải video xuống...');
591
- }
592
-
593
- // ===== PERSONAL OPINION POST: Viết bài dựa trên quan điểm cá nhân + tin HOT =====
594
- async function openPersonalPostPreview() {
595
- const opinion = document.getElementById('opinion-input')?.value.trim();
596
- if (!opinion || opinion.length < 10) {
597
- alert('Vui lòng nhập quan điểm cá nhân (ít nhất 10 ký tự)');
598
- return;
599
- }
600
-
601
- // Get selected hot topics from UI (if any)
602
- const selectedTopics = [];
603
- if (window._htTopic) {
604
- selectedTopics.push(window._htTopic);
605
- }
606
-
607
- // Get selected sources from hashtag view
608
- const selectedSources = [];
609
- document.querySelectorAll('.hashtag-src-item.selected').forEach(el => {
610
- const idx = parseInt(el.dataset.idx || '0');
611
- // Would need source tracking
612
- });
613
-
614
- const btn = event?.target;
615
- const origText = btn ? btn.textContent : '';
616
- if (btn) { btn.disabled = true; btn.textContent = '⏳ Đang tạo preview...'; }
617
-
618
- try {
619
- const resp = await fetch('/api/personal_post/preview', {
620
- method: 'POST',
621
- headers: { 'Content-Type': 'application/json' },
622
- body: JSON.stringify({
623
- opinion: opinion,
624
- selected_topics: selectedTopics,
625
- selected_sources: selectedSources
626
- })
627
- });
628
- const data = await resp.json();
629
- if (!resp.ok || data.error) throw new Error(data.error || 'Lỗi tạo preview');
630
-
631
- showPersonalPostModal(data.preview, opinion, selectedTopics, selectedSources);
632
- } catch (e) {
633
- toast('❌ ' + e.message);
634
- } finally {
635
- if (btn) { btn.disabled = false; btn.textContent = origText; }
636
- }
637
- }
638
-
639
- function showPersonalPostModal(preview, originalOpinion, selectedTopics, selectedSources) {
640
- // Remove existing modal
641
- const existing = document.getElementById('personal-post-modal');
642
- if (existing) existing.remove();
643
-
644
- const modal = document.createElement('div');
645
- modal.id = 'personal-post-modal';
646
- modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.9);z-index:99999;display:flex;align-items:center;justify-content:center;padding:16px;overflow-y:auto';
647
-
648
- const slidesHtml = (preview.slides || []).map((s, i) => {
649
- return '<div class="preview-slide" style="background:#1a1a1a;border:1px solid #2a2a2a;border-radius:12px;padding:16px;margin-bottom:12px;max-width:600px;margin-left:auto;margin-right:auto">'
650
- + '<div style="display:flex;gap:8px;align-items:flex-start">'
651
- + '<div style="flex:0 0 32px;color:#5cb87a;font-size:12px;font-weight:700;margin-top:4px">Slide ' + (i+1) + '</div>'
652
- + '<div style="flex:1;min-width:0">'
653
- + (s.image ? '<img src="' + s.image + '" style="width:100%;max-height:200px;object-fit:cover;border-radius:8px;margin-bottom:8px" onerror="this.style.display=\'none\'">' : '')
654
- + '<textarea class="preview-slide-text" data-index="' + i + '" style="width:100%;min-height:80px;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:13px;resize:vertical;font-family:inherit">' + (s.text || '').replace(/"/g, '"') + '</textarea>'
655
- + '</div></div></div>';
656
- }).join('');
657
-
658
- const sourcesHtml = (preview.sources || []).map((s, i) => {
659
- const imgSrc = (preview.images && preview.images[i+1]) ? preview.images[i+1] : '';
660
- return '<div style="display:flex;gap:8px;padding:8px;background:#202020;border-radius:8px;margin:4px 0;cursor:pointer" onclick="toggleSourceSelection(this, ' + i + ')" data-source-idx="' + i + '" data-url="' + (s.url || '') + '">'
661
- + '<div style="flex:0 0 60px;aspect-ratio:16/9;background:#333;border-radius:4px;overflow:hidden">'
662
- + (imgSrc ? '<img src="' + imgSrc + '" style="width:100%;height:100%;object-fit:cover" onerror="this.style.display=\'none\'">' : '')
663
- + '</div>'
664
- + '<div style="flex:1;min-width:0;font-size:11px;color:#ddd">' + (s.title || '') + '</div>'
665
- + '<input type="checkbox" class="source-checkbox" style="margin-top:4px" ' + (i < 3 ? 'checked' : '') + '>'
666
- + '</div>';
667
- }).join('');
668
-
669
- modal.innerHTML = '<div style="background:#1a1a1a;border:2px solid #2d8659;border-radius:16px;padding:20px;max-width:700px;width:100%;max-height:90vh;overflow-y:auto">'
670
- + '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">'
671
- + '<h3 style="color:#5cb87a;margin:0;font-size:16px">📝 Xem trước bài quan điểm cá nhân</h3>'
672
- + '<button onclick="this.closest(\'#personal-post-modal\').remove()" style="background:none;border:0;color:#fff;font-size:24px;cursor:pointer">✕</button>'
673
- + '</div>'
674
- + '<div style="margin-bottom:12px">'
675
- + '<label style="display:block;color:#888;font-size:11px;margin-bottom:4px">Tiêu đề:</label>'
676
- + '<input id="preview-title" value="' + (preview.title || '').replace(/"/g, '"') + '" style="width:100%;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px 12px;font-size:14px;font-weight:700">'
677
- + '</div>'
678
- + '<div style="margin-bottom:12px">'
679
- + '<label style="display:block;color:#888;font-size:11px;margin-bottom:4px">Quan điểm gốc:</label>'
680
- + '<textarea id="preview-opinion" readonly style="width:100%;min-height:60px;background:#141414;border:1px solid #2a2a2a;color:#aaa;border-radius:8px;padding:8px 12px;font-size:12px;resize:vertical">' + originalOpinion.replace(/"/g, '"') + '</textarea>'
681
- + '</div>'
682
- + '<div style="margin-bottom:12px;max-height:300px;overflow-y:auto">'
683
- + '<label style="display:block;color:#888;font-size:11px;margin-bottom:8px">Các slide (có thể chỉnh sửa):</label>'
684
- + slidesHtml
685
- + '</div>'
686
- + '<div style="margin-bottom:12px">'
687
- + '<label style="display:block;color:#888;font-size:11px;margin-bottom:8px">Nguồn tin tham khảo (bỏ chọn để loại):</label>'
688
- + '<div style="max-height:200px;overflow-y:auto">'
689
- + (sourcesHtml || '<div style="color:#777;font-size:12px;padding:8px">Không có nguồn tin</div>')
690
- + '</div></div>'
691
- + '<div style="display:flex;gap:8px">'
692
- + '<button onclick="publishPersonalPostFromModal()" style="flex:1;background:#2d8659;border:0;color:#fff;padding:12px;border-radius:12px;font-size:13px;font-weight:700;cursor:pointer">✅ Đăng lên Tường AI</button>'
693
- + '<button onclick="this.closest(\'#personal-post-modal\').remove()" style="background:#333;border:0;color:#ccc;padding:12px 16px;border-radius:12px;font-size:12px;cursor:pointer">Hủy</button>'
694
- + '</div></div>';
695
-
696
- document.body.appendChild(modal);
697
-
698
- // Store preview data for publishing
699
- window._personalPostPreview = preview;
700
- window._personalPostOpinion = originalOpinion;
701
- }
702
-
703
- function toggleSourceSelection(el, idx) {
704
- const checkbox = el.querySelector('.source-checkbox');
705
- checkbox.checked = !checkbox.checked;
706
- el.style.border = checkbox.checked ? '1px solid #2d8659' : '1px solid transparent';
707
- el.style.background = checkbox.checked ? '#1a2a1f' : '#202020';
708
- }
709
-
710
- async function publishPersonalPostFromModal() {
711
- const title = document.getElementById('preview-title')?.value.trim();
712
- if (!title) {
713
- alert('Vui lòng nhập tiêu đề');
714
- return;
715
- }
716
-
717
- // Collect edited slides
718
- const slides = [];
719
- document.querySelectorAll('.preview-slide-text').forEach(ta => {
720
- const text = ta.value.trim();
721
- if (text) slides.push({ text, image: '', index: slides.length + 1 });
722
- });
723
-
724
- // Collect selected sources
725
- const sources = [];
726
- document.querySelectorAll('[data-source-idx]').forEach((el, i) => {
727
- const checkbox = el.querySelector('.source-checkbox');
728
- if (checkbox.checked && window._personalPostPreview?.sources?.[i]) {
729
- sources.push(window._personalPostPreview.sources[i]);
730
- }
731
- });
732
-
733
- const btn = event.target;
734
- const origText = btn.textContent;
735
- btn.disabled = true;
736
- btn.textContent = '⏳ Đang đăng...';
737
-
738
- try {
739
- const resp = await fetch('/api/personal_post', {
740
- method: 'POST',
741
- headers: { 'Content-Type': 'application/json' },
742
- body: JSON.stringify({
743
- opinion: window._personalPostOpinion,
744
- selected_topics: [],
745
- selected_sources: sources,
746
- custom_title: title,
747
- custom_slides: slides
748
- })
749
- });
750
- const data = await resp.json();
751
- if (!resp.ok || data.error) throw new Error(data.error || 'Lỗi đăng bài');
752
-
753
- toast('✅ Đã đăng bài quan điểm lên Tường AI!');
754
-
755
- // Prepend to wall
756
- if (data.post && typeof prependWallPost === 'function') {
757
- prependWallPost(data.post);
758
- }
759
-
760
- // Close modal
761
- document.getElementById('personal-post-modal')?.remove();
762
-
763
- // Clear opinion input
764
- document.getElementById('opinion-input').value = '';
765
- } catch (e) {
766
- toast('❌ ' + e.message);
767
- } finally {
768
- btn.disabled = false;
769
- btn.textContent = origText;
770
- }
771
- }
772
-
 
70
  });
71
  }
72
 
73
+ // ===== rewriteUrl: tạo bài quan điểm nhân từ URL bài viết =====
74
  // Gọi /api/url_wall (backend thực tế), lấy post rồi đẩy lên Tường AI và mở slide viewer.
75
  async function rewriteUrl(){
76
  const inp = document.getElementById('url-input');
 
79
  if(!/^https?:\/\//i.test(url)){ alert('URL cần bắt đầu bằng http:// hoặc https://'); return; }
80
  let btn = null;
81
  try { btn = inp && inp.parentElement ? inp.parentElement.querySelector('button') : null; } catch(e){}
82
+ const orig = btn ? btn.textContent : 'Viết quan điểm';
83
+ if(btn){ btn.disabled = true; btn.textContent = '⏳ Đang viết...'; }
84
+ toast('⏳ Đang viết bài quan điểm cá nhân...');
85
  try {
86
+ const opinion = (document.getElementById('opinion-input')?.value || '').trim();
87
  const r = await fetch('/api/url_wall', {
88
  method:'POST',
89
  headers:{'Content-Type':'application/json'},
90
+ body: JSON.stringify({url: url, opinion: opinion})
91
  });
92
  const j = await r.json();
93
  if(!r.ok || j.error) throw new Error(j.error || 'Lỗi tạo bài');
 
95
  if(typeof prependWallPost === 'function'){ prependWallPost(j.post); }
96
  else if(Array.isArray(_wallPosts)){ _wallPosts.unshift(j.post); }
97
  if(inp) inp.value = '';
98
+ toast('✅ Đã viết bài quan điểm cá nhân!');
99
  const idx = (_wallPosts || []).indexOf(j.post);
100
  if(j.post.slides && j.post.slides.length){ readSlidePost(idx >= 0 ? idx : 0); }
101
  else if(typeof readWallPost === 'function'){ readWallPost(idx >= 0 ? idx : 0); }
 
149
 
150
  homeEl.innerHTML =
151
  '<div id="home-featured-area"></div>'
152
+ +'<div class="ai-compose"><div class="ai-compose-title">🤖 AI viết quan điểm cá nhân</div><div class="ai-compose-row"><input id="topic-input" placeholder="Nhập chủ đề..."><button onclick="searchTopic()">Tìm nguồn</button></div><div class="ai-compose-row"><input id="url-input" placeholder="Dán URL bài viết làm bằng chứng..."><button class="secondary" onclick="rewriteUrl()">Viết quan điểm</button></div><div class="ai-compose-row" style="margin-top:8px"><textarea id="opinion-input" placeholder="💭 Gợi ý quan điểm/cái bạn muốn bình luận (tùy chọn)...">Đừng để bài thành tóm tắt tin tức; hãy lập luận từ nguồn và nêu rõ góc nhìn của mình.</textarea></div><div class="ai-compose-row"><button onclick="openPersonalPostPreview()">📝 Xem trước & Đăng bài quan điểm</button></div><div id="hot-topics" class="hot-topic-row"></div></div>'
153
  +'<div id="ai-wall-under-compose"></div>'
154
  +'<div id="hashtag-box"></div>'
155
  +'<div class="ls-section"><div class="ls-header"><h3>⚽ Livescore</h3></div><div class="ls-tabs"><span class="ls-tab active" data-tab="updates7d" onclick="loadLivescore(\'updates7d\')">📋 Cập nhật</span><span class="ls-tab" data-tab="live" onclick="loadLivescore(\'live\')">🔴 Live</span><span class="ls-tab" data-tab="today" onclick="loadLivescore(\'today\')">📅 Hôm nay</span><span class="ls-tab" data-tab="incoming" onclick="loadLivescore(\'incoming\')">⏰ Sắp tới</span><span class="ls-tab" data-tab="results" onclick="loadLivescore(\'results\')">✅ Kết quả</span><span class="ls-tab" data-tab="bxh_nha" onclick="loadLivescore(\'bxh_nha\')">🏆 NHA</span><span class="ls-tab" data-tab="bxh_laliga" onclick="loadLivescore(\'bxh_laliga\')">🏆 La Liga</span></div><div class="ls-content" id="ls-content"><div class="loading">Đang tải...</div></div></div>'
 
218
 
219
  function _renderHLIn(afterEl){
220
  if(!_hlLeagueData||Object.keys(_hlLeagueData).length===0||!afterEl) return;
221
+ const HL_CONFIG={"world-cup":{name:"World Cup 2026",emoji:"🌍"},"premier-league":{name:"Premier League",emoji:""},"champions-league":{name:"Champions League",emoji:"⭐"},"la-liga":{name:"La Liga",emoji:"🇪🇸"},"serie-a":{name:"Serie A",emoji:"🇮🇹"},"bundesliga":{name:"Bundesliga",emoji:"🇩🇪"},"friendly":{name:"Giao hữu",emoji:"🤝"}};
222
  for(const[key,cfg] of Object.entries(HL_CONFIG)){
223
  const vids=_hlLeagueData[key];
224
  if(!vids||!vids.length) continue;
 
245
  });
246
  var emotOpts = '';
247
  EMOTION_LIST.forEach(function(e){
248
+ emotOpts += '<option value="'+e.id+'"'+(v.id===_ttsSelections[selKey].voice?' selected':'')+'>'+v.label+'</option>';
249
  });
250
  var spd = p.short_speed || '1.2';
251
  var voiceBar = '<div class="wall-tts-bar" style="margin:6px 0 4px;display:grid;grid-template-columns:1fr auto auto;gap:3px">'
 
387
  function detectLanguage(text){
388
  if(!text) return 'vi';
389
  var t=text.toLowerCase(), chars=new Set(t);
390
+ var vnChars='đăâêôơưàảãạáằẳẵặắầẩẫậấèẻẽẹéềễểệếìỉĩịíòỏõọóồổỗộốờởỡợớùủũũụúừửữựứỳỷỹỵý';
391
  var vnCount=0; for(var c of vnChars){if(chars.has(c)) vnCount++;}
392
  if(vnCount>=2) return 'vi';
393
  if(chars.has('ñ')||chars.has('¿')||chars.has('¡')) return 'es';
 
497
  let _htPage=0,_htTopic='';
498
  async function loadHotTopics(){const j=await fetch('/api/hot_topics').then(r=>r.json()).catch(()=>({topics:[]}));const el=document.getElementById('hot-topics');if(!el)return;el.innerHTML=(j.topics||[]).slice(0,18).map(t=>{const topicText=t.topic||t.label.replace(/^#/,'');return`<button class="hot-chip" onclick="searchTopic('${topicText.replace(/'/g,"\\'")}')">${esc(t.label)}</button>`;}).join('');}
499
  function searchTopic(topic){if(!topic){topic=document.getElementById('topic-input')?.value.trim();if(!topic){alert('Nhập chủ đề');return;}}document.getElementById('topic-input').value='';_htTopic=topic;_htPage=0;showHashtagSources(topic,0);}
500
+ async function showHashtagSources(topic,page){const box=document.getElementById('hashtag-box');if(!box)return;if(page===0)box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div class="hashtag-loading"><div class="hashtag-spinner"></div>Đang tìm...</div></div>`;try{const r=await fetch(`/api/hashtag/sources?topic=${encodeURIComponent(topic)}&page=${page}`);const j=await r.json();const sources=j.sources||[];if(!sources.length&&page===0){box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div style="color:#888;padding:8px">Không tìm được bài viết liên quan</div></div>`;return;}let h='';if(page===0)h=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)} <span style="font-size:10px;color:#888">(${j.total} bài từ 8 nguồn)</span></h3><div id="ht-list">`;sources.forEach((s,i)=>{const idx=page*8+i;h+=`<div class="hashtag-src-item" onclick="readArticle('${esc(s.url)}')"><div class="hashtag-src-img" id="ht-img-${idx}"></div><div class="hashtag-src-text"><div class="hashtag-src-title">${esc(s.title)}</div><div class="hashtag-src-via">${esc(s.via||'')}</div></div></div>`;});if(page===0){h+=`</div><button class="hashtag-rewrite-btn" onclick="rewriteHashtag('${esc(topic).replace(/'/g,"\\'")}')">🤖 Viết quan điểm từ nguồn</button>`;if(j.has_more)h+=`<button class="hashtag-load-more" id="ht-more" onclick="loadMoreHashtag()">Tải thêm ▼</button>`;h+=`</div>`;box.innerHTML=h;}else{document.getElementById('ht-list')?.insertAdjacentHTML('beforeend',h);const btn=document.getElementById('ht-more');if(btn){if(!j.has_more)btn.remove();else{btn.disabled=false;btn.textContent='Tải thêm ▼';}}}sources.forEach((s,i)=>{const idx=page*8+i;if(!s.url)return;const ctrl=new AbortController();setTimeout(()=>ctrl.abort(),4000);fetch('/api/article?url='+encodeURIComponent(s.url),{signal:ctrl.signal}).then(r=>r.json()).then(d=>{if(d&&(d.og_image||d.img)){const el=document.getElementById('ht-img-'+idx);if(el)el.innerHTML=`<img src="${_proxyImg(esc(d.og_image||d.img))}" onerror="this.style.display='none'">`;}}).catch(()=>{});});}catch(e){box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div style="color:#e74c3c;padding:8px">Lỗi: ${esc(e.message)}</div></div>`;}}
501
  function loadMoreHashtag(){_htPage++;const btn=document.getElementById('ht-more');if(btn){btn.disabled=true;btn.textContent='Đang tải...';}showHashtagSources(_htTopic,_htPage);}
502
+ async function rewriteHashtag(topic){const btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang viết...';}try{const opinion=(document.getElementById('opinion-input')?.value || '').trim();const r=await fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic, opinion})});const j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');toast('✅ Đã đăng bài quan điểm cá nhân!');if(btn)btn.textContent='✅ Đăng thành công!';if(j.post)prependWallPost(j.post);}catch(e){toast('❌ '+e.message);if(btn){btn.disabled=false;btn.textContent='🤖 Viết quan điểm';}}}
503
  async function loadLivescore(tab){document.querySelectorAll('.ls-tab').forEach(t=>t.classList.remove('active'));document.querySelector(`.ls-tab[data-tab="${tab}"]`)?.classList.add('active');const el=document.getElementById('ls-content');if(!el)return;el.innerHTML='<div class="loading">Đang tải...</div>';let ep='/api/livescore/'+tab;if(tab.startsWith('bxh_'))ep='/api/livescore/standings/'+tab.replace('bxh_','');try{const r=await fetch(ep);const d=await r.json();el.innerHTML=d.html&&d.html.length>50?d.html:'<div class="loading">Không có dữ liệu</div>';bindMatchClicks(el);}catch(e){el.innerHTML='<div class="loading">Lỗi</div>';}}
504
  function bindMatchClicks(el){el.querySelectorAll('.match-detail').forEach(md=>{md.style.cursor='pointer';md.addEventListener('click',function(e){const statusA=this.querySelector('.status a');const teamA=this.querySelector('.teams a[href*="/tran-dau/"]');const a = statusA || teamA;if(a){e.preventDefault();e.stopPropagation();const href=a.getAttribute('href')||'';const m=href.match(/\/tran-dau\/(\d+)\//);if(m){const fullUrl=href.startsWith('http')?href:'https://bongda.com.vn'+href;openMatch(m[1],fullUrl);}}});});el.querySelectorAll('a').forEach(a=>{a.addEventListener('click',e=>{e.preventDefault();e.stopPropagation()});});}
505
  function openMatch(id,url){if(!id)return;_currentEventId=id;if(url)_currentMatchUrl=url;document.getElementById('match-overlay').classList.add('active');document.body.style.overflow='hidden';loadMatchTab('detail')}
 
530
  var ta=document.createElement('textarea');
531
  ta.value=shareUrl;ta.style.position='fixed';ta.style.left='-9999px';ta.style.top='-9999px';ta.style.opacity='0';
532
  document.body.appendChild(ta);ta.select();ta.setSelectionRange(0,99999);
533
+ if(document.execCommand('copy')){toast('📋 Đã sao chép link!');}else{prompt('📋 Sao chép link:', shareUrl);}
 
534
  document.body.removeChild(ta);
535
  }catch(e){prompt('📋 Sao chép link:', shareUrl);}
536
  try{if(navigator.share)navigator.share({title:title||'',url:shareUrl}).catch(function(){});}catch(e){}
 
560
  async function loadCounters(videoIds){for(let i=0;i<videoIds.length;i++){const id=videoIds[i];if(!id)continue;const j=await getInteractions(id);const vc=document.getElementById('vc-'+i);if(vc)vc.textContent=fmtNum(j.views);const lc=document.getElementById('lc-'+i);if(lc)lc.textContent=fmtNum(j.likes);const cc=document.getElementById('cc-'+i);if(cc)cc.textContent=fmtNum(j.comments);}}
561
  async function toggleComments(videoId,idx){const panel=document.getElementById('cmt-inline-'+idx);if(!panel)return;if(panel.style.display!=='none'){panel.style.display='none';/* Restore snap alignment */var sl=panel.closest('.tiktok-slide');if(sl)sl.style.scrollSnapAlign='start';return;}panel.style.display='block';panel.innerHTML='<div style="padding:8px;color:#888;font-size:11px">Đang tải...</div>';const cmts=await getComments(videoId);renderInlineComments(panel,videoId,idx,cmts);/* Remove snap so feed can scroll to show comment input */var sl=panel.closest('.tiktok-slide');if(sl)sl.style.scrollSnapAlign='';setTimeout(function(){var feed=document.getElementById('tiktok-feed');if(feed){var slidePos=sl?sl.offsetTop:0;feed.scrollTop=slidePos+200;}/* Focus input */var inp=document.getElementById('cmt-input-'+idx);if(inp)inp.focus();},300);}
562
  function renderInlineComments(panel,videoId,idx,cmts){let h='<div class="inline-cmt-header"><span>💬 Bình luận</span><button onclick="toggleComments(\''+esc(videoId)+'\','+idx+')">✕</button></div><div class="inline-cmt-list">';if(cmts.length){cmts.slice(-30).forEach(c=>{h+=`<div class="inline-cmt-item"><span class="inline-cmt-time">${c.time||''}</span>${esc(c.text)}</div>`;});}else{h+='<div style="color:#777;font-size:11px;padding:4px">Chưa có bình luận</div>';}h+=`</div><div class="inline-cmt-input"><input id="cmt-input-${idx}" placeholder="Viết bình luận..." onkeydown="if(event.key==='Enter')submitInlineCmt('${esc(videoId)}',${idx})"><button onclick="submitInlineCmt('${esc(videoId)}',${idx})">Gửi</button></div>`;panel.innerHTML=h;const list=panel.querySelector('.inline-cmt-list');if(list)list.scrollTop=list.scrollHeight;}
563
+ async function submitInlineCmt(videoId,idx){const inp=document.getElementById('cmt-input-${idx}');if(!inp)return;const text=inp.value.trim();if(!text)return;inp.value='';inp.disabled=true;const cmts=await postComment(videoId,text);inp.disabled=false;const panel=document.getElementById('cmt-inline-${idx}');if(panel)renderInlineComments(panel,videoId,idx,cmts);const cc=document.getElementById('cc-${idx}');if(cc)cc.textContent=fmtNum(cmts.length);/* Keep focus on input after submit */var inp2=document.getElementById('cmt-input-${idx}');if(inp2)inp2.focus();}
564
  function toggle169View(videoId,iconId){const slide=videoId?document.querySelector(`.tiktok-slide[data-vid="${videoId}"]`):null;if(slide){slide.classList.toggle('ratio-wide');const iconEl=iconId?document.getElementById(iconId):null;if(iconEl)iconEl.textContent=slide.classList.contains('ratio-wide')?'📺':'🖥️';else{const btn=slide.querySelector('.tiktok-right-btn .icon');if(btn)btn.textContent=slide.classList.contains('ratio-wide')?'📺':'🖥️';}return}document.querySelectorAll('.tiktok-slide.ratio-wide').forEach(s=>s.classList.remove('ratio-wide'));document.querySelectorAll('.tiktok-slide').forEach(s=>s.classList.add('ratio-wide'));document.querySelectorAll('.tiktok-right-btn .icon').forEach(b=>{if(b.textContent==='🖥️')b.textContent='📺';})}
565
+ function initTikTokFeed(){const feed=document.getElementById('tiktok-feed');if(!feed)return;const slides=feed.querySelectorAll('.tiktok-slide');let cur=-1;function act(i){if(i===cur)return;slides.forEach((sl,idx)=>{const v=sl.querySelector('video');const fr=sl.querySelector('iframe');if(idx===i){if(v&&v.dataset.hls&&!v._hls&&typeof Hls!=='undefined'&&Hls.isSupported()){const