feat: enhance chat functionality with web context debugging and improved message handling

- Added state management for web context and sources to improve debugging capabilities in chat.
- Implemented a debug view for displaying extracted context and sources used in chat responses.
- Refactored message handling to incorporate web context into user messages, enhancing response accuracy.
- Updated UI components to support new debugging features and improve user interaction.
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-20 05:34:44 -03:00
1 parent 3d91678109
commit 5c1a859925
3 files changed
+194 -56

No files matched your search

+26 -26
View File
@@ -1307,40 +1307,40 @@ fn force_kill_browser() -> Result<u32, String> {
}
// Processo identificado como headless - pode matar com segurança
#[cfg(target_os = "windows")]
{
use std::process::Command;
match Command::new("taskkill")
.args(&["/F", "/PID", &pid.to_string()])
.output()
#[cfg(target_os = "windows")]
{
Ok(output) => {
if output.status.success() {
killed_count += 1;
use std::process::Command;
match Command::new("taskkill")
.args(&["/F", "/PID", &pid.to_string()])
.output()
{
Ok(output) => {
if output.status.success() {
killed_count += 1;
log::info!("Processo Chrome headless encerrado: PID {} ({})", pid, name);
}
}
Err(e) => {
log::warn!("Erro ao encerrar processo {}: {}", pid, e);
}
}
Err(e) => {
log::warn!("Erro ao encerrar processo {}: {}", pid, e);
}
}
}
#[cfg(not(target_os = "windows"))]
{
use std::process::Command;
match Command::new("kill")
.args(&["-9", &pid.to_string()])
.output()
#[cfg(not(target_os = "windows"))]
{
Ok(output) => {
if output.status.success() {
killed_count += 1;
use std::process::Command;
match Command::new("kill")
.args(&["-9", &pid.to_string()])
.output()
{
Ok(output) => {
if output.status.success() {
killed_count += 1;
log::info!("Processo Chrome headless encerrado: PID {} ({})", pid, name);
}
}
}
Err(e) => {
log::warn!("Erro ao encerrar processo {}: {}", pid, e);
Err(e) => {
log::warn!("Erro ao encerrar processo {}: {}", pid, e);
}
}
}
+100 -26
View File
@@ -532,33 +532,43 @@ fn fetch_and_convert_sync(browser: &Browser, url: &str) -> Result<ScrapedContent
}
};
let product = match readability::extractor::extract(&mut reader, &url_obj) {
Ok(p) => p,
Err(e) => {
log::warn!("Falha ao extrair conteúdo legível de {}: {}", url, e);
return Err(anyhow::anyhow!("Falha ao processar conteúdo: {}", e));
match readability::extractor::extract(&mut reader, &url_obj) {
Ok(product) => {
let mut markdown = html2text::from_read(product.content.as_bytes(), 80);
// Se o markdown for muito curto, significa que o readability pode ter falhado
if markdown.trim().chars().count() < 400 {
if let Some(fallback) = extract_paragraph_fallback(url, &content) {
log::info!("Fallback de parágrafos aplicado para {}", url);
return Ok(fallback);
}
}
let title = if product.title.is_empty() {
fallback_title(&content).unwrap_or_else(|| "Fonte externa sem título".to_string())
} else {
product.title.clone()
};
Ok(ScrapedContent {
title: title.clone(),
url: url.to_string(),
content: product.content,
markdown: format!(
"---\nTitle: {}\nSource: {}\n---\n\n{}",
title,
url,
markdown
),
})
}
};
// Conversão para Markdown
let markdown = html2text::from_read(product.content.as_bytes(), 80);
// Clonar title antes de mover para poder usar no format!
let title = product.title.clone();
let result = ScrapedContent {
title: product.title,
url: url.to_string(),
content: product.content,
markdown: format!(
"---\nTitle: {}\nSource: {}\n---\n\n{}",
title,
url,
markdown
),
};
Ok(result)
Err(e) => {
log::warn!("Falha ao extrair conteúdo legível de {}: {}. Tentando fallback...", url, e);
if let Some(fallback) = extract_paragraph_fallback(url, &content) {
return Ok(fallback);
}
Err(anyhow::anyhow!("Falha ao processar conteúdo: {}", e))
}
}
}
/// Cria uma instância do Browser (singleton para reutilização)
@@ -572,3 +582,67 @@ pub fn create_browser() -> Result<Browser> {
.map_err(|e| anyhow::anyhow!("Falha ao criar browser: {}", e))
}
fn extract_paragraph_fallback(url: &str, html: &str) -> Option<ScrapedContent> {
use scraper::{Html, Selector};
let document = Html::parse_document(html);
let paragraph_selector = Selector::parse("p").ok()?;
let mut paragraphs = Vec::new();
for element in document.select(&paragraph_selector) {
let text = element.text().collect::<Vec<_>>().join(" ");
let normalized = text.split_whitespace().collect::<Vec<_>>().join(" ");
if normalized.chars().count() >= 100 {
paragraphs.push(normalized);
}
if paragraphs.len() >= 20 {
break;
}
}
if paragraphs.len() < 3 {
return None;
}
let fallback_body = paragraphs.join("\n\n");
let title = fallback_title(html).unwrap_or_else(|| "Conteúdo externo".to_string());
Some(ScrapedContent {
title: title.clone(),
url: url.to_string(),
content: fallback_body.clone(),
markdown: format!(
"---\nTitle: {}\nSource: {}\n---\n\n{}",
title,
url,
fallback_body
),
})
}
fn fallback_title(html: &str) -> Option<String> {
use scraper::{Html, Selector};
let document = Html::parse_document(html);
if let Ok(selector) = Selector::parse("title") {
if let Some(node) = document.select(&selector).next() {
let text = node.text().collect::<Vec<_>>().join(" ").trim().to_string();
if !text.is_empty() {
return Some(text);
}
}
}
if let Ok(selector) = Selector::parse("h1") {
if let Some(node) = document.select(&selector).next() {
let text = node.text().collect::<Vec<_>>().join(" ").trim().to_string();
if !text.is_empty() {
return Some(text);
}
}
}
None
}