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:
1 parent
c06e7ae5cf
commit
4006a72eec
20 files changed
+741
-235
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user