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:
Evandro Fonseca Junior committed 2025-11-26 19:04:46 -03:00
1 parent 84ea2dec19
commit e5f062040c
33 files changed
+4670 -465

No files matched your search

+165 -80
View File
@@ -5,7 +5,8 @@ import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { MessageSquare, Settings, Server, Moon, Sun, PanelLeftClose, PanelLeftOpen, Loader2, ChevronDown, Plus, ScrollText, Copy, Check } from "lucide-react";
import { MessageSquare, Settings, Server, Moon, Sun, PanelLeftClose, PanelLeftOpen, Loader2, ChevronDown, Plus, ScrollText, Copy, Check, Globe } from "lucide-react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { ChatInput } from "@/components/chat/chat-input";
import { ChatMessage } from "@/components/chat/chat-message";
@@ -120,7 +121,7 @@ export default function ChatPage() {
const initializedRef = useRef(false);
// Initialize with default format prompt
const [systemPrompt, setSystemPrompt] = useState(defaultFormatPrompt || "Você é um assistente útil e prestativo.");
const [isChatsSidebarCollapsed, setIsChatsSidebarCollapsed] = useState(false);
const [isChatsSidebarCollapsed, setIsChatsSidebarCollapsed] = useState(true); // Iniciar colapsada
const [thinkingStep, setThinkingStep] = useState<ThinkingStep | null>(null);
const [processSteps, setProcessSteps] = useState<ProcessStep[]>([]);
const [error, setError] = useState<string | Error | null>(null);
@@ -398,7 +399,11 @@ export default function ChatPage() {
});
try {
// Executar busca progressiva
// Obter contexto e queries enriquecidas do deep research
const context = deepResearch.state.context;
const enrichedQueries = deepResearch.state.enrichedQueries;
// Executar busca progressiva com semantic search se disponível
const fallbackResult = await executeProgressiveSearch(
query,
async (q, limit) => {
@@ -410,7 +415,10 @@ export default function ChatPage() {
maxResultsPerRound: maxResults,
maxTotalResults: 30, // Máximo 30 resultados por query
minRelevanceScore: 0.3,
enableQueryExpansion: true,
enableQueryExpansion: !enrichedQueries, // Desabilitar expansão tradicional se usar enriquecimento
useSemanticSearch: !!enrichedQueries, // Usar semantic search se temos queries enriquecidas
context: context || undefined,
enrichedQueries: enrichedQueries || undefined,
}
);
@@ -442,9 +450,25 @@ export default function ChatPage() {
});
}
// Se houve falhas mas ainda tem resultados, avisar discretamente
const failedRounds = fallbackResult.attempts.filter(a => a.results.length === 0);
if (failedRounds.length > 0 && fallbackResult.scrapedSources.length > 0) {
updateThinkingMessage('web-research', {
details: `Consulta ${idx + 1}: Algumas tentativas falharam, mas ${fallbackResult.scrapedSources.length} resultados foram encontrados.`,
});
}
return { query, results: fallbackResult.scrapedSources, fallbackResult };
} catch (err) {
chatLog.error(` ✗ Progressive search ${idx + 1} failed: ${err}`);
// Logar erro mas continuar (não quebrar o fluxo)
const errorMsg = err instanceof Error ? err.message : String(err);
chatLog.warn(` ⚠️ Progressive search ${idx + 1} failed (continuando): ${errorMsg}`);
// Atualizar mensagem de processo com aviso
updateThinkingMessage('web-research', {
details: `Consulta ${idx + 1}: Erro na busca (${errorMsg}). Continuando com outras consultas...`,
});
return { query, results: [] as ScrapedContent[], fallbackResult: null };
}
});
@@ -563,25 +587,19 @@ export default function ChatPage() {
}
} catch (error) {
// Tratar erro graciosamente sem quebrar o fluxo
const errorMsg = error instanceof Error ? error.message : String(error);
chatLog.error(`❌ Error in Deep Research pipeline: ${errorMsg}`);
chatLog.warn(`⚠️ Error in Deep Research pipeline (continuing with partial results): ${errorMsg}`);
// Atualizar mensagem de processo com erro
// Atualizar mensagem de processo com aviso (não erro fatal)
updateThinkingMessage('web-research', {
status: 'error',
error: errorMsg,
status: 'completed', // Marcar como completed mesmo com erro parcial
details: `Alguns erros ocorreram durante a pesquisa: ${errorMsg}. Continuando com resultados parciais...`,
duration: Date.now() - webResearchStart,
});
// Atualizar step com erro (para compatibilidade)
setProcessSteps(prev => prev.map(s =>
s.id === 'web-research'
? { ...s, status: 'error' as const, error: errorMsg }
: s
));
// Definir erro global
setError(error);
// Não definir erro global - permitir que o fluxo continue
// O sistema continuará com conhecimento interno se necessário
}
} else {
if (!webSearch.isEnabled) {
@@ -849,7 +867,12 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
const panel = chatsSidebarRef.current;
if (panel) {
if (isChatsSidebarCollapsed) {
panel.expand();
// No mobile, apenas atualizar estado (overlay será mostrado)
if (typeof window !== 'undefined' && window.innerWidth < 768) {
setIsChatsSidebarCollapsed(false);
} else {
panel.expand();
}
} else {
panel.collapse();
}
@@ -905,7 +928,7 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
defaultSize={4}
minSize={4}
maxSize={4}
className="border-r flex flex-col items-center py-4 gap-4 bg-muted/20 w-[60px]"
className="border-r flex flex-col items-center py-4 gap-4 bg-muted/20 w-[60px] min-w-[60px] max-w-[60px] flex-shrink-0"
>
<Button variant="ghost" size="icon" className="rounded-lg bg-primary/10 text-primary">
<MessageSquare className="w-5 h-5" />
@@ -932,48 +955,63 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
</Button>
</ResizablePanel>
<ResizableHandle />
<ResizableHandle className="hidden md:block" />
{/* Sessions List */}
{/* Sessions List - Desktop: ResizablePanel, Mobile: Overlay */}
<ResizablePanel
ref={chatsSidebarRef}
defaultSize={15}
minSize={10}
defaultSize={0}
minSize={0}
maxSize={25}
collapsible={true}
collapsedSize={0}
onCollapse={() => setIsChatsSidebarCollapsed(true)}
onExpand={() => setIsChatsSidebarCollapsed(false)}
className="min-w-[200px]"
className="min-w-0 md:min-w-[220px] md:max-w-[360px] hidden md:block"
style={{
minWidth: 'clamp(220px, 20vw, 360px)',
maxWidth: '360px',
}}
>
<SidebarList
sessions={sessions}
currentSessionId={currentSessionId}
onSelectSession={handleSelectSession}
onSelectSession={(id) => {
handleSelectSession(id);
// Fechar sidebar no mobile após seleção
if (typeof window !== 'undefined' && window.innerWidth < 768) {
setIsChatsSidebarCollapsed(true);
chatsSidebarRef.current?.collapse();
}
}}
onDeleteSession={deleteSession}
onNewChat={handleNewChat}
/>
</ResizablePanel>
<ResizableHandle />
<ResizableHandle className="hidden md:block" />
{/* Main Chat Area */}
<ResizablePanel defaultSize={60} minSize={40}>
<ResizablePanel defaultSize={100} minSize={75} className="transition-all duration-300">
<div className="h-full flex flex-col">
{/* Header */}
<div className="h-14 border-b flex items-center px-4 justify-between bg-background/50 backdrop-blur gap-4">
<div className="h-14 border-b flex items-center px-3 sm:px-4 justify-between bg-background/50 backdrop-blur gap-2 sm:gap-4 flex-shrink-0" style={{ paddingLeft: 'clamp(12px, 2vw, 16px)', paddingRight: 'clamp(12px, 2vw, 16px)' }}>
<div className="flex items-center gap-2">
{isChatsSidebarCollapsed ? (
<Button variant="ghost" size="icon" onClick={toggleChatsSidebar} className="h-8 w-8">
<Button
variant="ghost"
size="icon"
onClick={toggleChatsSidebar}
className="h-8 w-8 flex-shrink-0"
aria-label={isChatsSidebarCollapsed ? "Abrir conversas" : "Fechar conversas"}
>
{isChatsSidebarCollapsed ? (
<PanelLeftOpen className="w-4 h-4" />
</Button>
) : (
<Button variant="ghost" size="icon" onClick={toggleChatsSidebar} className="h-8 w-8 text-muted-foreground">
) : (
<PanelLeftClose className="w-4 h-4" />
</Button>
)}
<div className="font-semibold flex items-center gap-2">
Chat
)}
</Button>
<div className="font-semibold flex items-center gap-2 text-sm sm:text-base">
<span className="hidden sm:inline">Chat</span>
{isGeneratingTitle && <Loader2 className="w-3 h-3 animate-spin text-muted-foreground" />}
{isDownloading && (
<span className="text-xs text-muted-foreground flex items-center gap-1">
@@ -984,40 +1022,62 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
</div>
</div>
<div className="flex-1 max-w-[420px] flex items-center gap-2">
<div className="flex-1">
<Select
value={selectedModel || undefined}
onValueChange={(v) => {
if (v === "__add_model__") {
setShowDownloadDialog(true);
// Não alterar selectedModel quando abrir dialog
} else {
setSelectedModel(v);
settings.setSelectedModel(v);
}
}}
>
<SelectTrigger className="h-9">
<SelectValue placeholder="Selecione um modelo..." />
</SelectTrigger>
<SelectContent>
{models.map(m => (
<SelectItem key={m.name} value={m.name}>
<span className="flex items-center justify-between w-full">
<span className="truncate">{m.name}</span>
<span className="ml-3 text-xs text-muted-foreground">{m.size}</span>
<div className="flex-1 max-w-full sm:max-w-[420px] flex items-center gap-2 min-w-0">
<div className="flex-1 min-w-0">
<TooltipProvider>
<Select
value={selectedModel || undefined}
onValueChange={(v) => {
if (v === "__add_model__") {
setShowDownloadDialog(true);
// Não alterar selectedModel quando abrir dialog
} else {
setSelectedModel(v);
settings.setSelectedModel(v);
}
}}
>
<Tooltip>
<TooltipTrigger asChild>
<SelectTrigger className="h-9 min-w-0 max-w-full">
<SelectValue placeholder="Selecione um modelo..." className="truncate" />
</SelectTrigger>
</TooltipTrigger>
{selectedModel && (
<TooltipContent side="top" className="max-w-[70vw] break-words">
<p className="text-sm">{selectedModel}</p>
</TooltipContent>
)}
</Tooltip>
<SelectContent className="max-w-[90vw] sm:max-w-[420px]">
{models.map(m => (
<Tooltip key={m.name}>
<TooltipTrigger asChild>
<SelectItem value={m.name} className="min-w-0">
<span className="flex items-center justify-between w-full min-w-0 gap-2">
<span className="truncate flex-1 min-w-0" style={{ maxWidth: '70vw' }}>
{m.name}
</span>
<span className="ml-2 text-xs text-muted-foreground shrink-0">{m.size}</span>
</span>
</SelectItem>
</TooltipTrigger>
{m.name.length > 40 && (
<TooltipContent side="right" className="max-w-[70vw] break-words">
<p className="text-sm">{m.name}</p>
</TooltipContent>
)}
</Tooltip>
))}
<SelectItem value="__add_model__" className="text-primary font-medium">
<span className="flex items-center gap-2">
<Plus className="h-4 w-4" />
Adicionar modelo...
</span>
</SelectItem>
))}
<SelectItem value="__add_model__" className="text-primary font-medium">
<span className="flex items-center gap-2">
<Plus className="h-4 w-4" />
Adicionar modelo...
</span>
</SelectItem>
</SelectContent>
</Select>
</SelectContent>
</Select>
</TooltipProvider>
</div>
<Button variant="outline" size="icon" className="h-9 w-9" onClick={refresh} title="Atualizar modelos">
<Loader2 className="w-4 h-4" />
@@ -1028,8 +1088,8 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
{/* Messages Area with Floating Input Layout */}
<div className="flex-1 relative h-full overflow-hidden flex flex-col">
{/* Scrollable Content Area */}
<div className="flex-1 overflow-y-auto scroll-smooth w-full">
<div className="max-w-3xl mx-auto w-full px-4 md:px-0 pt-6 pb-4">
<div className="flex-1 overflow-y-auto scroll-smooth w-full min-h-0">
<div className="max-w-3xl mx-auto w-full px-3 sm:px-4 md:px-6 pt-4 sm:pt-6 pb-4 transition-all duration-300" style={{ paddingLeft: 'clamp(12px, 4vw, 24px)', paddingRight: 'clamp(12px, 4vw, 24px)' }}>
{messages.length === 0 ? (
<div className="min-h-[50vh] flex flex-col items-center justify-center text-muted-foreground space-y-6">
<div className="p-6 rounded-2xl bg-muted/30 ring-1 ring-border/50 shadow-sm">
@@ -1041,7 +1101,7 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
</div>
</div>
) : (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4 sm:gap-6">
{messages.map((msg, i) => {
const isLastMessage = i === messages.length - (isLoading ? 1 : 0); // Ajuste simples
// Streaming logica mantida simples
@@ -1062,11 +1122,7 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
)}
</div>
)}
{/* Elemento invisível para scroll automático */}
<div ref={messagesEndRef} className="h-1" />
</div>
{/* Debug Info (Moved inside scroll) */}
{(deepResearch.state.logs.length > 0 || lastWebContext) && (
<div className="mt-12 pt-6 border-t border-dashed border-muted/50 opacity-70 hover:opacity-100 transition-opacity">
@@ -1138,12 +1194,12 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
</div>
{/* Fixed Input Container */}
<div className="absolute bottom-0 left-0 right-0 z-20 pointer-events-none">
<div className="absolute bottom-0 left-0 right-0 z-30 pointer-events-none">
{/* Gradient Overlay */}
<div className="absolute inset-0 -top-20 bg-gradient-to-t from-background via-background/90 to-transparent pointer-events-none" />
{/* Input Wrapper */}
<div className="relative max-w-3xl mx-auto px-4 pb-6 pt-4 pointer-events-auto">
<div className="relative max-w-3xl mx-auto px-3 sm:px-4 md:px-6 pb-4 sm:pb-6 pt-4 pointer-events-auto transition-all duration-300" style={{ paddingLeft: 'clamp(12px, 4vw, 24px)', paddingRight: 'clamp(12px, 4vw, 24px)' }}>
<ChatInput
onSend={handleSend}
onStop={stop}
@@ -1163,9 +1219,38 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
</div>
</div>
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
{/* Mobile Sidebar Overlay */}
{!isChatsSidebarCollapsed && (
<>
<div
className="fixed inset-0 bg-black/50 z-40 md:hidden transition-opacity"
onClick={() => {
setIsChatsSidebarCollapsed(true);
chatsSidebarRef.current?.collapse();
}}
/>
<div className="fixed left-0 top-0 bottom-0 w-[280px] min-w-[220px] max-w-[85vw] bg-background border-r z-50 md:hidden shadow-xl animate-in slide-in-from-left duration-300 overflow-y-auto">
<SidebarList
sessions={sessions}
currentSessionId={currentSessionId}
onSelectSession={(id) => {
handleSelectSession(id);
setIsChatsSidebarCollapsed(true);
}}
onDeleteSession={deleteSession}
onNewChat={() => {
handleNewChat();
setIsChatsSidebarCollapsed(true);
}}
/>
</div>
</>
)}
<ModelDownloadDialog
open={showDownloadDialog}
onOpenChange={(open) => {
+145
View File
@@ -152,3 +152,148 @@
@apply bg-background text-foreground;
}
}
/* Custom Scrollbar Styles - Padronizado para toda interface */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: hsl(var(--muted));
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: hsl(var(--primary));
border-radius: 4px;
transition: background 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background: hsl(var(--primary) / 0.8);
}
/* Firefox scrollbar */
* {
scrollbar-width: thin;
scrollbar-color: hsl(var(--primary)) hsl(var(--muted));
}
/* Responsive adjustments */
@media (max-width: 640px) {
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
}
/* Responsive layout adjustments */
@media (max-width: 650px) {
/* Sidebar compact mode - hide labels, show only icons */
.sidebar-compact .sidebar-label {
display: none;
}
.sidebar-compact .sidebar-icon {
font-size: 24px;
}
}
/* Ensure proper spacing and padding at all resolutions */
@media (min-width: 320px) and (max-width: 2560px) {
/* Prevent layout breaking at extreme sizes */
.chat-container {
min-width: 320px;
max-width: 100vw;
}
.message-container {
padding-left: clamp(12px, 4vw, 24px);
padding-right: clamp(12px, 4vw, 24px);
}
}
/* Prevent text overflow in cards and messages */
.truncate-with-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Ensure cards never stick to edges */
.card-container {
padding: clamp(8px, 2vw, 16px);
}
/* Responsive font sizes */
@media (max-width: 640px) {
.responsive-text {
font-size: clamp(14px, 4vw, 16px);
}
}
/* Model name truncation - ensure long model names don't break layout */
.model-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 70vw;
display: inline-block;
vertical-align: middle;
}
@media (max-width: 600px) {
.model-name {
max-width: 54vw;
font-size: 15px;
}
}
/* Select value truncation */
[data-radix-select-trigger] span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
display: block;
}
/* Ensure SelectItems also truncate properly */
[data-radix-select-item] {
min-width: 0;
}
[data-radix-select-item] span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 70vw;
display: block;
}
/* Scrollbar específico para lista de chats - mais fino e discreto */
.sidebar-chat-list::-webkit-scrollbar {
width: 6px;
}
.sidebar-chat-list::-webkit-scrollbar-track {
background: transparent;
border-radius: 3px;
}
.sidebar-chat-list::-webkit-scrollbar-thumb {
background: hsl(var(--muted-foreground) / 0.3);
border-radius: 3px;
transition: background 0.2s ease;
}
.sidebar-chat-list::-webkit-scrollbar-thumb:hover {
background: hsl(var(--primary) / 0.6);
}
/* Firefox scrollbar para lista de chats */
.sidebar-chat-list {
scrollbar-width: thin;
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
}
+177 -18
View File
@@ -11,6 +11,7 @@ import { Switch } from '@/components/ui/switch';
import { Slider } from '@/components/ui/slider';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import {
Dialog,
DialogContent,
@@ -304,21 +305,43 @@ export default function SettingsPage() {
Carregando modelos...
</div>
) : availableModels.length > 0 ? (
<Select
value={settings.selectedModel}
onValueChange={settings.setSelectedModel}
>
<SelectTrigger id="model-select">
<SelectValue placeholder="Selecione um modelo..." />
</SelectTrigger>
<SelectContent>
{availableModels.map((model) => (
<SelectItem key={model.name} value={model.name}>
{model.name}
</SelectItem>
))}
</SelectContent>
</Select>
<TooltipProvider>
<Select
value={settings.selectedModel}
onValueChange={settings.setSelectedModel}
>
<Tooltip>
<TooltipTrigger asChild>
<SelectTrigger id="model-select" className="min-w-0 max-w-full">
<SelectValue placeholder="Selecione um modelo..." className="truncate" />
</SelectTrigger>
</TooltipTrigger>
{settings.selectedModel && (
<TooltipContent side="top" className="max-w-[70vw] break-words">
<p className="text-sm">{settings.selectedModel}</p>
</TooltipContent>
)}
</Tooltip>
<SelectContent className="max-w-[90vw]">
{availableModels.map((model) => (
<Tooltip key={model.name}>
<TooltipTrigger asChild>
<SelectItem value={model.name} className="min-w-0">
<span className="truncate block" style={{ maxWidth: '70vw' }}>
{model.name}
</span>
</SelectItem>
</TooltipTrigger>
{model.name.length > 40 && (
<TooltipContent side="right" className="max-w-[70vw] break-words">
<p className="text-sm">{model.name}</p>
</TooltipContent>
)}
</Tooltip>
))}
</SelectContent>
</Select>
</TooltipProvider>
) : (
<Input
id="model-input"
@@ -463,12 +486,12 @@ export default function SettingsPage() {
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>10s</span>
<span className="font-medium">{settings.webSearch.timeout}s</span>
<span className="font-medium">{settings.webSearch.timeout / 1000}s</span>
<span>60s</span>
</div>
<Slider
value={[settings.webSearch.timeout]}
onValueChange={([value]) => settings.setWebSearchTimeout(value)}
value={[settings.webSearch.timeout / 1000]}
onValueChange={([value]) => settings.setWebSearchTimeout(value * 1000)}
min={10}
max={60}
step={5}
@@ -487,6 +510,142 @@ export default function SettingsPage() {
/>
</CardContent>
</Card>
{/* Card: Search Engines Configuration */}
<Card>
<CardHeader>
<CardTitle>Motores de Busca</CardTitle>
<CardDescription>
Configure a ordem e comportamento dos motores de busca
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label>Ordem dos Motores</Label>
<p className="text-xs text-muted-foreground">
Arraste para reordenar. O sistema tentará cada motor na ordem especificada até encontrar resultados suficientes.
</p>
<div className="space-y-2 border rounded-lg p-4">
{settings.webSearch.engineOrder.map((engine, index) => (
<div
key={engine}
className="flex items-center justify-between p-2 rounded-md hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-2">
<Badge variant="outline" className="font-mono text-xs">
{index + 1}
</Badge>
<span className="text-sm font-medium capitalize">{engine}</span>
</div>
<div className="flex items-center gap-1">
{index > 0 && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => {
const newOrder = [...settings.webSearch.engineOrder];
[newOrder[index], newOrder[index - 1]] = [newOrder[index - 1], newOrder[index]];
settings.setWebSearchEngineOrder(newOrder);
}}
>
<ArrowLeft className="w-3 h-3" />
</Button>
)}
{index < settings.webSearch.engineOrder.length - 1 && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => {
const newOrder = [...settings.webSearch.engineOrder];
[newOrder[index], newOrder[index + 1]] = [newOrder[index + 1], newOrder[index]];
settings.setWebSearchEngineOrder(newOrder);
}}
>
<ArrowLeft className="w-3 h-3 rotate-180" />
</Button>
)}
</div>
</div>
))}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="min-results-per-engine">Mínimo de Resultados por Motor</Label>
<div className="space-y-2">
<div className="flex items-center gap-3">
<Input
id="min-results-per-engine"
type="number"
value={settings.webSearch.minResultsPerEngine}
onChange={(e) => {
const v = parseInt(e.target.value || '1', 10);
const clamped = Math.min(10, Math.max(1, isNaN(v) ? 1 : v));
settings.setWebSearchMinResultsPerEngine(clamped);
}}
className="w-24"
min={1}
max={10}
disabled={!settings.webSearch.enabled}
/>
<div className="flex-1">
<Slider
value={[settings.webSearch.minResultsPerEngine]}
onValueChange={([value]) => settings.setWebSearchMinResultsPerEngine(value)}
min={1}
max={10}
step={1}
disabled={!settings.webSearch.enabled}
/>
</div>
</div>
<p className="text-xs text-muted-foreground">
Número mínimo de resultados para considerar um motor como bem-sucedido. Se um motor retornar menos resultados, o sistema tentará o próximo.
</p>
</div>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="semantic-expansion">Expansão Semântica de Queries</Label>
<p className="text-xs text-muted-foreground">
Adiciona sinônimos e variantes às queries para melhorar resultados
</p>
</div>
<Switch
id="semantic-expansion"
checked={settings.webSearch.enableSemanticExpansion}
onCheckedChange={settings.setWebSearchSemanticExpansion}
disabled={!settings.webSearch.enabled}
/>
</div>
{settings.webSearch.enableSemanticExpansion && (
<div className="space-y-2">
<Label htmlFor="semantic-language">Idioma para Expansão Semântica</Label>
<Select
value={settings.webSearch.semanticExpansionLanguage}
onValueChange={settings.setWebSearchSemanticExpansionLanguage}
disabled={!settings.webSearch.enabled}
>
<SelectTrigger id="semantic-language">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="pt-BR">Português (Brasil)</SelectItem>
<SelectItem value="en">English</SelectItem>
<SelectItem value="es">Español</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Idioma usado para identificar stopwords e sinônimos durante a expansão semântica.
</p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* Tab: Sources & Knowledge */}