bep40 commited on
Commit
bd92176
·
verified ·
1 Parent(s): 58d24bc

Fix: use VnExpress/Dantri/Vietnamnet direct search for real URLs, not Google News"

Browse files
Files changed (1) hide show
  1. app_run.py +86 -110
app_run.py CHANGED
@@ -1,4 +1,4 @@
1
- """Wrapper: relevant hashtag with pagination + ALL articles in-app."""
2
  from app_final import *
3
  from app_final import app, f6, f5, rt, PATCH_INJECT, UNIFIED_INJECT_FIXED, HIGHLIGHT_FULL_OVERRIDE, EXTRA_WALL_FIX
4
  from fastapi.responses import HTMLResponse, JSONResponse
@@ -7,29 +7,80 @@ import requests as req
7
  from urllib.parse import quote
8
  from bs4 import BeautifulSoup
9
  import re, html as html_lib
 
10
 
11
  def _clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or ""))).strip()
 
12
 
13
- def _follow_redirect(url):
 
 
14
  try:
15
- r=req.head(url,allow_redirects=True,timeout=10,headers={'User-Agent':'Mozilla/5.0'})
16
- return r.url
17
- except:
18
- try:r=req.get(url,allow_redirects=True,timeout=10,headers={'User-Agent':'Mozilla/5.0'},stream=True);u=r.url;r.close();return u
19
- except:return url
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  def _scrape_any_article(url):
22
- if 'news.google.com' in url or 'google.com/rss' in url:url=_follow_redirect(url)
23
  try:
