bep40 commited on
Commit
decdd25
·
verified ·
1 Parent(s): d529d07

Fix: persistent data via HF Dataset, single ask box, working rewrite"

Browse files
Files changed (1) hide show
  1. ai_runtime_patch_fast.py +95 -69
ai_runtime_patch_fast.py CHANGED
@@ -1,4 +1,4 @@
1
- """Patch over c1e2703: working topic rewrite, natural AI answers, 24h TTL, short comments."""
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
@@ -6,8 +6,14 @@ import html as html_lib
6
 
7
  def clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or ""))).strip()
8
 
 
 
 
9
  DATA_DIR="/data" if os.path.isdir('/data') else "/app/data"
 
10
  SHORT_COMMENTS_FILE=os.path.join(DATA_DIR,'short_comments.json')
 
 
11
  TTL_24H=86400
12
 
13
  def _load_json(path,default):
@@ -64,6 +70,8 @@ app.router.routes=[r for r in app.router.routes if not (
64
  (getattr(r,'path',None)=='/api/short/comment' and 'POST' in getattr(r,'methods',set())) or
65
  (getattr(r,'path',None)=='/api/article/ask' and 'POST' in getattr(r,'methods',set())) or
66
  (getattr(r,'path',None)=='/api/topic/rewrite' and 'POST' in getattr(r,'methods',set())) or
 
 
67
  (getattr(r,'path',None)=='/api/ai_wall' and 'GET' in getattr(r,'methods',set())) or
68
  (getattr(r,'path',None)=='/' and 'GET' in getattr(r,'methods',set()))
69
  )]
@@ -106,7 +114,7 @@ async def post_short_comment(request:Request):
106
  db=_load_json(SHORT_COMMENTS_FILE,{});comments=db.get(vid,[]);comments.insert(0,{'text':text[:300],'ts':int(time.time())})
107
  db[vid]=comments[:100];_save_json(SHORT_COMMENTS_FILE,db);return JSONResponse({'comments':db[vid]})
108
 
109
- # ===== ARTICLE ASK AI — natural and friendly =====
110
  @app.post('/api/article/ask')
111
  async def article_ask(request:Request):
112
  body=await request.json();url=clean(body.get('url',''));question=clean(body.get('question',''));context=clean(body.get('context',''))
@@ -119,9 +127,9 @@ async def article_ask(request:Request):
119
  except Exception:pass
120
  if not raw and context:raw=context[:12000]
121
  if not raw:raw=question
122
- prompt=f"""Bạn là trợ lý thông minh của VNEWS. Người dùng đang đọc một bài viết/xem video và muốn hỏi bạn.
123
 
124
- Nội dung bài viết/video mà người dùng đang xem:
125
  ---
126
  Tiêu đề: {title}
127
  {raw[:10000]}
@@ -129,46 +137,72 @@ Tiêu đề: {title}
129
 
130
  Câu hỏi: {question}
131
 
132
- Hãy trả lời bằng tiếng Việt, tự nhiên thân thiện như đang trò chuyện:
133
- - Lấy thông tin từ nội dung bài viết/video ở trên làm cơ sở chính.
134
- - Giải thích dễ hiểu, dụ nếu cần.
135
- - Nếu câu hỏi liên quan đến bài → trả lời chi tiết từ nội dung bài.
136
- - Nếu câu hỏi mở rộng hơn → bổ sung kiến thức chung nhưng nói rõ "ngoài bài viết, theo hiểu biết chung thì..."
137
- - Không trả lời khô khan, máy móc. Hãy viết như một người bạn am hiểu đang giải thích.
138
  """
139
  ans=await f5.base.qwen_generate(prompt,max_tokens=1200)
140
- if not ans:ans='AI chưa trả lời được lúc này. Bạn thử hỏi lại nhé.'
141
  return JSONResponse({'answer':ans,'title':title})
142
 
143
- # ===== TOPIC REWRITE — takes post_id, rewrites using all source content =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  @app.post('/api/topic/rewrite')
145
  async def topic_rewrite(request:Request):
146
  body=await request.json();post_id=str(body.get('post_id','')).strip()
147
  if not post_id:return JSONResponse({'error':'missing post_id'},status_code=400)
148
  posts=f5.base._load_ai_wall();p=next((x for x in posts if str(x.get('id'))==post_id),None)
149
- if not p:return JSONResponse({'error':'Không tìm thấy bài viết.'},status_code=404)
150
  all_content=(p.get('text') or '')
