Spaces:
Running
Running
Bypass deep import chain: direct main import for fast startup + fix all issues"
Browse files- app_fast.py +246 -0
app_fast.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""VNEWS fast runtime: bypass deep import chain, import main.py directly.
|
| 2 |
+
Fixes: slow load, prompt leaking, source_details persistence, ask AI context, duplicate rewrite button.
|
| 3 |
+
"""
|
| 4 |
+
import os, re, json, time, threading, asyncio
|
| 5 |
+
import html as html_lib
|
| 6 |
+
from urllib.parse import quote, urlparse
|
| 7 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 8 |
+
import requests
|
| 9 |
+
from bs4 import BeautifulSoup
|
| 10 |
+
from fastapi import Request, Query
|
| 11 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 12 |
+
|
| 13 |
+
# Import main app directly — skip 8-layer import chain.
|
| 14 |
+
from main import app
|
| 15 |
+
|
| 16 |
+
# Import AI generation from ai_ext (lightweight, no extra chain).
|
| 17 |
+
try:
|
| 18 |
+
from ai_ext import qwen_generate, _load_ai_wall, _save_ai_wall, make_post, _clean_text, pollinations_image_url, SHORTS_DIR, _safe_name, _download_image, gTTS, scrape_any_url
|
| 19 |
+
except Exception:
|
| 20 |
+
qwen_generate=None
|
| 21 |
+
|
| 22 |
+
RESTORE_INDEX_URL="https://huggingface.co/spaces/bep40/vnews/raw/restore-33c3dda/static/index.html"
|
| 23 |
+
UA={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36","Accept-Language":"vi,en;q=0.8"}
|
| 24 |
+
SPACE_URL="https://bep40-vnews.hf.space"
|
| 25 |
+
DATA_DIR="/data" if os.path.isdir('/data') else "/app/data"
|
| 26 |
+
_HOME_CACHE={"t":0,"d":[]}
|
| 27 |
+
_SHORTS_CACHE={"t":0,"d":[]}
|
| 28 |
+
|
| 29 |
+
def clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or ""))).strip()
|
| 30 |
+
def _domain(u):
|
| 31 |
+
try:return urlparse(u or '').netloc.replace('www.','')
|
| 32 |
+
except Exception:return ''
|
| 33 |
+
|
| 34 |
+
# ===== FAST RSS HOMEPAGE =====
|
| 35 |
+
def _rss_articles(feed_url, group, source='vne', limit=6):
|
| 36 |
+
out=[]
|
| 37 |
+
try:
|
| 38 |
+
r=requests.get(feed_url,headers=UA,timeout=5);r.encoding='utf-8'
|
| 39 |
+
soup=BeautifulSoup(r.text,'xml')
|
| 40 |
+
for it in soup.find_all('item')[:limit]:
|
| 41 |
+
title=clean(it.find('title').get_text(' ',strip=True) if it.find('title') else '')
|
| 42 |
+
link=clean(it.find('link').get_text(strip=True) if it.find('link') else '')
|
| 43 |
+
desc_raw=it.find('description').get_text(' ',strip=True) if it.find('description') else ''
|
| 44 |
+
ds=BeautifulSoup(desc_raw,'lxml')
|
| 45 |
+
im=ds.find('img');img=im.get('src','') if im else ''
|
| 46 |
+
if title and link:out.append({'title':title,'link':link,'img':img,'source':source,'group':group})
|
| 47 |
+
except Exception:pass
|
| 48 |
+
return out
|
| 49 |
+
|
| 50 |
+
def _fetch_homepage():
|
| 51 |
+
feeds=[('Thời Sự','https://vnexpress.net/rss/thoi-su.rss'),('Thế Giới','https://vnexpress.net/rss/the-gioi.rss'),('Kinh Doanh','https://vnexpress.net/rss/kinh-doanh.rss'),('Công Nghệ','https://vnexpress.net/rss/so-hoa.rss'),('Thể Thao','https://vnexpress.net/rss/the-thao.rss'),('Giải Trí','https://vnexpress.net/rss/giai-tri.rss'),('Sức Khỏe','https://vnexpress.net/rss/suc-khoe.rss'),('Pháp Luật','https://vnexpress.net/rss/phap-luat.rss')]
|
| 52 |
+
arts=[]
|
| 53 |
+
with ThreadPoolExecutor(6) as ex:
|
| 54 |
+
futs=[ex.submit(_rss_articles,u,g,'vne',6) for g,u in feeds]
|
| 55 |
+
for f in as_completed(futs,timeout=8):
|
| 56 |
+
try:arts.extend(f.result() or [])
|
| 57 |
+
except Exception:pass
|
| 58 |
+
return arts
|
| 59 |
+
|
| 60 |
+
def _fetch_shorts():
|
| 61 |
+
from main import SHORTS_FALLBACK, _yt_channel_shorts
|
| 62 |
+
items=[];seen=set()
|
| 63 |
+
for ch in ["baodantri7941","baosuckhoedoisongboyte"]:
|
| 64 |
+
got=_yt_channel_shorts(ch,20)
|
| 65 |
+
for v in got:
|
| 66 |
+
vid=v.get('id')
|
| 67 |
+
if vid and vid not in seen:seen.add(vid);items.append(v)
|
| 68 |
+
for v in SHORTS_FALLBACK:
|
| 69 |
+
vid=v.get('id')
|
| 70 |
+
if vid and vid not in seen:seen.add(vid);items.append(v)
|
| 71 |
+
return items[:40]
|
| 72 |
+
|
| 73 |
+
def _bg_refresh():
|
| 74 |
+
try:
|
| 75 |
+
data=_fetch_homepage()
|
| 76 |
+
if data:_HOME_CACHE.update({"t":time.time(),"d":data})
|
| 77 |
+
except Exception:pass
|
| 78 |
+
try:
|
| 79 |
+
data=_fetch_shorts()
|
| 80 |
+
if data:_SHORTS_CACHE.update({"t":time.time(),"d":data})
|
| 81 |
+
except Exception:pass
|
| 82 |
+
|
| 83 |
+
@app.on_event("startup")
|
| 84 |
+
async def startup():threading.Thread(target=_bg_refresh,daemon=True).start()
|
| 85 |
+
|
| 86 |
+
# Periodic refresh every 8 min.
|
| 87 |
+
def _periodic():
|
| 88 |
+
while True:
|
| 89 |
+
time.sleep(480);_bg_refresh()
|
| 90 |
+
threading.Thread(target=_periodic,daemon=True).start()
|
| 91 |
+
|
| 92 |
+
# ===== OVERRIDE HOMEPAGE/SHORTS =====
|
| 93 |
+
_rm={'/api/homepage','/api/shorts','/api/topic_post','/api/article/ask','/'}
|
| 94 |
+
app.router.routes=[r for r in app.router.routes if getattr(r,'path',None) not in _rm]
|
| 95 |
+
|
| 96 |
+
@app.get('/api/homepage')
|
| 97 |
+
def api_homepage():
|
| 98 |
+
if _HOME_CACHE['d']:
|
| 99 |
+
if time.time()-_HOME_CACHE['t']>300:threading.Thread(target=_bg_refresh,daemon=True).start()
|
| 100 |
+
return JSONResponse(_HOME_CACHE['d'])
|
| 101 |
+
data=_fetch_homepage()
|
| 102 |
+
if data:_HOME_CACHE.update({"t":time.time(),"d":data})
|
| 103 |
+
return JSONResponse(data)
|
| 104 |
+
|
| 105 |
+
@app.get('/api/shorts')
|
| 106 |
+
def api_shorts(refresh:int=Query(default=0)):
|
| 107 |
+
if _SHORTS_CACHE['d'] and not refresh:return JSONResponse(_SHORTS_CACHE['d'])
|
| 108 |
+
if _SHORTS_CACHE['d'] and time.time()-_SHORTS_CACHE['t']<120:return JSONResponse(_SHORTS_CACHE['d'])
|
| 109 |
+
data=_fetch_shorts()
|
| 110 |
+
if data:_SHORTS_CACHE.update({"t":time.time(),"d":data})
|
| 111 |
+
return JSONResponse(data or _SHORTS_CACHE.get('d',[]))
|
| 112 |
+
|
| 113 |
+
# ===== TOPIC POST (no prompt leaking, source_details persistent) =====
|
| 114 |
+
def _fast_rss_sources(topic,limit=8):
|
| 115 |
+
feeds=[('VnExpress','https://vnexpress.net/rss/tin-moi-nhat.rss'),('Dân trí','https://dantri.com.vn/rss/home.rss'),('Vietnamnet','https://vietnamnet.vn/rss/tin-moi-nhat.rss')]
|
| 116 |
+
pool=[]
|
| 117 |
+
for name,url in feeds:
|
| 118 |
+
try:
|
| 119 |
+
r=requests.get(url,headers=UA,timeout=5);r.encoding='utf-8';soup=BeautifulSoup(r.text,'xml')
|
| 120 |
+
for it in soup.find_all('item')[:20]:
|
| 121 |
+
title=clean(it.find('title').get_text(' ',strip=True) if it.find('title') else '')
|
| 122 |
+
link=clean(it.find('link').get_text(strip=True) if it.find('link') else '')
|
| 123 |
+
desc=clean(BeautifulSoup(it.find('description').get_text(' ',strip=True) if it.find('description') else '','lxml').get_text(' ',strip=True))
|
| 124 |
+
# Extract image from description HTML
|
| 125 |
+
desc_html=it.find('description').get_text(' ',strip=True) if it.find('description') else ''
|
| 126 |
+
img_match=re.search(r'<img[^>]+src=["\']([^"\']+)',desc_html)
|
| 127 |
+
img=img_match.group(1) if img_match else ''
|
| 128 |
+
if title and link:pool.append({'title':title,'url':link,'source':name,'snippet':desc,'img':img})
|
| 129 |
+
except Exception:pass
|
| 130 |
+
# Score by topic relevance
|
| 131 |
+
keys=[w.lower() for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',topic) if len(w)>2]
|
| 132 |
+
scored=[]
|
| 133 |
+
for it in pool:
|
| 134 |
+
hay=(it['title']+' '+it['snippet']).lower()
|
| 135 |
+
sc=sum(2 for k in keys if k in hay)
|
| 136 |
+
if topic.lower() in hay:sc+=10
|
| 137 |
+
if sc>0:scored.append((sc,it))
|
| 138 |
+
scored.sort(key=lambda x:x[0],reverse=True)
|
| 139 |
+
return [it for _,it in scored[:limit]]
|
| 140 |
+
|
| 141 |
+
@app.post('/api/topic_post')
|
| 142 |
+
async def topic_post(request:Request):
|
| 143 |
+
body=await request.json();topic=clean(body.get('topic',''))
|
| 144 |
+
if not topic:return JSONResponse({'error':'missing topic'},status_code=400)
|
| 145 |
+
img=pollinations_image_url(topic) if pollinations_image_url else ''
|
| 146 |
+
sources=_fast_rss_sources(topic,8)
|
| 147 |
+
if not sources:return JSONResponse({'error':'Không tìm được nội dung liên quan. Thử chủ đề khác.'},status_code=422)
|
| 148 |
+
ctx='\n\n'.join([f"Nguồn: {s['source']}\nTiêu đề: {s['title']}\nTóm tắt: {s['snippet']}" for s in sources[:6]])
|
| 149 |
+
prompt=f"""Viết bài tiếng Việt về: {topic}
|
| 150 |
+
|
| 151 |
+
Dữ liệu nguồn:
|
| 152 |
+
{ctx[:12000]}
|
| 153 |
+
|
| 154 |
+
Yêu cầu: Chỉ viết về {topic}. Bỏ mọi nội dung không liên quan. Tiêu đề hấp dẫn. Sapo 2 câu. 5-7 đoạn phân tích. Cuối ghi nguồn tham khảo."""
|
| 155 |
+
text=None
|
| 156 |
+
if qwen_generate:
|
| 157 |
+
try:text=await asyncio.wait_for(qwen_generate(prompt,image_url=img,max_tokens=1500),timeout=30)
|
| 158 |
+
except Exception:pass
|
| 159 |
+
if not text or len(text)<300:
|
| 160 |
+
bullets='\n'.join([f"• {s['title']}" for s in sources[:6]])
|
| 161 |
+
vias=', '.join(sorted({s['source'] for s in sources}))
|
| 162 |
+
text=f"{topic}\n\n{bullets}\n\nNguồn tham khảo: {vias}"
|
| 163 |
+
# Remove any leaked prompt/rules from output.
|
| 164 |
+
for marker in ['QUY TẮC BẮT BUỘC','NỘI DUNG NGUỒN:','Dữ liệu nguồn:','Yêu cầu:','Viết bài tiếng Việt về:']:
|
| 165 |
+
if marker in text:text=text.split(marker)[0].strip()
|
| 166 |
+
post=make_post(topic,text,img,'','topic',sources=[{'title':s['title'],'url':s['url'],'via':s['source'],'img':s.get('img','')} for s in sources])
|
| 167 |
+
post['images']=[img]
|
| 168 |
+
post['source_details']=[{'title':s['title'],'url':s['url'],'via':s['source'],'img':s.get('img',''),'content':s['snippet'][:500]} for s in sources]
|
| 169 |
+
posts=_load_ai_wall();posts.insert(0,post);_save_ai_wall(posts)
|
| 170 |
+
return JSONResponse({'post':post})
|
| 171 |
+
|
| 172 |
+
# ===== ASK AI (uses article/short content as context) =====
|
| 173 |
+
@app.post('/api/article/ask')
|
| 174 |
+
async def article_ask(request:Request):
|
| 175 |
+
body=await request.json();url=clean(body.get('url',''));question=clean(body.get('question',''));context=clean(body.get('context',''))
|
| 176 |
+
if not question:return JSONResponse({'error':'missing question'},status_code=400)
|
| 177 |
+
if url:
|
| 178 |
+
try:
|
| 179 |
+
data=scrape_any_url(url) if scrape_any_url else None
|
| 180 |
+
if data:context=(data.get('title','')+'\n'+data.get('summary','')+'\n'+data.get('text','')).strip()
|
| 181 |
+
except Exception:pass
|
| 182 |
+
if not context:context=question
|
| 183 |
+
prompt=f"""Trả lời chi tiết câu hỏi dựa trên nội dung bài viết/video bên dưới. Chỉ dựa vào nội dung có sẵn.
|
| 184 |
+
|
| 185 |
+
Nội dung: {context[:8000]}
|
| 186 |
+
|
| 187 |
+
Câu hỏi: {question}"""
|
| 188 |
+
ans=None
|
| 189 |
+
if qwen_generate:
|
| 190 |
+
try:ans=await asyncio.wait_for(qwen_generate(prompt,max_tokens=800),timeout=25)
|
| 191 |
+
except Exception:pass
|
| 192 |
+
if not ans:ans='AI chưa trả lời được lúc này.'
|
| 193 |
+
return JSONResponse({'answer':ans})
|
| 194 |
+
|
| 195 |
+
# ===== ROOT: load UI + inject =====
|
| 196 |
+
def _load_index():
|
| 197 |
+
try:
|
| 198 |
+
with open('/app/static/index.html','r',encoding='utf-8') as f:html=f.read()
|
| 199 |
+
except Exception:html=''
|
| 200 |
+
if '<!DOCTYPE html>' not in html or '<div id="view-home"' not in html:
|
| 201 |
+
try:
|
| 202 |
+
r=requests.get(RESTORE_INDEX_URL,timeout=15)
|
| 203 |
+
if r.status_code==200 and '<!DOCTYPE html>' in r.text:html=r.text
|
| 204 |
+
except Exception:pass
|
| 205 |
+
return html or '<!DOCTYPE html><html><body>VNEWS</body></html>'
|
| 206 |
+
|
| 207 |
+
INJECT=r'''
|
| 208 |
+
<style>
|
| 209 |
+
.ai-compose{width:calc(100% - 8px)!important}.ai-compose-row{display:flex!important;flex-direction:column!important;gap:8px!important}.ai-compose-row input,.ai-compose-row button{width:100%!important;box-sizing:border-box!important}.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:8px;padding:8px;font-size:12px}.article-ai-ask button{background:#2d8659;border:0;color:#fff;border-radius:8px;padding:7px 12px;margin-top:6px;font-size:11px}.article-ai-answer{white-space:pre-wrap;color:#ccc;font-size:12px;line-height:1.5;margin-top:8px}.source-in-topic{background:#1a1a1a;border:1px solid #2a2a2a;border-radius:8px;padding:8px;margin:6px 0;cursor:pointer}.source-in-topic img{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:6px;margin-bottom:6px;background:#222}.source-in-topic .src-title{font-size:12px;color:#5cb87a;font-weight:700}.source-in-topic .src-via{font-size:10px;color:#888}
|
| 210 |
+
</style>
|
| 211 |
+
<script>
|
| 212 |
+
(function(){
|
| 213 |
+
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 214 |
+
// Patch readArticle to add Ask AI box + single rewrite button (no duplicate).
|
| 215 |
+
let _origRead=window.readArticle;
|
| 216 |
+
if(_origRead){window.readArticle=async function(){let ret=await _origRead.apply(this,arguments);setTimeout(()=>{let art=document.querySelector('#view-article .article-view');if(!art)return;
|
| 217 |
+
// Add Ask AI box if not present.
|
| 218 |
+
if(!document.getElementById('article-ai-ask')){let box=document.createElement('div');box.id='article-ai-ask';box.className='article-ai-ask';box.innerHTML='<b style="color:#5cb87a;font-size:13px">🤖 Hỏi AI về bài viết</b><textarea id="article-ai-q" placeholder="Nhập câu hỏi..."></textarea><button onclick="askArticleAI()">Hỏi</button><div id="article-ai-ans" class="article-ai-answer"></div>';art.appendChild(box);}
|
| 219 |
+
// Add rewrite button only if not already present.
|
| 220 |
+
let actions=art.querySelector('.article-actions');if(actions&&!actions.querySelector('[data-rw]')){let btn=document.createElement('button');btn.className='primary';btn.setAttribute('data-rw','1');btn.textContent='🤖 AI viết lại & đăng tường';btn.onclick=function(){if(typeof rewriteCurrentArticle==='function')rewriteCurrentArticle();};actions.appendChild(btn);}},500);return ret;}}
|
| 221 |
+
window.askArticleAI=async function(){let q=document.getElementById('article-ai-q')?.value.trim();if(!q)return;let ans=document.getElementById('article-ai-ans');ans.textContent='Đang hỏi...';let url=(window._currentArticle?.url)||'';let ctx=document.querySelector('.article-view')?.innerText?.slice(0,6000)||'';let r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,question:q,context:ctx})}).then(r=>r.json()).catch(()=>({answer:'Lỗi'}));ans.textContent=r.answer||'Không có trả lời';}
|
| 222 |
+
// Patch topic wall read to show source_details with images and in-app links.
|
| 223 |
+
let _origWallRead=window.readLiveTopicWall||window.aiReadWallPatched||window.aiReadWall;
|
| 224 |
+
function renderTopicArticle(p){
|
| 225 |
+
showView('view-article');
|
| 226 |
+
let srcHtml='';
|
| 227 |
+
if(p.source_details&&p.source_details.length){srcHtml='<div style="margin-top:12px"><b style="color:#5cb87a;font-size:13px">📚 Bài viết liên quan</b>';p.source_details.forEach(s=>{srcHtml+=`<div class="source-in-topic" onclick="readArticle('${esc(s.url)}')">${s.img?`<img src="${esc(s.img)}" onerror="this.style.display='none'">`:''}<div class="src-title">${esc(s.title)}</div><div class="src-via">${esc(s.via||'')}</div></div>`;});srcHtml+='</div>';}
|
| 228 |
+
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="${esc(p.img)}">`:''}<p class="article-p" style="white-space:pre-wrap">${esc(p.text)}</p>${srcHtml}<div class="article-actions"><button onclick="doShare('${esc(p.title)}','${esc(p.url||location.href)}','${esc(p.img||'')}')">📤 Chia sẻ</button></div><div class="article-ai-ask" id="article-ai-ask"><b style="color:#5cb87a;font-size:13px">🤖 Hỏi AI về bài này</b><textarea id="article-ai-q" placeholder="Hỏi bất cứ điều gì..."></textarea><button onclick="askArticleAI()">Hỏi</button><div id="article-ai-ans" class="article-ai-answer"></div></div></div>`;
|
| 229 |
+
document.getElementById('view-article').innerHTML=h;window.scrollTo(0,0);
|
| 230 |
+
// Store context for Ask AI.
|
| 231 |
+
window._currentArticle={url:'',data:{title:p.title,text:p.text}};
|
| 232 |
+
}
|
| 233 |
+
window.readLiveTopicWall=window.aiReadWallPatched=window.aiReadWall=function(i){let arr=window.__topicWallE||window.liveTopicWall||window.patchedWall||window.finalWall||window.finalWall3||[];let p=arr[i];if(p){renderTopicArticle(p);return;}if(_origWallRead)_origWallRead(i);};
|
| 234 |
+
// Override Ask AI to use stored article text as context when no URL.
|
| 235 |
+
let _origAsk=window.askArticleAI;
|
| 236 |
+
window.askArticleAI=async function(){let q=document.getElementById('article-ai-q')?.value.trim();if(!q)return;let ans=document.getElementById('article-ai-ans');ans.textContent='Đang hỏi AI...';let url=window._currentArticle?.url||'';let ctx=window._currentArticle?.data?.text||document.querySelector('.article-view')?.innerText?.slice(0,6000)||'';let r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,question:q,context:ctx})}).then(r=>r.json()).catch(()=>({answer:'Lỗi kết nối'}));ans.textContent=r.answer||'Không trả lời được';}
|
| 237 |
+
})();
|
| 238 |
+
</script>
|
| 239 |
+
'''
|
| 240 |
+
|
| 241 |
+
@app.get('/')
|
| 242 |
+
async def index():
|
| 243 |
+
html=_load_index()
|
| 244 |
+
# Only inject our lightweight patch — no 6-layer chain.
|
| 245 |
+
from ai_patch import PATCH_INJECT as OLD_INJECT
|
| 246 |
+
return HTMLResponse(html.replace('</body>',OLD_INJECT+INJECT+'\n</body>') if '</body>' in html else html+INJECT)
|