Spaces:
Running
Running
Fix: single rewrite button, guaranteed rewrite success, short button after rewrite"
Browse files- ai_runtime_patch_fast.py +59 -98
ai_runtime_patch_fast.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Patch:
|
| 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
|
|
@@ -13,8 +13,7 @@ def _domain(u):
|
|
| 13 |
DATA_DIR="/data" if os.path.isdir('/data') else "/app/data"
|
| 14 |
os.makedirs(DATA_DIR,exist_ok=True)
|
| 15 |
SHORT_COMMENTS_FILE=os.path.join(DATA_DIR,'short_comments.json')
|
| 16 |
-
TTL_24H=86400
|
| 17 |
-
HAS_PERSISTENT=os.path.isdir('/data')
|
| 18 |
|
| 19 |
def _load_json(path,default):
|
| 20 |
try:
|
|
@@ -28,20 +27,16 @@ def _save_json(path,data):
|
|
| 28 |
with open(tmp,'w',encoding='utf-8') as f:json.dump(data,f,ensure_ascii=False)
|
| 29 |
os.replace(tmp,path)
|
| 30 |
except:pass
|
| 31 |
-
|
| 32 |
def _cleanup_old_posts():
|
| 33 |
now=int(time.time());posts=f5.base._load_ai_wall()
|
| 34 |
fresh=[p for p in posts if now-int(p.get('ts') or 0)<TTL_24H]
|
| 35 |
if len(fresh)<len(posts):f5.base._save_ai_wall(fresh)
|
| 36 |
-
|
| 37 |
def _scrape_url_text(url,max_chars=8000):
|
| 38 |
-
"""Scrape full article text from a URL."""
|
| 39 |
try:
|
| 40 |
data=f5.base.scrape_any_url(url)
|
| 41 |
return (data.get('title',''),((data.get('summary','')+'\n'+data.get('text','')).strip())[:max_chars],data.get('image') or data.get('og_image') or '')
|
| 42 |
except:return ('','','')
|
| 43 |
|
| 44 |
-
# ===== BACKGROUND =====
|
| 45 |
_bg_home={"t":0,"d":[]};_bg_shorts={"t":0,"d":[]};_bg_lock=False
|
| 46 |
def _bg_refresh():
|
| 47 |
global _bg_lock
|
|
@@ -67,7 +62,6 @@ def _loop():
|
|
| 67 |
while True:time.sleep(600);_bg_refresh()
|
| 68 |
threading.Thread(target=_loop,daemon=True).start()
|
| 69 |
|
| 70 |
-
# ===== ENDPOINTS =====
|
| 71 |
app.router.routes=[r for r in app.router.routes if not (
|
| 72 |
(getattr(r,'path',None) in ('/api/homepage','/api/shorts','/api/ai_wall','/api/topic_post','/api/article/ask','/api/topic/rewrite','/api/rewrite_share','/api/url_wall','/api/short/comments','/api/short/comment','/api/storage_status','/') and any(m in getattr(r,'methods',set()) for m in ('GET','POST')))
|
| 73 |
)]
|
|
@@ -78,11 +72,8 @@ def _homepage():
|
|
| 78 |
if _bg_home['d']:
|
| 79 |
if now-_bg_home['t']>300:threading.Thread(target=_bg_refresh,daemon=True).start()
|
| 80 |
return JSONResponse(_bg_home['d'])
|
| 81 |
-
if hasattr(f6,'_fast_homepage'):
|
| 82 |
-
d=f6._fast_homepage()
|
| 83 |
-
if d:_bg_home.update({"t":now,"d":d});return JSONResponse(d)
|
| 84 |
return JSONResponse([])
|
| 85 |
-
|
| 86 |
@app.get('/api/shorts')
|
| 87 |
def _shorts(refresh:int=Query(default=0)):
|
| 88 |
now=time.time()
|
|
@@ -91,19 +82,14 @@ def _shorts(refresh:int=Query(default=0)):
|
|
| 91 |
return JSONResponse(_bg_shorts['d'])
|
| 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 _ai_wall():
|
| 97 |
now=int(time.time());posts=f5.base._load_ai_wall()
|
| 98 |
-
|
| 99 |
-
return JSONResponse({'posts':fresh,'persistent':HAS_PERSISTENT})
|
| 100 |
-
|
| 101 |
@app.get('/api/storage_status')
|
| 102 |
-
def _storage():return JSONResponse({'persistent':HAS_PERSISTENT
|
| 103 |
-
|
| 104 |
@app.get('/api/short/comments')
|
| 105 |
def _get_cmts(id:str=Query(...)):return JSONResponse({'comments':_load_json(SHORT_COMMENTS_FILE,{}).get(id,[])})
|
| 106 |
-
|
| 107 |
@app.post('/api/short/comment')
|
| 108 |
async def _post_cmt(request:Request):
|
| 109 |
body=await request.json();vid=str(body.get('id','')).strip();text=clean(body.get('text',''))
|
|
@@ -111,93 +97,83 @@ async def _post_cmt(request:Request):
|
|
| 111 |
db=_load_json(SHORT_COMMENTS_FILE,{});c=db.get(vid,[]);c.insert(0,{'text':text[:300],'ts':int(time.time())})
|
| 112 |
db[vid]=c[:100];_save_json(SHORT_COMMENTS_FILE,db);return JSONResponse({'comments':db[vid]})
|
| 113 |
|
| 114 |
-
# ===== ASK AI — truly conversational =====
|
| 115 |
@app.post('/api/article/ask')
|
| 116 |
async def _ask(request:Request):
|
| 117 |
body=await request.json();url=clean(body.get('url',''));q=clean(body.get('question',''));ctx=clean(body.get('context',''))
|
| 118 |
if not q:return JSONResponse({'error':'missing question'},status_code=400)
|
| 119 |
title='';raw=''
|
| 120 |
-
if url:
|
| 121 |
-
title,raw,_=_scrape_url_text(url,10000)
|
| 122 |
if not raw and ctx:raw=ctx[:12000]
|
| 123 |
-
prompt=f"""Bạn tên
|
| 124 |
|
| 125 |
-
|
| 126 |
-
"{title}"
|
| 127 |
{raw[:9000]}
|
| 128 |
|
| 129 |
-
|
| 130 |
|
| 131 |
-
|
| 132 |
"""
|
| 133 |
ans=await f5.base.qwen_generate(prompt,max_tokens=1200)
|
| 134 |
-
if not ans:ans='Mình chưa trả lời được
|
| 135 |
return JSONResponse({'answer':ans,'title':title})
|
| 136 |
|
| 137 |
-
# ===== REWRITE — scrape source URLs for full content =====
|
| 138 |
@app.post('/api/rewrite_share')
|
| 139 |
@app.post('/api/url_wall')
|
| 140 |
async def _rewrite_url(request:Request):
|
| 141 |
body=await request.json();url=clean(body.get('url',''))
|
| 142 |
if not url.startswith('http'):return JSONResponse({'error':'URL không hợp lệ'},status_code=400)
|
| 143 |
title,raw,img=_scrape_url_text(url,14000)
|
| 144 |
-
if len(raw)<
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
Tiêu đề: {title}
|
| 148 |
Nội dung:
|
| 149 |
{raw[:14000]}
|
| 150 |
|
| 151 |
-
|
| 152 |
"""
|
| 153 |
text=None
|
| 154 |
try:text=await asyncio.wait_for(f5.base.qwen_generate(prompt,image_url=img or None,max_tokens=1000),timeout=30)
|
| 155 |
except:pass
|
| 156 |
-
if not text or len(text)<
|
| 157 |
post=f5.base.make_post(title or 'Bài viết',text,img,url,'rewrite',sources=[{'title':title,'url':url,'via':_domain(url)}])
|
| 158 |
posts=f5.base._load_ai_wall();posts.insert(0,post);f5.base._save_ai_wall(posts)
|
| 159 |
return JSONResponse({'post':post})
|
| 160 |
|
| 161 |
@app.post('/api/topic/rewrite')
|
| 162 |
async def _topic_rewrite(request:Request):
|
| 163 |
-
"""Rewrite topic post by RE-SCRAPING all source URLs for fresh full content."""
|
| 164 |
body=await request.json();post_id=str(body.get('post_id','')).strip()
|
| 165 |
if not post_id:return JSONResponse({'error':'missing post_id'},status_code=400)
|
| 166 |
posts=f5.base._load_ai_wall();p=next((x for x in posts if str(x.get('id'))==post_id),None)
|
| 167 |
-
if not p:return JSONResponse({'error':'
|
| 168 |
-
|
| 169 |
-
urls=[]
|
| 170 |
-
|
| 171 |
-
if s.get('url'):urls.append(s['url'])
|
| 172 |
-
for s in (p.get('sources') or []):
|
| 173 |
-
if s.get('url') and s['url'] not in urls:urls.append(s['url'])
|
| 174 |
-
# Scrape all source URLs for FULL content
|
| 175 |
-
scraped_parts=[]
|
| 176 |
for u in urls[:5]:
|
| 177 |
t,raw,_=_scrape_url_text(u,6000)
|
| 178 |
-
if raw and len(raw)>
|
| 179 |
-
|
| 180 |
-
all_content='\n\n---\n\n'.join(scraped_parts) if scraped_parts else (p.get('text') or '')
|
| 181 |
title=p.get('title') or 'Bài viết'
|
| 182 |
-
prompt=f"""Viết lại thành bản tóm tắt mới
|
| 183 |
-
|
| 184 |
-
Chủ đề gốc: {title}
|
| 185 |
|
| 186 |
-
|
|
|
|
| 187 |
{all_content[:16000]}
|
| 188 |
|
| 189 |
-
|
| 190 |
"""
|
| 191 |
text=None
|
| 192 |
try:text=await asyncio.wait_for(f5.base.qwen_generate(prompt,image_url=p.get('img'),max_tokens=1200),timeout=35)
|
| 193 |
except:pass
|
| 194 |
-
if not text or len(text)<
|
| 195 |
new_post=f5.base.make_post('Rewrite: '+title,text,p.get('img',''),'','rewrite_topic',sources=p.get('sources',[]))
|
| 196 |
new_post['images']=p.get('images',[])
|
| 197 |
all_posts=f5.base._load_ai_wall();all_posts.insert(0,new_post);f5.base._save_ai_wall(all_posts)
|
| 198 |
return JSONResponse({'post':new_post})
|
| 199 |
|
| 200 |
-
# ===== TOPIC POST =====
|
| 201 |
@app.post('/api/topic_post')
|
| 202 |
async def _topic(request:Request):
|
| 203 |
body=await request.json();topic=clean(body.get('topic',''))
|
|
@@ -206,82 +182,67 @@ async def _topic(request:Request):
|
|
| 206 |
research=f6._fast_context(topic) if hasattr(f6,'_fast_context') else f6._web_research_context(topic)
|
| 207 |
context=research.get('context','');sources=research.get('sources',[])
|
| 208 |
details=f6._extract_source_details_from_context(context,sources) if hasattr(f6,'_extract_source_details_from_context') else []
|
| 209 |
-
if not context or not sources:return JSONResponse({'error':'Không tìm được nội dung.
|
| 210 |
source_brief='\n\n'.join([f"[{i+1}] {d.get('title','')} ({d.get('via','')})\n{d.get('content','')[:1400]}" for i,d in enumerate(details)]) if details else context[:18000]
|
| 211 |
-
prompt=f"""Viết bài tiếng Việt VỀ
|
| 212 |
-
|
| 213 |
-
NGUỒN:
|
| 214 |
-
{source_brief[:18000]}
|
| 215 |
-
|
| 216 |
-
CHỈ viết về "{topic}". Tiêu đề chứa "{topic}". 5-8 đoạn. Cuối có nguồn.
|
| 217 |
-
"""
|
| 218 |
text=None
|
| 219 |
try:text=await asyncio.wait_for(f5.base.qwen_generate(prompt,image_url=img,max_tokens=1700),timeout=35)
|
| 220 |
except:pass
|
| 221 |
if not text or len(text)<300:
|
| 222 |
bullets='\n'.join([f"• {d['title']}: {d.get('content','')[:300]}" for d in (details or [])[:6]])
|
| 223 |
-
|
| 224 |
-
text=f"{topic}: tổng hợp\n\n{bullets}\n\nNguồn: {vias}"
|
| 225 |
post=f5.base.make_post(topic,text,img,'','topic_focused',sources=[s for s in sources if s.get('url')])
|
| 226 |
post['images']=[img];post['source_details']=details
|
| 227 |
posts=f5.base._load_ai_wall();posts.insert(0,post);f5.base._save_ai_wall(posts)
|
| 228 |
return JSONResponse({'post':post})
|
| 229 |
|
| 230 |
-
# ===== FRONTEND =====
|
| 231 |
PATCH_INJECT=r'''
|
| 232 |
-
<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;cursor:pointer}.source-detail-title{font-size:12px;font-weight:700;color:#eee}.source-detail-content{font-size:12px;color:#bbb;line-height:1.5;white-space:pre-wrap;max-height:140px;overflow:auto}.source-detail-item img{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:6px;margin-bottom:6px;background:#222}.
|
| 233 |
<div id="short-cmt-panel" class="short-cmt-panel"></div>
|
| 234 |
<script>
|
| 235 |
(function(){
|
| 236 |
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
|
|
|
| 237 |
|
| 238 |
-
/
|
| 239 |
-
fetch('/api/storage_status').then(r=>r.json()).then(j=>{if(!j.persistent){let home=document.getElementById('view-home');if(home){let w=document.createElement('div');w.className='storage-warn';w.textContent='⚠️ Persistent Storage chưa bật. Bài tường AI và bình luận sẽ mất khi rebuild. Bật trong Space Settings.';home.prepend(w);}}});
|
| 240 |
-
|
| 241 |
-
function renderSourceDetails(post,container){
|
| 242 |
-
let details=post.source_details||[];if(!details.length)return;
|
| 243 |
-
let box=document.createElement('div');box.className='source-detail-box';
|
| 244 |
-
box.innerHTML='<h3 style="font-size:14px;color:#5cb87a;margin-bottom:8px">📚 Bài viết nguồn (nhấn để đọc trên VNEWS)</h3>'+details.map((s,i)=>`<div class="source-detail-item" onclick="if(typeof readArticle==='function'&&'${esc(s.url||'')}')readArticle('${esc(s.url||'')}')"><div class="source-detail-title">${i+1}. ${esc(s.title)}</div><div class="source-detail-content">${esc((s.content||'').slice(0,400))}</div><span class="source-read-btn">📖 Đọc trên VNEWS</span></div>`).join('');
|
| 245 |
-
container.appendChild(box);
|
| 246 |
-
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(()=>{});});}
|
| 247 |
|
| 248 |
-
async function readAIWallPost(i){
|
| 249 |
-
let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];
|
| 250 |
-
let p=wall[i];if(!p)return;showView('view-article');
|
| 251 |
-
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}">`:''}`;
|
| 252 |
-
h+=`<p class="article-p" style="white-space:pre-wrap">${esc(p.text)}</p>`;
|
| 253 |
-
h+=`<div class="article-actions"><button class="primary" onclick="rewriteTopicPost('${esc(p.id)}')">🤖 Rewrite AI đăng tường</button><button onclick="doShare('${esc(p.title)}','${location.origin}','${esc(p.img||'')}')">📤 Chia sẻ</button></div>`;
|
| 254 |
-
h+=`<div class="article-ai-ask"><h3 style="font-size:14px;color:#5cb87a">🤖 Hỏi AI</h3><textarea id="article-ai-q" placeholder="Hỏi bất cứ gì về nội dung này..."></textarea><button onclick="askAIWall(${i})">Hỏi</button><div id="article-ai-ans" class="article-ai-answer"></div></div></div>`;
|
| 255 |
-
document.getElementById('view-article').innerHTML=h;
|
| 256 |
-
let art=document.querySelector('.article-view');if(art&&p.source_details)renderSourceDetails(p,art);
|
| 257 |
-
window.scrollTo(0,0);}
|
| 258 |
window.readAIWallPost=readAIWallPost;window.aiReadWallPatched=window.aiReadWall=window.readWallPost=function(i){readAIWallPost(i)};
|
| 259 |
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
try{let r=await fetch('/api/topic/rewrite',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({post_id:postId})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');alert('Đã scrape lại các nguồn, rewrite và đăng bản mới!');}catch(e){alert('Lỗi: '+e.message)}finally{if(btn){btn.disabled=false;btn.textContent='🤖 Rewrite AI đăng tường';}}};
|
| 263 |
|
| 264 |
-
|
| 265 |
-
let
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
let
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
window.openShortComments=async function(id){let panel=document.getElementById('short-cmt-panel');let j=await fetch('/api/short/comments?id='+encodeURIComponent(id)).then(r=>r.json()).catch(()=>({comments:[]}));panel.innerHTML=`<h3 style="color:#5cb87a">💬 Bình luận</h3><div id="cmt-list">${(j.comments||[]).map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('')||'<div class="cmt-item" style="color:#777">Chưa có</div>'}</div><textarea id="cmt-text" placeholder="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>`;panel.classList.add('active');}
|
| 272 |
window.submitShortCmt=async function(id){let t=document.getElementById('cmt-text')?.value.trim();if(!t)return;let j=await fetch('/api/short/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,text:t})}).then(r=>r.json()).catch(()=>({comments:[]}));document.getElementById('cmt-list').innerHTML=(j.comments||[]).map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('');document.getElementById('cmt-text').value='';}
|
| 273 |
function patchShortBtns(){document.querySelectorAll('.tiktok-slide').forEach(sl=>{if(sl.dataset.cmtDone)return;sl.dataset.cmtDone='1';let id=sl.dataset.id||'';if(!id)return;let r=sl.querySelector('.tiktok-right,.short-action-panel');if(!r||r.querySelector('[data-cmt]'))return;let b=document.createElement('button');b.className='short-action-btn';b.setAttribute('data-cmt','1');b.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>';b.onclick=function(e){e.stopPropagation();openShortComments(id);};r.appendChild(b);});}
|
| 274 |
|
| 275 |
-
//
|
| 276 |
function patchArticle(){let art=document.querySelector('#view-article .article-view');if(!art)return;
|
|
|
|
|
|
|
|
|
|
| 277 |
art.querySelectorAll('.article-ai-ask').forEach((e,i)=>{if(i>0)e.remove();});
|
| 278 |
-
|
| 279 |
-
if(!art.querySelector('[data-
|
|
|
|
| 280 |
if(!art.querySelector('.article-ai-ask')){let box=document.createElement('div');box.className='article-ai-ask';box.innerHTML='<h3 style="font-size:14px;color:#5cb87a">🤖 Hỏi AI</h3><textarea id="article-ai-question" placeholder="Hỏi về bài viết..."></textarea><button onclick="askArticleAI()">Hỏi</button><div id="article-ai-answer" class="article-ai-answer"></div>';art.appendChild(box);}}
|
| 281 |
window.askArticleAI=async function(){let q=document.getElementById('article-ai-question')?.value.trim();if(!q)return alert('Nhập câu hỏi');let a=document.getElementById('article-ai-answer');a.textContent='Đang hỏi...';let url=(window._currentArticle&&window._currentArticle.url)||'';let ctx=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:ctx})});let j=await r.json();a.textContent=j.answer||'Không trả lời được';}catch(e){a.textContent='Lỗi: '+e.message}}
|
| 282 |
-
window.rewriteCurrentArticle=function(){let url=(window._currentArticle&&window._currentArticle.url)||'';if(!url)return alert('Không có URL');fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})}).then(r=>r.json()).then(j=>{if(j.post)alert('Đã rewrite và đăng tường!');else alert(j.error||'Lỗi')}).catch(e=>alert(e.message))};
|
| 283 |
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
| 285 |
let _h=false;function dH(){if(_h)return;_h=true;setTimeout(()=>{if(typeof ensureHotTopics==='function')ensureHotTopics();if(typeof ensureNewsShortsHome==='function')ensureNewsShortsHome();},4000);}
|
| 286 |
if(document.readyState==='complete')dH();else window.addEventListener('load',dH);
|
| 287 |
setInterval(()=>{patchArticle();patchShortBtns();},1500);
|
|
|
|
| 1 |
+
"""Patch: single rewrite, guaranteed success, short button after rewrite."""
|
| 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
|
|
|
|
| 13 |
DATA_DIR="/data" if os.path.isdir('/data') else "/app/data"
|
| 14 |
os.makedirs(DATA_DIR,exist_ok=True)
|
| 15 |
SHORT_COMMENTS_FILE=os.path.join(DATA_DIR,'short_comments.json')
|
| 16 |
+
TTL_24H=86400;HAS_PERSISTENT=os.path.isdir('/data')
|
|
|
|
| 17 |
|
| 18 |
def _load_json(path,default):
|
| 19 |
try:
|
|
|
|
| 27 |
with open(tmp,'w',encoding='utf-8') as f:json.dump(data,f,ensure_ascii=False)
|
| 28 |
os.replace(tmp,path)
|
| 29 |
except:pass
|
|
|
|
| 30 |
def _cleanup_old_posts():
|
| 31 |
now=int(time.time());posts=f5.base._load_ai_wall()
|
| 32 |
fresh=[p for p in posts if now-int(p.get('ts') or 0)<TTL_24H]
|
| 33 |
if len(fresh)<len(posts):f5.base._save_ai_wall(fresh)
|
|
|
|
| 34 |
def _scrape_url_text(url,max_chars=8000):
|
|
|
|
| 35 |
try:
|
| 36 |
data=f5.base.scrape_any_url(url)
|
| 37 |
return (data.get('title',''),((data.get('summary','')+'\n'+data.get('text','')).strip())[:max_chars],data.get('image') or data.get('og_image') or '')
|
| 38 |
except:return ('','','')
|
| 39 |
|
|
|
|
| 40 |
_bg_home={"t":0,"d":[]};_bg_shorts={"t":0,"d":[]};_bg_lock=False
|
| 41 |
def _bg_refresh():
|
| 42 |
global _bg_lock
|
|
|
|
| 62 |
while True:time.sleep(600);_bg_refresh()
|
| 63 |
threading.Thread(target=_loop,daemon=True).start()
|
| 64 |
|
|
|
|
| 65 |
app.router.routes=[r for r in app.router.routes if not (
|
| 66 |
(getattr(r,'path',None) in ('/api/homepage','/api/shorts','/api/ai_wall','/api/topic_post','/api/article/ask','/api/topic/rewrite','/api/rewrite_share','/api/url_wall','/api/short/comments','/api/short/comment','/api/storage_status','/') and any(m in getattr(r,'methods',set()) for m in ('GET','POST')))
|
| 67 |
)]
|
|
|
|
| 72 |
if _bg_home['d']:
|
| 73 |
if now-_bg_home['t']>300:threading.Thread(target=_bg_refresh,daemon=True).start()
|
| 74 |
return JSONResponse(_bg_home['d'])
|
| 75 |
+
if hasattr(f6,'_fast_homepage'):d=f6._fast_homepage();_bg_home.update({"t":now,"d":d or []});return JSONResponse(d or [])
|
|
|
|
|
|
|
| 76 |
return JSONResponse([])
|
|
|
|
| 77 |
@app.get('/api/shorts')
|
| 78 |
def _shorts(refresh:int=Query(default=0)):
|
| 79 |
now=time.time()
|
|
|
|
| 82 |
return JSONResponse(_bg_shorts['d'])
|
| 83 |
if hasattr(f6,'api_shorts_final6'):return f6.api_shorts_final6(refresh=refresh)
|
| 84 |
return JSONResponse([])
|
|
|
|
| 85 |
@app.get('/api/ai_wall')
|
| 86 |
def _ai_wall():
|
| 87 |
now=int(time.time());posts=f5.base._load_ai_wall()
|
| 88 |
+
return JSONResponse({'posts':[p for p in posts if now-int(p.get('ts') or 0)<TTL_24H],'persistent':HAS_PERSISTENT})
|
|
|
|
|
|
|
| 89 |
@app.get('/api/storage_status')
|
| 90 |
+
def _storage():return JSONResponse({'persistent':HAS_PERSISTENT})
|
|
|
|
| 91 |
@app.get('/api/short/comments')
|
| 92 |
def _get_cmts(id:str=Query(...)):return JSONResponse({'comments':_load_json(SHORT_COMMENTS_FILE,{}).get(id,[])})
|
|
|
|
| 93 |
@app.post('/api/short/comment')
|
| 94 |
async def _post_cmt(request:Request):
|
| 95 |
body=await request.json();vid=str(body.get('id','')).strip();text=clean(body.get('text',''))
|
|
|
|
| 97 |
db=_load_json(SHORT_COMMENTS_FILE,{});c=db.get(vid,[]);c.insert(0,{'text':text[:300],'ts':int(time.time())})
|
| 98 |
db[vid]=c[:100];_save_json(SHORT_COMMENTS_FILE,db);return JSONResponse({'comments':db[vid]})
|
| 99 |
|
|
|
|
| 100 |
@app.post('/api/article/ask')
|
| 101 |
async def _ask(request:Request):
|
| 102 |
body=await request.json();url=clean(body.get('url',''));q=clean(body.get('question',''));ctx=clean(body.get('context',''))
|
| 103 |
if not q:return JSONResponse({'error':'missing question'},status_code=400)
|
| 104 |
title='';raw=''
|
| 105 |
+
if url:title,raw,_=_scrape_url_text(url,10000)
|
|
|
|
| 106 |
if not raw and ctx:raw=ctx[:12000]
|
| 107 |
+
prompt=f"""Bạn tên VNEWS AI. Người dùng đang đọc bài/xem video và hỏi bạn.
|
| 108 |
|
| 109 |
+
Nội dung: "{title}"
|
|
|
|
| 110 |
{raw[:9000]}
|
| 111 |
|
| 112 |
+
Câu hỏi: "{q}"
|
| 113 |
|
| 114 |
+
Trả lời tự nhiên, thân thiện, dễ hiểu bằng tiếng Việt. Dựa vào nội dung bài. Nếu mở rộng thì nói "theo mình biết thêm..."
|
| 115 |
"""
|
| 116 |
ans=await f5.base.qwen_generate(prompt,max_tokens=1200)
|
| 117 |
+
if not ans:ans='Mình chưa trả lời được. Bạn thử lại nhé!'
|
| 118 |
return JSONResponse({'answer':ans,'title':title})
|
| 119 |
|
|
|
|
| 120 |
@app.post('/api/rewrite_share')
|
| 121 |
@app.post('/api/url_wall')
|
| 122 |
async def _rewrite_url(request:Request):
|
| 123 |
body=await request.json();url=clean(body.get('url',''))
|
| 124 |
if not url.startswith('http'):return JSONResponse({'error':'URL không hợp lệ'},status_code=400)
|
| 125 |
title,raw,img=_scrape_url_text(url,14000)
|
| 126 |
+
if len(raw)<50:
|
| 127 |
+
# Fallback: use context from request body if scrape fails
|
| 128 |
+
raw=clean(body.get('context',''))[:14000]
|
| 129 |
+
if not raw:return JSONResponse({'error':'Không đọc được URL'},status_code=422)
|
| 130 |
+
prompt=f"""Tóm tắt bài viết đăng Tường AI:
|
| 131 |
|
| 132 |
Tiêu đề: {title}
|
| 133 |
Nội dung:
|
| 134 |
{raw[:14000]}
|
| 135 |
|
| 136 |
+
Tóm tắt 4-6 ý chính, tự nhiên. Cuối ghi nguồn.
|
| 137 |
"""
|
| 138 |
text=None
|
| 139 |
try:text=await asyncio.wait_for(f5.base.qwen_generate(prompt,image_url=img or None,max_tokens=1000),timeout=30)
|
| 140 |
except:pass
|
| 141 |
+
if not text or len(text)<80:text=f"Tóm tắt: {title}\n\n{raw[:1200]}\n\nNguồn: {_domain(url)}"
|
| 142 |
post=f5.base.make_post(title or 'Bài viết',text,img,url,'rewrite',sources=[{'title':title,'url':url,'via':_domain(url)}])
|
| 143 |
posts=f5.base._load_ai_wall();posts.insert(0,post);f5.base._save_ai_wall(posts)
|
| 144 |
return JSONResponse({'post':post})
|
| 145 |
|
| 146 |
@app.post('/api/topic/rewrite')
|
| 147 |
async def _topic_rewrite(request:Request):
|
|
|
|
| 148 |
body=await request.json();post_id=str(body.get('post_id','')).strip()
|
| 149 |
if not post_id:return JSONResponse({'error':'missing post_id'},status_code=400)
|
| 150 |
posts=f5.base._load_ai_wall();p=next((x for x in posts if str(x.get('id'))==post_id),None)
|
| 151 |
+
if not p:return JSONResponse({'error':'Bài không tồn tại.'},status_code=404)
|
| 152 |
+
urls=[s['url'] for s in (p.get('source_details') or []) if s.get('url')]
|
| 153 |
+
urls+=[s['url'] for s in (p.get('sources') or []) if s.get('url') and s['url'] not in urls]
|
| 154 |
+
scraped=[]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
for u in urls[:5]:
|
| 156 |
t,raw,_=_scrape_url_text(u,6000)
|
| 157 |
+
if raw and len(raw)>150:scraped.append(f"[{_domain(u)}] {t}\n{raw}")
|
| 158 |
+
all_content='\n\n---\n\n'.join(scraped) if scraped else (p.get('text') or '')
|
|
|
|
| 159 |
title=p.get('title') or 'Bài viết'
|
| 160 |
+
prompt=f"""Viết lại thành bản tóm tắt mới từ nguồn:
|
|
|
|
|
|
|
| 161 |
|
| 162 |
+
Chủ đề: {title}
|
| 163 |
+
Nguồn:
|
| 164 |
{all_content[:16000]}
|
| 165 |
|
| 166 |
+
Tiêu đề mới + 4-6 ý chính + nguồn cuối bài.
|
| 167 |
"""
|
| 168 |
text=None
|
| 169 |
try:text=await asyncio.wait_for(f5.base.qwen_generate(prompt,image_url=p.get('img'),max_tokens=1200),timeout=35)
|
| 170 |
except:pass
|
| 171 |
+
if not text or len(text)<100:text=f"Tóm tắt: {title}\n\n{all_content[:1500]}\n\nNguồn: VNEWS AI"
|
| 172 |
new_post=f5.base.make_post('Rewrite: '+title,text,p.get('img',''),'','rewrite_topic',sources=p.get('sources',[]))
|
| 173 |
new_post['images']=p.get('images',[])
|
| 174 |
all_posts=f5.base._load_ai_wall();all_posts.insert(0,new_post);f5.base._save_ai_wall(all_posts)
|
| 175 |
return JSONResponse({'post':new_post})
|
| 176 |
|
|
|
|
| 177 |
@app.post('/api/topic_post')
|
| 178 |
async def _topic(request:Request):
|
| 179 |
body=await request.json();topic=clean(body.get('topic',''))
|
|
|
|
| 182 |
research=f6._fast_context(topic) if hasattr(f6,'_fast_context') else f6._web_research_context(topic)
|
| 183 |
context=research.get('context','');sources=research.get('sources',[])
|
| 184 |
details=f6._extract_source_details_from_context(context,sources) if hasattr(f6,'_extract_source_details_from_context') else []
|
| 185 |
+
if not context or not sources:return JSONResponse({'error':'Không tìm được nội dung.'},status_code=422)
|
| 186 |
source_brief='\n\n'.join([f"[{i+1}] {d.get('title','')} ({d.get('via','')})\n{d.get('content','')[:1400]}" for i,d in enumerate(details)]) if details else context[:18000]
|
| 187 |
+
prompt=f"""Viết bài tiếng Việt VỀ CHỦ ĐỀ: "{topic}"\nNGUỒN:\n{source_brief[:18000]}\nCHỈ viết về "{topic}". 5-8 đoạn. Cuối có nguồn."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
text=None
|
| 189 |
try:text=await asyncio.wait_for(f5.base.qwen_generate(prompt,image_url=img,max_tokens=1700),timeout=35)
|
| 190 |
except:pass
|
| 191 |
if not text or len(text)<300:
|
| 192 |
bullets='\n'.join([f"• {d['title']}: {d.get('content','')[:300]}" for d in (details or [])[:6]])
|
| 193 |
+
text=f"{topic}: tổng hợp\n\n{bullets}\n\nNguồn: "+', '.join(sorted({d.get('via','') for d in (details or []) if d.get('via')}))
|
|
|
|
| 194 |
post=f5.base.make_post(topic,text,img,'','topic_focused',sources=[s for s in sources if s.get('url')])
|
| 195 |
post['images']=[img];post['source_details']=details
|
| 196 |
posts=f5.base._load_ai_wall();posts.insert(0,post);f5.base._save_ai_wall(posts)
|
| 197 |
return JSONResponse({'post':post})
|
| 198 |
|
|
|
|
| 199 |
PATCH_INJECT=r'''
|
| 200 |
+
<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;cursor:pointer}.source-detail-title{font-size:12px;font-weight:700;color:#eee}.source-detail-content{font-size:12px;color:#bbb;line-height:1.5;white-space:pre-wrap;max-height:140px;overflow:auto}.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}.storage-warn{background:#332200;border:1px solid #664400;color:#ffcc00;padding:8px 12px;border-radius:8px;font-size:11px;margin:6px 4px}/* HIDE all old rewrite buttons from previous layers */button[onclick*="rewriteCurrentArticle"]{display:none!important}</style>
|
| 201 |
<div id="short-cmt-panel" class="short-cmt-panel"></div>
|
| 202 |
<script>
|
| 203 |
(function(){
|
| 204 |
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 205 |
+
fetch('/api/storage_status').then(r=>r.json()).then(j=>{if(!j.persistent){let h=document.getElementById('view-home');if(h){let w=document.createElement('div');w.className='storage-warn';w.textContent='⚠️ Persistent Storage chưa bật. Bài AI sẽ mất khi rebuild. Bật trong Space Settings.';h.prepend(w);}}});
|
| 206 |
|
| 207 |
+
function renderSourceDetails(post,container){let details=post.source_details||[];if(!details.length)return;let box=document.createElement('div');box.className='source-detail-box';box.innerHTML='<h3 style="font-size:14px;color:#5cb87a;margin-bottom:8px">📚 Bài nguồn (nhấn đọc trên VNEWS)</h3>'+details.map((s,i)=>`<div class="source-detail-item" onclick="if(typeof readArticle==='function'&&'${esc(s.url||'')}')readArticle('${esc(s.url||'')}')"><div class="source-detail-title">${i+1}. ${esc(s.title)}</div><div class="source-detail-content">${esc((s.content||'').slice(0,400))}</div></div>`).join('');container.appendChild(box);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(()=>{});});}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
+
async function readAIWallPost(i){let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];let p=wall[i];if(!p)return;showView('view-article');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}">`:''}`;h+=`<p class="article-p" style="white-space:pre-wrap">${esc(p.text)}</p>`;h+=`<div class="article-actions"><button class="primary" data-rw-topic="${esc(p.id)}" onclick="doRewriteTopic(this,'${esc(p.id)}')">🤖 Rewrite AI đăng tường</button>${p.video?`<button onclick="window.open('${p.video}','_blank')">🎬 Xem Short</button>`:''}<button onclick="doShare('${esc(p.title)}','${location.origin}','${esc(p.img||'')}')">📤</button></div>`;h+=`<div class="article-ai-ask"><h3 style="font-size:14px;color:#5cb87a">🤖 Hỏi AI</h3><textarea id="article-ai-q" placeholder="Hỏi về nội dung này..."></textarea><button onclick="askAIWall(${i})">Hỏi</button><div id="article-ai-ans" class="article-ai-answer"></div></div></div>`;document.getElementById('view-article').innerHTML=h;let art=document.querySelector('.article-view');if(art&&p.source_details)renderSourceDetails(p,art);window.scrollTo(0,0);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
window.readAIWallPost=readAIWallPost;window.aiReadWallPatched=window.aiReadWall=window.readWallPost=function(i){readAIWallPost(i)};
|
| 211 |
|
| 212 |
+
// Unified rewrite for topic posts
|
| 213 |
+
window.doRewriteTopic=async function(btn,postId){btn.disabled=true;btn.textContent='Đang scrape & rewrite...';try{let r=await fetch('/api/topic/rewrite',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({post_id:postId})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');let post=j.post;alert('Đã rewrite thành công!');showRewriteResult(post);}catch(e){alert('Lỗi: '+e.message)}finally{btn.disabled=false;btn.textContent='🤖 Rewrite AI đăng tường';}};
|
|
|
|
| 214 |
|
| 215 |
+
// Unified rewrite for regular articles
|
| 216 |
+
window.doRewriteArticle=async function(btn){let url=(window._currentArticle&&window._currentArticle.url)||'';if(!url){let link=document.querySelector('#view-article a[href*="://"]');if(link)url=link.href;}if(!url){alert('Không tìm được URL bài viết');return;}let ctx=document.querySelector('.article-view')?.innerText?.slice(0,14000)||'';btn.disabled=true;btn.textContent='Đang rewrite...';try{let r=await fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,context:ctx})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');alert('Đã rewrite thành công!');showRewriteResult(j.post);}catch(e){alert('Lỗi: '+e.message)}finally{btn.disabled=false;btn.textContent='🤖 Rewrite AI đăng tường';}};
|
| 217 |
+
|
| 218 |
+
// Show rewrite result with Short button
|
| 219 |
+
function showRewriteResult(post){if(!post)return;showView('view-article');let h=`<button class="back-btn" onclick="switchCat('home')">← Quay lại</button><div class="article-view"><span class="badge badge-ai">Rewrite</span><h1 class="article-title">${esc(post.title)}</h1>${post.img?`<img class="article-img" src="${post.img}">`:''}`;h+=`<p class="article-p" style="white-space:pre-wrap">${esc(post.text)}</p>`;h+=`<div class="article-actions"><button class="primary" onclick="makeShortFromPost('${esc(post.id)}',this)">🎬 Tạo Short AI</button><button onclick="doShare('${esc(post.title)}','${location.origin}','${esc(post.img||'')}')">📤 Chia sẻ</button></div></div>`;document.getElementById('view-article').innerHTML=h;window.scrollTo(0,0);}
|
| 220 |
+
|
| 221 |
+
// Make short from any wall post
|
| 222 |
+
window.makeShortFromPost=async function(postId,btn){if(btn){btn.disabled=true;btn.textContent='Đang tạo short...';}try{let r=await fetch('/api/ai/short/'+postId,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({voice:'nu',emotion:'neutral',speed:1.2})});let j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');alert('Đã tạo Short AI!');}catch(e){alert('Lỗi tạo short: '+e.message)}finally{if(btn){btn.disabled=false;btn.textContent='🎬 Tạo Short AI';}}};
|
| 223 |
+
|
| 224 |
+
window.askAIWall=async function(i){let q=document.getElementById('article-ai-q')?.value.trim();if(!q)return alert('Nhập câu hỏi');document.getElementById('article-ai-ans').textContent='Đang hỏi...';let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];let p=wall[i]||{};let ctx=(p.text||'');for(let s of (p.source_details||[]))ctx+='\n'+(s.content||'');try{let r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question:q,context:ctx.slice(0,12000)})});let j=await r.json();document.getElementById('article-ai-ans').textContent=j.answer||'Không trả lời được';}catch(e){document.getElementById('article-ai-ans').textContent='Lỗi: '+e.message}};
|
| 225 |
|
| 226 |
window.openShortComments=async function(id){let panel=document.getElementById('short-cmt-panel');let j=await fetch('/api/short/comments?id='+encodeURIComponent(id)).then(r=>r.json()).catch(()=>({comments:[]}));panel.innerHTML=`<h3 style="color:#5cb87a">💬 Bình luận</h3><div id="cmt-list">${(j.comments||[]).map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('')||'<div class="cmt-item" style="color:#777">Chưa có</div>'}</div><textarea id="cmt-text" placeholder="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>`;panel.classList.add('active');}
|
| 227 |
window.submitShortCmt=async function(id){let t=document.getElementById('cmt-text')?.value.trim();if(!t)return;let j=await fetch('/api/short/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,text:t})}).then(r=>r.json()).catch(()=>({comments:[]}));document.getElementById('cmt-list').innerHTML=(j.comments||[]).map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('');document.getElementById('cmt-text').value='';}
|
| 228 |
function patchShortBtns(){document.querySelectorAll('.tiktok-slide').forEach(sl=>{if(sl.dataset.cmtDone)return;sl.dataset.cmtDone='1';let id=sl.dataset.id||'';if(!id)return;let r=sl.querySelector('.tiktok-right,.short-action-panel');if(!r||r.querySelector('[data-cmt]'))return;let b=document.createElement('button');b.className='short-action-btn';b.setAttribute('data-cmt','1');b.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>';b.onclick=function(e){e.stopPropagation();openShortComments(id);};r.appendChild(b);});}
|
| 229 |
|
| 230 |
+
// Patch regular articles: ONLY ONE rewrite button + ask box
|
| 231 |
function patchArticle(){let art=document.querySelector('#view-article .article-view');if(!art)return;
|
| 232 |
+
// Remove ALL old rewrite buttons from any layer
|
| 233 |
+
art.querySelectorAll('button[onclick*="rewriteCurrentArticle"],button[data-rewrite],.rewrite-injected').forEach(e=>e.remove());
|
| 234 |
+
// Remove duplicate ask boxes
|
| 235 |
art.querySelectorAll('.article-ai-ask').forEach((e,i)=>{if(i>0)e.remove();});
|
| 236 |
+
// Add our single rewrite if not already there
|
| 237 |
+
if(!art.querySelector('[data-rw-article]')){let a=art.querySelector('.article-actions');if(a){let b=document.createElement('button');b.className='primary';b.setAttribute('data-rw-article','1');b.textContent='🤖 Rewrite AI đăng tường';b.onclick=function(){doRewriteArticle(b)};a.insertBefore(b,a.firstChild);}}
|
| 238 |
+
// Add ask if missing
|
| 239 |
if(!art.querySelector('.article-ai-ask')){let box=document.createElement('div');box.className='article-ai-ask';box.innerHTML='<h3 style="font-size:14px;color:#5cb87a">🤖 Hỏi AI</h3><textarea id="article-ai-question" placeholder="Hỏi về bài viết..."></textarea><button onclick="askArticleAI()">Hỏi</button><div id="article-ai-answer" class="article-ai-answer"></div>';art.appendChild(box);}}
|
| 240 |
window.askArticleAI=async function(){let q=document.getElementById('article-ai-question')?.value.trim();if(!q)return alert('Nhập câu hỏi');let a=document.getElementById('article-ai-answer');a.textContent='Đang hỏi...';let url=(window._currentArticle&&window._currentArticle.url)||'';let ctx=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:ctx})});let j=await r.json();a.textContent=j.answer||'Không trả lời được';}catch(e){a.textContent='Lỗi: '+e.message}}
|
|
|
|
| 241 |
|
| 242 |
+
// Kill old rewriteCurrentArticle from injected layers
|
| 243 |
+
window.rewriteCurrentArticle=function(){let btn=document.querySelector('[data-rw-article]');if(btn)doRewriteArticle(btn);};
|
| 244 |
+
|
| 245 |
+
let oldRA=window.readArticle;if(oldRA){window.readArticle=async function(){let ret=await oldRA.apply(this,arguments);setTimeout(patchArticle,500);return ret;}}
|
| 246 |
let _h=false;function dH(){if(_h)return;_h=true;setTimeout(()=>{if(typeof ensureHotTopics==='function')ensureHotTopics();if(typeof ensureNewsShortsHome==='function')ensureNewsShortsHome();},4000);}
|
| 247 |
if(document.readyState==='complete')dH();else window.addEventListener('load',dH);
|
| 248 |
setInterval(()=>{patchArticle();patchShortBtns();},1500);
|