ultima antes do format
This commit is contained in:
1 parent
4006a72eec
commit
4275295e4f
7 files changed
+991
-12
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user