feat(ui): enhance chat UI with new components and styling

refactor(chat): improve message display and actions
feat(chat): add high contrast mode toggle
style(chat): update spacing and sizing for better readability
feat(chat): add new Section and Stack layout components
docs: add UI style guide documentation
fix(chat): improve browser connection handling in scraper
feat(settings): expand default sources and improve UI
refactor: consolidate chat page layout and components
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-20 15:13:54 -03:00
1 parent c06e7ae5cf
commit 4006a72eec
20 files changed
+741 -235

No files matched your search

+6 -1
View File
@@ -1178,7 +1178,12 @@ pub fn get_or_create_browser(state: State<BrowserState>) -> Result<Arc<Browser>,
let mut browser_opt = state.lock().map_err(|e| format!("Erro ao acessar estado do browser: {}", e))?;
if let Some(ref browser) = *browser_opt {
return Ok(browser.clone());
let alive = browser.new_tab().is_ok();
if alive {
return Ok(browser.clone());
} else {
*browser_opt = None;
}
}
// Criar nova instância
+68 -6
View File
@@ -46,6 +46,21 @@ impl Default for SourcesConfig {
"pubmed.ncbi.nlm.nih.gov".to_string(),
"ieee.org".to_string(),
"acm.org".to_string(),
"sciencedirect.com".to_string(),
"springer.com".to_string(),
"jstor.org".to_string(),
"researchgate.net".to_string(),
"plos.org".to_string(),
"nature.com".to_string(),
"sciencemag.org".to_string(),
"mit.edu".to_string(),
"phys.org".to_string(),
"semanticscholar.org".to_string(),
"core.ac.uk".to_string(),
"paperswithcode.com".to_string(),
"base-search.net".to_string(),
"doaj.org".to_string(),
"worldscientific.com".to_string(),
],
enabled: true,
},
@@ -53,11 +68,26 @@ impl Default for SourcesConfig {
id: "tech".to_string(),
name: "Tech".to_string(),
base_sites: vec![
"github.com".to_string(),
"stackoverflow.com".to_string(),
"dev.to".to_string(),
"medium.com".to_string(),
"github.com".to_string(),
"reddit.com/r/programming".to_string(),
"dev.to".to_string(),
"hackernoon.com".to_string(),
"tabnews.com.br".to_string(),
"lobste.rs".to_string(),
"news.ycombinator.com".to_string(),
"hashnode.com".to_string(),
"codesandbox.io".to_string(),
"developer.mozilla.org".to_string(),
"web.dev".to_string(),
"smashingmagazine.com".to_string(),
"martinfowler.com".to_string(),
"infoq.com".to_string(),
"dzone.com".to_string(),
"freecodecamp.org".to_string(),
"digitalocean.com".to_string(),
"realpython.com".to_string(),
"css-tricks.com".to_string(),
],
enabled: true,
},
@@ -65,10 +95,26 @@ impl Default for SourcesConfig {
id: "news".to_string(),
name: "News".to_string(),
base_sites: vec![
"news.ycombinator.com".to_string(),
"techcrunch.com".to_string(),
"theverge.com".to_string(),
"techcrunch.com".to_string(),
"arstechnica.com".to_string(),
"engadget.com".to_string(),
"wired.com".to_string(),
"venturebeat.com".to_string(),
"cnet.com".to_string(),
"mashable.com".to_string(),
"gizmodo.com".to_string(),
"zdnet.com".to_string(),
"tomshardware.com".to_string(),
"anandtech.com".to_string(),
"bleepingcomputer.com".to_string(),
"theregister.com".to_string(),
"9to5mac.com".to_string(),
"androidauthority.com".to_string(),
"techradar.com".to_string(),
"fastcompany.com".to_string(),
"g1.globo.com".to_string(),
"techtudo.com.br".to_string(),
],
enabled: true,
},
@@ -78,8 +124,24 @@ impl Default for SourcesConfig {
base_sites: vec![
"bloomberg.com".to_string(),
"reuters.com".to_string(),
"financialtimes.com".to_string(),
"wsj.com".to_string(),
"ft.com".to_string(),
"cnbc.com".to_string(),
"marketwatch.com".to_string(),
"economist.com".to_string(),
"forbes.com".to_string(),
"investopedia.com".to_string(),
"seekingalpha.com".to_string(),
"infomoney.com.br".to_string(),
"valor.globo.com".to_string(),
"exame.com".to_string(),
"braziljournal.com".to_string(),
"coindesk.com".to_string(),
"cointelegraph.com".to_string(),
"moneytimes.com.br".to_string(),
"suno.com.br".to_string(),
"b3.com.br".to_string(),
"imf.org".to_string(),
],
enabled: true,
},
+80 -44
View File
@@ -54,7 +54,7 @@ fn default_max_concurrent() -> usize {
}
fn default_total_sources() -> usize {
40
100
}
/// Pool de User-Agents para rotação (evita bloqueios 429)
@@ -80,55 +80,51 @@ pub async fn search_duckduckgo(query: &str, limit: usize) -> Result<Vec<String>>
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()?;
let url = format!("https://html.duckduckgo.com/html/?q={}",
urlencoding::encode(query));
// User-Agent rotativo para evitar bloqueios
let user_agent = get_random_user_agent();
log::debug!("Usando User-Agent: {}", user_agent);
let res = client
.get(&url)
.header(USER_AGENT, user_agent)
.send()
.await?
.text()
.await?;
let document = Html::parse_document(&res);
// Seletores para links orgânicos do DuckDuckGo
// Tenta múltiplos seletores para maior compatibilidade
let mut links = Vec::new();
let mut offset = 0usize;
let max_pages = 5usize;
let selectors = vec![
".result__a",
".web-result__link",
"a.result__a",
];
let mut links = Vec::new();
for selector_str in selectors {
if let Ok(selector) = Selector::parse(selector_str) {
for element in document.select(&selector).take(limit) {
if let Some(href) = element.value().attr("href") {
// DuckDuckGo usa redirecionamento, extrair URL real
if let Some(real_url) = extract_real_url(href) {
if !links.contains(&real_url) {
links.push(real_url);
if links.len() >= limit {
break;
for _ in 0..max_pages {
if links.len() >= limit { break; }
let url = format!(
"https://html.duckduckgo.com/html/?q={}&s={}",
urlencoding::encode(query),
offset
);
let res = client
.get(&url)
.header(USER_AGENT, user_agent)
.send()
.await?
.text()
.await?;
{
let document = Html::parse_document(&res);
for selector_str in &selectors {
if let Ok(selector) = Selector::parse(selector_str) {
for element in document.select(&selector) {
if let Some(href) = element.value().attr("href") {
if let Some(real_url) = extract_real_url(href) {
if !links.contains(&real_url) {
links.push(real_url);
if links.len() >= limit { break; }
}
}
}
}
if links.len() >= limit { break; }
}
}
if !links.is_empty() {
break;
}
}
offset += 50;
if res.is_empty() { break; }
}
links.truncate(limit);
Ok(links)
}
@@ -485,23 +481,25 @@ pub async fn search_and_scrape_with_config(
let semaphore = Arc::new(Semaphore::new(config.max_concurrent_tabs));
let mut handles = Vec::new();
for url in urls {
for url in urls.clone() {
let browser_clone = browser.clone();
let permit = semaphore.clone().acquire_owned().await.unwrap();
let url_clone = url.clone();
let handle = tokio::task::spawn_blocking(move || {
let result = fetch_and_convert_sync(&browser_clone, &url);
drop(permit); // Libera o semáforo após processar
result
let res = fetch_and_convert_sync(&browser_clone, &url_clone);
drop(permit);
(url_clone, res)
});
handles.push(handle);
}
// 3. Coletar resultados (ignorar erros individuais, continuar com sucessos)
let mut results = Vec::new();
let mut failed_urls = Vec::new();
let mut connection_closed = false;
for handle in handles {
match handle.await {
Ok(Ok(content)) => {
Ok((_, Ok(content))) => {
// Filtrar conteúdo muito curto (< 200 caracteres)
let content_length = content.content.chars().count();
let markdown_length = content.markdown.chars().count();
@@ -516,12 +514,17 @@ pub async fn search_and_scrape_with_config(
results.push(content);
}
}
Ok(Err(e)) => {
Ok((url, Err(e))) => {
let err_msg = format!("{}", e);
if err_msg.contains("Timeout") || err_msg.contains("ERR_HTTP") {
log::debug!("URL ignorada (timeout/erro HTTP): {}", err_msg);
failed_urls.push(url);
} else {
log::warn!("Erro ao processar URL: {}", e);
if err_msg.contains("underlying connection is closed") {
connection_closed = true;
failed_urls.push(url);
}
}
}
Err(e) => {
@@ -530,6 +533,39 @@ pub async fn search_and_scrape_with_config(
}
}
if connection_closed && !failed_urls.is_empty() {
let retry_concurrency = std::cmp::min(3, config.max_concurrent_tabs.max(1));
let semaphore = Arc::new(Semaphore::new(retry_concurrency));
let browser_new = Arc::new(create_browser()?);
let mut retry_handles = Vec::new();
for url in failed_urls.clone() {
let browser_clone = browser_new.clone();
let permit = semaphore.clone().acquire_owned().await.unwrap();
let url_clone = url.clone();
let handle = tokio::task::spawn_blocking(move || {
let res = fetch_and_convert_sync(&browser_clone, &url_clone);
drop(permit);
(url_clone, res)
});
retry_handles.push(handle);
}
for h in retry_handles {
match h.await {
Ok((_, Ok(content))) => {
let content_length = content.content.chars().count();
let markdown_length = content.markdown.chars().count();
if content_length >= 200 || markdown_length >= 200 {
results.push(content);
}
}
Ok((url, Err(e))) => {
log::warn!("Falha após retry para URL {}: {}", url, e);
}
Err(e) => log::warn!("Erro na task de retry: {}", e),
}
}
}
if results.is_empty() {
log::warn!("Nenhuma fonte foi extraída com sucesso para a query: {}", query);
} else {