151
- for s in (p.get('source_details') or []):
152
- all_content+='\n\n--- Nguồn: '+s.get('title','')+' ---\n'+s.get('content','')
153
- title=p.get('title') or 'Bài viết AI'
154
- prompt=f"""Bạn là biên tập viên VNEWS. Viết lại nội dung bên dưới thành BẢN TÓM TẮT MỚI, hấp dẫn, đăng lên Tường AI.
155
 
156
  Tiêu đề gốc: {title}
157
- Nội dung gốc và nguồn tham khảo:
158
  {all_content[:16000]}
159
 
160
- Yêu cầu:
161
- - Viết lại tự nhiên, mạch lạc, không sao chép nguyên văn.
162
- - 1 tiêu đề mới hấp dẫn.
163
- - 1 đoạn mở đầu + 3-5 ý chính.
164
- - Giữ sự thật, không bịa.
165
- - Cuối bài ghi nguồn tham khảo.
166
  """
167
  text=None
168
  try:text=await asyncio.wait_for(f5.base.qwen_generate(prompt,image_url=p.get('img'),max_tokens=1200),timeout=30)
169
  except Exception:text=None
170
- if not text or len(text)<200:
171
- text=f"Tóm tắt: {title}\n\n{all_content[:1500]}\n\nNguồn tham khảo: 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)
@@ -183,36 +217,30 @@ async def topic_post_focused(request:Request):
183
  research=f6._fast_context(topic) if hasattr(f6,'_fast_context') else f6._web_research_context(topic)
184
  context=research.get('context','');sources=research.get('sources',[])
185
  details=f6._extract_source_details_from_context(context,sources) if hasattr(f6,'_extract_source_details_from_context') else []
186
- if not context or not sources:
187
- return JSONResponse({'error':'Không tìm/crawl được đủ nội dung về chủ đề này.'},status_code=422)
188
  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]
189
- prompt=f"""Bạn là biên tập viên VNEWS. Viết MỘT BÀI VIẾT bằng tiếng Việt VỀ ĐÚNG CHỦ ĐỀ: "{topic}"
190
 
191
- NỘI DUNG NGUỒN:
192
  {source_brief[:18000]}
193
 
194
- QUY TẮC:
195
- 1. CHỈ viết về "{topic}". Loại bỏ mọi nội dung không liên quan.
196
- 2. Tiêu đề chứa từ khóa "{topic}".
197
- 3. Sapo 2-3 câu. 5-8 đoạn phân tích liên quan TRỰC TIẾP.
198
- 4. Không mở đầu bằng "Dưới đây là".
199
- 5. Cuối bài có "Nguồn tham khảo".
200
  """
201
  text=None
202
  try:text=await asyncio.wait_for(f5.base.qwen_generate(prompt,image_url=img,max_tokens=1700),timeout=35)
203
  except Exception:text=None
204
- if not text or len(text)<350:
205
- bullets='\n'.join([f"• {d['title']}: {d.get('content','')[:320]}" for d in (details or [])[:6]])
206
- vias=', '.join(sorted({d.get('via','') for d in (details or []) if d.get('via')})) or ', '.join(sorted({s.get('via','') for s in sources if s.get('via')}))
207
- text=f"{topic}: tổng hợp\n\n{bullets}\n\nNguồn tham khảo: {vias}"
208
  post=f5.base.make_post(topic,text,img,'','topic_focused',sources=[s for s in sources if s.get('url')])
209
  post['images']=[img];post['source_details']=details
210
  posts=f5.base._load_ai_wall();posts.insert(0,post);f5.base._save_ai_wall(posts)
211
- return JSONResponse({'post':post,'sources_count':len(details or sources)})
212
 
