feat: enhance UI and functionality across chat and settings
- Added custom scrollbar styles for improved aesthetics and usability across the interface. - Implemented responsive adjustments for chat sidebar and input components to enhance mobile experience. - Integrated tooltip components in select elements for better user guidance and information display. - Updated chat input and message components for improved styling and functionality. - Enhanced settings page with semantic query expansion options and improved model selection UI. - Refactored sidebar list to include better date formatting and tooltip support for session titles. - Introduced GPU stats monitoring in the system panel for better hardware insights.
This commit is contained in:
1 parent
84ea2dec19
commit
e5f062040c
33 files changed
+4670
-465
No files matched your search
+203
-67
@@ -3,25 +3,43 @@
|
||||
*
|
||||
* Implementa múltiplas tentativas de busca com query expansion,
|
||||
* validação de relevância e fallback para conhecimento interno.
|
||||
*
|
||||
* Agora suporta multi-round semantic search com estratégias diferentes.
|
||||
*/
|
||||
|
||||
import type { ScrapedContent } from '@/services/webSearch';
|
||||
import type { QueryContext } from './contextual-analyzer';
|
||||
import type { EnrichedQueries } from './query-enricher';
|
||||
import { chatLog } from '@/lib/terminal-logger';
|
||||
|
||||
export type SearchStrategy = 'literal' | 'semantic' | 'related' | 'expanded' | 'contextual';
|
||||
|
||||
export interface RoundConfig {
|
||||
strategy: SearchStrategy;
|
||||
queries: string[];
|
||||
minResults: number;
|
||||
}
|
||||
|
||||
export interface FallbackConfig {
|
||||
maxRounds: number; // Máximo de rodadas de busca (padrão: 4)
|
||||
maxResultsPerRound: number; // Máximo de resultados por rodada (padrão: 10)
|
||||
maxTotalResults: number; // Máximo total de resultados analisados (padrão: 40)
|
||||
minRelevanceScore: number; // Score mínimo de relevância (0-1, padrão: 0.3)
|
||||
enableQueryExpansion: boolean; // Ativar expansão de queries (padrão: true)
|
||||
useSemanticSearch?: boolean; // Usar multi-round semantic search (padrão: false)
|
||||
context?: QueryContext; // Contexto analisado (para semantic search)
|
||||
enrichedQueries?: EnrichedQueries; // Queries enriquecidas (para semantic search)
|
||||
}
|
||||
|
||||
export interface FallbackAttempt {
|
||||
round: number;
|
||||
strategy?: SearchStrategy;
|
||||
query: string;
|
||||
queries?: string[]; // Múltiplas queries se usar estratégia
|
||||
results: ScrapedContent[];
|
||||
relevanceScore: number;
|
||||
timestamp: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface FallbackResult {
|
||||
@@ -108,8 +126,9 @@ Query expandida:`;
|
||||
|
||||
/**
|
||||
* Calcula score de relevância de um resultado baseado na query
|
||||
* (Mantido para compatibilidade, mas agora usa função de relevance-scorer)
|
||||
*/
|
||||
export function calculateRelevanceScore(
|
||||
export function calculateRelevanceScoreForFallback(
|
||||
result: ScrapedContent,
|
||||
query: string
|
||||
): number {
|
||||
@@ -167,7 +186,7 @@ export function areResultsRelevant(
|
||||
}
|
||||
|
||||
// Calcular scores de relevância
|
||||
const scores = results.map(r => calculateRelevanceScore(r, query));
|
||||
const scores = results.map(r => calculateRelevanceScoreForFallback(r, query));
|
||||
|
||||
// Verificar se pelo menos um resultado tem score suficiente
|
||||
const maxScore = Math.max(...scores);
|
||||
@@ -181,8 +200,66 @@ export function areResultsRelevant(
|
||||
return maxScore >= minScore || avgScore >= (minScore * 0.7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria configuração de rounds para multi-round semantic search
|
||||
*/
|
||||
function createSemanticRounds(
|
||||
enriched: EnrichedQueries,
|
||||
originalQuery: string
|
||||
): RoundConfig[] {
|
||||
const rounds: RoundConfig[] = [];
|
||||
|
||||
// Round 1: Literal + Semântico (mais direto)
|
||||
if (enriched.literal.length > 0 || enriched.semantic.length > 0) {
|
||||
rounds.push({
|
||||
strategy: 'literal',
|
||||
queries: [...enriched.literal, ...enriched.semantic].slice(0, 5),
|
||||
minResults: 3,
|
||||
});
|
||||
}
|
||||
|
||||
// Round 2: Relacionadas (se Round 1 não foi suficiente)
|
||||
if (enriched.related.length > 0) {
|
||||
rounds.push({
|
||||
strategy: 'related',
|
||||
queries: enriched.related.slice(0, 4),
|
||||
minResults: 2,
|
||||
});
|
||||
}
|
||||
|
||||
// Round 3: Contextual (adicionar contexto temporal/geográfico)
|
||||
if (enriched.contextual.length > 0) {
|
||||
rounds.push({
|
||||
strategy: 'contextual',
|
||||
queries: enriched.contextual.slice(0, 3),
|
||||
minResults: 2,
|
||||
});
|
||||
}
|
||||
|
||||
// Round 4: Expandido (temas específicos, exemplos concretos)
|
||||
if (enriched.expanded.length > 0) {
|
||||
rounds.push({
|
||||
strategy: 'expanded',
|
||||
queries: enriched.expanded.slice(0, 3),
|
||||
minResults: 1,
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback: se não há queries enriquecidas, usar original
|
||||
if (rounds.length === 0) {
|
||||
rounds.push({
|
||||
strategy: 'literal',
|
||||
queries: [originalQuery],
|
||||
minResults: 1,
|
||||
});
|
||||
}
|
||||
|
||||
return rounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa busca progressiva com fallback
|
||||
* Agora suporta multi-round semantic search com estratégias diferentes
|
||||
*/
|
||||
export async function executeProgressiveSearch(
|
||||
originalQuery: string,
|
||||
@@ -198,21 +275,63 @@ export async function executeProgressiveSearch(
|
||||
|
||||
chatLog.info(`\n=== PROGRESSIVE SEARCH START ===`);
|
||||
chatLog.info(`Original Query: "${originalQuery}"`);
|
||||
chatLog.info(`Config: ${JSON.stringify(finalConfig)}`);
|
||||
chatLog.info(`Config: ${JSON.stringify({ ...finalConfig, context: finalConfig.context ? 'present' : 'none', enrichedQueries: finalConfig.enrichedQueries ? 'present' : 'none' })}`);
|
||||
|
||||
for (let round = 1; round <= finalConfig.maxRounds; round++) {
|
||||
chatLog.info(`\n--- Round ${round}/${finalConfig.maxRounds} ---`);
|
||||
// Se usar semantic search, criar rounds baseados em estratégias
|
||||
let rounds: Array<{ round: number; config: RoundConfig }> = [];
|
||||
|
||||
if (finalConfig.useSemanticSearch && finalConfig.enrichedQueries) {
|
||||
const semanticRounds = createSemanticRounds(finalConfig.enrichedQueries, originalQuery);
|
||||
rounds = semanticRounds.map((roundConfig, idx) => ({
|
||||
round: idx + 1,
|
||||
config: roundConfig,
|
||||
}));
|
||||
chatLog.info(`[SemanticSearch] Created ${rounds.length} semantic rounds with strategies`);
|
||||
} else {
|
||||
// Modo tradicional: rounds sequenciais com expansão
|
||||
for (let i = 1; i <= finalConfig.maxRounds; i++) {
|
||||
rounds.push({
|
||||
round: i,
|
||||
config: {
|
||||
strategy: 'literal',
|
||||
queries: [originalQuery],
|
||||
minResults: finalConfig.maxResultsPerRound,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Limitar número de rounds
|
||||
rounds = rounds.slice(0, finalConfig.maxRounds);
|
||||
|
||||
for (const { round, config: roundConfig } of rounds) {
|
||||
chatLog.info(`\n--- Round ${round}/${rounds.length} [${roundConfig.strategy}] ---`);
|
||||
|
||||
// Expandir query se necessário
|
||||
const query = finalConfig.enableQueryExpansion
|
||||
? await expandQuery(originalQuery, model, round)
|
||||
: originalQuery;
|
||||
const roundStartTime = Date.now();
|
||||
let roundResults: ScrapedContent[] = [];
|
||||
let roundQueries: string[] = [];
|
||||
|
||||
chatLog.info(`Searching with query: "${query}"`);
|
||||
// Executar todas as queries do round em paralelo
|
||||
const queryPromises = roundConfig.queries.map(async (query) => {
|
||||
// Expandir query se necessário (modo tradicional)
|
||||
const finalQuery = finalConfig.enableQueryExpansion && !finalConfig.useSemanticSearch
|
||||
? await expandQuery(query, model, round)
|
||||
: query;
|
||||
|
||||
try {
|
||||
const results = await searchFn(finalQuery, finalConfig.maxResultsPerRound);
|
||||
return { query: finalQuery, results };
|
||||
} catch (error) {
|
||||
chatLog.warn(`[Round ${round}] Query "${finalQuery}" failed:`, error);
|
||||
return { query: finalQuery, results: [] };
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// Executar busca
|
||||
const results = await searchFn(query, finalConfig.maxResultsPerRound);
|
||||
const queryResults = await Promise.all(queryPromises);
|
||||
|
||||
// Agregar resultados de todas as queries do round
|
||||
for (const { query, results } of queryResults) {
|
||||
roundQueries.push(query);
|
||||
|
||||
// Filtrar duplicatas
|
||||
const uniqueResults = results.filter(r => {
|
||||
@@ -223,61 +342,78 @@ export async function executeProgressiveSearch(
|
||||
return true;
|
||||
});
|
||||
|
||||
chatLog.info(`Found ${uniqueResults.length} unique results (${results.length} total)`);
|
||||
|
||||
// Calcular relevância
|
||||
const relevanceScore = uniqueResults.length > 0
|
||||
? uniqueResults.reduce((sum, r) => sum + calculateRelevanceScore(r, query), 0) / uniqueResults.length
|
||||
: 0;
|
||||
|
||||
// Registrar tentativa
|
||||
const attempt: FallbackAttempt = {
|
||||
round,
|
||||
query,
|
||||
results: uniqueResults,
|
||||
relevanceScore,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
attempts.push(attempt);
|
||||
|
||||
allResults.push(...uniqueResults);
|
||||
totalResultsAnalyzed += uniqueResults.length;
|
||||
|
||||
// Verificar se encontrou resultados relevantes
|
||||
if (areResultsRelevant(uniqueResults, query, finalConfig.minRelevanceScore)) {
|
||||
chatLog.info(`✓ Relevant results found in round ${round}!`);
|
||||
chatLog.info(`Total results: ${allResults.length}, Total analyzed: ${totalResultsAnalyzed}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
knowledgeBaseContext: '', // Será preenchido pelo caller
|
||||
scrapedSources: allResults,
|
||||
attempts,
|
||||
totalResultsAnalyzed,
|
||||
usedFallback: false,
|
||||
finalQuery: query,
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se atingiu limite de resultados
|
||||
if (totalResultsAnalyzed >= finalConfig.maxTotalResults) {
|
||||
chatLog.warn(`⚠️ Reached max total results limit (${finalConfig.maxTotalResults})`);
|
||||
break;
|
||||
}
|
||||
|
||||
// Se não encontrou resultados relevantes, continuar para próxima rodada
|
||||
chatLog.warn(`⚠️ Round ${round} did not find relevant results, continuing...`);
|
||||
|
||||
} catch (error) {
|
||||
chatLog.error(`✗ Error in round ${round}:`, error);
|
||||
attempts.push({
|
||||
round,
|
||||
query,
|
||||
results: [],
|
||||
relevanceScore: 0,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
roundResults.push(...uniqueResults);
|
||||
}
|
||||
|
||||
const roundDuration = Date.now() - roundStartTime;
|
||||
|
||||
chatLog.info(`[Round ${round}] Executed ${roundQueries.length} queries, found ${roundResults.length} unique results in ${roundDuration}ms`);
|
||||
|
||||
// Log detalhado se houver resultados
|
||||
if (roundResults.length > 0) {
|
||||
roundResults.slice(0, 3).forEach((result, idx) => {
|
||||
chatLog.info(` → Result ${idx + 1}: ${result.title.substring(0, 60)}... (${result.url.substring(0, 50)}...)`);
|
||||
});
|
||||
if (roundResults.length > 3) {
|
||||
chatLog.info(` ... and ${roundResults.length - 3} more results`);
|
||||
}
|
||||
}
|
||||
|
||||
// Calcular relevância usando contexto se disponível
|
||||
const relevanceScore = roundResults.length > 0
|
||||
? roundResults.reduce((sum, r) => {
|
||||
// Usar contexto para cálculo de relevância se disponível
|
||||
if (finalConfig.context) {
|
||||
// Importar função dinamicamente para evitar circular dependency
|
||||
const { calculateSemanticSimilarity } = require('./relevance-scorer');
|
||||
return sum + calculateSemanticSimilarity(finalConfig.context, r.markdown || r.title);
|
||||
}
|
||||
return sum + calculateRelevanceScoreForFallback(r, originalQuery);
|
||||
}, 0) / roundResults.length
|
||||
: 0;
|
||||
|
||||
// Registrar tentativa
|
||||
const attempt: FallbackAttempt = {
|
||||
round,
|
||||
strategy: roundConfig.strategy,
|
||||
query: roundQueries[0] || originalQuery,
|
||||
queries: roundQueries.length > 1 ? roundQueries : undefined,
|
||||
results: roundResults,
|
||||
relevanceScore,
|
||||
timestamp: Date.now(),
|
||||
duration: roundDuration,
|
||||
};
|
||||
attempts.push(attempt);
|
||||
|
||||
allResults.push(...roundResults);
|
||||
totalResultsAnalyzed += roundResults.length;
|
||||
|
||||
// Verificar se encontrou resultados relevantes
|
||||
const queryForRelevance = roundQueries[0] || originalQuery;
|
||||
if (areResultsRelevant(roundResults, queryForRelevance, finalConfig.minRelevanceScore)) {
|
||||
chatLog.info(`[Round ${round}] ✓ Relevant results found!`);
|
||||
chatLog.info(`[Round ${round}] Total results: ${allResults.length}, Total analyzed: ${totalResultsAnalyzed}`);
|
||||
chatLog.info(`[Round ${round}] Final queries: ${roundQueries.join(', ')}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
knowledgeBaseContext: '', // Será preenchido pelo caller
|
||||
scrapedSources: allResults,
|
||||
attempts,
|
||||
totalResultsAnalyzed,
|
||||
usedFallback: false,
|
||||
finalQuery: roundQueries.join(' | '),
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar se atingiu limite de resultados
|
||||
if (totalResultsAnalyzed >= finalConfig.maxTotalResults) {
|
||||
chatLog.warn(`[Round ${round}] ⚠️ Reached max total results limit (${finalConfig.maxTotalResults})`);
|
||||
break;
|
||||
}
|
||||
|
||||
// Se não encontrou resultados relevantes, continuar para próxima rodada
|
||||
chatLog.warn(`[Round ${round}] ⚠️ Did not find relevant results (min score: ${finalConfig.minRelevanceScore}), continuing...`);
|
||||
}
|
||||
|
||||
// Se chegou aqui, não encontrou resultados relevantes
|
||||
|
||||
Reference in new issue
Block a user