24
- r=req.get(url,headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36','Accept-Language':'vi-VN,vi;q=0.9'},timeout=15,allow_redirects=True)
25
- r.encoding='utf-8';soup=BeautifulSoup(r.text,'lxml')
26
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript','iframe']):tag.decompose()
27
  h1=soup.find('h1');ogt=soup.find('meta',property='og:title')
28
  title=(h1.get_text(' ',strip=True) if h1 else '') or (ogt.get('content','') if ogt else '') or (soup.title.get_text(strip=True) if soup.title else '')
29
  ogd=soup.find('meta',property='og:description') or soup.find('meta',attrs={'name':'description'})
30
  summary=ogd.get('content','') if ogd else ''
31
- ogi=soup.find('meta',property='og:image') or soup.find('meta',attrs={'name':'twitter:image'})
32
- og_image=ogi.get('content','') if ogi else ''
33
  if og_image and og_image.startswith('//'):og_image='https:'+og_image
34
  selectors=['article','main','.article-content','.detail-content','.singular-content','.fck_detail','.entry-content','.story-body','.knc-content','.cms-body']
35
  block=None
@@ -37,19 +88,15 @@ def _scrape_any_article(url):
37
  el=soup.select_one(sel)
38
  if el and len(el.find_all('p'))>=2:block=el;break
39
  if not block:
40
- best=None;best_score=0
41
  for el in soup.find_all(['article','main','section','div']):
42
- ps=el.find_all('p');score=len(ps)*100+sum(len(p.get_text())for p in ps[:10])
43
- if score>best_score:best=el;best_score=score
44
  block=best or soup.body or soup
45
  body=[]
46
  for el in block.find_all(['p','h2','h3','figure','img'],recursive=True):
47
- if el.name=='p':
48
- t=_clean(el.get_text(' ',strip=True))
49
- if len(t)>30:body.append({'type':'p','text':t})
50
- elif el.name in('h2','h3'):
51
- t=_clean(el.get_text(' ',strip=True))
52
- if t:body.append({'type':'heading','text':t})
53
  elif el.name in('figure','img'):
54
  im=el if el.name=='img' else el.find('img')
55
  if im:
@@ -61,52 +108,6 @@ def _scrape_any_article(url):
61
  return {'title':_clean(title),'summary':_clean(summary),'og_image':og_image,'body':body[:50],'source':'generic','url':url}
62
  except:return None
63
 
64
- def _google_news_search(topic, limit=20):
65
- """Search Google News RSS — returns newest first, strict relevance."""
66
- items=[]
67
- try:
68
- # Use exact topic as search query for better relevance
69
- url='https://news.google.com/rss/search?q='+quote('"'+topic+'"')+'&hl=vi&gl=VN&ceid=VN:vi'
70
- r=req.get(url,headers={'User-Agent':'Mozilla/5.0'},timeout=10);r.encoding='utf-8'
71
- soup=BeautifulSoup(r.text,'xml')
72
- for it in soup.find_all('item')[:limit*3]:
73
- title=_clean(it.find('title').get_text(' ',strip=True) if it.find('title') else '')
74
- link=_clean(it.find('link').get_text(strip=True) if it.find('link') else '')
75
- src=_clean(it.find('source').get_text(' ',strip=True) if it.find('source') else '')
76
- pub=_clean(it.find('pubDate').get_text(strip=True) if it.find('pubDate') else '')
77
- if not title or not link:continue
78
- # Strict relevance: topic phrase or majority of words must be in title
79
- topic_lower=topic.lower();title_lower=title.lower()
80
- topic_words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',topic_lower) if len(w)>1]
81
- if topic_lower in title_lower:
82
- pass # exact match — always include
83
- else:
84
- match_count=sum(1 for w in topic_words if w in title_lower)
85
- if len(topic_words)>0 and match_count<max(1,len(topic_words)//2):continue
86
- items.append({'title':title,'url':link,'via':src,'snippet':'','pub':pub})
87
- if len(items)>=limit:break
88
- except:pass
89
- # Also try without quotes for broader results if too few
90
- if len(items)<5:
91
- try:
92
- url2='https://news.google.com/rss/search?q='+quote(topic+' Việt Nam')+'&hl=vi&gl=VN&ceid=VN:vi'
93
- r2=req.get(url2,headers={'User-Agent':'Mozilla/5.0'},timeout=8);r2.encoding='utf-8'
94
- soup2=BeautifulSoup(r2.text,'xml')
95
- seen_urls={s['url'] for s in items}
96
- for it in soup2.find_all('item')[:limit]:
97
- title=_clean(it.find('title').get_text(' ',strip=True) if it.find('title') else '')
98
- link=_clean(it.find('link').get_text(strip=True) if it.find('link') else '')
99
- src=_clean(it.find('source').get_text(' ',strip=True) if it.find('source') else '')
100
- if not title or not link or link in seen_urls:continue
101
- topic_lower=topic.lower();title_lower=title.lower()
102
- topic_words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',topic_lower) if len(w)>1]
103
- match_count=sum(1 for w in topic_words if w in title_lower)
104
- if match_count==0:continue
105
- seen_urls.add(link);items.append({'title':title,'url':link,'via':src,'snippet':''})
106
- if len(items)>=limit:break
107
- except:pass
108
- return items
109
-
110
  # Override endpoints
111
  app.router.routes=[r for r in app.router.routes if not (
112
  (getattr(r,'path',None)=='/api/hashtag/sources' and 'GET' in getattr(r,'methods',set())) or
@@ -115,7 +116,7 @@ app.router.routes=[r for r in app.router.routes if not (
115
  )]
116
 
117
  @app.get('/api/article')
118
- def _article_universal(url:str=Query(...)):
119
  data=_scrape_any_article(url)
120
  if data and data.get('body'):return JSONResponse(data)
121
  from main import scrape_vne_article,scrape_bbc_article,scrape_dantri_article,scrape_genk_article,scrape_ttvh_article
@@ -129,24 +130,20 @@ def _article_universal(url:str=Query(...)):
129
  return JSONResponse({'error':'Không đọc được bài viết','url':url})
130
 
131
  @app.get('/api/hashtag/sources')
132
- def _hashtag_paginated(topic:str=Query(...),offset:int=Query(default=0),limit:int=Query(default=6)):
133
- """Return relevant sources with pagination. Newest first."""
134
- all_sources=_google_news_search(topic,20)
135
- # Dedupe
136
- seen=set();unique=[]
137
- for s in all_sources:
138
- if s['url'] not in seen:seen.add(s['url']);unique.append(s)
139
- total=len(unique)
140
- page=unique[offset:offset+limit]
141
  has_more=offset+limit<total
142
- return JSONResponse({'sources':page,'topic':topic,'total':total,'offset':offset,'has_more':has_more})
143
 
144
  FAST_HASHTAG_JS = r'''
145
  <style>
146
  .hashtag-loading{display:flex;align-items:center;gap:8px;padding:12px;color:#888;font-size:12px}
147
  .hashtag-spinner{width:16px;height:16px;border:2px solid #333;border-top-color:#5cb87a;border-radius:50%;animation:ht-spin .8s linear infinite}
148
  @keyframes ht-spin{to{transform:rotate(360deg)}}
149
- .hashtag-load-more{width:100%;margin-top:8px;background:#222;border:1px solid #333;color:#ccc;padding:9px;border-radius:10px;font-size:12px;cursor:pointer;text-align:center}.hashtag-load-more:active{opacity:.7}
150
  </style>
151
  <script>
152
  (function(){
@@ -170,57 +167,36 @@ window.readArticle=async function(url){
170
  window.doRewriteArticle=async function(btn){var url=(window._currentArticle&&window._currentArticle.url)||'';if(!url){alert('Không có URL');return;}var ctx=document.querySelector('.article-view')?.innerText?.slice(0,14000)||'';btn.disabled=true;btn.textContent='Đang rewrite...';try{var r=await fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:url,context:ctx})});var j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');alert('Đã đăng Tường AI!');}catch(e){alert(e.message);}finally{btn.disabled=false;btn.textContent='🤖 Rewrite AI đăng tường';}};
171
  window.askArticleAI=async function(){var q=document.getElementById('article-ai-question')?.value.trim();if(!q)return alert('Nhập câu hỏi');var a=document.getElementById('article-ai-answer');a.textContent='Đang hỏi...';var url=(window._currentArticle&&window._currentArticle.url)||'';var ctx=document.querySelector('.article-view')?.innerText?.slice(0,12000)||'';try{var r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:url,question:q,context:ctx})});var j=await r.json();a.textContent=j.answer||'Không trả lời được';}catch(e){a.textContent='Lỗi: '+e.message}};
172
 
173
- // === HASHTAG SOURCES with pagination ===
174
  function renderSources(sources,append){
175
- var list=document.getElementById('hashtag-src-list');if(!list)return;
176
- var h='';
177
- sources.forEach(function(s){
178
- var idx=_htImgIdx++;
179
  h+='<div class="hashtag-src-item" onclick="readArticle(\''+esc(s.url||'')+'\')">';
180
  h+='<div class="hashtag-src-img" id="ht-img-'+idx+'"></div>';
181
- h+='<div class="hashtag-src-text"><div class="hashtag-src-title">'+esc(s.title)+'</div><div class="hashtag-src-via">'+esc(s.via||'')+'</div></div>';
182
- h+='</div>';
183
- // Lazy load image
184
- if(s.url)fetch('/api/article?url='+encodeURIComponent(s.url)).then(function(r){return r.json()}).then(function(d){if(d&&(d.og_image||d.img)){var el=document.getElementById('ht-img-'+idx);if(el)el.innerHTML='<img src="'+esc(d.og_image||d.img)+'" onerror="this.style.display=\'none\'" loading="lazy">';}}).catch(function(){});
185
  });
186
  if(append)list.insertAdjacentHTML('beforeend',h);else list.innerHTML=h;
187
  }
188
-
189
  window.showHashtagSources=async function(topic){
190
  _htTopic=topic;_htOffset=0;_htImgIdx=0;
191
  var home=document.getElementById('view-home');if(!home)return;
192
  document.getElementById('hashtag-sources-box')?.remove();
193
  var box=document.createElement('div');box.id='hashtag-sources-box';box.className='hashtag-sources';
194
  box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div class="hashtag-loading"><div class="hashtag-spinner"></div>Đang tìm bài viết mới nhất...</div>';
195
- var compose=home.querySelector('.ai-compose');
196
- if(compose)compose.after(box);else home.prepend(box);
197
  box.scrollIntoView({behavior:'smooth',block:'start'});
198
  try{
199
  var r=await fetch('/api/hashtag/sources?topic='+encodeURIComponent(topic)+'&offset=0&limit=6');
200
  var j=await r.json();var sources=j.sources||[];
201
- if(!sources.length){box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div style="color:#888;font-size:12px;padding:8px">Không tìm được bài viết liên quan</div>';return;}
202
  _htOffset=sources.length;
203
- var h='<h3>🔍 '+esc(topic)+' <span style="font-size:10px;color:#888">('+j.total+' bài)</span></h3>';
204
- h+='<div id="hashtag-src-list"></div>';
205
  if(j.has_more)h+='<button class="hashtag-load-more" id="ht-load-more" onclick="loadMoreHashtag()">Tải thêm bài viết...</button>';
206
  h+='<button class="hashtag-rewrite-btn" onclick="rewriteHashtagTopic(\''+esc(topic)+'\')">🤖 Rewrite AI tổng hợp & đăng tường</button>';
207
- box.innerHTML=h;
208
- renderSources(sources,false);
209
- }catch(e){box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div style="color:#e74c3c;font-size:12px;padding:8px">Lỗi: '+esc(e.message)+'</div>';}
210
- };
211
-
212
- window.loadMoreHashtag=async function(){
213
- var btn=document.getElementById('ht-load-more');if(btn){btn.textContent='Đang tải...';btn.disabled=true;}
214
- try{
215
- var r=await fetch('/api/hashtag/sources?topic='+encodeURIComponent(_htTopic)+'&offset='+_htOffset+'&limit=6');
216
- var j=await r.json();var sources=j.sources||[];
217
- _htOffset+=sources.length;
218
- renderSources(sources,true);
219
- if(!j.has_more&&btn)btn.remove();
220
- else if(btn){btn.textContent='Tải thêm bài viết...';btn.disabled=false;}
221
- }catch(e){if(btn){btn.textContent='Lỗi, thử lại';btn.disabled=false;}}
222
  };
223
-
224
  window.rewriteHashtagTopic=async function(topic){var btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang tổng hợp...';}try{var r=await fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic:topic})});var j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');if(btn)btn.textContent='✅ Đã đăng!';setTimeout(function(){document.getElementById('hashtag-sources-box')?.remove();},2000);}catch(e){if(btn){btn.disabled=false;btn.textContent='❌ '+e.message;}}};
225
  window.createTopicPost=function(){var inp=document.getElementById('ai-topic-input');var topic=(inp&&inp.value||'').trim();if(!topic){alert('Nhập chủ đề');return;}showHashtagSources(topic);if(inp)inp.value='';};
226
  window.createTopicPostFinal5=function(){var inp=document.getElementById('ai-topic-input-final5')||document.getElementById('ai-topic-input');var topic=(inp&&inp.value||'').trim();if(!topic){alert('Nhập chủ đề');return;}showHashtagSources(topic);if(inp)inp.value='';};
 
1
+ """Wrapper: real article URLs from VN news search, universal scrape, pagination."""
2
  from app_final import *
3
  from app_final import app, f6, f5, rt, PATCH_INJECT, UNIFIED_INJECT_FIXED, HIGHLIGHT_FULL_OVERRIDE, EXTRA_WALL_FIX
4
  from fastapi.responses import HTMLResponse, JSONResponse
 
7
  from urllib.parse import quote
8
  from bs4 import BeautifulSoup
9
  import re, html as html_lib
10
+ from concurrent.futures import ThreadPoolExecutor, as_completed
11
 
12
  def _clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or ""))).strip()
13
+ UA={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36','Accept-Language':'vi-VN,vi;q=0.9'}
14
 
15
+ def _vne_search(topic, limit=10):
16
+ """Search VnExpress — returns real URLs."""
17
+ items=[]
18
  try:
19
+ r=req.get(f'https://timkiem.vnexpress.net/?q={quote(topic)}',headers=UA,timeout=8);r.encoding='utf-8'
20
+ soup=BeautifulSoup(r.text,'lxml')
21
+ for a in soup.select('article.item-news h2 a, article.item-news h3 a, h2.title-news a, h3.title-news a')[:limit]:
22
+ title=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
23
+ if title and href and href.startswith('http'):items.append({'title':title,'url':href,'via':'VnExpress'})
24
+ except:pass
25
+ return items
26
+
27
+ def _dantri_search(topic, limit=8):
28
+ """Search Dantri — returns real URLs."""
29
+ items=[]
30
+ try:
31
+ r=req.get(f'https://dantri.com.vn/tim-kiem/{quote(topic)}.htm',headers=UA,timeout=8);r.encoding='utf-8'
32
+ soup=BeautifulSoup(r.text,'lxml')
33
+ for a in soup.select('h3.article-title a, h2.article-title a, .article-item a[href]')[:limit]:
34
+ title=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
35
+ if not href.startswith('http'):href='https://dantri.com.vn'+href
36
+ if title and len(title)>15 and href.endswith('.htm'):items.append({'title':title,'url':href,'via':'Dân trí'})
37
+ except:pass
38
+ return items
39
+
40
+ def _vietnamnet_search(topic, limit=8):
41
+ """Search Vietnamnet — returns real URLs."""
42
+ items=[]
43
+ try:
44
+ r=req.get(f'https://vietnamnet.vn/tim-kiem?q={quote(topic)}',headers=UA,timeout=8);r.encoding='utf-8'
45
+ soup=BeautifulSoup(r.text,'lxml')
46
+ for a in soup.select('h3 a[href], .verticalPost__main a[href], .horizontalPost__main a[href]')[:limit]:
47
+ title=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
48
+ if not href.startswith('http'):href='https://vietnamnet.vn'+href
49
+ if title and len(title)>15:items.append({'title':title,'url':href,'via':'Vietnamnet'})
50
+ except:pass
51
+ return items
52
+
53
+ def _multi_search(topic, limit=20):
54
+ """Search multiple VN news sources in parallel for real URLs."""
55
+ all_items=[];seen=set()
56
+ with ThreadPoolExecutor(3) as ex:
57
+ futs=[ex.submit(_vne_search,topic,10),ex.submit(_dantri_search,topic,8),ex.submit(_vietnamnet_search,topic,8)]
58
+ for f in as_completed(futs,timeout=12):
59
+ try:
60
+ for item in f.result():
61
+ if item['url'] not in seen:seen.add(item['url']);all_items.append(item)
62
+ except:pass
63
+ # Filter relevance
64
+ topic_lower=topic.lower()
65
+ topic_words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',topic_lower) if len(w)>1]
66
+ relevant=[]
67
+ for item in all_items:
68
+ title_lower=item['title'].lower()
69
+ if topic_lower in title_lower:relevant.append(item);continue
70
+ match=sum(1 for w in topic_words if w in title_lower)
71
+ if match>=max(1,len(topic_words)//2):relevant.append(item)
72
+ return relevant[:limit]
73
 
74
  def _scrape_any_article(url):
 
75
  try:
76
+ r=req.get(url,headers=UA,timeout=15,allow_redirects=True);r.encoding='utf-8'
77
+ soup=BeautifulSoup(r.text,'lxml')
78
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript','iframe']):tag.decompose()
79
  h1=soup.find('h1');ogt=soup.find('meta',property='og:title')
80
  title=(h1.get_text(' ',strip=True) if h1 else '') or (ogt.get('content','') if ogt else '') or (soup.title.get_text(strip=True) if soup.title else '')
81
  ogd=soup.find('meta',property='og:description') or soup.find('meta',attrs={'name':'description'})
82
  summary=ogd.get('content','') if ogd else ''
83
+ ogi=soup.find('meta',property='og:image');og_image=ogi.get('content','') if ogi else ''
 
84
  if og_image and og_image.startswith('//'):og_image='https:'+og_image
85
  selectors=['article','main','.article-content','.detail-content','.singular-content','.fck_detail','.entry-content','.story-body','.knc-content','.cms-body']
86
  block=None
 
88
  el=soup.select_one(sel)
89
  if el and len(el.find_all('p'))>=2:block=el;break
90
  if not block:
91
+ best=None;bs=0
92
  for el in soup.find_all(['article','main','section','div']):
93
+ ps=el.find_all('p');sc=len(ps)*100+sum(len(p.get_text())for p in ps[:10])
94
+ if sc>bs:best=el;bs=sc
95
  block=best or soup.body or soup
96
  body=[]
97
  for el in block.find_all(['p','h2','h3','figure','img'],recursive=True):
98
+ if el.name=='p':t=_clean(el.get_text(' ',strip=True));(body.append({'type':'p','text':t}) if len(t)>30 else None)
99
+ elif el.name in('h2','h3'):t=_clean(el.get_text(' ',strip=True));(body.append({'type':'heading','text':t}) if t else None)
 
 
 
 
100
  elif el.name in('figure','img'):
101
  im=el if el.name=='img' else el.find('img')
102
  if im:
 
108
  return {'title':_clean(title),'summary':_clean(summary),'og_image':og_image,'body':body[:50],'source':'generic','url':url}
109
  except:return None
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  # Override endpoints
112
  app.router.routes=[r for r in app.router.routes if not (
113
  (getattr(r,'path',None)=='/api/hashtag/sources' and 'GET' in getattr(r,'methods',set())) or
 
116
  )]
117
 
118
  @app.get('/api/article')
119
+ def _article(url:str=Query(...)):
120
  data=_scrape_any_article(url)
121
  if data and data.get('body'):return JSONResponse(data)
122
  from main import scrape_vne_article,scrape_bbc_article,scrape_dantri_article,scrape_genk_article,scrape_ttvh_article
 
130
  return JSONResponse({'error':'Không đọc được bài viết','url':url})
131
 
132
  @app.get('/api/hashtag/sources')
133
+ def _hashtag(topic:str=Query(...),offset:int=Query(default=0),limit:int=Query(default=6)):
134
+ """Return relevant sources from VN news search with real URLs. Supports pagination."""
135
+ all_sources=_multi_search(topic,20)
136
+ total=len(all_sources)
137
+ page=all_sources[offset:offset+limit]
 
 
 
 
138
  has_more=offset+limit<total
139
+ return JSONResponse({'sources':[{'title':s['title'],'url':s['url'],'via':s['via']} for s in page],'topic':topic,'total':total,'offset':offset,'has_more':has_more})
140
 
141
  FAST_HASHTAG_JS = r'''
142
  <style>
143
  .hashtag-loading{display:flex;align-items:center;gap:8px;padding:12px;color:#888;font-size:12px}
144
  .hashtag-spinner{width:16px;height:16px;border:2px solid #333;border-top-color:#5cb87a;border-radius:50%;animation:ht-spin .8s linear infinite}
145
  @keyframes ht-spin{to{transform:rotate(360deg)}}
146
+ .hashtag-load-more{width:100%;margin-top:8px;background:#222;border:1px solid #333;color:#ccc;padding:9px;border-radius:10px;font-size:12px;cursor:pointer;text-align:center}
147
  </style>
148
  <script>
149
  (function(){
 
167
  window.doRewriteArticle=async function(btn){var url=(window._currentArticle&&window._currentArticle.url)||'';if(!url){alert('Không có URL');return;}var ctx=document.querySelector('.article-view')?.innerText?.slice(0,14000)||'';btn.disabled=true;btn.textContent='Đang rewrite...';try{var r=await fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:url,context:ctx})});var j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');alert('Đã đăng Tường AI!');}catch(e){alert(e.message);}finally{btn.disabled=false;btn.textContent='🤖 Rewrite AI đăng tường';}};
168
  window.askArticleAI=async function(){var q=document.getElementById('article-ai-question')?.value.trim();if(!q)return alert('Nhập câu hỏi');var a=document.getElementById('article-ai-answer');a.textContent='Đang hỏi...';var url=(window._currentArticle&&window._currentArticle.url)||'';var ctx=document.querySelector('.article-view')?.innerText?.slice(0,12000)||'';try{var r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:url,question:q,context:ctx})});var j=await r.json();a.textContent=j.answer||'Không trả lời được';}catch(e){a.textContent='Lỗi: '+e.message}};
169
 
 
170
  function renderSources(sources,append){
171
+ var list=document.getElementById('hashtag-src-list');if(!list)return;var h='';
172
+ sources.forEach(function(s){var idx=_htImgIdx++;
 
 
173
  h+='<div class="hashtag-src-item" onclick="readArticle(\''+esc(s.url||'')+'\')">';
174
  h+='<div class="hashtag-src-img" id="ht-img-'+idx+'"></div>';
175
+ h+='<div class="hashtag-src-text"><div class="hashtag-src-title">'+esc(s.title)+'</div><div class="hashtag-src-via">'+esc(s.via||'')+'</div></div></div>';
176
+ fetch('/api/article?url='+encodeURIComponent(s.url)).then(function(r){return r.json()}).then(function(d){if(d&&(d.og_image||d.img)){var el=document.getElementById('ht-img-'+idx);if(el)el.innerHTML='<img src="'+esc(d.og_image||d.img)+'" onerror="this.style.display=\'none\'" loading="lazy">';}}).catch(function(){});
 
 
177
  });
178
  if(append)list.insertAdjacentHTML('beforeend',h);else list.innerHTML=h;
179
  }
 
180
  window.showHashtagSources=async function(topic){
181
  _htTopic=topic;_htOffset=0;_htImgIdx=0;
182
  var home=document.getElementById('view-home');if(!home)return;
183
  document.getElementById('hashtag-sources-box')?.remove();
184
  var box=document.createElement('div');box.id='hashtag-sources-box';box.className='hashtag-sources';
185
  box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div class="hashtag-loading"><div class="hashtag-spinner"></div>Đang tìm bài viết mới nhất...</div>';
186
+ var compose=home.querySelector('.ai-compose');if(compose)compose.after(box);else home.prepend(box);
 
187
  box.scrollIntoView({behavior:'smooth',block:'start'});
188
  try{
189
  var r=await fetch('/api/hashtag/sources?topic='+encodeURIComponent(topic)+'&offset=0&limit=6');
190
  var j=await r.json();var sources=j.sources||[];
191
+ if(!sources.length){box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div style="color:#888;font-size:12px;padding:8px">Không tìm được bài viết</div>';return;}
192
  _htOffset=sources.length;
193
+ var h='<h3>🔍 '+esc(topic)+' <span style="font-size:10px;color:#888">('+j.total+' bài)</span></h3><div id="hashtag-src-list"></div>';
 
194
  if(j.has_more)h+='<button class="hashtag-load-more" id="ht-load-more" onclick="loadMoreHashtag()">Tải thêm bài viết...</button>';
195
  h+='<button class="hashtag-rewrite-btn" onclick="rewriteHashtagTopic(\''+esc(topic)+'\')">🤖 Rewrite AI tổng hợp & đăng tường</button>';
196
+ box.innerHTML=h;renderSources(sources,false);
197
+ }catch(e){box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div style="color:#e74c3c;font-size:12px">Lỗi: '+esc(e.message)+'</div>';}
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  };
199
+ window.loadMoreHashtag=async function(){var btn=document.getElementById('ht-load-more');if(btn){btn.textContent='Đang tải...';btn.disabled=true;}try{var r=await fetch('/api/hashtag/sources?topic='+encodeURIComponent(_htTopic)+'&offset='+_htOffset+'&limit=6');var j=await r.json();_htOffset+=j.sources.length;renderSources(j.sources,true);if(!j.has_more&&btn)btn.remove();else if(btn){btn.textContent='Tải thêm...';btn.disabled=false;}}catch(e){if(btn){btn.textContent='Lỗi';btn.disabled=false;}}};
200
  window.rewriteHashtagTopic=async function(topic){var btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang tổng hợp...';}try{var r=await fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic:topic})});var j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');if(btn)btn.textContent='✅ Đã đăng!';setTimeout(function(){document.getElementById('hashtag-sources-box')?.remove();},2000);}catch(e){if(btn){btn.disabled=false;btn.textContent='❌ '+e.message;}}};
201
  window.createTopicPost=function(){var inp=document.getElementById('ai-topic-input');var topic=(inp&&inp.value||'').trim();if(!topic){alert('Nhập chủ đề');return;}showHashtagSources(topic);if(inp)inp.value='';};
202
  window.createTopicPostFinal5=function(){var inp=document.getElementById('ai-topic-input-final5')||document.getElementById('ai-topic-input');var topic=(inp&&inp.value||'').trim();if(!topic){alert('Nhập chủ đề');return;}showHashtagSources(topic);if(inp)inp.value='';};