213
- # ===== FRONTEND =====
214
  PATCH_INJECT=r'''
215
- <style>.short-cmt-panel{position:fixed;bottom:0;left:0;right:0;max-height:55vh;background:#181818;border-radius:16px 16px 0 0;z-index:99999;padding:14px;display:none;overflow-y:auto}.short-cmt-panel.active{display:block}.short-cmt-panel textarea{width:100%;background:#222;border:1px solid #444;color:#eee;border-radius:10px;padding:9px;margin:6px 0;min-height:60px}.short-cmt-panel button{background:#2d8659;border:0;color:#fff;border-radius:10px;padding:8px 12px;margin:4px}.cmt-item{background:#222;border-radius:8px;padding:7px;margin:5px 0;color:#ccc;font-size:12px}.source-detail-box{margin-top:14px;background:#151515;border:1px solid #2b2b2b;border-radius:10px;padding:10px}.source-detail-item{background:#202020;border-radius:8px;padding:9px;margin:7px 0}.source-detail-title{font-size:12px;font-weight:700;color:#eee}.source-detail-meta{font-size:10px;color:#888;margin:3px 0}.source-detail-content{font-size:12px;color:#bbb;line-height:1.5;white-space:pre-wrap;max-height:160px;overflow:auto}.source-detail-item a{color:#5cb87a;font-size:11px;text-decoration:none;cursor:pointer}.source-detail-item img{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:6px;margin-bottom:6px;background:#222}.article-ai-ask{margin-top:12px;background:#141414;border:1px solid #2a2a2a;border-radius:10px;padding:10px}.article-ai-ask textarea{width:100%;min-height:60px;background:#222;border:1px solid #444;color:#eee;border-radius:10px;padding:9px}.article-ai-ask button{background:#2d8659;border:0;color:#fff;border-radius:10px;padding:8px 12px;margin-top:6px}.article-ai-answer{white-space:pre-wrap;color:#ccc;font-size:13px;line-height:1.55;margin-top:8px}</style>
216
  <div id="short-cmt-panel" class="short-cmt-panel"></div>
217
  <script>
218
  (function(){
@@ -221,7 +249,7 @@ function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&l
221
  function renderSourceDetails(post,container){
222
  let details=post.source_details||[];if(!details.length)return;
223
  let box=document.createElement('div');box.className='source-detail-box';
224
- box.innerHTML='<h3 style="font-size:14px;color:#5cb87a;margin-bottom:8px">📚 Bài viết nguồn</h3>'+details.map((s,i)=>`<div class="source-detail-item"><div class="source-detail-title">${i+1}. ${esc(s.title)}</div><div class="source-detail-meta">${esc(s.via||'')}</div><div class="source-detail-content">${esc((s.content||'').slice(0,600))}</div>${s.url?`<a onclick="event.preventDefault();if(typeof readArticle==='function')readArticle('${esc(s.url)}')">📖 Đọc trên VNEWS</a>`:''}</div>`).join('');
225
  container.appendChild(box);
226
  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(()=>{});});}
227
 
@@ -229,52 +257,50 @@ async function readAIWallPost(i){
229
  let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];
230
  let p=wall[i];if(!p)return;showView('view-article');
231
  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}">`:''}`;
232
- if(p.sources&&p.sources.length){h+=`<div class="article-summary"><b>Nguồn:</b> ${p.sources.map(s=>esc(s.via||s.title||'')).join(', ')}</div>`;}
233
  h+=`<p class="article-p" style="white-space:pre-wrap">${esc(p.text)}</p>`;
234
  h+=`<div class="article-actions"><button class="primary" onclick="rewriteTopicPost('${esc(p.id)}')">🤖 Tóm tắt AI đăng tường</button><button onclick="doShare('${esc(p.title)}','${location.origin}','${esc(p.img||'')}')">📤 Chia sẻ</button></div>`;
235
- h+=`<div class="article-ai-ask"><h3 style="font-size:14px;color:#5cb87a;margin-bottom:6px">🤖 Hỏi AI về bài viết</h3><textarea id="article-ai-q" placeholder="Hỏi bất cứ điều gì về nội dung này..."></textarea><button onclick="askAIWall(${i})">Hỏi AI</button><div id="article-ai-ans" class="article-ai-answer"></div></div>`;
236
- h+=`</div>`;
237
  document.getElementById('view-article').innerHTML=h;
238
  let art=document.querySelector('.article-view');if(art&&p.source_details)renderSourceDetails(p,art);
239
  window.scrollTo(0,0);}
240
  window.readAIWallPost=readAIWallPost;window.aiReadWallPatched=window.aiReadWall=window.readWallPost=function(i){readAIWallPost(i)};
241
 
242
  window.rewriteTopicPost=async function(postId){
243
- if(!postId)return alert('Không có bài để rewrite');
244
  let btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang tóm tắt...';}
245
- 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('Đã tóm tắt lại và đăng bản mới lên Tường AI.');}catch(e){alert(e.message)}finally{if(btn){btn.disabled=false;btn.textContent='🤖 Tóm tắt AI đăng tường';}}};
246
 
247
  window.askAIWall=async function(i){
248
  let q=document.getElementById('article-ai-q')?.value.trim();if(!q)return alert('Nhập câu hỏi');
249
- let ans=document.getElementById('article-ai-ans');ans.textContent='Đang hỏi AI...';
250
  let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];
251
  let p=wall[i]||{};let context=(p.text||'');for(let s of (p.source_details||[]))context+='\n'+s.content;
252
- let url=(p.source_details||[]).find(s=>s.url)?.url||(p.sources||[]).find(s=>s.url)?.url||'';
253
- try{let r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,question:q,context:context.slice(0,12000)})});let j=await r.json();ans.textContent=j.answer||'Không có trả lời';}catch(e){ans.textContent='Lỗi: '+e.message}};
 
 
 
254
 
255
- window.openShortComments=async function(id){
256
- let panel=document.getElementById('short-cmt-panel');
257
- let j=await fetch('/api/short/comments?id='+encodeURIComponent(id)).then(r=>r.json()).catch(()=>({comments:[]}));
258
- let cmts=j.comments||[];
259
- panel.innerHTML=`<h3 style="color:#5cb87a;font-size:14px">💬 Bình luận</h3><div id="cmt-list">${cmts.map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('')||'<div class="cmt-item" style="color:#777">Chưa có bình luận</div>'}</div><textarea id="cmt-text" placeholder="Nhập bình luận..."></textarea><button onclick="submitShortCmt('${esc(id)}')">Gửi</button><button onclick="document.getElementById('short-cmt-panel').classList.remove('active')">Đóng</button>`;
260
- panel.classList.add('active');}
261
- window.submitShortCmt=async function(id){
262
- let text=document.getElementById('cmt-text')?.value.trim();if(!text)return;
263
- let j=await fetch('/api/short/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,text})}).then(r=>r.json()).catch(()=>({comments:[]}));
264
- document.getElementById('cmt-list').innerHTML=(j.comments||[]).map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('');
265
- document.getElementById('cmt-text').value='';}
266
 
267
- function patchShortButtons(){document.querySelectorAll('.tiktok-slide').forEach(sl=>{if(sl.dataset.cmtDone)return;sl.dataset.cmtDone='1';let id=sl.dataset.id||'';if(!id)return;let right=sl.querySelector('.tiktok-right,.short-action-panel');if(!right)return;if(right.querySelector('[data-cmt]'))return;let btn=document.createElement('button');btn.className='short-action-btn';btn.setAttribute('data-cmt','1');btn.innerHTML='<div class="ico" style="width:42px;height:42px;border-radius:50%;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;font-size:20px">💬</div><span>BL</span>';btn.onclick=function(e){e.stopPropagation();openShortComments(id);};right.appendChild(btn);});}
 
 
 
 
 
 
 
 
 
268
 
269
- function addSingleRewriteBtn(){let art=document.querySelector('#view-article .article-view');if(!art)return;let btns=art.querySelectorAll('[data-rewrite]');if(btns.length>1)for(let i=1;i<btns.length;i++)btns[i].remove();if(btns.length>=1)return;let actions=art.querySelector('.article-actions');if(!actions)return;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(){if(typeof rewriteCurrentArticle==='function')rewriteCurrentArticle();else alert('Chức năng rewrite chưa sẵn sàng.')};actions.insertBefore(btn,actions.firstChild);}
270
- function addArticleAskBox(){let art=document.querySelector('#view-article .article-view');if(!art||art.querySelector('.article-ai-ask'))return;let box=document.createElement('div');box.className='article-ai-ask';box.innerHTML='<h3 style="font-size:14px;color:#5cb87a;margin-bottom:6px">🤖 Hỏi AI về bài viết</h3><textarea id="article-ai-question" placeholder="Hỏi bất cứ điều về nội dung này..."></textarea><button onclick="askArticleAI()">Hỏi AI</button><div id="article-ai-answer" class="article-ai-answer"></div>';art.appendChild(box);}
271
- window.askArticleAI=async function(){let q=document.getElementById('article-ai-question')?.value.trim();if(!q)return alert('Nhập câu hỏi');let ans=document.getElementById('article-ai-answer');ans.textContent='Đang hỏi AI...';let url=(window._currentArticle&&window._currentArticle.url)||'';let context=document.querySelector('.article-view')?.innerText?.slice(0,12000)||'';try{let r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,question:q,context})});let j=await r.json();ans.textContent=j.answer||'Không có trả lời';}catch(e){ans.textContent='Lỗi: '+e.message}}
272
 
273
- let oldRA=window.readArticle;if(oldRA){window.readArticle=async function(){let ret=await oldRA.apply(this,arguments);setTimeout(()=>{addSingleRewriteBtn();addArticleAskBox();},600);return ret;}}
274
 
275
  let _hotLoaded=false;function deferHot(){if(_hotLoaded)return;_hotLoaded=true;setTimeout(()=>{if(typeof ensureHotTopics==='function')ensureHotTopics();if(typeof ensureNewsShortsHome==='function')ensureNewsShortsHome();},4000);}
276
  if(document.readyState==='complete')deferHot();else window.addEventListener('load',deferHot);
277
- setInterval(()=>{addSingleRewriteBtn();patchShortButtons();addArticleAskBox();},1500);
278
  })();
279
  </script>
280
  '''
 
