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:
1 parent
3d91678109
commit
5c1a859925
3 files changed
+194
-56
No files matched your search
+68
-4
@@ -7,7 +7,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { MessageSquare, Settings, Server, Moon, Sun, PanelLeftClose, PanelLeftOpen, Loader2 } from "lucide-react";
|
import { MessageSquare, Settings, Server, Moon, Sun, PanelLeftClose, PanelLeftOpen, Loader2, ChevronDown } from "lucide-react";
|
||||||
import { ChatInput } from "@/components/chat/chat-input";
|
import { ChatInput } from "@/components/chat/chat-input";
|
||||||
import { ChatMessage } from "@/components/chat/chat-message";
|
import { ChatMessage } from "@/components/chat/chat-message";
|
||||||
import { SystemPanel } from "@/components/chat/system-panel";
|
import { SystemPanel } from "@/components/chat/system-panel";
|
||||||
@@ -22,6 +22,7 @@ import { SearchProgress } from "@/components/chat/search-progress";
|
|||||||
import { ThinkingIndicator, ThinkingStep } from "@/components/chat/thinking-indicator";
|
import { ThinkingIndicator, ThinkingStep } from "@/components/chat/thinking-indicator";
|
||||||
import { useQueryGenerator } from "@/hooks/use-query-generator";
|
import { useQueryGenerator } from "@/hooks/use-query-generator";
|
||||||
import { useSettingsStore } from "@/store/settings-store";
|
import { useSettingsStore } from "@/store/settings-store";
|
||||||
|
import type { ScrapedContent } from "@/services/webSearch";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
@@ -64,6 +65,9 @@ export default function ChatPage() {
|
|||||||
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const lastSavedMessagesRef = useRef<number>(0);
|
const lastSavedMessagesRef = useRef<number>(0);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [showContextDebug, setShowContextDebug] = useState(false);
|
||||||
|
const [lastWebContext, setLastWebContext] = useState('');
|
||||||
|
const [lastContextSources, setLastContextSources] = useState<ScrapedContent[]>([]);
|
||||||
|
|
||||||
// Auto-select first model
|
// Auto-select first model
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -120,6 +124,9 @@ export default function ChatPage() {
|
|||||||
|
|
||||||
const handleSend = async (content: string) => {
|
const handleSend = async (content: string) => {
|
||||||
if (!selectedModel) return;
|
if (!selectedModel) return;
|
||||||
|
setShowContextDebug(false);
|
||||||
|
setLastWebContext('');
|
||||||
|
setLastContextSources([]);
|
||||||
|
|
||||||
// Generate ID if new session (but don't create card yet - wait for title)
|
// Generate ID if new session (but don't create card yet - wait for title)
|
||||||
if (!currentSessionId) {
|
if (!currentSessionId) {
|
||||||
@@ -131,7 +138,7 @@ export default function ChatPage() {
|
|||||||
// ========== LÓGICA DE AGENTE DE PESQUISA ==========
|
// ========== LÓGICA DE AGENTE DE PESQUISA ==========
|
||||||
let webContext = '';
|
let webContext = '';
|
||||||
let searchQuery = '';
|
let searchQuery = '';
|
||||||
let scrapedSources: any[] = [];
|
let scrapedSources: ScrapedContent[] = [];
|
||||||
|
|
||||||
// Iniciar indicador de pensamento
|
// Iniciar indicador de pensamento
|
||||||
setThinkingStep('analyzing');
|
setThinkingStep('analyzing');
|
||||||
@@ -188,7 +195,13 @@ export default function ChatPage() {
|
|||||||
// Log para debug: verificar tamanho do contexto gerado
|
// Log para debug: verificar tamanho do contexto gerado
|
||||||
const contextLength = webContext.length;
|
const contextLength = webContext.length;
|
||||||
console.debug(`Contexto web gerado (sanitizado): ${contextLength} caracteres de ${scrapedSources.length} fontes`);
|
console.debug(`Contexto web gerado (sanitizado): ${contextLength} caracteres de ${scrapedSources.length} fontes`);
|
||||||
|
setLastWebContext(webContext);
|
||||||
|
setLastContextSources(scrapedSources);
|
||||||
|
setShowContextDebug(false);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setLastWebContext('');
|
||||||
|
setLastContextSources([]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erro na pesquisa web:', error);
|
console.error('Erro na pesquisa web:', error);
|
||||||
@@ -283,7 +296,12 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
|
|||||||
}
|
}
|
||||||
// [DEBUG INJECTION END]
|
// [DEBUG INJECTION END]
|
||||||
|
|
||||||
await sendMessage(content, selectedModel, enhancedSystemPrompt);
|
let finalUserContent = content;
|
||||||
|
if (webContext) {
|
||||||
|
finalUserContent = `[CONTEXTO WEB OBRIGATÓRIO]\n${webContext}\n[/CONTEXTO WEB]\n\nCom base EXCLUSIVAMENTE no texto acima, responda: ${content}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendMessage(finalUserContent, selectedModel, enhancedSystemPrompt);
|
||||||
|
|
||||||
// Marcar como completo quando começar a streamar
|
// Marcar como completo quando começar a streamar
|
||||||
setTimeout(() => setThinkingStep('complete'), 100);
|
setTimeout(() => setThinkingStep('complete'), 100);
|
||||||
@@ -516,10 +534,56 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
|
|||||||
currentStep={thinkingStep}
|
currentStep={thinkingStep}
|
||||||
searchQuery={webSearch.currentQuery}
|
searchQuery={webSearch.currentQuery}
|
||||||
sourcesRead={webSearch.scrapedSources.length}
|
sourcesRead={webSearch.scrapedSources.length}
|
||||||
totalSources={5}
|
totalSources={settings.webSearch.maxResults}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Raw Context Debug */}
|
||||||
|
{lastContextSources.length > 0 && (
|
||||||
|
<div className="border-t border-muted px-6 py-4 bg-muted/40 space-y-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowContextDebug((prev) => !prev)}
|
||||||
|
className="w-full flex items-center justify-between text-left text-sm font-medium"
|
||||||
|
>
|
||||||
|
<span>Ver contexto extraído (debug)</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={`w-4 h-4 transition-transform ${showContextDebug ? 'rotate-180' : ''}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{showContextDebug && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold text-muted-foreground mb-1">
|
||||||
|
Fontes utilizadas ({lastContextSources.length}):
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1 max-h-32 overflow-auto pr-2">
|
||||||
|
{lastContextSources.map((source, idx) => (
|
||||||
|
<a
|
||||||
|
key={`${source.url}-${idx}`}
|
||||||
|
href={source.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="block text-xs text-primary hover:underline truncate"
|
||||||
|
>
|
||||||
|
{source.title || source.url}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold text-muted-foreground mb-1">
|
||||||
|
Bloco enviado na mensagem do usuário:
|
||||||
|
</p>
|
||||||
|
<pre className="text-[11px] leading-relaxed font-mono whitespace-pre-wrap bg-background border border-muted rounded-lg p-3 max-h-64 overflow-auto">
|
||||||
|
{lastWebContext.trim().length > 0 ? lastWebContext : '[Contexto vazio após sanitização]'}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Input */}
|
{/* Input */}
|
||||||
<ChatInput
|
<ChatInput
|
||||||
onSend={handleSend}
|
onSend={handleSend}
|
||||||
|
|||||||
+26
-26
@@ -1307,40 +1307,40 @@ fn force_kill_browser() -> Result<u32, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Processo identificado como headless - pode matar com segurança
|
// Processo identificado como headless - pode matar com segurança
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
|
||||||
use std::process::Command;
|
|
||||||
match Command::new("taskkill")
|
|
||||||
.args(&["/F", "/PID", &pid.to_string()])
|
|
||||||
.output()
|
|
||||||
{
|
{
|
||||||
Ok(output) => {
|
use std::process::Command;
|
||||||
if output.status.success() {
|
match Command::new("taskkill")
|
||||||
killed_count += 1;
|
.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);
|
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"))]
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
{
|
|
||||||
use std::process::Command;
|
|
||||||
match Command::new("kill")
|
|
||||||
.args(&["-9", &pid.to_string()])
|
|
||||||
.output()
|
|
||||||
{
|
{
|
||||||
Ok(output) => {
|
use std::process::Command;
|
||||||
if output.status.success() {
|
match Command::new("kill")
|
||||||
killed_count += 1;
|
.args(&["-9", &pid.to_string()])
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
killed_count += 1;
|
||||||
log::info!("Processo Chrome headless encerrado: PID {} ({})", pid, name);
|
log::info!("Processo Chrome headless encerrado: PID {} ({})", pid, name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
Err(e) => {
|
||||||
Err(e) => {
|
log::warn!("Erro ao encerrar processo {}: {}", pid, e);
|
||||||
log::warn!("Erro ao encerrar processo {}: {}", pid, e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-26
@@ -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) {
|
match readability::extractor::extract(&mut reader, &url_obj) {
|
||||||
Ok(p) => p,
|
Ok(product) => {
|
||||||
Err(e) => {
|
let mut markdown = html2text::from_read(product.content.as_bytes(), 80);
|
||||||
log::warn!("Falha ao extrair conteúdo legível de {}: {}", url, e);
|
// Se o markdown for muito curto, significa que o readability pode ter falhado
|
||||||
return Err(anyhow::anyhow!("Falha ao processar conteúdo: {}", e));
|
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
|
||||||
|
),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
};
|
Err(e) => {
|
||||||
|
log::warn!("Falha ao extrair conteúdo legível de {}: {}. Tentando fallback...", url, e);
|
||||||
// Conversão para Markdown
|
if let Some(fallback) = extract_paragraph_fallback(url, &content) {
|
||||||
let markdown = html2text::from_read(product.content.as_bytes(), 80);
|
return Ok(fallback);
|
||||||
|
}
|
||||||
// Clonar title antes de mover para poder usar no format!
|
Err(anyhow::anyhow!("Falha ao processar conteúdo: {}", e))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cria uma instância do Browser (singleton para reutilização)
|
/// 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))
|
.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(¶graph_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
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user