ultima antes do format

This commit is contained in:
Evandro Fonseca Junior committed 2025-11-22 01:59:12 -03:00
1 parent 4006a72eec
commit 4275295e4f
7 files changed
+991 -12

No files matched your search

+154
View File
@@ -0,0 +1,154 @@
'use client';
import { Card } from '@/components/ui/card';
import { ExternalLink } from 'lucide-react';
import { ScrapedContent } from '@/services/webSearch';
import { useRef, useState } from 'react';
import { cn } from '@/lib/utils';
interface SourcesCarouselProps {
sources: ScrapedContent[];
}
export function SourcesCarousel({ sources }: SourcesCarouselProps) {
if (!sources || sources.length === 0) return null;
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [startX, setStartX] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const [hasMoved, setHasMoved] = useState(false);
// Handlers para mouse drag
const handleMouseDown = (e: React.MouseEvent) => {
if (!scrollContainerRef.current) return;
setIsDragging(true);
setStartX(e.pageX - scrollContainerRef.current.offsetLeft);
setScrollLeft(scrollContainerRef.current.scrollLeft);
setHasMoved(false);
};
const handleMouseMove = (e: React.MouseEvent) => {
if (!isDragging || !scrollContainerRef.current) return;
e.preventDefault();
const x = e.pageX - scrollContainerRef.current.offsetLeft;
const walk = (x - startX) * 1.5; // Velocidade do scroll
scrollContainerRef.current.scrollLeft = scrollLeft - walk;
// Se moveu mais de 3px, marca como movimento (não clique)
if (Math.abs(walk) > 3) {
setHasMoved(true);
}
};
const handleMouseUp = () => {
setIsDragging(false);
// Resetar após um pequeno delay
setTimeout(() => setHasMoved(false), 150);
};
const handleMouseLeave = () => {
setIsDragging(false);
setTimeout(() => setHasMoved(false), 150);
};
// Handlers para touch drag (mobile)
const handleTouchStart = (e: React.TouchEvent) => {
if (!scrollContainerRef.current) return;
setIsDragging(true);
setStartX(e.touches[0].pageX - scrollContainerRef.current.offsetLeft);
setScrollLeft(scrollContainerRef.current.scrollLeft);
setHasMoved(false);
};
const handleTouchMove = (e: React.TouchEvent) => {
if (!isDragging || !scrollContainerRef.current) return;
const x = e.touches[0].pageX - scrollContainerRef.current.offsetLeft;
const walk = (x - startX) * 1.5;
scrollContainerRef.current.scrollLeft = scrollLeft - walk;
if (Math.abs(walk) > 3) {
setHasMoved(true);
}
};
const handleTouchEnd = () => {
setIsDragging(false);
setTimeout(() => setHasMoved(false), 150);
};
// Prevenir navegação do link durante drag
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (hasMoved || isDragging) {
e.preventDefault();
e.stopPropagation();
}
};
return (
<div className="w-full max-w-3xl mx-auto mb-4 mt-2">
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3 px-1">
Fontes Analisadas
</h3>
<div
ref={scrollContainerRef}
className={cn(
"flex gap-3 overflow-x-auto pb-4 scrollbar-hide -mx-4 px-4 sm:mx-0 sm:px-0 mask-fade-sides",
"cursor-grab active:cursor-grabbing select-none touch-pan-x",
isDragging && "cursor-grabbing"
)}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{
scrollBehavior: isDragging ? 'auto' : 'smooth',
WebkitOverflowScrolling: 'touch' // Smooth scrolling no iOS
}}
>
{sources.map((source, idx) => {
const hostname = new URL(source.url).hostname.replace('www.', '');
const faviconUrl = `https://www.google.com/s2/favicons?domain=${hostname}&sz=32`;
return (
<a
key={idx}
href={source.url}
target="_blank"
rel="noopener noreferrer"
onClick={handleLinkClick}
className="flex-none w-40 group transition-all hover:-translate-y-1 block"
style={{ pointerEvents: (isDragging || hasMoved) ? 'none' : 'auto' }}
>
<Card className="h-24 p-3 flex flex-col justify-between hover:shadow-md hover:border-primary/20 transition-colors bg-card/50 backdrop-blur-sm cursor-pointer overflow-hidden">
<div className="flex items-start justify-between min-h-0 flex-shrink-0">
<img
src={faviconUrl}
alt=""
className="w-4 h-4 rounded-sm opacity-70 group-hover:opacity-100 transition-opacity flex-shrink-0"
onError={(e) => (e.target as HTMLImageElement).style.display = 'none'}
/>
<ExternalLink className="w-3 h-3 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0 ml-1" />
</div>
<div className="space-y-1 min-w-0 flex-1 overflow-hidden flex flex-col justify-end">
<div className="text-xs font-medium leading-tight line-clamp-2 text-card-foreground/90 group-hover:text-primary transition-colors break-words overflow-hidden text-ellipsis">
{source.title}
</div>
<div className="text-[10px] text-muted-foreground truncate overflow-hidden text-ellipsis">
{hostname}
</div>
</div>
</Card>
</a>
);
})}
</div>
</div>
);
}
+97
View File
@@ -0,0 +1,97 @@
'use client';
import { usePythonBootstrap } from '@/hooks/use-python-bootstrap';
import { Progress } from '@/components/ui/progress';
import { Loader2, XCircle } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
const STAGE_LABELS: Record<string, string> = {
checking: 'Verificando',
downloading: 'Baixando',
extracting: 'Extraindo',
creating_venv: 'Criando ambiente virtual',
installing_pip: 'Instalando pip',
copying_scripts: 'Copiando scripts',
installing_deps: 'Instalando dependências',
validating: 'Validando',
done: 'Concluído',
};
export function PythonBootstrapProgress() {
const bootstrap = usePythonBootstrap();
// Se estiver completo e sem erro, não mostrar nada
if (bootstrap.isComplete && !bootstrap.error) {
return null;
}
// Se houver erro, mostrar alerta
if (bootstrap.error) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
<div className="w-full max-w-md mx-4">
<Card className="border-destructive">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-destructive">
<XCircle className="h-5 w-5" />
Erro no Bootstrap do Python
</CardTitle>
<CardDescription>
O aplicativo não pode funcionar sem o runtime Python
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="rounded-md bg-destructive/10 p-3">
<p className="text-sm text-destructive font-medium">{bootstrap.error}</p>
</div>
<p className="text-sm text-muted-foreground">
Por favor, reinicie o aplicativo ou verifique os logs para mais detalhes.
</p>
</CardContent>
</Card>
</div>
</div>
);
}
// Mostrar progresso durante bootstrap
const stageLabel = STAGE_LABELS[bootstrap.stage] || bootstrap.stage;
const progressValue = bootstrap.progress ?? 0;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
<div className="w-full max-w-md mx-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-3">
<Loader2 className="h-5 w-5 animate-spin text-primary" />
Inicializando Python Runtime
</CardTitle>
<CardDescription>
Configurando ambiente Python para scraping web
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">{stageLabel}</span>
{bootstrap.progress !== null && (
<span className="text-muted-foreground">{Math.round(progressValue * 100)}%</span>
)}
</div>
{bootstrap.progress !== null && (
<Progress value={progressValue * 100} className="h-2" />
)}
<p className="text-sm text-muted-foreground mt-2">
{bootstrap.message}
</p>
</div>
</CardContent>
</Card>
</div>
</div>
);
}
@@ -0,0 +1,89 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { Loader2, Search, FileText } from "lucide-react";
import { useWebSearch } from "@/hooks/use-web-search";
export function ScrapingProgressIndicator() {
const webSearch = useWebSearch();
// Não mostrar se idle ou completed
if (webSearch.status === 'idle' || webSearch.status === 'completed') {
return null;
}
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.9 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="fixed bottom-24 right-6 bg-card border border-primary/20 rounded-lg p-4 shadow-lg backdrop-blur-sm z-50 min-w-[280px]"
>
<div className="flex items-start gap-3">
{/* Icon animado */}
<div className="flex-shrink-0 mt-0.5">
{webSearch.status === 'searching' && (
<Search className="w-5 h-5 text-primary animate-pulse" />
)}
{webSearch.status === 'scraping' && (
<FileText className="w-5 h-5 text-primary animate-pulse" />
)}
{webSearch.status === 'error' && (
<div className="w-5 h-5 rounded-full bg-destructive/20 flex items-center justify-center">
<span className="text-destructive text-xs font-bold">!</span>
</div>
)}
</div>
{/* Content */}
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin text-primary" />
<p className="text-sm font-medium">
{webSearch.status === 'searching' && 'Buscando fontes...'}
{webSearch.status === 'scraping' && `Analisando fontes (${webSearch.scrapedSources.length})...`}
{webSearch.status === 'error' && 'Erro na busca'}
</p>
</div>
{/* Mostrar query atual se disponível */}
{webSearch.currentQuery && webSearch.status !== 'error' && (
<p className="text-xs text-muted-foreground line-clamp-1">
{webSearch.currentQuery}
</p>
)}
{/* Mostrar erro se houver */}
{webSearch.error && webSearch.status === 'error' && (
<p className="text-xs text-destructive line-clamp-2">
{webSearch.error}
</p>
)}
{/* Barra de progresso simulada */}
{webSearch.status !== 'error' && (
<div className="w-full h-1 bg-muted rounded-full overflow-hidden">
<motion.div
className="h-full bg-primary"
initial={{ width: "0%" }}
animate={{
width: webSearch.status === 'searching' ? "40%" : "80%"
}}
transition={{
duration: 2,
ease: "easeInOut"
}}
/>
</div>
)}
</div>
</div>
</motion.div>
</AnimatePresence>
);
}