Spaces:
Running
Running
Fix: scrape URLs on rewrite, in-app links, natural AI, persistent check"
Browse files- ai_runtime_patch_fast.py +126 -142
ai_runtime_patch_fast.py
CHANGED
|
@@ -1,207 +1,196 @@
|
|
| 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
|
| 5 |
import html as html_lib
|
|
|
|
| 6 |
|
| 7 |
def clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or ""))).strip()
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
# === PERSISTENT STORAGE ===
|
| 10 |
-
# HF Spaces with persistent storage mount /data. Without it, use /app/data (lost on rebuild).
|
| 11 |
-
# To survive rebuild: Space Settings → enable Persistent Storage.
|
| 12 |
DATA_DIR="/data" if os.path.isdir('/data') else "/app/data"
|
| 13 |
os.makedirs(DATA_DIR,exist_ok=True)
|
| 14 |
SHORT_COMMENTS_FILE=os.path.join(DATA_DIR,'short_comments.json')
|
| 15 |
-
# AI wall file is managed by f5.base (ai_ext.py) which uses /data/ai_wall_posts.json if /data exists.
|
| 16 |
-
# We just need to ensure the directory exists.
|
| 17 |
TTL_24H=86400
|
|
|
|
| 18 |
|
| 19 |
def _load_json(path,default):
|
| 20 |
try:
|
| 21 |
if os.path.exists(path):
|
| 22 |
with open(path,'r',encoding='utf-8') as f:return json.load(f)
|
| 23 |
-
except
|
| 24 |
return default
|
| 25 |
def _save_json(path,data):
|
| 26 |
try:
|
| 27 |
os.makedirs(os.path.dirname(path),exist_ok=True);tmp=path+'.tmp'
|
| 28 |
with open(tmp,'w',encoding='utf-8') as f:json.dump(data,f,ensure_ascii=False)
|
| 29 |
os.replace(tmp,path)
|
| 30 |
-
except
|
| 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 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
def _bg_refresh():
|
| 40 |
-
global
|
| 41 |
-
if
|
| 42 |
-
|
| 43 |
try:
|
| 44 |
if hasattr(f6,'_fast_homepage'):
|
| 45 |
-
|
| 46 |
-
if
|
| 47 |
raw=[]
|
| 48 |
for h in f6.YOUTUBE_HANDLES:raw.extend(f6._yt_ytdlp(h,20) or f6._yt_html(h,20))
|
| 49 |
raw.extend(f6._fallback_shorts());seen=set();out=[]
|
| 50 |
for v in raw:
|
| 51 |
vid=v.get('id') or ''
|
| 52 |
if vid and vid not in seen:seen.add(vid);out.append(v)
|
| 53 |
-
if out:
|
| 54 |
_cleanup_old_posts()
|
| 55 |
-
except
|
| 56 |
-
finally:
|
| 57 |
-
|
| 58 |
@app.on_event("startup")
|
| 59 |
-
async def
|
| 60 |
-
def
|
| 61 |
while True:time.sleep(600);_bg_refresh()
|
| 62 |
-
threading.Thread(target=
|
| 63 |
|
| 64 |
-
# =====
|
| 65 |
app.router.routes=[r for r in app.router.routes if not (
|
| 66 |
-
(getattr(r,'path',None)
|
| 67 |
-
(getattr(r,'path',None)=='/api/shorts' and 'GET' in getattr(r,'methods',set())) or
|
| 68 |
-
(getattr(r,'path',None)=='/api/topic_post' and 'POST' in getattr(r,'methods',set())) or
|
| 69 |
-
(getattr(r,'path',None)=='/api/short/comments' and 'GET' in getattr(r,'methods',set())) or
|
| 70 |
-
(getattr(r,'path',None)=='/api/short/comment' and 'POST' in getattr(r,'methods',set())) or
|
| 71 |
-
(getattr(r,'path',None)=='/api/article/ask' and 'POST' in getattr(r,'methods',set())) or
|
| 72 |
-
(getattr(r,'path',None)=='/api/topic/rewrite' and 'POST' in getattr(r,'methods',set())) or
|
| 73 |
-
(getattr(r,'path',None)=='/api/rewrite_share' and 'POST' in getattr(r,'methods',set())) or
|
| 74 |
-
(getattr(r,'path',None)=='/api/url_wall' and 'POST' in getattr(r,'methods',set())) or
|
| 75 |
-
(getattr(r,'path',None)=='/api/ai_wall' and 'GET' in getattr(r,'methods',set())) or
|
| 76 |
-
(getattr(r,'path',None)=='/' and 'GET' in getattr(r,'methods',set()))
|
| 77 |
)]
|
| 78 |
|
| 79 |
@app.get('/api/homepage')
|
| 80 |
-
def
|
| 81 |
now=time.time()
|
| 82 |
-
if
|
| 83 |
-
if now-
|
| 84 |
-
return JSONResponse(
|
| 85 |
if hasattr(f6,'_fast_homepage'):
|
| 86 |
-
|
| 87 |
-
if
|
| 88 |
return JSONResponse([])
|
| 89 |
|
| 90 |
@app.get('/api/shorts')
|
| 91 |
-
def
|
| 92 |
now=time.time()
|
| 93 |
-
if
|
| 94 |
-
if now-
|
| 95 |
-
return JSONResponse(
|
| 96 |
if hasattr(f6,'api_shorts_final6'):return f6.api_shorts_final6(refresh=refresh)
|
| 97 |
return JSONResponse([])
|
| 98 |
|
| 99 |
@app.get('/api/ai_wall')
|
| 100 |
-
def
|
| 101 |
now=int(time.time());posts=f5.base._load_ai_wall()
|
| 102 |
fresh=[p for p in posts if now-int(p.get('ts') or 0)<TTL_24H]
|
| 103 |
-
return JSONResponse({'posts':fresh})
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
# ===== SHORT COMMENTS =====
|
| 106 |
@app.get('/api/short/comments')
|
| 107 |
-
def
|
| 108 |
-
db=_load_json(SHORT_COMMENTS_FILE,{});return JSONResponse({'comments':db.get(id,[])})
|
| 109 |
|
| 110 |
@app.post('/api/short/comment')
|
| 111 |
-
async def
|
| 112 |
body=await request.json();vid=str(body.get('id','')).strip();text=clean(body.get('text',''))
|
| 113 |
-
if not vid or not text:return JSONResponse({'error':'missing
|
| 114 |
-
db=_load_json(SHORT_COMMENTS_FILE,{});
|
| 115 |
-
db[vid]=
|
| 116 |
|
| 117 |
-
# ===== ASK AI —
|
| 118 |
@app.post('/api/article/ask')
|
| 119 |
-
async def
|
| 120 |
-
body=await request.json();url=clean(body.get('url',''));
|
| 121 |
-
if not
|
| 122 |
title='';raw=''
|
| 123 |
if url:
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
Nội dung bài:
|
| 133 |
-
---
|
| 134 |
-
Tiêu đề: {title}
|
| 135 |
-
{raw[:10000]}
|
| 136 |
-
---
|
| 137 |
|
| 138 |
-
|
| 139 |
|
| 140 |
-
|
| 141 |
-
- Dựa vào nội dung bài làm cơ sở chính
|
| 142 |
-
- Giải thích dễ hiểu như đang trò chuyện
|
| 143 |
-
- Nếu câu hỏi mở rộng hơn bài → bổ sung kiến thức chung, nói rõ "theo hiểu biết chung..."
|
| 144 |
"""
|
| 145 |
ans=await f5.base.qwen_generate(prompt,max_tokens=1200)
|
| 146 |
-
if not ans:ans='
|
| 147 |
return JSONResponse({'answer':ans,'title':title})
|
| 148 |
|
| 149 |
-
# ===== REWRITE —
|
| 150 |
@app.post('/api/rewrite_share')
|
| 151 |
@app.post('/api/url_wall')
|
| 152 |
-
async def
|
| 153 |
body=await request.json();url=clean(body.get('url',''))
|
| 154 |
if not url.startswith('http'):return JSONResponse({'error':'URL không hợp lệ'},status_code=400)
|
| 155 |
-
title
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
if data:title=data.get('title','');raw=(data.get('summary','')+'\n'+data.get('text','')).strip()
|
| 159 |
-
except Exception as e:
|
| 160 |
-
return JSONResponse({'error':'Không đọc được URL: '+str(e)[:150]},status_code=422)
|
| 161 |
-
if len(raw)<100:return JSONResponse({'error':'URL không có đủ nội dung'},status_code=422)
|
| 162 |
-
img=(data.get('image') or data.get('og_image') or '') if data else ''
|
| 163 |
-
prompt=f"""Tóm tắt bài viết sau đăng lên Tường AI VNEWS.
|
| 164 |
|
| 165 |
Tiêu đề: {title}
|
| 166 |
Nội dung:
|
| 167 |
{raw[:14000]}
|
| 168 |
|
| 169 |
-
Yêu cầu:
|
| 170 |
-
- Tóm tắt ngắn gọn, cụ thể, 4-6 ý chính.
|
| 171 |
-
- Không lặp ý, không bịa.
|
| 172 |
-
- Cuối bài ghi nguồn.
|
| 173 |
"""
|
| 174 |
text=None
|
| 175 |
try:text=await asyncio.wait_for(f5.base.qwen_generate(prompt,image_url=img or None,max_tokens=1000),timeout=30)
|
| 176 |
-
except
|
| 177 |
-
if not text or len(text)<100:
|
| 178 |
-
|
| 179 |
-
from urllib.parse import urlparse
|
| 180 |
-
via=urlparse(url).netloc.replace('www.','')
|
| 181 |
-
post=f5.base.make_post(title or 'Bài viết',text,img,url,'rewrite',sources=[{'title':title,'url':url,'via':via}])
|
| 182 |
posts=f5.base._load_ai_wall();posts.insert(0,post);f5.base._save_ai_wall(posts)
|
| 183 |
return JSONResponse({'post':post})
|
| 184 |
|
| 185 |
@app.post('/api/topic/rewrite')
|
| 186 |
-
async def
|
|
|
|
| 187 |
body=await request.json();post_id=str(body.get('post_id','')).strip()
|
| 188 |
if not post_id:return JSONResponse({'error':'missing post_id'},status_code=400)
|
| 189 |
posts=f5.base._load_ai_wall();p=next((x for x in posts if str(x.get('id'))==post_id),None)
|
| 190 |
if not p:return JSONResponse({'error':'Không tìm thấy bài.'},status_code=404)
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
title=p.get('title') or 'Bài viết'
|
| 194 |
-
prompt=f"""Viết lại
|
| 195 |
|
| 196 |
-
|
| 197 |
-
|
|
|
|
| 198 |
{all_content[:16000]}
|
| 199 |
|
| 200 |
-
Yêu cầu: tiêu đề mới +
|
| 201 |
"""
|
| 202 |
text=None
|
| 203 |
-
try:text=await asyncio.wait_for(f5.base.qwen_generate(prompt,image_url=p.get('img'),max_tokens=1200),timeout=
|
| 204 |
-
except
|
| 205 |
if not text or len(text)<150:text=f"Tóm tắt: {title}\n\n{all_content[:1500]}\n\nNguồn: VNEWS AI"
|
| 206 |
new_post=f5.base.make_post('Rewrite: '+title,text,p.get('img',''),'','rewrite_topic',sources=p.get('sources',[]))
|
| 207 |
new_post['images']=p.get('images',[])
|
|
@@ -210,7 +199,7 @@ Yêu cầu: tiêu đề mới + 3-5 ý chính + nguồn tham khảo.
|
|
| 210 |
|
| 211 |
# ===== TOPIC POST =====
|
| 212 |
@app.post('/api/topic_post')
|
| 213 |
-
async def
|
| 214 |
body=await request.json();topic=clean(body.get('topic',''))
|
| 215 |
if not topic:return JSONResponse({'error':'missing topic'},status_code=400)
|
| 216 |
img=f6._topic_image(topic)
|
|
@@ -219,16 +208,16 @@ async def topic_post_focused(request:Request):
|
|
| 219 |
details=f6._extract_source_details_from_context(context,sources) if hasattr(f6,'_extract_source_details_from_context') else []
|
| 220 |
if not context or not sources:return JSONResponse({'error':'Không tìm được nội dung. Thử chủ đề cụ thể hơn.'},status_code=422)
|
| 221 |
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]
|
| 222 |
-
prompt=f"""Viết
|
| 223 |
|
| 224 |
NGUỒN:
|
| 225 |
{source_brief[:18000]}
|
| 226 |
|
| 227 |
-
|
| 228 |
"""
|
| 229 |
text=None
|
| 230 |
try:text=await asyncio.wait_for(f5.base.qwen_generate(prompt,image_url=img,max_tokens=1700),timeout=35)
|
| 231 |
-
except
|
| 232 |
if not text or len(text)<300:
|
| 233 |
bullets='\n'.join([f"• {d['title']}: {d.get('content','')[:300]}" for d in (details or [])[:6]])
|
| 234 |
vias=', '.join(sorted({d.get('via','') for d in (details or []) if d.get('via')}))
|
|
@@ -238,18 +227,21 @@ QUY TẮC: CHỈ viết về "{topic}". Tiêu đề chứa "{topic}". 5-8 đoạ
|
|
| 238 |
posts=f5.base._load_ai_wall();posts.insert(0,post);f5.base._save_ai_wall(posts)
|
| 239 |
return JSONResponse({'post':post})
|
| 240 |
|
| 241 |
-
# ===== FRONTEND
|
| 242 |
PATCH_INJECT=r'''
|
| 243 |
-
<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-content{font-size:12px;color:#bbb;line-height:1.5;white-space:pre-wrap;max-height:
|
| 244 |
<div id="short-cmt-panel" class="short-cmt-panel"></div>
|
| 245 |
<script>
|
| 246 |
(function(){
|
| 247 |
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 248 |
|
|
|
|
|
|
|
|
|
|
| 249 |
function renderSourceDetails(post,container){
|
| 250 |
let details=post.source_details||[];if(!details.length)return;
|
| 251 |
let box=document.createElement('div');box.className='source-detail-box';
|
| 252 |
-
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-content">${esc((s.content||'').slice(0,
|
| 253 |
container.appendChild(box);
|
| 254 |
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(()=>{});});}
|
| 255 |
|
|
@@ -258,55 +250,47 @@ let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]})))
|
|
| 258 |
let p=wall[i];if(!p)return;showView('view-article');
|
| 259 |
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}">`:''}`;
|
| 260 |
h+=`<p class="article-p" style="white-space:pre-wrap">${esc(p.text)}</p>`;
|
| 261 |
-
h+=`<div class="article-actions"><button class="primary" onclick="rewriteTopicPost('${esc(p.id)}')">🤖
|
| 262 |
-
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>`;
|
| 263 |
document.getElementById('view-article').innerHTML=h;
|
| 264 |
let art=document.querySelector('.article-view');if(art&&p.source_details)renderSourceDetails(p,art);
|
| 265 |
window.scrollTo(0,0);}
|
| 266 |
window.readAIWallPost=readAIWallPost;window.aiReadWallPatched=window.aiReadWall=window.readWallPost=function(i){readAIWallPost(i)};
|
| 267 |
|
| 268 |
window.rewriteTopicPost=async function(postId){
|
| 269 |
-
let btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang
|
| 270 |
-
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('Đã
|
| 271 |
|
| 272 |
window.askAIWall=async function(i){
|
| 273 |
let q=document.getElementById('article-ai-q')?.value.trim();if(!q)return alert('Nhập câu hỏi');
|
| 274 |
-
document.getElementById('article-ai-ans').textContent='Đang hỏi
|
| 275 |
let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];
|
| 276 |
-
let p=wall[i]||{};let context=(p.text||'');for(let s of (p.source_details||[]))context+='\n'+s.content;
|
| 277 |
try{let r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question:q,context:context.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}};
|
| 278 |
|
| 279 |
-
//
|
| 280 |
-
window.
|
| 281 |
-
|
| 282 |
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
let asks=art.querySelectorAll('.article-ai-ask');if(asks.length>1)for(let i=1;i<asks.length;i++)asks[i].remove();
|
| 289 |
-
let rws=art.querySelectorAll('[data-rewrite]');if(rws.length>1)for(let i=1;i<rws.length;i++)rws[i].remove();
|
| 290 |
-
// Add rewrite if missing
|
| 291 |
-
if(!art.querySelector('[data-rewrite]')){let actions=art.querySelector('.article-actions');if(actions){let btn=document.createElement('button');btn.className='primary';btn.setAttribute('data-rewrite','1');btn.textContent='🤖 Tóm tắt AI đăng tường';btn.onclick=function(){let url=(window._currentArticle&&window._currentArticle.url)||'';if(!url){alert('Không có URL bài viết');return;}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('Đã tóm tắt và đăng lên Tường AI!');else alert(j.error||'Lỗi')}).catch(e=>alert(e.message))};actions.insertBefore(btn,actions.firstChild);}}
|
| 292 |
-
// Add ask if missing
|
| 293 |
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);}}
|
| 294 |
-
window.askArticleAI=async function(){let q=document.getElementById('article-ai-question')?.value.trim();if(!q)return alert('Nhập câu hỏi');let
|
| 295 |
-
|
| 296 |
-
// Override rewriteCurrentArticle used by old layers
|
| 297 |
-
window.rewriteCurrentArticle=function(){let url=(window._currentArticle&&window._currentArticle.url)||'';if(!url){alert('Không có URL bài viết để rewrite');return;}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('Đã tóm tắt và đăng lên Tường AI!');else alert(j.error||'Lỗi')}).catch(e=>alert(e.message))};
|
| 298 |
-
|
| 299 |
-
let oldRA=window.readArticle;if(oldRA){window.readArticle=async function(){let ret=await oldRA.apply(this,arguments);setTimeout(patchArticleView,600);return ret;}}
|
| 300 |
|
| 301 |
-
let
|
| 302 |
-
if(
|
| 303 |
-
|
|
|
|
| 304 |
})();
|
| 305 |
</script>
|
| 306 |
'''
|
| 307 |
|
| 308 |
@app.get('/')
|
| 309 |
-
async def
|
| 310 |
html=f5.f4.f3.f2.f1._load_index_html()
|
| 311 |
body=getattr(rt.old,'PATCH_INJECT','')+f5.f4.f3.f2.f1.FINAL_INJECT+f5.f4.f3.FINAL3_INJECT+f5.f4.FINAL4_INJECT+f5.FINAL5_INJECT
|
| 312 |
body+=getattr(f6,'FINAL6_INJECT','')
|
|
|
|
| 1 |
+
"""Patch: scrape source URLs on rewrite, in-app article links, natural AI chat, persistent data."""
|
| 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
|
| 6 |
+
from urllib.parse import urlparse
|
| 7 |
|
| 8 |
def clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or ""))).strip()
|
| 9 |
+
def _domain(u):
|
| 10 |
+
try:return urlparse(u or '').netloc.replace('www.','')
|
| 11 |
+
except:return ''
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 21 |
if os.path.exists(path):
|
| 22 |
with open(path,'r',encoding='utf-8') as f:return json.load(f)
|
| 23 |
+
except:pass
|
| 24 |
return default
|
| 25 |
def _save_json(path,data):
|
| 26 |
try:
|
| 27 |
os.makedirs(os.path.dirname(path),exist_ok=True);tmp=path+'.tmp'
|
| 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
|
| 48 |
+
if _bg_lock:return
|
| 49 |
+
_bg_lock=True
|
| 50 |
try:
|
| 51 |
if hasattr(f6,'_fast_homepage'):
|
| 52 |
+
d=f6._fast_homepage()
|
| 53 |
+
if d:_bg_home.update({"t":time.time(),"d":d})
|
| 54 |
raw=[]
|
| 55 |
for h in f6.YOUTUBE_HANDLES:raw.extend(f6._yt_ytdlp(h,20) or f6._yt_html(h,20))
|
| 56 |
raw.extend(f6._fallback_shorts());seen=set();out=[]
|
| 57 |
for v in raw:
|
| 58 |
vid=v.get('id') or ''
|
| 59 |
if vid and vid not in seen:seen.add(vid);out.append(v)
|
| 60 |
+
if out:_bg_shorts.update({"t":time.time(),"d":out[:40]})
|
| 61 |
_cleanup_old_posts()
|
| 62 |
+
except:pass
|
| 63 |
+
finally:_bg_lock=False
|
|
|
|
| 64 |
@app.on_event("startup")
|
| 65 |
+
async def _startup():threading.Thread(target=_bg_refresh,daemon=True).start()
|
| 66 |
+
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 |
)]
|
| 74 |
|
| 75 |
@app.get('/api/homepage')
|
| 76 |
+
def _homepage():
|
| 77 |
now=time.time()
|
| 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()
|
| 89 |
+
if _bg_shorts['d'] and (not refresh or now-_bg_shorts['t']<120):
|
| 90 |
+
if now-_bg_shorts['t']>600:threading.Thread(target=_bg_refresh,daemon=True).start()
|
| 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 |
fresh=[p for p in posts if now-int(p.get('ts') or 0)<TTL_24H]
|
| 99 |
+
return JSONResponse({'posts':fresh,'persistent':HAS_PERSISTENT})
|
| 100 |
+
|
| 101 |
+
@app.get('/api/storage_status')
|
| 102 |
+
def _storage():return JSONResponse({'persistent':HAS_PERSISTENT,'data_dir':DATA_DIR})
|
| 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',''))
|
| 110 |
+
if not vid or not text:return JSONResponse({'error':'missing'},status_code=400)
|
| 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 là VNEWS AI, một người bạn thông minh và am hiểu. Người dùng đang đọc bài viết/xem video và hỏi bạn một câu.
|
| 124 |
+
|
| 125 |
+
Đây là nội dung họ đang xem:
|
| 126 |
+
"{title}"
|
| 127 |
+
{raw[:9000]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
+
Họ hỏi: "{q}"
|
| 130 |
|
| 131 |
+
Hãy trả lời như một người bạn đang giải thích cho người khác nghe — tự nhiên, dễ hiểu, có chiều sâu. Đừng nói kiểu robot hay liệt kê khô khan. Nếu bài không nói đến phần họ hỏi, bạn vẫn có thể chia sẻ kiến thức của mình nhưng nói rõ "theo mình biết thêm thì..." Trả lời bằng tiếng Việt.
|
|
|
|
|
|
|
|
|
|
| 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 lúc này. Bạn thử hỏi lại nhé!'
|
| 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)<100:return JSONResponse({'error':'Không đọc được bài viết từ URL này'},status_code=422)
|
| 145 |
+
prompt=f"""Tóm tắt bài viết sau để đăng Tường AI:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
Tiêu đề: {title}
|
| 148 |
Nội dung:
|
| 149 |
{raw[:14000]}
|
| 150 |
|
| 151 |
+
Yêu cầu: tóm tắt 4-6 ý chính, tự nhiên, không bịa. Cuối ghi nguồn.
|
|
|
|
|
|
|
|
|
|
| 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)<100:text=f"Tóm tắt: {title}\n\n{raw[:1200]}\n\nNguồn: {_domain(url)}"
|
| 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':'Không tìm thấy bài.'},status_code=404)
|
| 168 |
+
# Collect URLs from source_details and sources
|
| 169 |
+
urls=[]
|
| 170 |
+
for s in (p.get('source_details') or []):
|
| 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)>200:
|
| 179 |
+
scraped_parts.append(f"NGUỒN: {_domain(u)}\nTIÊU ĐỀ: {t}\n{raw}")
|
| 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 hấp dẫn từ các nguồn sau:
|
| 183 |
|
| 184 |
+
Chủ đề gốc: {title}
|
| 185 |
+
|
| 186 |
+
Nội dung các nguồn đã scrape:
|
| 187 |
{all_content[:16000]}
|
| 188 |
|
| 189 |
+
Yêu cầu: tiêu đề mới hấp dẫn + 4-6 ý chính tổng hợp + nguồn tham khảo cuối bài.
|
| 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)<150:text=f"Tóm tắt: {title}\n\n{all_content[:1500]}\n\nNguồn: VNEWS AI"
|
| 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',[])
|
|
|
|
| 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',''))
|
| 204 |
if not topic:return JSONResponse({'error':'missing topic'},status_code=400)
|
| 205 |
img=f6._topic_image(topic)
|
|
|
|
| 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. Thử chủ đề cụ thể hơn.'},status_code=422)
|
| 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Ề ĐÚNG CHỦ ĐỀ: "{topic}"
|
| 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 |
vias=', '.join(sorted({d.get('via','') for d in (details or []) if d.get('via')}))
|
|
|
|
| 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}.source-read-btn{color:#5cb87a;font-size:11px;margin-top:4px;display:inline-block}.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}</style>
|
| 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 |
+
// Check persistent storage and warn
|
| 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 |
|
|
|
|
| 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 |
window.rewriteTopicPost=async function(postId){
|
| 261 |
+
let btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang scrape nguồn & rewrite...';}
|
| 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 |
window.askAIWall=async function(i){
|
| 265 |
let q=document.getElementById('article-ai-q')?.value.trim();if(!q)return alert('Nhập câu hỏi');
|
| 266 |
+
document.getElementById('article-ai-ans').textContent='Đang hỏi...';
|
| 267 |
let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];
|
| 268 |
+
let p=wall[i]||{};let context=(p.text||'');for(let s of (p.source_details||[]))context+='\n'+(s.content||'');
|
| 269 |
try{let r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question:q,context:context.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}};
|
| 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 |
+
// Regular articles: single rewrite + ask
|
| 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 |
+
art.querySelectorAll('[data-rewrite]').forEach((e,i)=>{if(i>0)e.remove();});
|
| 279 |
+
if(!art.querySelector('[data-rewrite]')){let a=art.querySelector('.article-actions');if(a){let b=document.createElement('button');b.className='primary';b.setAttribute('data-rewrite','1');b.textContent='🤖 Rewrite AI đăng tường';b.onclick=function(){let url=(window._currentArticle&&window._currentArticle.url)||'';if(!url){alert('Không có URL');return;}b.disabled=true;b.textContent='Đang rewrite...';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)).finally(()=>{b.disabled=false;b.textContent='🤖 Rewrite AI đăng tường'})};a.insertBefore(b,a.firstChild);}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
let oldRA=window.readArticle;if(oldRA){window.readArticle=async function(){let ret=await oldRA.apply(this,arguments);setTimeout(patchArticle,600);return ret;}}
|
| 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);
|
| 288 |
})();
|
| 289 |
</script>
|
| 290 |
'''
|
| 291 |
|
| 292 |
@app.get('/')
|
| 293 |
+
async def _index():
|
| 294 |
html=f5.f4.f3.f2.f1._load_index_html()
|
| 295 |
body=getattr(rt.old,'PATCH_INJECT','')+f5.f4.f3.f2.f1.FINAL_INJECT+f5.f4.f3.FINAL3_INJECT+f5.f4.FINAL4_INJECT+f5.FINAL5_INJECT
|
| 296 |
body+=getattr(f6,'FINAL6_INJECT','')
|