{authors_display}
{"📅 " + str(year) + "" if year else ""}
{source_label}
{grade_html}
{copy_btn}
{doi_html}
{pdf_html}
{abstract_section}
'''
return f'''
{cards_html}
'''
def _build_stats_html(results_list, sources_used=None, query="", search_type="simple"):
"""Build a stats bar matching Next.js SourcesStatsBlock"""
if not results_list:
return ""
total = len(results_list)
source_counts = {}
years = []
with_doi = 0
with_pdf = 0
for r in results_list:
src = r.get("source", "unknown")
source_counts[src] = source_counts.get(src, 0) + 1
if r.get("year"):
try:
years.append(int(r["year"]))
except:
pass
if r.get("doi"):
with_doi += 1
if r.get("pdf_url"):
with_pdf += 1
year_range = ""
if years:
year_range = f"{min(years)}–{max(years)}"
# Source badges
badges = " ".join([_build_source_badge_html(src, cnt) for src, cnt in sorted(source_counts.items(), key=lambda x: -x[1])])
mode_labels = {"simple": "Simple", "smart": "Smart", "batch": "Batch"}
mode_label = mode_labels.get(search_type, search_type)
return f'''
📊
{total} documentos encontrados
Modo {mode_label} · {len(source_counts)} fuentes · {year_range if year_range else "Años variados"}
🔗 {with_doi}📄 {with_pdf}
{badges}
'''
async def search_handler(query, sources, max_results, year_start, year_end, university, search_type):
if not query or not query.strip():
return "", "", ""
try:
results_list = []
search_info = {"type": search_type, "query": query.strip()}
if search_type == "batch":
queries = [q.strip() for q in query.split("\n") if q.strip()]
if not queries:
queries = [query.strip()]
all_results = []
for q in queries:
result = await search(q, sources=sources or ["latam", "global"], max_results=int(max_results))
all_results.extend(result.get("results", []))
seen = set()
for r in all_results:
key = r.get("doi") or r.get("title", "")[:60]
if key not in seen:
seen.add(key)
results_list.append(r)
results_list = results_list[:int(max_results)]
elif search_type == "smart":
variations = [query.strip()]
translations = {
"inteligencia artificial": "artificial intelligence",
"aprendizaje automático": "machine learning",
"aprendizaje profundo": "deep learning",
"red neuronal": "neural network",
"procesamiento de lenguaje natural": "natural language processing",
}
for es, en in translations.items():
if es in query.lower():
variations.append(query.lower().replace(es, en))
break
all_results = []
for v in variations:
result = await search(v, sources=sources or ["global", "latam"], max_results=int(max_results) // len(variations) + 10)
all_results.extend(result.get("results", []))
seen = set()
for r in all_results:
key = r.get("doi") or r.get("title", "")[:60]
if key not in seen:
seen.add(key)
results_list.append(r)
results_list = results_list[:int(max_results)]
else:
result = await search(query.strip(), sources=sources or ["all"], max_results=int(max_results),
year_start=year_start or None, year_end=year_end or None)
results_list = result.get("results", [])
if university and university.strip():
uni = university.strip().lower()
results_list = [r for r in results_list if uni in (r.get("university") or "").lower() or uni in (r.get("title") or "").lower()]
if not results_list:
empty_html = '''
🔍
Sin resultados
Intente con otros términos o fuentes.
'''
return "", empty_html, ""
# Build the DataFrame for table view
df = format_results_for_dataframe(results_list)
# Build stats HTML
stats_html = _build_stats_html(results_list, query=query.strip(), search_type=search_type)
# Build cards HTML
cards_html = _build_paper_card_html(results_list)
return stats_html, cards_html, ""
except Exception as e:
error_html = f'''