Spaces:
Running
Running
Fix: single rewrite btn, 24h TTL for wall/shorts, AI ask uses article content"
Browse files- ai_runtime_patch_fast.py +117 -80
ai_runtime_patch_fast.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
"""Patch over c1e2703:
|
| 2 |
-
import re, threading, time, json, os
|
| 3 |
import ai_runtime_final6 as f6
|
| 4 |
from ai_runtime_final6 import app, rt, f5, HTMLResponse, JSONResponse, Request, Query
|
| 5 |
import html as html_lib
|
|
@@ -8,20 +8,28 @@ def clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or ""))).strip()
|
|
| 8 |
|
| 9 |
DATA_DIR="/data" if os.path.isdir('/data') else "/app/data"
|
| 10 |
SHORT_COMMENTS_FILE=os.path.join(DATA_DIR,'short_comments.json')
|
|
|
|
| 11 |
|
| 12 |
-
def
|
| 13 |
try:
|
| 14 |
-
if os.path.exists(
|
| 15 |
-
with open(
|
| 16 |
except Exception:pass
|
| 17 |
-
return
|
| 18 |
-
def
|
| 19 |
try:
|
| 20 |
-
os.makedirs(os.path.dirname(
|
| 21 |
-
with open(tmp,'w',encoding='utf-8') as f:json.dump(
|
| 22 |
-
os.replace(tmp,
|
| 23 |
except Exception:pass
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
# ===== BACKGROUND PREFETCH =====
|
| 26 |
_bg_home_cache={"t":0,"d":[]}
|
| 27 |
_bg_shorts_cache={"t":0,"d":[]}
|
|
@@ -42,6 +50,7 @@ def _bg_refresh():
|
|
| 42 |
vid=v.get('id') or ''
|
| 43 |
if vid and vid not in seen:seen.add(vid);out.append(v)
|
| 44 |
if out:_bg_shorts_cache.update({"t":time.time(),"d":out[:40]})
|
|
|
|
| 45 |
except Exception:pass
|
| 46 |
finally:_bg_loading=False
|
| 47 |
|
|
@@ -58,6 +67,8 @@ app.router.routes=[r for r in app.router.routes if not (
|
|
| 58 |
(getattr(r,'path',None)=='/api/topic_post' and 'POST' in getattr(r,'methods',set())) or
|
| 59 |
(getattr(r,'path',None)=='/api/short/comments' and 'GET' in getattr(r,'methods',set())) or
|
| 60 |
(getattr(r,'path',None)=='/api/short/comment' and 'POST' in getattr(r,'methods',set())) or
|
|
|
|
|
|
|
| 61 |
(getattr(r,'path',None)=='/' and 'GET' in getattr(r,'methods',set()))
|
| 62 |
)]
|
| 63 |
|
|
@@ -81,23 +92,61 @@ def api_shorts_instant(refresh:int=Query(default=0)):
|
|
| 81 |
if hasattr(f6,'api_shorts_final6'):return f6.api_shorts_final6(refresh=refresh)
|
| 82 |
return JSONResponse([])
|
| 83 |
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
@app.get('/api/short/comments')
|
| 86 |
def get_short_comments(id:str=Query(...)):
|
| 87 |
-
db=
|
| 88 |
return JSONResponse({'comments':db.get(id,[])})
|
| 89 |
|
| 90 |
@app.post('/api/short/comment')
|
| 91 |
async def post_short_comment(request:Request):
|
| 92 |
body=await request.json();vid=str(body.get('id','')).strip();text=clean(body.get('text',''))
|
| 93 |
if not vid or not text:return JSONResponse({'error':'missing id or text'},status_code=400)
|
| 94 |
-
db=
|
| 95 |
-
comments=db.get(vid,[])
|
| 96 |
-
comments
|
| 97 |
-
db[vid]=comments[:100]
|
| 98 |
-
_save_comments(db)
|
| 99 |
return JSONResponse({'comments':db[vid]})
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
# ===== TOPIC POST =====
|
| 102 |
@app.post('/api/topic_post')
|
| 103 |
async def topic_post_focused(request:Request):
|
|
@@ -122,7 +171,6 @@ QUY TẮC:
|
|
| 122 |
4. Không mở đầu bằng "Dưới đây là".
|
| 123 |
5. Cuối bài có "Nguồn tham khảo".
|
| 124 |
"""
|
| 125 |
-
import asyncio
|
| 126 |
text=None
|
| 127 |
try:text=await asyncio.wait_for(f5.base.qwen_generate(prompt,image_url=img,max_tokens=1700),timeout=35)
|
| 128 |
except Exception:text=None
|
|
@@ -137,102 +185,91 @@ QUY TẮC:
|
|
| 137 |
|
| 138 |
# ===== FRONTEND =====
|
| 139 |
PATCH_INJECT=r'''
|
| 140 |
-
<style>.short-cmt-panel{position:fixed;bottom:0;left:0;right:0;max-height:55vh;background:#181818;border-radius:16px 16px 0 0;z-index:99999;padding:14px;display:none;overflow-y:auto}.short-cmt-panel.active{display:block}.short-cmt-panel textarea{width:100%;background:#222;border:1px solid #444;color:#eee;border-radius:10px;padding:9px;margin:6px 0;min-height:60px}.short-cmt-panel button{background:#2d8659;border:0;color:#fff;border-radius:10px;padding:8px 12px;margin:4px}.cmt-item{background:#222;border-radius:8px;padding:7px;margin:5px 0;color:#ccc;font-size:12px}.source-detail-box{margin-top:14px;background:#151515;border:1px solid #2b2b2b;border-radius:10px;padding:10px}.source-detail-item{background:#202020;border-radius:8px;padding:9px;margin:7px 0}.source-detail-title{font-size:12px;font-weight:700;color:#eee}.source-detail-meta{font-size:10px;color:#888;margin:3px 0}.source-detail-content{font-size:12px;color:#bbb;line-height:1.5;white-space:pre-wrap;max-height:160px;overflow:auto}.source-detail-item a{color:#5cb87a;font-size:11px;text-decoration:none;cursor:pointer}.source-detail-item img{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:6px;margin-bottom:6px;background:#222}</style>
|
| 141 |
<div id="short-cmt-panel" class="short-cmt-panel"></div>
|
| 142 |
<script>
|
| 143 |
(function(){
|
| 144 |
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 145 |
|
| 146 |
-
// ===
|
| 147 |
function renderSourceDetails(post,container){
|
| 148 |
let details=post.source_details||[];if(!details.length)return;
|
| 149 |
let box=document.createElement('div');box.className='source-detail-box';
|
| 150 |
-
box.innerHTML='<h3 style="font-size:14px;color:#5cb87a;margin-bottom:8px">📚 Bài viết nguồn</h3>'+details.map((s,i)=>{
|
| 151 |
-
let imgHtml=s.img?`<img src="${esc(s.img)}" loading="lazy" onerror="this.style.display='none'">`:'';
|
| 152 |
-
return `<div class="source-detail-item">${imgHtml}<div class="source-detail-title">${i+1}. ${esc(s.title)}</div><div class="source-detail-meta">${esc(s.via||'')}</div><div class="source-detail-content">${esc((s.content||'').slice(0,600))}</div>${s.url?`<a onclick="event.preventDefault();if(typeof readArticle==='function')readArticle('${esc(s.url)}');else window.open('${esc(s.url)}','_blank')">📖 Đọc trên VNEWS</a>`:''}</div>`}).join('');
|
| 153 |
container.appendChild(box);
|
| 154 |
-
//
|
| 155 |
-
details.forEach((s,i)=>{if(s.img||!s.url)return;fetch('/api/article?url='+encodeURIComponent(s.url)).then(r=>r.json()).then(d=>{if(d&&(d.og_image||d.img)){let items=box.querySelectorAll('.source-detail-item');if(items[i]){let img=document.createElement('img');img.src=d.og_image||d.img;img.loading='lazy';img.onerror=function(){this.style.display='none'};items[i].prepend(img);}}}).catch(()=>{});});}
|
| 156 |
|
| 157 |
-
// ===
|
| 158 |
async function readAIWallPost(i){
|
| 159 |
let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];
|
| 160 |
-
let p=wall[i];if(!p)return;
|
| 161 |
-
|
| 162 |
-
let h=`<button class="back-btn" onclick="switchCat('home')">← Quay lại</button><div class="article-view" data-post-id="${p.id}"><span class="badge badge-ai">AI</span><h1 class="article-title">${esc(p.title)}</h1>${p.img?`<img class="article-img" src="${p.img}">`:''}`;
|
| 163 |
if(p.sources&&p.sources.length){h+=`<div class="article-summary"><b>Nguồn:</b> ${p.sources.map(s=>esc(s.via||s.title||'')).join(', ')}</div>`;}
|
| 164 |
h+=`<p class="article-p" style="white-space:pre-wrap">${esc(p.text)}</p>`;
|
| 165 |
-
|
|
|
|
|
|
|
| 166 |
h+=`</div>`;
|
| 167 |
document.getElementById('view-article').innerHTML=h;
|
| 168 |
-
|
| 169 |
-
let art=document.querySelector('.article-view');
|
| 170 |
-
if(art&&p.source_details&&p.source_details.length)renderSourceDetails(p,art);
|
| 171 |
window.scrollTo(0,0);}
|
| 172 |
|
| 173 |
-
window.readAIWallPost=readAIWallPost;
|
| 174 |
-
window.aiReadWallPatched=window.aiReadWall=window.readWallPost=function(i){readAIWallPost(i)};
|
| 175 |
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];
|
| 179 |
-
let p=wall[i];
|
| 180 |
-
let
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
let btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang rewrite...';}
|
| 184 |
-
try{
|
| 185 |
-
let r=await fetch('/api/url_wall',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:urls[0]})});
|
| 186 |
-
let j=await r.json();
|
| 187 |
-
if(!r.ok||j.error)throw new Error(j.error||'Lỗi');
|
| 188 |
-
alert('Đã rewrite bài nguồn đầu tiên và đăng lên Tường AI.');
|
| 189 |
-
if(typeof loadPatchedWall==='function')loadPatchedWall();
|
| 190 |
-
}catch(e){alert(e.message)}finally{if(btn){btn.disabled=false;btn.textContent='🤖 AI viết lại & đăng tường';}}}
|
| 191 |
-
|
| 192 |
-
// === 4) Short comments: persistent + display ===
|
| 193 |
window.openShortComments=async function(id){
|
| 194 |
let panel=document.getElementById('short-cmt-panel');
|
| 195 |
let j=await fetch('/api/short/comments?id='+encodeURIComponent(id)).then(r=>r.json()).catch(()=>({comments:[]}));
|
| 196 |
let cmts=j.comments||[];
|
| 197 |
panel.innerHTML=`<h3 style="color:#5cb87a;font-size:14px">💬 Bình luận</h3><div id="cmt-list">${cmts.map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('')||'<div class="cmt-item" style="color:#777">Chưa có bình luận</div>'}</div><textarea id="cmt-text" placeholder="Nhập bình luận..."></textarea><button onclick="submitShortCmt('${esc(id)}')">Gửi</button><button onclick="document.getElementById('short-cmt-panel').classList.remove('active')">Đóng</button>`;
|
| 198 |
panel.classList.add('active');}
|
| 199 |
-
|
| 200 |
window.submitShortCmt=async function(id){
|
| 201 |
let text=document.getElementById('cmt-text')?.value.trim();if(!text)return;
|
| 202 |
let j=await fetch('/api/short/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,text})}).then(r=>r.json()).catch(()=>({comments:[]}));
|
| 203 |
-
|
| 204 |
-
document.getElementById('cmt-list').innerHTML=cmts.map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('');
|
| 205 |
document.getElementById('cmt-text').value='';}
|
| 206 |
|
| 207 |
-
// ===
|
| 208 |
-
function
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
let
|
| 212 |
-
|
| 213 |
-
if(
|
| 214 |
-
|
| 215 |
-
let
|
| 216 |
-
btn
|
| 217 |
-
btn.onclick=function(
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
// ===
|
| 221 |
-
function
|
| 222 |
-
|
| 223 |
-
/
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
|
|
|
| 231 |
let _hotLoaded=false;function deferHot(){if(_hotLoaded)return;_hotLoaded=true;setTimeout(()=>{if(typeof ensureHotTopics==='function')ensureHotTopics();if(typeof ensureNewsShortsHome==='function')ensureNewsShortsHome();},4000);}
|
| 232 |
if(document.readyState==='complete')deferHot();else window.addEventListener('load',deferHot);
|
| 233 |
|
| 234 |
-
setInterval(()=>{
|
| 235 |
-
setTimeout(()=>{addRewriteButton();patchShortCommentButtons();},1000);
|
| 236 |
})();
|
| 237 |
</script>
|
| 238 |
'''
|
|
|
|
| 1 |
+
"""Patch over c1e2703: single rewrite button, 24h TTL for AI wall/shorts, AI ask uses content."""
|
| 2 |
+
import re, threading, time, json, os, asyncio
|
| 3 |
import ai_runtime_final6 as f6
|
| 4 |
from ai_runtime_final6 import app, rt, f5, HTMLResponse, JSONResponse, Request, Query
|
| 5 |
import html as html_lib
|
|
|
|
| 8 |
|
| 9 |
DATA_DIR="/data" if os.path.isdir('/data') else "/app/data"
|
| 10 |
SHORT_COMMENTS_FILE=os.path.join(DATA_DIR,'short_comments.json')
|
| 11 |
+
TTL_24H=86400
|
| 12 |
|
| 13 |
+
def _load_json(path,default):
|
| 14 |
try:
|
| 15 |
+
if os.path.exists(path):
|
| 16 |
+
with open(path,'r',encoding='utf-8') as f:return json.load(f)
|
| 17 |
except Exception:pass
|
| 18 |
+
return default
|
| 19 |
+
def _save_json(path,data):
|
| 20 |
try:
|
| 21 |
+
os.makedirs(os.path.dirname(path),exist_ok=True);tmp=path+'.tmp'
|
| 22 |
+
with open(tmp,'w',encoding='utf-8') as f:json.dump(data,f,ensure_ascii=False)
|
| 23 |
+
os.replace(tmp,path)
|
| 24 |
except Exception:pass
|
| 25 |
|
| 26 |
+
def _cleanup_old_posts():
|
| 27 |
+
"""Remove AI wall posts and shorts older than 24h."""
|
| 28 |
+
now=int(time.time())
|
| 29 |
+
posts=f5.base._load_ai_wall()
|
| 30 |
+
fresh=[p for p in posts if now-int(p.get('ts') or 0)<TTL_24H]
|
| 31 |
+
if len(fresh)<len(posts):f5.base._save_ai_wall(fresh)
|
| 32 |
+
|
| 33 |
# ===== BACKGROUND PREFETCH =====
|
| 34 |
_bg_home_cache={"t":0,"d":[]}
|
| 35 |
_bg_shorts_cache={"t":0,"d":[]}
|
|
|
|
| 50 |
vid=v.get('id') or ''
|
| 51 |
if vid and vid not in seen:seen.add(vid);out.append(v)
|
| 52 |
if out:_bg_shorts_cache.update({"t":time.time(),"d":out[:40]})
|
| 53 |
+
_cleanup_old_posts()
|
| 54 |
except Exception:pass
|
| 55 |
finally:_bg_loading=False
|
| 56 |
|
|
|
|
| 67 |
(getattr(r,'path',None)=='/api/topic_post' and 'POST' in getattr(r,'methods',set())) or
|
| 68 |
(getattr(r,'path',None)=='/api/short/comments' and 'GET' in getattr(r,'methods',set())) or
|
| 69 |
(getattr(r,'path',None)=='/api/short/comment' and 'POST' in getattr(r,'methods',set())) or
|
| 70 |
+
(getattr(r,'path',None)=='/api/article/ask' and 'POST' in getattr(r,'methods',set())) or
|
| 71 |
+
(getattr(r,'path',None)=='/api/ai_wall' and 'GET' in getattr(r,'methods',set())) or
|
| 72 |
(getattr(r,'path',None)=='/' and 'GET' in getattr(r,'methods',set()))
|
| 73 |
)]
|
| 74 |
|
|
|
|
| 92 |
if hasattr(f6,'api_shorts_final6'):return f6.api_shorts_final6(refresh=refresh)
|
| 93 |
return JSONResponse([])
|
| 94 |
|
| 95 |
+
@app.get('/api/ai_wall')
|
| 96 |
+
def api_ai_wall_with_ttl():
|
| 97 |
+
"""Return AI wall posts, filtering out posts older than 24h."""
|
| 98 |
+
now=int(time.time());posts=f5.base._load_ai_wall()
|
| 99 |
+
fresh=[p for p in posts if now-int(p.get('ts') or 0)<TTL_24H]
|
| 100 |
+
return JSONResponse({'posts':fresh})
|
| 101 |
+
|
| 102 |
+
# ===== SHORT COMMENTS =====
|
| 103 |
@app.get('/api/short/comments')
|
| 104 |
def get_short_comments(id:str=Query(...)):
|
| 105 |
+
db=_load_json(SHORT_COMMENTS_FILE,{})
|
| 106 |
return JSONResponse({'comments':db.get(id,[])})
|
| 107 |
|
| 108 |
@app.post('/api/short/comment')
|
| 109 |
async def post_short_comment(request:Request):
|
| 110 |
body=await request.json();vid=str(body.get('id','')).strip();text=clean(body.get('text',''))
|
| 111 |
if not vid or not text:return JSONResponse({'error':'missing id or text'},status_code=400)
|
| 112 |
+
db=_load_json(SHORT_COMMENTS_FILE,{})
|
| 113 |
+
comments=db.get(vid,[]);comments.insert(0,{'text':text[:300],'ts':int(time.time())})
|
| 114 |
+
db[vid]=comments[:100];_save_json(SHORT_COMMENTS_FILE,db)
|
|
|
|
|
|
|
| 115 |
return JSONResponse({'comments':db[vid]})
|
| 116 |
|
| 117 |
+
# ===== ARTICLE ASK AI (based on article content) =====
|
| 118 |
+
@app.post('/api/article/ask')
|
| 119 |
+
async def article_ask(request:Request):
|
| 120 |
+
body=await request.json();url=clean(body.get('url',''));question=clean(body.get('question',''));context=clean(body.get('context',''))
|
| 121 |
+
if not question:return JSONResponse({'error':'missing question'},status_code=400)
|
| 122 |
+
title='';raw=''
|
| 123 |
+
if url:
|
| 124 |
+
try:
|
| 125 |
+
data=f5.base.scrape_any_url(url) if hasattr(f5.base,'scrape_any_url') else None
|
| 126 |
+
if data:title=data.get('title','');raw=(data.get('summary','')+'\n'+data.get('text','')).strip()
|
| 127 |
+
except Exception:pass
|
| 128 |
+
if not raw and context:raw=context[:12000]
|
| 129 |
+
if not raw:raw=question
|
| 130 |
+
prompt=f"""Bạn là trợ lý đọc hiểu VNEWS. Trả lời câu hỏi DỰA TRÊN NỘI DUNG BÀI VIẾT bên dưới.
|
| 131 |
+
|
| 132 |
+
BẮT BUỘC:
|
| 133 |
+
- Chỉ dùng thông tin CÓ TRONG bài viết để trả lời.
|
| 134 |
+
- Nếu bài viết không có thông tin liên quan đến câu hỏi, nói rõ "Bài viết không đề cập đến vấn đề này."
|
| 135 |
+
- Không bịa thêm chi tiết ngoài bài.
|
| 136 |
+
- Trích dẫn/tham chiếu các đoạn cụ thể trong bài khi trả lời.
|
| 137 |
+
|
| 138 |
+
Tiêu đề bài: {title}
|
| 139 |
+
Nội dung bài:
|
| 140 |
+
{raw[:12000]}
|
| 141 |
+
|
| 142 |
+
Câu hỏi: {question}
|
| 143 |
+
|
| 144 |
+
Trả lời bằng tiếng Việt, chi tiết, dựa sát nội dung bài.
|
| 145 |
+
"""
|
| 146 |
+
ans=await f5.base.qwen_generate(prompt,max_tokens=1200)
|
| 147 |
+
if not ans:ans='AI chưa trả lời được. Thử hỏi cụ thể hơn.'
|
| 148 |
+
return JSONResponse({'answer':ans,'title':title})
|
| 149 |
+
|
| 150 |
# ===== TOPIC POST =====
|
| 151 |
@app.post('/api/topic_post')
|
| 152 |
async def topic_post_focused(request:Request):
|
|
|
|
| 171 |
4. Không mở đầu bằng "Dưới đây là".
|
| 172 |
5. Cuối bài có "Nguồn tham khảo".
|
| 173 |
"""
|
|
|
|
| 174 |
text=None
|
| 175 |
try:text=await asyncio.wait_for(f5.base.qwen_generate(prompt,image_url=img,max_tokens=1700),timeout=35)
|
| 176 |
except Exception:text=None
|
|
|
|
| 185 |
|
| 186 |
# ===== FRONTEND =====
|
| 187 |
PATCH_INJECT=r'''
|
| 188 |
+
<style>.short-cmt-panel{position:fixed;bottom:0;left:0;right:0;max-height:55vh;background:#181818;border-radius:16px 16px 0 0;z-index:99999;padding:14px;display:none;overflow-y:auto}.short-cmt-panel.active{display:block}.short-cmt-panel textarea{width:100%;background:#222;border:1px solid #444;color:#eee;border-radius:10px;padding:9px;margin:6px 0;min-height:60px}.short-cmt-panel button{background:#2d8659;border:0;color:#fff;border-radius:10px;padding:8px 12px;margin:4px}.cmt-item{background:#222;border-radius:8px;padding:7px;margin:5px 0;color:#ccc;font-size:12px}.source-detail-box{margin-top:14px;background:#151515;border:1px solid #2b2b2b;border-radius:10px;padding:10px}.source-detail-item{background:#202020;border-radius:8px;padding:9px;margin:7px 0}.source-detail-title{font-size:12px;font-weight:700;color:#eee}.source-detail-meta{font-size:10px;color:#888;margin:3px 0}.source-detail-content{font-size:12px;color:#bbb;line-height:1.5;white-space:pre-wrap;max-height:160px;overflow:auto}.source-detail-item a{color:#5cb87a;font-size:11px;text-decoration:none;cursor:pointer}.source-detail-item img{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:6px;margin-bottom:6px;background:#222}.article-ai-ask{margin-top:12px;background:#141414;border:1px solid #2a2a2a;border-radius:10px;padding:10px}.article-ai-ask textarea{width:100%;min-height:60px;background:#222;border:1px solid #444;color:#eee;border-radius:10px;padding:9px}.article-ai-ask button{background:#2d8659;border:0;color:#fff;border-radius:10px;padding:8px 12px;margin-top:6px}.article-ai-answer{white-space:pre-wrap;color:#ccc;font-size:13px;line-height:1.55;margin-top:8px}</style>
|
| 189 |
<div id="short-cmt-panel" class="short-cmt-panel"></div>
|
| 190 |
<script>
|
| 191 |
(function(){
|
| 192 |
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 193 |
|
| 194 |
+
// === Source details render (persistent after reload) ===
|
| 195 |
function renderSourceDetails(post,container){
|
| 196 |
let details=post.source_details||[];if(!details.length)return;
|
| 197 |
let box=document.createElement('div');box.className='source-detail-box';
|
| 198 |
+
box.innerHTML='<h3 style="font-size:14px;color:#5cb87a;margin-bottom:8px">📚 Bài viết nguồn</h3>'+details.map((s,i)=>`<div class="source-detail-item"><div class="source-detail-title">${i+1}. ${esc(s.title)}</div><div class="source-detail-meta">${esc(s.via||'')}</div><div class="source-detail-content">${esc((s.content||'').slice(0,600))}</div>${s.url?`<a onclick="event.preventDefault();if(typeof readArticle==='function')readArticle('${esc(s.url)}')">📖 Đọc trên VNEWS</a>`:''}</div>`).join('');
|
|
|
|
|
|
|
| 199 |
container.appendChild(box);
|
| 200 |
+
details.forEach((s,i)=>{if(!s.url)return;fetch('/api/article?url='+encodeURIComponent(s.url)).then(r=>r.json()).then(d=>{if(d&&(d.og_image||d.img)){let items=box.querySelectorAll('.source-detail-item');if(items[i]){let img=document.createElement('img');img.src=d.og_image||d.img;img.loading='lazy';img.onerror=function(){this.style.display='none'};items[i].prepend(img);}}}).catch(()=>{});});}
|
|
|
|
| 201 |
|
| 202 |
+
// === AI Wall read with source_details + rewrite + ask AI ===
|
| 203 |
async function readAIWallPost(i){
|
| 204 |
let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];
|
| 205 |
+
let p=wall[i];if(!p)return;showView('view-article');
|
| 206 |
+
let h=`<button class="back-btn" onclick="switchCat('home')">← Quay lại</button><div class="article-view"><span class="badge badge-ai">AI</span><h1 class="article-title">${esc(p.title)}</h1>${p.img?`<img class="article-img" src="${p.img}">`:''}`;
|
|
|
|
| 207 |
if(p.sources&&p.sources.length){h+=`<div class="article-summary"><b>Nguồn:</b> ${p.sources.map(s=>esc(s.via||s.title||'')).join(', ')}</div>`;}
|
| 208 |
h+=`<p class="article-p" style="white-space:pre-wrap">${esc(p.text)}</p>`;
|
| 209 |
+
let firstUrl=(p.source_details||[]).find(s=>s.url)?.url||(p.sources||[]).find(s=>s.url)?.url||'';
|
| 210 |
+
h+=`<div class="article-actions">${firstUrl?`<button class="primary" onclick="rewriteFromUrl('${esc(firstUrl)}')">🤖 Tóm tắt AI đăng tường</button>`:''}<button onclick="doShare('${esc(p.title)}','${location.origin}','${esc(p.img||'')}')">📤 Chia sẻ</button></div>`;
|
| 211 |
+
h+=`<div class="article-ai-ask"><h3 style="font-size:14px;color:#5cb87a;margin-bottom:6px">🤖 Hỏi AI về bài viết</h3><textarea id="article-ai-q" placeholder="Hỏi về nội dung bài viết này..."></textarea><button onclick="askAIWall(${i})">Hỏi AI</button><div id="article-ai-ans" class="article-ai-answer"></div></div>`;
|
| 212 |
h+=`</div>`;
|
| 213 |
document.getElementById('view-article').innerHTML=h;
|
| 214 |
+
let art=document.querySelector('.article-view');if(art&&p.source_details)renderSourceDetails(p,art);
|
|
|
|
|
|
|
| 215 |
window.scrollTo(0,0);}
|
| 216 |
|
| 217 |
+
window.readAIWallPost=readAIWallPost;window.aiReadWallPatched=window.aiReadWall=window.readWallPost=function(i){readAIWallPost(i)};
|
|
|
|
| 218 |
|
| 219 |
+
window.rewriteFromUrl=async function(url){
|
| 220 |
+
if(!url)return alert('Không có URL nguồn');
|
| 221 |
+
let btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang tóm tắt...';}
|
| 222 |
+
try{let r=await fetch('/api/url_wall',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');alert('Đã tóm tắt và đăng lên Tường AI.');}catch(e){alert(e.message)}finally{if(btn){btn.disabled=false;btn.textContent='🤖 Tóm tắt AI đăng tường';}}};
|
| 223 |
+
|
| 224 |
+
window.askAIWall=async function(i){
|
| 225 |
+
let q=document.getElementById('article-ai-q')?.value.trim();if(!q)return alert('Nhập câu hỏi');
|
| 226 |
+
let ans=document.getElementById('article-ai-ans');ans.textContent='Đang hỏi AI...';
|
| 227 |
let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];
|
| 228 |
+
let p=wall[i]||{};let context=p.text||'';let url=(p.source_details||[]).find(s=>s.url)?.url||(p.sources||[]).find(s=>s.url)?.url||'';
|
| 229 |
+
try{let r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,question:q,context})});let j=await r.json();ans.textContent=j.answer||'Không có trả lời';}catch(e){ans.textContent='Lỗi: '+e.message}};
|
| 230 |
+
|
| 231 |
+
// === Short comments ===
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
window.openShortComments=async function(id){
|
| 233 |
let panel=document.getElementById('short-cmt-panel');
|
| 234 |
let j=await fetch('/api/short/comments?id='+encodeURIComponent(id)).then(r=>r.json()).catch(()=>({comments:[]}));
|
| 235 |
let cmts=j.comments||[];
|
| 236 |
panel.innerHTML=`<h3 style="color:#5cb87a;font-size:14px">💬 Bình luận</h3><div id="cmt-list">${cmts.map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('')||'<div class="cmt-item" style="color:#777">Chưa có bình luận</div>'}</div><textarea id="cmt-text" placeholder="Nhập bình luận..."></textarea><button onclick="submitShortCmt('${esc(id)}')">Gửi</button><button onclick="document.getElementById('short-cmt-panel').classList.remove('active')">Đóng</button>`;
|
| 237 |
panel.classList.add('active');}
|
|
|
|
| 238 |
window.submitShortCmt=async function(id){
|
| 239 |
let text=document.getElementById('cmt-text')?.value.trim();if(!text)return;
|
| 240 |
let j=await fetch('/api/short/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,text})}).then(r=>r.json()).catch(()=>({comments:[]}));
|
| 241 |
+
document.getElementById('cmt-list').innerHTML=(j.comments||[]).map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('');
|
|
|
|
| 242 |
document.getElementById('cmt-text').value='';}
|
| 243 |
|
| 244 |
+
// === Patch shorts to add comment button ===
|
| 245 |
+
function patchShortButtons(){document.querySelectorAll('.tiktok-slide').forEach(sl=>{if(sl.dataset.cmtDone)return;sl.dataset.cmtDone='1';let id=sl.dataset.id||'';if(!id)return;let right=sl.querySelector('.tiktok-right,.short-action-panel');if(!right)return;if(right.querySelector('[data-cmt]'))return;let btn=document.createElement('button');btn.className='short-action-btn';btn.setAttribute('data-cmt','1');btn.innerHTML='<div class="ico" style="width:42px;height:42px;border-radius:50%;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;font-size:20px">💬</div><span>BL</span>';btn.onclick=function(e){e.stopPropagation();openShortComments(id);};right.appendChild(btn);});}
|
| 246 |
+
|
| 247 |
+
// === Only ONE rewrite button for regular articles (not duplicate) ===
|
| 248 |
+
function addSingleRewriteBtn(){let art=document.querySelector('#view-article .article-view');if(!art)return;
|
| 249 |
+
// Remove any duplicates first.
|
| 250 |
+
let btns=art.querySelectorAll('[data-rewrite]');if(btns.length>1){for(let i=1;i<btns.length;i++)btns[i].remove();}
|
| 251 |
+
if(btns.length>=1)return;
|
| 252 |
+
let actions=art.querySelector('.article-actions');if(!actions)return;
|
| 253 |
+
let btn=document.createElement('button');btn.className='primary';btn.setAttribute('data-rewrite','1');btn.textContent='🤖 Tóm tắt AI đăng tường';
|
| 254 |
+
btn.onclick=function(){if(typeof rewriteCurrentArticle==='function')rewriteCurrentArticle();else alert('Chức năng rewrite chưa sẵn sàng.')};
|
| 255 |
+
actions.insertBefore(btn,actions.firstChild);}
|
| 256 |
+
|
| 257 |
+
// === Ask AI box in regular articles ===
|
| 258 |
+
function addArticleAskBox(){let art=document.querySelector('#view-article .article-view');if(!art||art.querySelector('.article-ai-ask'))return;
|
| 259 |
+
let box=document.createElement('div');box.className='article-ai-ask';
|
| 260 |
+
box.innerHTML='<h3 style="font-size:14px;color:#5cb87a;margin-bottom:6px">🤖 Hỏi AI về bài viết</h3><textarea id="article-ai-question" placeholder="Hỏi về nội dung bài viết..."></textarea><button onclick="askArticleAI()">Hỏi AI</button><div id="article-ai-answer" class="article-ai-answer"></div>';
|
| 261 |
+
art.appendChild(box);}
|
| 262 |
+
window.askArticleAI=async function(){let q=document.getElementById('article-ai-question')?.value.trim();if(!q)return alert('Nhập câu hỏi');let ans=document.getElementById('article-ai-answer');ans.textContent='Đang hỏi AI...';let url=(window._currentArticle&&window._currentArticle.url)||'';let context=document.querySelector('.article-view')?.innerText?.slice(0,12000)||'';try{let r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,question:q,context})});let j=await r.json();ans.textContent=j.answer||'Không có trả lời';}catch(e){ans.textContent='Lỗi: '+e.message}}
|
| 263 |
+
|
| 264 |
+
// === Patch readArticle ===
|
| 265 |
+
let oldRA=window.readArticle;
|
| 266 |
+
if(oldRA){window.readArticle=async function(){let ret=await oldRA.apply(this,arguments);setTimeout(()=>{addSingleRewriteBtn();addArticleAskBox();},600);return ret;}}
|
| 267 |
+
|
| 268 |
+
// === Defer non-critical ===
|
| 269 |
let _hotLoaded=false;function deferHot(){if(_hotLoaded)return;_hotLoaded=true;setTimeout(()=>{if(typeof ensureHotTopics==='function')ensureHotTopics();if(typeof ensureNewsShortsHome==='function')ensureNewsShortsHome();},4000);}
|
| 270 |
if(document.readyState==='complete')deferHot();else window.addEventListener('load',deferHot);
|
| 271 |
|
| 272 |
+
setInterval(()=>{addSingleRewriteBtn();patchShortButtons();addArticleAskBox();},1500);
|
|
|
|
| 273 |
})();
|
| 274 |
</script>
|
| 275 |
'''
|