1
+ """Patch: persistent data, single ask AI box, working rewrite, Qwen AI answers."""
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
 
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):
 
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
  )]
 
114
  db=_load_json(SHORT_COMMENTS_FILE,{});comments=db.get(vid,[]);comments.insert(0,{'text':text[:300],'ts':int(time.time())})
115
  db[vid]=comments[:100];_save_json(SHORT_COMMENTS_FILE,db);return JSONResponse({'comments':db[vid]})
116
 
117
+ # ===== ASK AI — uses Qwen model =====
118
  @app.post('/api/article/ask')
119
  async def article_ask(request:Request):
120
  body=await request.json();url=clean(body.get('url',''));question=clean(body.get('question',''));context=clean(body.get('context',''))
 
127
  except Exception:pass
128
  if not raw and context:raw=context[:12000]
129
  if not raw:raw=question
130
+ prompt=f"""Bạn là trợ lý thông minh VNEWS. Người dùng đang đọc bài viết/xem video và hỏi bạn.
131
 
132
+ Nội dung bài:
133
  ---
134
  Tiêu đề: {title}
135
  {raw[:10000]}
 
137
 
138
  Câu hỏi: {question}
139
 
140
+ Trả lời tự nhiên, thân thiện bằng tiếng Việt:
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 "theo hiểu biết chung..."
 
 
144
  """
