Spaces:
Running
Running
| """Final wrapper with complete highlight override including interaction buttons. | |
| PLUS: Hashtag inline sources on homepage with rewrite button.""" | |
| import json, os, time | |
| from app_patch_unified import * | |
| from app_patch_unified import app, UNIFIED_INJECT, f5, f6, rt, PATCH_INJECT | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| from fastapi import Request, Query | |
| DATA_DIR="/data" if os.path.isdir('/data') else "/app/data" | |
| os.makedirs(DATA_DIR,exist_ok=True) | |
| HL_STATS_FILE=os.path.join(DATA_DIR,'highlight_stats.json') | |
| def _load_hl(): | |
| try: | |
| if os.path.exists(HL_STATS_FILE):return json.load(open(HL_STATS_FILE,'r',encoding='utf-8')) | |
| except:pass | |
| return {} | |
| def _save_hl(db): | |
| try:open(HL_STATS_FILE+'.tmp','w',encoding='utf-8').write(json.dumps(db,ensure_ascii=False));os.replace(HL_STATS_FILE+'.tmp',HL_STATS_FILE) | |
| except:pass | |
| app.router.routes=[r for r in app.router.routes if not ( | |
| (getattr(r,'path',None)=='/api/highlight/interact' and 'POST' in getattr(r,'methods',set())) or | |
| (getattr(r,'path',None)=='/api/highlight/stats' and 'GET' in getattr(r,'methods',set())) or | |
| (getattr(r,'path',None)=='/api/hashtag/sources' and 'GET' in getattr(r,'methods',set())) or | |
| (getattr(r,'path',None)=='/' and 'GET' in getattr(r,'methods',set())) | |
| )] | |
| async def _hl_act(request:Request): | |
| b=await request.json();vid=str(b.get('id','')).strip();action=str(b.get('action','')).strip() | |
| if not vid or action not in ('view','like','share'):return JSONResponse({'error':'invalid'},status_code=400) | |
| db=_load_hl();st=db.get(vid,{'views':0,'likes':0,'shares':0}) | |
| st[action+'s']=st.get(action+'s',0)+1 | |
| db[vid]=st;_save_hl(db);return JSONResponse({'stats':st}) | |
| def _hl_stats(ids:str=Query(default='')): | |
| db=_load_hl();out={} | |
| for vid in ids.split(','): | |
| vid=vid.strip() | |
| if vid:out[vid]=db.get(vid,{'views':0,'likes':0,'shares':0}) | |
| return JSONResponse({'stats':out}) | |
| def _hashtag_sources(topic:str=Query(...)): | |
| """Return sources for a hashtag topic to display inline on homepage.""" | |
| research=f6._fast_context(topic) if hasattr(f6,'_fast_context') else f6._web_research_context(topic) | |
| sources=research.get('sources',[]) | |
| # Add og:image for each source | |
| from ai_runtime_patch_fast import _scrape | |
| for s in sources[:6]: | |
| if s.get('url') and not s.get('img'): | |
| try:_,_,img=_scrape(s['url'],500) | |
| except:img='' | |
| s['img']=img if img and len(img)>20 else '' | |
| return JSONResponse({'sources':sources[:6],'topic':topic}) | |
| # PRE_KILL fix | |
| UNIFIED_INJECT_FIXED = UNIFIED_INJECT.replace( | |
| """Object.defineProperty(window,'renderAIShorts7',{get:function(){return function(){}},set:function(){},configurable:true});""", | |
| """Object.defineProperty(window,'renderAIShorts7',{get:function(){return function(){}},set:function(){},configurable:true}); | |
| Object.defineProperty(window,'renderPatchedWall',{get:function(){return function(){}},set:function(){},configurable:true}); | |
| Object.defineProperty(window,'renderAiShorts',{get:function(){return function(){}},set:function(){},configurable:true}); | |
| Object.defineProperty(window,'renderWall',{get:function(){return function(){}},set:function(){},configurable:true}); | |
| Object.defineProperty(window,'renderAIShorts',{get:function(){return function(){}},set:function(){},configurable:true}); | |
| Object.defineProperty(window,'loadPatchedWall',{get:function(){return function(){}},set:function(){},configurable:true}); | |
| Object.defineProperty(window,'refreshFinalWall3',{get:function(){return function(){}},set:function(){},configurable:true});""" | |
| ) | |
| # Fix highlight fetch | |
| UNIFIED_INJECT_FIXED = UNIFIED_INJECT_FIXED.replace( | |
| "var articles=(window._hlLeagueData||{})[league]||[];\n if(!articles.length){el.innerHTML=", | |
| "var articles=(window._hlLeagueData||{})[league]||[];\n if(!articles.length){try{var _r=await fetch('/api/highlights/'+league);articles=await _r.json();if(!Array.isArray(articles))articles=[];}catch(e){articles=[];}}\n if(!articles.length){el.innerHTML=" | |
| ) | |
| # Highlight full override (same as 5a5b626) | |
| HIGHLIGHT_FULL_OVERRIDE = r''' | |
| <style> | |
| .tiktok-slide.ratio-wide video,.tiktok-slide.ratio-wide iframe{object-fit:contain!important} | |
| .hl-ask-panel{position:fixed;bottom:0;left:0;right:0;max-height:50vh;background:#181818;border-radius:16px 16px 0 0;z-index:99999;padding:14px;display:none;overflow-y:auto}.hl-ask-panel.active{display:block}.hl-ask-panel textarea,.hl-ask-panel input{width:100%;background:#222;border:1px solid #444;color:#eee;border-radius:10px;padding:9px;margin:6px 0}.hl-ask-panel button{background:#2d8659;border:0;color:#fff;border-radius:10px;padding:8px 12px;margin:4px}.hl-ask-answer{white-space:pre-wrap;color:#ccc;font-size:12px;margin-top:8px} | |
| .hashtag-sources{margin:8px 4px;background:#1a1a1a;border:1px solid #2a2a2a;border-radius:10px;padding:10px}.hashtag-sources h3{font-size:13px;color:#5cb87a;margin-bottom:8px}.hashtag-src-item{display:flex;gap:8px;padding:8px;background:#202020;border-radius:8px;margin:6px 0;cursor:pointer}.hashtag-src-item:active{opacity:.8}.hashtag-src-img{flex:0 0 80px;aspect-ratio:16/9;background:#333;border-radius:6px;overflow:hidden}.hashtag-src-img img{width:100%;height:100%;object-fit:cover}.hashtag-src-text{flex:1;min-width:0}.hashtag-src-title{font-size:12px;font-weight:700;color:#eee;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.hashtag-src-via{font-size:10px;color:#888;margin-top:2px}.hashtag-rewrite-btn{width:100%;margin-top:8px;background:#2d8659;border:0;color:#fff;padding:9px;border-radius:10px;font-size:12px;font-weight:700;cursor:pointer} | |
| </style> | |
| <div id="hl-ask-panel" class="hl-ask-panel"></div> | |
| <script> | |
| (function(){ | |
| function esc(s){return String(s||'').replace(/[&<>"']/g,function(m){return{'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]});} | |
| // === HASHTAG INLINE: click hashtag → show sources on homepage + rewrite button === | |
| window.showHashtagSources=async function(topic){ | |
| var home=document.getElementById('view-home');if(!home)return; | |
| document.getElementById('hashtag-sources-box')?.remove(); | |
| var box=document.createElement('div');box.id='hashtag-sources-box';box.className='hashtag-sources'; | |
| box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div style="color:#888;font-size:11px">Đang tìm nguồn...</div>'; | |
| var compose=home.querySelector('.ai-compose'); | |
| if(compose)compose.after(box);else home.prepend(box); | |
| try{ | |
| var r=await fetch('/api/hashtag/sources?topic='+encodeURIComponent(topic)); | |
| var j=await r.json();var sources=j.sources||[]; | |
| if(!sources.length){box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div style="color:#888;font-size:12px">Không tìm được nguồn</div>';return;} | |
| var h='<h3>🔍 '+esc(topic)+' <span style="font-size:10px;color:#888">('+sources.length+' nguồn)</span></h3>'; | |
| sources.forEach(function(s){ | |
| h+='<div class="hashtag-src-item" onclick="if(typeof readArticle===\'function\')readArticle(\''+esc(s.url||'')+'\')">'; | |
| h+='<div class="hashtag-src-img">'+(s.img?'<img src="'+esc(s.img)+'" onerror="this.style.display=\'none\'">':'')+'</div>'; | |
| h+='<div class="hashtag-src-text"><div class="hashtag-src-title">'+esc(s.title)+'</div><div class="hashtag-src-via">'+esc(s.via||s.source||'')+'</div></div>'; | |
| h+='</div>'; | |
| }); | |
| h+='<button class="hashtag-rewrite-btn" onclick="rewriteHashtagTopic(\''+esc(topic)+'\')">🤖 Rewrite AI tổng hợp nguồn & đăng tường</button>'; | |
| box.innerHTML=h; | |
| }catch(e){box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div style="color:#e74c3c;font-size:12px">Lỗi: '+esc(e.message)+'</div>';} | |
| }; | |
| window.rewriteHashtagTopic=async function(topic){ | |
| var btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang tổng hợp...';} | |
| try{ | |
| var r=await fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic:topic})}); | |
| var j=await r.json(); | |
| if(!r.ok||j.error)throw new Error(j.error||'Lỗi'); | |
| if(btn)btn.textContent='✅ Đã đăng lên Tường AI!'; | |
| setTimeout(function(){document.getElementById('hashtag-sources-box')?.remove();},2000); | |
| }catch(e){ | |
| if(btn){btn.disabled=false;btn.textContent='❌ '+e.message;} | |
| } | |
| }; | |
| // Override hashtag chip click to use showHashtagSources instead of topic input | |
| setTimeout(function(){ | |
| document.querySelectorAll('.hot-chip').forEach(function(chip){ | |
| chip.onclick=function(e){ | |
| e.preventDefault();e.stopPropagation(); | |
| var topic=chip.textContent.replace(/^#/,'').trim(); | |
| if(topic)showHashtagSources(topic); | |
| }; | |
| }); | |
| },3000); | |
| // Re-patch after hot topics load | |
| setInterval(function(){ | |
| document.querySelectorAll('.hot-chip:not([data-patched])').forEach(function(chip){ | |
| chip.dataset.patched='1'; | |
| chip.onclick=function(e){ | |
| e.preventDefault();e.stopPropagation(); | |
| var topic=chip.textContent.replace(/^#/,'').trim(); | |
| if(topic)showHashtagSources(topic); | |
| }; | |
| }); | |
| },2000); | |
| // === FULL openLeaguePlayer override (same as before) === | |
| window.openLeaguePlayer=async function(league,idx){ | |
| showView('view-tiktok');document.querySelectorAll('.cat').forEach(function(x){x.classList.remove('active')}); | |
| var el=document.getElementById('view-tiktok');el.innerHTML='<div class="loading">Đang tải highlight...</div>'; | |
| var cfg=(window.HL_CONFIG||{})[league]||{name:league,emoji:'🎬'}; | |
| var articles=(window._hlLeagueData||{})[league]||[]; | |
| if(!articles.length){try{var resp=await fetch('/api/highlights/'+league);articles=await resp.json();if(!Array.isArray(articles))articles=[];}catch(e){articles=[];}} | |
| if(!articles.length){el.innerHTML='<div class="loading">Không có video</div>';return;} | |
| var vids=[];var results=await Promise.all(articles.map(async function(a,i){try{var r=await fetch('/api/video_url?url='+encodeURIComponent(a.link));var v=await r.json();if(v&&v.src)return Object.assign({},a,v,{_idx:i});}catch(e){}return null;}));results.forEach(function(r){if(r)vids.push(r);});vids.sort(function(a,b){return a._idx-b._idx;}); | |
| if(!vids.length){el.innerHTML='<div class="loading">Không tìm thấy video</div>';return;} | |
| var ti=vids.findIndex(function(v){return v._idx===idx;});if(ti<0)ti=0;var ordered=ti>0?vids.slice(ti).concat(vids.slice(0,ti)):vids; | |
| var h='<button class="back-btn" onclick="switchCat(\'home\')">← '+esc(cfg.emoji)+' '+esc(cfg.name)+'</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">'; | |
| ordered.forEach(function(v,i){var hlid=encodeURIComponent(v.link||v.title);var isYT=v.type==='youtube';var isHLS=!isYT&&v.src&&v.src.indexOf('.m3u8')>-1;var poster=v.poster?' poster="'+v.poster+'"':'';var vtag=isYT?'<iframe data-yt-src="'+v.src+'" allowfullscreen allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture" style="width:100%;height:100%;border:none"></iframe>':isHLS?'<video playsinline preload="none"'+poster+' data-hls="'+v.src+'" loop controls style="width:100%;height:100%;object-fit:cover"></video>':'<video playsinline preload="none"'+poster+' loop controls style="width:100%;height:100%;object-fit:cover"><source src="'+v.src+'" type="video/mp4"></video>';h+='<div class="tiktok-slide" id="tslide-'+i+'" data-hlid="'+hlid+'">'+vtag+'<div class="tiktok-bottom"><span class="badge badge-fpt">'+esc(cfg.name)+'</span><p class="tiktok-title">'+esc(v.title)+'</p></div><div class="tiktok-right"><button class="tiktok-right-btn" onclick="event.stopPropagation();hlAct(this,\'view\')"><div class="icon">👁</div><div class="count" data-a="views">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();hlAct(this,\'like\')"><div class="icon">❤️</div><div class="count" data-a="likes">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();openHlComments(\''+hlid+'\')"><div class="icon">💬</div><div class="count">BL</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();openHlAsk(\''+hlid+'\',\''+esc(v.title)+'\')"><div class="icon">🤖</div><div class="count">Hỏi</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();hlAct(this,\'share\');if(typeof doShareVideo===\'function\')doShareVideo(\''+esc(v.title)+'\',\''+esc(v.link||'')+'\',\''+esc(v.poster||v.img||'')+'\',\'highlights\')"><div class="icon">📤</div><div class="count" data-a="shares">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();toggleHlRatio(this)"><div class="icon">⬜</div><div class="count">16:9</div></button></div><span class="tiktok-counter">'+(i+1)+'/'+ordered.length+'</span></div>';}); | |
| h+='</div></div>';el.innerHTML=h; | |
| var feed=document.getElementById('tiktok-feed');if(!feed)return;var slides=feed.querySelectorAll('.tiktok-slide');var cur=-1; | |
| function act(i){if(i===cur)return;slides.forEach(function(sl,idx){var v=sl.querySelector('video');var fr=sl.querySelector('iframe');if(idx===i){if(v&&v.dataset.hls){if(!v._hls&&typeof Hls!=='undefined'&&Hls.isSupported()){var hls=new Hls();hls.loadSource(v.dataset.hls);hls.attachMedia(v);hls.on(Hls.Events.MANIFEST_PARSED,function(){v.play().catch(function(){});});v._hls=hls;}else if(v._hls)v.play().catch(function(){});}else if(v)v.play().catch(function(){});if(fr&&!fr.src&&fr.dataset.ytSrc)fr.src=fr.dataset.ytSrc;hlAct(sl.querySelector('.tiktok-right .tiktok-right-btn'),'view');}else{if(v){v.pause();if(v._hls){v._hls.destroy();v._hls=null;}}if(fr&&fr.src)fr.src='';}});cur=i;} | |
| var sT;feed.addEventListener('scroll',function(){clearTimeout(sT);sT=setTimeout(function(){var rect=feed.getBoundingClientRect(),ctr=rect.top+rect.height/2,best=-1,bestD=1e9;slides.forEach(function(sl,i){var 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(function(){act(0);},400);slides.forEach(function(sl){var v=sl.querySelector('video');if(v)v.addEventListener('click',function(e){e.preventDefault();v.paused?v.play().catch(function(){}):v.pause();});}); | |
| var ids=[];slides.forEach(function(sl){if(sl.dataset.hlid)ids.push(sl.dataset.hlid);}); | |
| if(ids.length)fetch('/api/highlight/stats?ids='+ids.join(',')).then(function(r){return r.json()}).then(function(j){var stats=j.stats||{};slides.forEach(function(sl){var st=stats[sl.dataset.hlid];if(!st)return;var r=sl.querySelector('.tiktok-right');if(!r)return;var vc=r.querySelector('[data-a="views"]');if(vc)vc.textContent=st.views||0;var lc=r.querySelector('[data-a="likes"]');if(lc)lc.textContent=st.likes||0;var sc=r.querySelector('[data-a="shares"]');if(sc)sc.textContent=st.shares||0;});}).catch(function(){}); | |
| }; | |
| window.hlAct=async function(btn,action){var slide=btn?btn.closest('.tiktok-slide'):null;var id=slide?slide.dataset.hlid:'';if(!id)return;try{var r=await fetch('/api/highlight/interact',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:id,action:action})});var j=await r.json();if(j.stats&&slide){var right=slide.querySelector('.tiktok-right');if(right){var vc=right.querySelector('[data-a="views"]');if(vc)vc.textContent=j.stats.views||0;var lc=right.querySelector('[data-a="likes"]');if(lc)lc.textContent=j.stats.likes||0;var sc=right.querySelector('[data-a="shares"]');if(sc)sc.textContent=j.stats.shares||0;}}}catch(e){}}; | |
| window.toggleHlRatio=function(btn){var slide=btn.closest('.tiktok-slide');if(!slide)return;slide.classList.toggle('ratio-wide');var label=btn.querySelector('.count');if(label)label.textContent=slide.classList.contains('ratio-wide')?'1:1':'16:9';}; | |
| window.openHlComments=async function(id){var panel=document.getElementById('hl-ask-panel');var j=await fetch('/api/short/comments?id='+id).then(function(r){return r.json()}).catch(function(){return{comments:[]}});var cmts=j.comments||[];panel.innerHTML='<h3 style="color:#5cb87a;font-size:14px">💬 Bình luận</h3><div id="hl-cmt-list">'+(cmts.map(function(c){return'<div style="background:#222;border-radius:8px;padding:7px;margin:5px 0;color:#ccc;font-size:12px">'+esc(c.text)+'</div>'}).join('')||'<div style="color:#777;font-size:12px">Chưa có</div>')+'</div><textarea id="hl-cmt-text" placeholder="Bình luận..."></textarea><button onclick="submitHlCmt(\''+id+'\')">Gửi</button><button onclick="document.getElementById(\'hl-ask-panel\').classList.remove(\'active\')">Đóng</button>';panel.classList.add('active');}; | |
| window.submitHlCmt=async function(id){var t=document.getElementById('hl-cmt-text');if(!t||!t.value.trim())return;var j=await fetch('/api/short/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:id,text:t.value.trim()})}).then(function(r){return r.json()}).catch(function(){return{comments:[]}});document.getElementById('hl-cmt-list').innerHTML=(j.comments||[]).map(function(c){return'<div style="background:#222;border-radius:8px;padding:7px;margin:5px 0;color:#ccc;font-size:12px">'+esc(c.text)+'</div>'}).join('');t.value='';}; | |
| window.openHlAsk=function(id,title){var panel=document.getElementById('hl-ask-panel');panel.innerHTML='<h3 style="color:#5cb87a;font-size:14px">🤖 Hỏi AI</h3><input id="hl-ask-q" placeholder="Hỏi về: '+esc(title)+'..."><div id="hl-ask-ans" class="hl-ask-answer"></div><button onclick="submitHlAsk(\''+id+'\',\''+esc(title)+'\')">Hỏi</button><button onclick="document.getElementById(\'hl-ask-panel\').classList.remove(\'active\')">Đóng</button>';panel.classList.add('active');}; | |
| window.submitHlAsk=async function(id,title){var q=document.getElementById('hl-ask-q');if(!q||!q.value.trim())return;var ans=document.getElementById('hl-ask-ans');ans.textContent='Đang hỏi...';try{var r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question:q.value.trim(),context:'Video highlight: '+decodeURIComponent(title||id)})});var j=await r.json();ans.textContent=j.answer||'Không trả lời được';}catch(e){ans.textContent='Lỗi: '+e.message}}; | |
| })(); | |
| </script> | |
| ''' | |
| EXTRA_WALL_FIX = r''' | |
| <style>[data-wall-live="1"]{display:none!important}</style> | |
| <script> | |
| (function(){ | |
| var _wc=setInterval(function(){ | |
| var home=document.getElementById('view-home');if(!home||!home.classList.contains('active'))return; | |
| var has=document.getElementById('short-ai-final-slide'); | |
| if(!has&&typeof renderShortAISlide==='function')renderShortAISlide(); | |
| if(!document.querySelector('.slider-wrap[data-wall-live]')){ | |
| fetch('/api/ai_wall').then(function(r){return r.json()}).then(function(j){ | |
| var posts=(j&&j.posts)||[];if(!posts.length)return; | |
| if(typeof window._serverWall!=='undefined')window._serverWall=posts; | |
| if(typeof prependWallPost==='function')prependWallPost(posts[0]); | |
| }).catch(function(){}); | |
| } | |
| },4000); | |
| setTimeout(function(){clearInterval(_wc);},30000); | |
| })(); | |
| </script> | |
| ''' | |
| async def _index_fixed(): | |
| html=f5.f4.f3.f2.f1._load_index_html() | |
| body='' | |
| body+=getattr(rt.old,'PATCH_INJECT','') | |
| body+=f5.f4.f3.f2.f1.FINAL_INJECT+f5.f4.f3.FINAL3_INJECT+f5.f4.FINAL4_INJECT+f5.FINAL5_INJECT | |
| body+=getattr(f6,'FINAL6_INJECT','') | |
| body+=getattr(f6,'FINAL6_FAST_HOME_INJECT','') | |
| body+=getattr(f6,'FINAL6E_INJECT','') | |
| body+=PATCH_INJECT | |
| body+=UNIFIED_INJECT_FIXED | |
| body+=HIGHLIGHT_FULL_OVERRIDE | |
| body+=EXTRA_WALL_FIX | |
| return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body) | |