chore: expurgo de blobs excedentes e realinhamento de estado
release.yml (push) Invalid workflow file
release.yml (push) Invalid workflow file
This commit is contained in:
1 parent
593efd42e0
commit
a7cbd1a3b4
114 files changed
+20716
-4258
No files matched your search
@@ -18,7 +18,11 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
<<<<<<< HEAD
|
||||
import { Message, ThinkingMessageMetadata } from '@/hooks/use-chat';
|
||||
=======
|
||||
import { Message } from '@/hooks/use-chat';
|
||||
>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5
|
||||
import type { DeepResearchState } from '@/hooks/use-deep-research';
|
||||
import type { ScrapedContent } from '@/services/webSearch';
|
||||
|
||||
@@ -39,6 +43,18 @@ export interface DebugData {
|
||||
contextual: string[];
|
||||
};
|
||||
sources: ScrapedContent[];
|
||||
<<<<<<< HEAD
|
||||
sitesResearched?: Array<{
|
||||
url: string;
|
||||
title: string;
|
||||
status: string;
|
||||
source: string;
|
||||
duration?: number;
|
||||
contentLength: number;
|
||||
scrapedAt: number;
|
||||
}>;
|
||||
=======
|
||||
>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5
|
||||
logs: Array<{
|
||||
stage: string;
|
||||
timestamp: number;
|
||||
@@ -53,6 +69,35 @@ export interface DebugData {
|
||||
title: string;
|
||||
content: string;
|
||||
}>;
|
||||
<<<<<<< HEAD
|
||||
activeQueries?: Array<{
|
||||
query: string;
|
||||
source: string;
|
||||
round?: number;
|
||||
startedAt: number;
|
||||
}>;
|
||||
activeUrls?: Array<{
|
||||
url: string;
|
||||
title?: string;
|
||||
status: string;
|
||||
source?: string;
|
||||
startedAt: number;
|
||||
duration?: number;
|
||||
}>;
|
||||
};
|
||||
deepResearchState?: DeepResearchState;
|
||||
thinkingSteps?: ThinkingMessageMetadata[];
|
||||
reasoning?: {
|
||||
steps?: string[];
|
||||
intermediateResults?: any[];
|
||||
};
|
||||
finalResponse: string;
|
||||
rawResponse: string;
|
||||
tokenUsage?: {
|
||||
input: number;
|
||||
output: number;
|
||||
total: number;
|
||||
=======
|
||||
};
|
||||
deepResearchState?: DeepResearchState;
|
||||
finalResponse: string;
|
||||
@@ -60,6 +105,7 @@ export interface DebugData {
|
||||
tokenUsage?: {
|
||||
input: number;
|
||||
output: number;
|
||||
>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useState } from 'react';
|
||||
import type { ThinkingMessageMetadata } from '@/hooks/use-chat';
|
||||
import { SearchQueriesMonitor } from './search-queries-monitor';
|
||||
import { ScrapingUrlsMonitor } from './scraping-urls-monitor';
|
||||
|
||||
interface MessageStepThinkingProps {
|
||||
metadata: ThinkingMessageMetadata;
|
||||
@@ -105,13 +107,31 @@ export function MessageStepThinking({ metadata }: MessageStepThinkingProps) {
|
||||
|
||||
{/* Conteúdo Expandido */}
|
||||
{isExpanded && (
|
||||
<div className="mt-2 ml-1 p-3 border-l-2 border-muted/30 space-y-2 text-sm text-muted-foreground animate-in fade-in">
|
||||
<div className="mt-2 ml-1 p-3 border-l-2 border-muted/30 space-y-4 text-sm text-muted-foreground animate-in fade-in">
|
||||
{metadata.details && (
|
||||
<div className="whitespace-pre-wrap">{metadata.details}</div>
|
||||
)}
|
||||
|
||||
{/* Monitoramento em Tempo Real - Queries */}
|
||||
{metadata.activeQueries && metadata.activeQueries.length > 0 && (
|
||||
<div className="pt-2 border-t border-muted/30">
|
||||
<SearchQueriesMonitor queries={metadata.activeQueries} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Monitoramento em Tempo Real - URLs */}
|
||||
{metadata.activeUrls && metadata.activeUrls.length > 0 && (
|
||||
<div className="pt-2 border-t border-muted/30">
|
||||
<ScrapingUrlsMonitor urls={metadata.activeUrls} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{metadata.sources && metadata.sources.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 pt-1">
|
||||
<div className="pt-2 border-t border-muted/30">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">
|
||||
Fontes Encontradas ({metadata.sources.length})
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{metadata.sources.map((source, idx) => (
|
||||
<div key={idx} className="flex items-center gap-1 bg-background/50 border border-muted/50 px-2 py-1 rounded text-xs max-w-[200px]">
|
||||
<ExternalLink className="w-3 h-3 opacity-50" />
|
||||
@@ -121,6 +141,7 @@ export function MessageStepThinking({ metadata }: MessageStepThinkingProps) {
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Check, ChevronsUpDown, Search, DownloadCloud, Cpu, Loader2 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useModelSelection } from '@/hooks/use-model-selection';
|
||||
import { useDownloadContext } from '@/contexts/download-context';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
export function ModelSelector() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const { myModels, availableModels, selectedModel, setSelectedModel, installedVariantsByFamily, refreshModels } = useModelSelection();
|
||||
const { startDownload, state: downloadState } = useDownloadContext();
|
||||
const { toast } = useToast();
|
||||
|
||||
// Track selected variant for each model in the catalog
|
||||
const [selectedVariants, setSelectedVariants] = React.useState<Record<string, string>>({});
|
||||
|
||||
const handleDownload = async (e: React.MouseEvent, modelName: string) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await startDownload(modelName);
|
||||
toast({
|
||||
title: "Download Iniciado",
|
||||
description: `Baixando ${modelName}...`,
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erro",
|
||||
description: "Falha ao iniciar download.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSmartDownload = async () => {
|
||||
const term = searchQuery.trim();
|
||||
if (!term) return;
|
||||
|
||||
try {
|
||||
await startDownload(term);
|
||||
setOpen(false);
|
||||
setSearchQuery("");
|
||||
toast({
|
||||
title: "Download Iniciado",
|
||||
description: `Baixando ${term} da biblioteca Ollama...`,
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erro",
|
||||
description: "Falha ao iniciar download.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (downloadState?.isSuccess) {
|
||||
refreshModels();
|
||||
}
|
||||
}, [downloadState?.isSuccess, refreshModels]);
|
||||
|
||||
const filteredMyModels = React.useMemo(() => {
|
||||
if (!searchQuery) return myModels;
|
||||
return myModels.filter(m =>
|
||||
m.model.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [myModels, searchQuery]);
|
||||
|
||||
const filteredAvailableModels = React.useMemo(() => {
|
||||
if (!searchQuery) return availableModels;
|
||||
return availableModels.filter(m =>
|
||||
m.model.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [availableModels, searchQuery]);
|
||||
|
||||
// Helper to check if a model is currently selected (partial match for variants)
|
||||
const isSelected = (modelName: string) => {
|
||||
if (!selectedModel) return false;
|
||||
return selectedModel.startsWith(modelName) || selectedModel === modelName;
|
||||
};
|
||||
|
||||
// Helper to extract tag/parameters from model name
|
||||
const getVariantDisplay = (name: string) => {
|
||||
if (name.includes(':')) {
|
||||
return name.split(':')[1];
|
||||
}
|
||||
return 'latest';
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-[260px] justify-between"
|
||||
>
|
||||
{selectedModel ? (
|
||||
<span className="flex items-center gap-2 truncate">
|
||||
<Cpu className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
<span className="truncate">{selectedModel}</span>
|
||||
</span>
|
||||
) : (
|
||||
"Selecione um modelo..."
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0" align="start">
|
||||
<div className="flex flex-col">
|
||||
{/* Search Input */}
|
||||
<div className="flex items-center border-b border-border/50 px-3 bg-muted/30">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<input
|
||||
className="flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder="Buscar modelos..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && searchQuery && filteredAvailableModels.length === 0) {
|
||||
handleSmartDownload();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="installed" className="w-full">
|
||||
<TabsList className="w-full rounded-none border-b bg-transparent p-0">
|
||||
<TabsTrigger
|
||||
value="installed"
|
||||
className="flex-1 rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent h-10"
|
||||
>
|
||||
Instalados
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="available"
|
||||
className="flex-1 rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent h-10"
|
||||
>
|
||||
Catálogo
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="installed" className="m-0 max-h-[400px] overflow-y-auto p-2">
|
||||
{filteredMyModels.length === 0 ? (
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">
|
||||
{searchQuery ? (
|
||||
<span>Nenhum modelo instalado encontrado.</span>
|
||||
) : (
|
||||
<span>Nenhum modelo instalado.</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{filteredMyModels.map((modelDef) => {
|
||||
const family = modelDef.model;
|
||||
const installedTags = installedVariantsByFamily[family] || [];
|
||||
const isFamilySelected = selectedModel ? (selectedModel === family || selectedModel.startsWith(family + ":")) : false;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={family}
|
||||
className="relative flex flex-col rounded-md border border-transparent hover:border-border/50 bg-card/50 hover:bg-accent/30 p-3 text-sm transition-all"
|
||||
>
|
||||
<div className="flex items-start justify-between w-full mb-2">
|
||||
<div className="flex flex-col gap-0.5 overflow-hidden pr-2">
|
||||
<span className="font-bold text-base truncate flex items-center gap-2">
|
||||
{modelDef.model}
|
||||
{isFamilySelected && (
|
||||
<Check className="h-3 w-3 text-primary animate-in fade-in zoom-in" />
|
||||
)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground line-clamp-2" title={modelDef.description}>
|
||||
{modelDef.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5 mt-1">
|
||||
{installedTags.map((fullTag) => {
|
||||
const tag = getVariantDisplay(fullTag);
|
||||
const isExactSelected = selectedModel === fullTag;
|
||||
// Find variant def for size info if available
|
||||
const variantDef = modelDef.variants.find(v => v.name === fullTag);
|
||||
|
||||
return (
|
||||
<Badge
|
||||
key={fullTag}
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"cursor-pointer text-[10px] px-2 h-5 transition-all border-transparent",
|
||||
isExactSelected
|
||||
? "bg-foreground text-background hover:bg-foreground/90"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground"
|
||||
)}
|
||||
title={variantDef ? `Tamanho: ${variantDef.size}` : undefined}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedModel(fullTag);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="available" className="m-0 max-h-[400px] overflow-y-auto p-2">
|
||||
{filteredAvailableModels.length === 0 ? (
|
||||
<div className="py-6 text-center text-sm">
|
||||
{searchQuery ? (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<p className="text-muted-foreground">Modelo não encontrado no catálogo.</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-auto py-2 px-4 border-dashed border-zinc-500/50 hover:border-primary hover:bg-primary/10"
|
||||
onClick={handleSmartDownload}
|
||||
>
|
||||
<DownloadCloud className="mr-2 h-4 w-4" />
|
||||
<span>Baixar <span className="font-bold ml-1">{searchQuery}</span></span>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Nenhum modelo encontrado.</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{filteredAvailableModels.map((modelDef) => {
|
||||
// Determine active variant for this item
|
||||
const activeVariantName = selectedVariants[modelDef.model] || modelDef.variants[0]?.name;
|
||||
const activeVariant = modelDef.variants.find(v => v.name === activeVariantName) || modelDef.variants[0];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={modelDef.model}
|
||||
className="relative flex flex-col rounded-md border border-transparent hover:border-border/50 bg-card/50 hover:bg-accent/30 p-3 text-sm transition-all"
|
||||
>
|
||||
<div className="flex items-start justify-between w-full mb-2">
|
||||
<div className="flex flex-col gap-0.5 overflow-hidden pr-2">
|
||||
<span className="font-bold text-base truncate">{modelDef.model}</span>
|
||||
<span className="text-xs text-muted-foreground line-clamp-2" title={modelDef.description}>
|
||||
{modelDef.description}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={(e) => handleDownload(e, activeVariant.name)}
|
||||
disabled={downloadState.isDownloading && downloadState.currentModel === activeVariant.name}
|
||||
title={`Baixar ${activeVariant.name}`}
|
||||
>
|
||||
{downloadState.isDownloading && downloadState.currentModel === activeVariant.name ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<DownloadCloud className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Variants Pills */}
|
||||
<div className="flex flex-wrap gap-1.5 mt-1">
|
||||
{modelDef.variants.map((variant) => {
|
||||
const isVariantSelected = activeVariant.name === variant.name;
|
||||
return (
|
||||
<Badge
|
||||
key={variant.name}
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"cursor-pointer text-[10px] px-2 h-5 transition-all border-transparent",
|
||||
isVariantSelected
|
||||
? "bg-foreground text-background hover:bg-foreground/90"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground"
|
||||
)}
|
||||
title={`Tamanho em disco: ${variant.size}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedVariants(prev => ({
|
||||
...prev,
|
||||
[modelDef.model]: variant.name
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{getVariantDisplay(variant.name)}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)})}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
'use client';
|
||||
|
||||
import { Loader2, CheckCircle2, XCircle, ExternalLink, Globe } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ActiveUrl } from '@/hooks/use-web-search-events';
|
||||
|
||||
interface ScrapingUrlsMonitorProps {
|
||||
urls: ActiveUrl[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ScrapingUrlsMonitor({ urls, className }: ScrapingUrlsMonitorProps) {
|
||||
if (urls.length === 0) return null;
|
||||
|
||||
const getFaviconUrl = (url: string): string => {
|
||||
try {
|
||||
const domain = new URL(url).hostname;
|
||||
return `https://www.google.com/s2/favicons?domain=${domain}&sz=16`;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: ActiveUrl['status']) => {
|
||||
switch (status) {
|
||||
case 'started':
|
||||
return <Loader2 className="w-3 h-3 animate-spin text-primary" />;
|
||||
case 'completed':
|
||||
case 'cached':
|
||||
return <CheckCircle2 className="w-3 h-3 text-green-500" />;
|
||||
case 'failed':
|
||||
return <XCircle className="w-3 h-3 text-red-500" />;
|
||||
default:
|
||||
return <Globe className="w-3 h-3 text-muted-foreground" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getSourceBadge = (source?: string) => {
|
||||
if (!source) return null;
|
||||
|
||||
const colors: Record<string, string> = {
|
||||
static: 'bg-blue-500/10 text-blue-600 border-blue-500/20',
|
||||
nodriver: 'bg-purple-500/10 text-purple-600 border-purple-500/20',
|
||||
playwright: 'bg-orange-500/10 text-orange-600 border-orange-500/20',
|
||||
cached: 'bg-gray-500/10 text-gray-600 border-gray-500/20',
|
||||
};
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={cn('text-xs', colors[source] || 'bg-muted')}>
|
||||
{source}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-2', className)}>
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">
|
||||
Sites Sendo Acessados ({urls.length})
|
||||
</div>
|
||||
{urls.map((url, idx) => (
|
||||
<div
|
||||
key={`${url.url}-${idx}`}
|
||||
className="flex items-start gap-2 p-2 rounded-md border bg-muted/30 hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
{getStatusIcon(url.status)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start gap-2">
|
||||
<img
|
||||
src={getFaviconUrl(url.url)}
|
||||
alt=""
|
||||
className="w-4 h-4 rounded mt-0.5 flex-shrink-0"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<a
|
||||
href={url.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs font-medium text-primary hover:underline line-clamp-1 flex items-center gap-1"
|
||||
>
|
||||
{url.title || url.url}
|
||||
<ExternalLink className="w-3 h-3 flex-shrink-0" />
|
||||
</a>
|
||||
{getSourceBadge(url.source)}
|
||||
{url.duration && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{url.duration}ms
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
||||
{url.url}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
'use client';
|
||||
|
||||
import { Loader2, CheckCircle2, Globe, Search } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ActiveQuery } from '@/hooks/use-web-search-events';
|
||||
|
||||
interface SearchQueriesMonitorProps {
|
||||
queries: ActiveQuery[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SearchQueriesMonitor({ queries, className }: SearchQueriesMonitorProps) {
|
||||
if (queries.length === 0) return null;
|
||||
|
||||
const getSourceColor = (source: string) => {
|
||||
switch (source) {
|
||||
case 'brave':
|
||||
return 'bg-blue-500/10 text-blue-600 border-blue-500/20';
|
||||
case 'searxng':
|
||||
return 'bg-green-500/10 text-green-600 border-green-500/20';
|
||||
case 'duckduckgo':
|
||||
return 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20';
|
||||
default:
|
||||
return 'bg-muted text-muted-foreground border-muted';
|
||||
}
|
||||
};
|
||||
|
||||
const getSourceLabel = (source: string) => {
|
||||
switch (source) {
|
||||
case 'brave':
|
||||
return 'Brave';
|
||||
case 'searxng':
|
||||
return 'SearXNG';
|
||||
case 'duckduckgo':
|
||||
return 'DuckDuckGo';
|
||||
default:
|
||||
return source;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-2', className)}>
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">
|
||||
Consultas Ativas ({queries.length})
|
||||
</div>
|
||||
{queries.map((query, idx) => (
|
||||
<div
|
||||
key={`${query.query}-${query.source}-${idx}`}
|
||||
className="flex items-center gap-2 p-2 rounded-md border bg-muted/30 hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<Loader2 className="w-3 h-3 animate-spin text-primary flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs truncate">{query.query}</span>
|
||||
<Badge variant="outline" className={cn('text-xs', getSourceColor(query.source))}>
|
||||
{getSourceLabel(query.source)}
|
||||
</Badge>
|
||||
{query.round && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Round {query.round}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user