145
  ans=await f5.base.qwen_generate(prompt,max_tokens=1200)
146
+ if not ans:ans='AI chưa trả lời được. Kiểm tra HF_TOKEN trong Space Settings → Secrets.'
147
  return JSONResponse({'answer':ans,'title':title})
148
 
149
+ # ===== REWRITE — works for both regular articles and topic posts =====
150
+ @app.post('/api/rewrite_share')
151
+ @app.post('/api/url_wall')
152
+ async def rewrite_url(request:Request):
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='';raw=''
156
+ try:
157
+ data=f5.base.scrape_any_url(url) if hasattr(f5.base,'scrape_any_url') else None
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 Exception:text=None
177
+ if not text or len(text)<100:
178
+ text=f"Tóm tắt: {title}\n\n{raw[:1200]}\n\nNguồn: {url}"
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 topic_rewrite(request:Request):
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
  all_content=(p.get('text') or '')
192
+ for s in (p.get('source_details') or []):all_content+='\n\n'+s.get('content','')
193
+ title=p.get('title') or 'Bài viết'
194
+ prompt=f"""Viết lại nội dung sau thành bản tóm tắt mới, hấp dẫn, đăng Tường AI.
 
195
 
196
  Tiêu đề gốc: {title}
197
+ Nội dung:
198
  {all_content[:16000]}
199
 
200
+ Yêu cầu: tiêu đề mới + 3-5 ý chính + nguồn tham khảo.
 
 
 
 
 
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=30)
204
  except Exception:text=None
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',[])
208
  all_posts=f5.base._load_ai_wall();all_posts.insert(0,new_post);f5.base._save_ai_wall(all_posts)
 
217
  research=f6._fast_context(topic) if hasattr(f6,'_fast_context') else f6._web_research_context(topic)
218
  context=research.get('context','');sources=research.get('sources',[])
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 MỘT BÀI VIẾT tiếng Việt VỀ ĐÚNG CHỦ ĐỀ: "{topic}"
223
 
224
+ NGUỒN:
225
  {source_brief[:18000]}
226
 
227
+ QUY TẮC: CHỈ viết về "{topic}". Tiêu đề chứa "{topic}". 5-8 đoạn. Cuối bài có nguồn.
 
 
 
 
 
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 Exception:text=None
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')}))
235
+ text=f"{topic}: tổng hợp\n\n{bullets}\n\nNguồn: {vias}"
236
  post=f5.base.make_post(topic,text,img,'','topic_focused',sources=[s for s in sources if s.get('url')])
237
  post['images']=[img];post['source_details']=details
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 — single ask box, working rewrite =====
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:160px;overflow:auto}.source-detail-item a{color:#5cb87a;font-size:11px;cursor:pointer}.source-detail-item img{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:6px;margin-bottom:6px;background:#222}.article-ai-ask{margin-top:12px;background:#141414;border:1px solid #2a2a2a;border-radius:10px;padding:10px}.article-ai-ask textarea{width:100%;min-height:60px;background:#222;border:1px solid #444;color:#eee;border-radius:10px;padding:9px}.article-ai-ask button{background:#2d8659;border:0;color:#fff;border-radius:10px;padding:8px 12px;margin-top:6px}.article-ai-answer{white-space:pre-wrap;color:#ccc;font-size:13px;line-height:1.55;margin-top:8px}</style>
244
  <div id="short-cmt-panel" class="short-cmt-panel"></div>
245
  <script>
246
  (function(){
 
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,500))}</div>${s.url?`<a onclick="event.preventDefault();if(typeof readArticle==='function')readArticle('${esc(s.url)}')">📖 Đọc trên VNEWS</a>`:''}</div>`).join('');
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
 
 
257
  let wall=(await fetch('/api/ai_wall').then(r=>r.json()).catch(()=>({posts:[]}))).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)}')">🤖 Tóm tắt AI đăng tường</button><button onclick="doShare('${esc(p.title)}','${location.origin}','${esc(p.img||'')}')">📤 Chia sẻ</button></div>`;
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 tóm tắt...';}
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('Đã tóm tắt và đăng lên Tường AI!');}catch(e){alert('Lỗi: '+e.message)}finally{if(btn){btn.disabled=false;btn.textContent='🤖 Tóm tắt AI đăng tường';}}};
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 AI...';
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
+ // Short comments
280
+ 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:[]}));let cmts=j.comments||[];panel.innerHTML=`<h3 style="color:#5cb87a">💬 Bình luận</h3><div id="cmt-list">${cmts.map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('')||'<div class="cmt-item" style="color:#777">Chưa có</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');}
281
+ window.submitShortCmt=async function(id){let text=document.getElementById('cmt-text')?.value.trim();if(!text)return;let j=await fetch('/api/short/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,text})}).then(r=>r.json()).catch(()=>({comments:[]}));document.getElementById('cmt-list').innerHTML=(j.comments||[]).map(c=>`<div class="cmt-item">${esc(c.text)}</div>`).join('');document.getElementById('cmt-text').value='';}
282
 
283
+ function patchShortButtons(){document.querySelectorAll('.tiktok-slide').forEach(sl=>{if(sl.dataset.cmtDone)return;sl.dataset.cmtDone='1';let id=sl.dataset.id||'';if(!id)return;let right=sl.querySelector('.tiktok-right,.short-action-panel');if(!right)return;if(right.querySelector('[data-cmt]'))return;let btn=document.createElement('button');btn.className='short-action-btn';btn.setAttribute('data-cmt','1');btn.innerHTML='<div class="ico" style="width:42px;height:42px;border-radius:50%;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;font-size:20px">💬</div>';btn.onclick=function(e){e.stopPropagation();openShortComments(id);};right.appendChild(btn);});}
 
 
 
 
 
 
 
 
 
 
284
 
285
+ // Single rewrite + ask for regular articles (only ONE of each)
286
+ function patchArticleView(){let art=document.querySelector('#view-article .article-view');if(!art)return;
287
+ // Remove ALL duplicate ask boxes and rewrite buttons injected by old layers
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 ans=document.getElementById('article-ai-answer');ans.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();ans.textContent=j.answer||'Không trả lời được';}catch(e){ans.textContent='Lỗi: '+e.message}}
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 đă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 _hotLoaded=false;function deferHot(){if(_hotLoaded)return;_hotLoaded=true;setTimeout(()=>{if(typeof ensureHotTopics==='function')ensureHotTopics();if(typeof ensureNewsShortsHome==='function')ensureNewsShortsHome();},4000);}
302
  if(document.readyState==='complete')deferHot();else window.addEventListener('load',deferHot);
303
+ setInterval(()=>{patchArticleView();patchShortButtons();},1500);
304
  })();
305
  </script>
306
  '''