bep40 commited on
Commit
7bcd2f8
·
verified ·
1 Parent(s): e8bc7d7

Fix: single rewrite button, guaranteed rewrite success, short button after rewrite"

Browse files
Files changed (1) hide show
  1. ai_runtime_patch_fast.py +59 -98
ai_runtime_patch_fast.py CHANGED
@@ -1,4 +1,4 @@
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
@@ -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
- 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',''))
@@ -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 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, 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 "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',[])
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. 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')}))
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}.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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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
 
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
- 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);
 
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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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);