modulo de agente pessoal de IA

This commit is contained in:
Vitex Tecnologia
2026-01-14 20:10:42 -03:00
parent ee7204982c
commit 76cbd80eef
25 changed files with 3619 additions and 3 deletions
@@ -0,0 +1,28 @@
import { NavLink as RouterNavLink, NavLinkProps } from "react-router-dom";
import { forwardRef } from "react";
import { cn } from "@/lib/utils";
interface NavLinkCompatProps extends Omit<NavLinkProps, "className"> {
className?: string;
activeClassName?: string;
pendingClassName?: string;
}
const NavLink = forwardRef<HTMLAnchorElement, NavLinkCompatProps>(
({ className, activeClassName, pendingClassName, to, ...props }, ref) => {
return (
<RouterNavLink
ref={ref}
to={to}
className={({ isActive, isPending }) =>
cn(className, isActive && activeClassName, isPending && pendingClassName)
}
{...props}
/>
);
},
);
NavLink.displayName = "NavLink";
export { NavLink };
@@ -0,0 +1,44 @@
import { ReactNode } from "react";
import { cn } from "@/lib/utils";
interface MetricCardProps {
title: string;
value: string | number;
subtitle?: string;
icon: ReactNode;
status?: "success" | "warning" | "error";
className?: string;
}
export function MetricCard({
title,
value,
subtitle,
icon,
status,
className,
}: MetricCardProps) {
return (
<div className={cn("metric-card animate-fade-in", className)}>
<div className="flex items-start justify-between mb-3">
<div className="p-2.5 rounded-lg bg-primary/10 text-primary">
{icon}
</div>
{status && (
<span
className={cn("status-dot", {
"status-connected": status === "success",
"status-warning": status === "warning",
"status-error": status === "error",
})}
/>
)}
</div>
<p className="text-muted-foreground text-sm mb-1">{title}</p>
<p className="text-2xl font-semibold text-foreground">{value}</p>
{subtitle && (
<p className="text-muted-foreground text-xs mt-1">{subtitle}</p>
)}
</div>
);
}
@@ -0,0 +1,48 @@
import { cn } from "@/lib/utils";
interface StatusBadgeProps {
status: "connected" | "error" | "disconnected" | "pending";
label?: string;
}
const statusConfig = {
connected: {
label: "Conectado",
className: "bg-success/10 text-success border-success/20",
},
error: {
label: "Erro",
className: "bg-destructive/10 text-destructive border-destructive/20",
},
disconnected: {
label: "Desconectado",
className: "bg-muted text-muted-foreground border-border",
},
pending: {
label: "Pendente",
className: "bg-warning/10 text-warning border-warning/20",
},
};
export function StatusBadge({ status, label }: StatusBadgeProps) {
const config = statusConfig[status];
return (
<span
className={cn(
"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border",
config.className
)}
>
<span
className={cn("w-1.5 h-1.5 rounded-full", {
"bg-success": status === "connected",
"bg-destructive": status === "error",
"bg-muted-foreground": status === "disconnected",
"bg-warning": status === "pending",
})}
/>
{label || config.label}
</span>
);
}
@@ -0,0 +1,291 @@
import { useState, useEffect } from "react";
import { Eye, EyeOff, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import asanaLogo from "@/modules/intelligence-ia/assets/asana-logo.png";
interface Workspace {
id: string;
name: string;
}
interface User {
id: string;
name: string;
}
interface AsanaCardProps {
apiKey?: string;
workspaces?: Workspace[];
users?: User[];
selectedWorkspaceId?: string;
selectedUserId?: string;
loadingWorkspaces?: boolean;
loadingUsers?: boolean;
loadingIntegration?: boolean;
hasIntegration?: boolean;
saving?: boolean;
onSave?: (apiKey: string, workspaceId: string, userId: string) => void | Promise<void>;
onConfirmApiKey?: (apiKey: string) => void | Promise<void>;
onWorkspaceChange?: (workspaceId: string) => void | Promise<void>;
}
export function AsanaCard({
apiKey = "",
workspaces = [],
users = [],
selectedWorkspaceId = "",
selectedUserId = "",
loadingWorkspaces = false,
loadingUsers = false,
loadingIntegration = false,
hasIntegration = false,
saving = false,
onSave,
onConfirmApiKey,
onWorkspaceChange,
}: AsanaCardProps) {
const [showKey, setShowKey] = useState(false);
const [key, setKey] = useState(apiKey);
const [selectedWorkspace, setSelectedWorkspace] = useState(selectedWorkspaceId);
const [selectedUser, setSelectedUser] = useState(selectedUserId);
const [isApiKeyConfirmed, setIsApiKeyConfirmed] = useState(!!apiKey);
// Atualiza os valores quando as props mudam (carregamento inicial)
useEffect(() => {
if (apiKey) {
setKey(apiKey);
setIsApiKeyConfirmed(true);
}
}, [apiKey]);
useEffect(() => {
if (selectedWorkspaceId) {
setSelectedWorkspace(selectedWorkspaceId);
}
}, [selectedWorkspaceId]);
useEffect(() => {
if (selectedUserId) {
setSelectedUser(selectedUserId);
}
}, [selectedUserId]);
const handleKeyChange = (value: string) => {
setKey(value);
setSelectedWorkspace("");
setSelectedUser("");
setIsApiKeyConfirmed(false);
};
const handleConfirmApiKey = async () => {
if (key.length >= 5) {
setIsApiKeyConfirmed(true);
// Chama a função assíncrona para buscar workspaces
await onConfirmApiKey?.(key);
}
};
const handleWorkspaceChange = async (value: string) => {
setSelectedWorkspace(value);
setSelectedUser("");
// Chama a função assíncrona para buscar usuários
await onWorkspaceChange?.(value);
};
const handleSave = async () => {
await onSave?.(key, selectedWorkspace, selectedUser);
};
return (
<div className="metric-card space-y-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-muted flex items-center justify-center">
<img src={asanaLogo} alt="Asana" className="w-6 h-6" />
</div>
<div>
<h3 className="text-foreground font-medium">Asana</h3>
<p className="text-muted-foreground text-sm">Gerencie tarefas e projetos</p>
</div>
</div>
<div className="space-y-3">
<div>
<label className="text-sm text-muted-foreground mb-1.5 block">
API Key / Token
</label>
<div className="relative">
{loadingIntegration ? (
<div className="relative">
<Skeleton className="h-10 w-full" />
<div className="absolute inset-0 flex items-center justify-center">
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
</div>
</div>
) : (
<div className="flex gap-2">
<div className="relative flex-1">
<Input
type={showKey ? "text" : "password"}
value={key}
onChange={(e) => handleKeyChange(e.target.value)}
placeholder="Insira sua chave de API"
className="pr-10 bg-card border-border"
disabled={loadingWorkspaces}
/>
<button
onClick={() => setShowKey(!showKey)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
disabled={loadingWorkspaces}
>
{showKey ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button>
</div>
<Button
size="sm"
variant="outline"
onClick={handleConfirmApiKey}
disabled={key.length < 5 || isApiKeyConfirmed || loadingWorkspaces}
className="shrink-0"
>
{loadingWorkspaces ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Carregando...
</>
) : isApiKeyConfirmed ? (
"Confirmado"
) : (
"Confirmar"
)}
</Button>
</div>
)}
</div>
</div>
<div>
<label className="text-sm text-muted-foreground mb-1.5 block">
Workspace
</label>
{loadingIntegration ? (
<div className="relative">
<Skeleton className="h-10 w-full" />
<div className="absolute inset-0 flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
<span className="text-sm text-muted-foreground">Carregando integração...</span>
</div>
</div>
) : (
<div className="relative">
<Select
value={selectedWorkspace}
onValueChange={handleWorkspaceChange}
disabled={loadingWorkspaces || loadingIntegration}
>
<SelectTrigger className="bg-card border-border">
<SelectValue
placeholder={
loadingWorkspaces
? "Carregando workspaces..."
: workspaces.length === 0
? "Insira o token e clique em Confirmar"
: "Selecione um workspace"
}
/>
</SelectTrigger>
<SelectContent className="bg-card border-border">
{workspaces.map((ws) => (
<SelectItem key={ws.id} value={ws.id}>
{ws.name}
</SelectItem>
))}
</SelectContent>
</Select>
{loadingWorkspaces && (
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
</div>
)}
</div>
)}
</div>
<div>
<label className="text-sm text-muted-foreground mb-1.5 block">
Usuário
</label>
{loadingUsers ? (
<div className="relative">
<Skeleton className="h-10 w-full" />
<div className="absolute inset-0 flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
<span className="text-sm text-muted-foreground">Carregando usuários...</span>
</div>
</div>
) : (
<div className="relative">
<Select
value={selectedUser}
onValueChange={setSelectedUser}
disabled={loadingUsers || !selectedWorkspace}
>
<SelectTrigger className="bg-card border-border">
<SelectValue
placeholder={
!selectedWorkspace
? "Selecione um workspace primeiro"
: users.length === 0
? "Nenhum usuário encontrado"
: "Selecione um usuário"
}
/>
</SelectTrigger>
<SelectContent className="bg-card border-border">
{users.map((user) => (
<SelectItem key={user.id} value={user.id}>
{user.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
</div>
<div className="flex items-center gap-2 pt-2">
<Button
size="sm"
onClick={handleSave}
disabled={!selectedWorkspace || !selectedUser || saving}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
{saving ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
{hasIntegration ? "Salvando..." : "Criando..."}
</>
) : hasIntegration ? (
"Salvar"
) : (
"Criar Integração"
)}
</Button>
</div>
</div>
);
}
@@ -0,0 +1,72 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import googleCalendarLogo from "@/modules/intelligence-ia/assets/google-calendar-logo.png";
export function GoogleCalendarCard() {
return (
<div className="metric-card space-y-4 opacity-60">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-muted flex items-center justify-center">
<img src={googleCalendarLogo} alt="Google Calendar" className="w-6 h-6 grayscale" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="text-foreground font-medium">Google Calendar</h3>
<span className="text-xs bg-muted text-muted-foreground px-2 py-0.5 rounded-full">
Em breve
</span>
</div>
<p className="text-muted-foreground text-sm">Sincronize reuniões e eventos</p>
</div>
</div>
<div className="space-y-3 pointer-events-none">
<div>
<label className="text-sm text-muted-foreground mb-1.5 block">
Client ID
</label>
<Input
type="text"
disabled
placeholder="Insira o Client ID"
className="bg-card border-border"
/>
</div>
<div>
<label className="text-sm text-muted-foreground mb-1.5 block">
Client Secret
</label>
<Input
type="password"
disabled
placeholder="Insira o Client Secret"
className="bg-card border-border"
/>
</div>
<div>
<label className="text-sm text-muted-foreground mb-1.5 block">
Nome da Agenda
</label>
<Input
type="text"
disabled
placeholder="Ex: Reuniões de Trabalho"
className="bg-card border-border"
/>
</div>
</div>
<div className="flex items-center gap-2 pt-2">
<Button
size="sm"
disabled
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
Salvar
</Button>
</div>
</div>
);
}
@@ -0,0 +1,72 @@
import { FileSpreadsheet } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export function GoogleSheetsCard() {
return (
<div className="metric-card space-y-4 opacity-60">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-muted flex items-center justify-center">
<FileSpreadsheet className="w-6 h-6 text-muted-foreground" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="text-foreground font-medium">Google Sheets</h3>
<span className="text-xs bg-muted text-muted-foreground px-2 py-0.5 rounded-full">
Em breve
</span>
</div>
<p className="text-muted-foreground text-sm">Gerencie planilhas e dados</p>
</div>
</div>
<div className="space-y-3 pointer-events-none">
<div>
<label className="text-sm text-muted-foreground mb-1.5 block">
Client ID
</label>
<Input
type="text"
disabled
placeholder="Insira o Client ID"
className="bg-card border-border"
/>
</div>
<div>
<label className="text-sm text-muted-foreground mb-1.5 block">
Client Secret
</label>
<Input
type="password"
disabled
placeholder="Insira o Client Secret"
className="bg-card border-border"
/>
</div>
<div>
<label className="text-sm text-muted-foreground mb-1.5 block">
Link da Planilha
</label>
<Input
type="text"
disabled
placeholder="Cole o link da planilha aqui"
className="bg-card border-border"
/>
</div>
</div>
<div className="flex items-center gap-2 pt-2">
<Button
size="sm"
disabled
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
Salvar
</Button>
</div>
</div>
);
}
@@ -0,0 +1,127 @@
import { useState } from "react";
import { NavLink } from "react-router-dom";
import {
LayoutDashboard,
Plug,
Bot,
ChevronLeft,
Menu,
X,
DollarSign,
UserCircle,
} from "lucide-react";
import { cn } from "@/lib/utils";
const navItems = [
{ title: "Home", path: "", icon: LayoutDashboard },
{ title: "Finanças", path: "financas", icon: DollarSign },
{ title: "Integrações", path: "integracoes", icon: Plug },
{ title: "Meu Perfil", path: "meu-perfil", icon: UserCircle },
];
export function AppSidebar() {
const [collapsed, setCollapsed] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const SidebarContent = () => (
<>
{/* Logo */}
<div className="flex items-center gap-3 px-4 py-6 border-b border-sidebar-border">
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center cyber-glow">
<Bot className="w-5 h-5 text-primary" />
</div>
{!collapsed && (
<div className="animate-fade-in">
<h1 className="text-sidebar-foreground font-semibold text-lg">AI Agent</h1>
<p className="text-muted-foreground text-xs">Painel Admin</p>
</div>
)}
</div>
{/* Navigation */}
<nav className="flex-1 px-3 py-4 space-y-1">
{navItems.map((item) => (
<NavLink
key={item.path}
to={item.path}
end={item.path === ""} // Apenas a Home precisa de match exato
onClick={() => setMobileOpen(false)}
className={({ isActive }) =>
cn(
"nav-item flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 w-full",
isActive
? "text-primary bg-sidebar-accent font-medium"
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent"
)
}
>
<item.icon className="w-5 h-5 flex-shrink-0" />
{!collapsed && <span className="animate-fade-in">{item.title}</span>}
</NavLink>
))}
</nav>
{/* Collapse button - desktop only */}
<div className="hidden lg:block px-3 py-4 border-t border-sidebar-border">
<button
onClick={() => setCollapsed(!collapsed)}
className="nav-item flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 w-full justify-center lg:justify-start text-muted-foreground hover:text-foreground hover:bg-sidebar-accent"
>
<ChevronLeft
className={cn(
"w-5 h-5 transition-transform duration-300 flex-shrink-0",
collapsed && "rotate-180"
)}
/>
{!collapsed && <span>Recolher</span>}
</button>
</div>
</>
);
return (
<>
{/* Mobile menu button */}
<button
onClick={() => setMobileOpen(true)}
className="lg:hidden fixed top-4 left-4 z-50 p-2 rounded-lg bg-card border border-border shadow-sm"
>
<Menu className="w-5 h-5" />
</button>
{/* Mobile overlay */}
{mobileOpen && (
<div
className="lg:hidden fixed inset-0 bg-background/80 backdrop-blur-sm z-40"
onClick={() => setMobileOpen(false)}
/>
)}
{/* Mobile sidebar */}
<aside
className={cn(
"lg:hidden fixed left-0 top-0 h-full w-64 bg-sidebar z-50 flex flex-col border-r border-sidebar-border transition-transform duration-300",
mobileOpen ? "translate-x-0" : "-translate-x-full"
)}
>
<button
onClick={() => setMobileOpen(false)}
className="absolute top-4 right-4 p-2 rounded-lg hover:bg-sidebar-accent"
>
<X className="w-5 h-5" />
</button>
<SidebarContent />
</aside>
{/* Desktop sidebar */}
<aside
className={cn(
"hidden lg:flex flex-col h-screen bg-sidebar border-r border-sidebar-border transition-all duration-300 sticky top-0",
collapsed ? "w-[72px]" : "w-64"
)}
>
<SidebarContent />
</aside>
</>
);
}
@@ -0,0 +1,19 @@
import { ReactNode } from "react";
import { AppSidebar } from "./AppSidebar";
interface MainLayoutProps {
children: ReactNode;
}
export function MainLayout({ children }: MainLayoutProps) {
return (
<div className="flex min-h-screen w-full bg-background">
<AppSidebar />
<main className="flex-1 overflow-auto">
<div className="p-4 lg:p-8 pt-16 lg:pt-8">
{children}
</div>
</main>
</div>
);
}