feat(03-03): create MiroFlowControl component with agent selection and analysis interface

- Toggle for scientific mode with Brain icon
- Agent selector (Statistician, Fiscal Auditor, Researcher)
- Task textarea with dynamic placeholder per agent
- Analysis button with loading state (Loader2 animation)
- Result display with metadata (model, duration, execution_id)
- Error handling with readable messages
- Styled for dark theme matching BiWorkspace
This commit is contained in:
Jonas Pacheco 2026-03-25 10:54:42 -03:00
parent e4e809fa5b
commit e5b915e7e8
1 changed files with 217 additions and 0 deletions

View File

@ -0,0 +1,217 @@
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { apiRequest } from "@/lib/queryClient";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Brain, Loader2 } from "lucide-react";
interface AnalyzeRequest {
agent: "statistician" | "fiscal_auditor" | "researcher";
task: string;
context?: Record<string, unknown>;
}
interface AnalyzeResponse {
agent: string;
model: string;
result: string;
execution_id: string;
duration_ms: number;
}
const AGENTS = [
{
value: "statistician",
label: "Statistician",
model: "deepseek-r1:14b",
placeholder:
"Ex: Analise a distribuição de vendas por região no último trimestre",
},
{
value: "fiscal_auditor",
label: "Fiscal Auditor",
model: "deepseek-r1:14b",
placeholder:
"Ex: Verifique inconsistências nos registros NFe do CNPJ 12.345.678/0001-90",
},
{
value: "researcher",
label: "Researcher",
model: "llama3.1:8b",
placeholder:
"Ex: Quais fornecedores têm maior correlação com atrasos de entrega?",
},
] as const;
interface MiroFlowControlProps {
currentDashboardData?: Record<string, unknown>;
}
export function MiroFlowControl({
currentDashboardData,
}: MiroFlowControlProps) {
const [enabled, setEnabled] = useState(false);
const [selectedAgent, setSelectedAgent] = useState<
"statistician" | "fiscal_auditor" | "researcher"
>("statistician");
const [task, setTask] = useState("");
const mutation = useMutation({
mutationFn: async (request: AnalyzeRequest) => {
const response = await apiRequest("POST", "/api/miroflow/analyze", request);
return response.json() as Promise<AnalyzeResponse>;
},
});
const handleAnalyze = () => {
if (!task.trim()) {
return;
}
mutation.mutate({
agent: selectedAgent,
task,
context: currentDashboardData,
});
};
const currentAgentConfig = AGENTS.find((a) => a.value === selectedAgent);
return (
<div className="space-y-6">
<Card className="bg-[#1f334d] border border-[#c89b3c]/20">
<CardHeader className="border-b border-[#c89b3c]/20 pb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Brain className="w-6 h-6 text-[#c89b3c]" />
<CardTitle className="text-white">Modo Científico</CardTitle>
</div>
<button
onClick={() => setEnabled(!enabled)}
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-colors ${
enabled ? "bg-[#c89b3c]" : "bg-gray-600"
}`}
>
<span
className={`inline-block h-6 w-6 transform rounded-full bg-white transition-transform ${
enabled ? "translate-x-7" : "translate-x-1"
}`}
/>
</button>
</div>
</CardHeader>
{enabled && (
<CardContent className="space-y-4 pt-6">
{/* Agent Selection */}
<div>
<label className="block text-sm font-medium text-white mb-2">
Selecione o Agente
</label>
<Select value={selectedAgent} onValueChange={(value: any) => setSelectedAgent(value)}>
<SelectTrigger className="bg-[#2a4466] border border-[#c89b3c]/20 text-white">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-[#2a4466] border border-[#c89b3c]/20">
{AGENTS.map((agent) => (
<SelectItem key={agent.value} value={agent.value} className="text-white">
<div>
<div>{agent.label}</div>
<div className="text-xs text-gray-400">{agent.model}</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Task Input */}
<div>
<label className="block text-sm font-medium text-white mb-2">
Digite a Tarefa
</label>
<Textarea
placeholder={currentAgentConfig?.placeholder || "Digite sua tarefa..."}
value={task}
onChange={(e) => setTask(e.target.value)}
disabled={mutation.isPending}
className="bg-[#2a4466] border border-[#c89b3c]/20 text-white placeholder:text-gray-500 min-h-24"
/>
</div>
{/* Analyze Button */}
<Button
onClick={handleAnalyze}
disabled={!task.trim() || mutation.isPending}
className="w-full bg-[#c89b3c] hover:bg-[#d4a94a] text-[#1f334d] font-semibold"
>
{mutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Analisando...
</>
) : (
"Analisar"
)}
</Button>
{/* Error Display */}
{mutation.isError && (
<div className="rounded-lg p-4 bg-red-900/20 border border-red-500/50">
<Badge className="mb-2 bg-red-600 text-white">Erro</Badge>
<p className="text-sm text-red-100">
{mutation.error instanceof Error
? mutation.error.message
: "Erro ao processar a análise"}
</p>
</div>
)}
{/* Result Display */}
{mutation.isSuccess && (
<div className="space-y-3">
<div className="flex items-center gap-2 mb-2">
<Badge className="bg-green-600 text-white">Sucesso</Badge>
</div>
<div className="rounded-lg p-4 bg-[#2a4466] border border-[#c89b3c]/20">
<ScrollArea className="h-64 w-full pr-4">
<p className="text-white text-sm whitespace-pre-wrap">
{mutation.data?.result}
</p>
</ScrollArea>
</div>
{/* Metadata */}
<div className="grid grid-cols-3 gap-2 text-xs text-gray-400">
<div>
<span className="font-semibold text-[#c89b3c]">Modelo:</span>{" "}
{mutation.data?.model}
</div>
<div>
<span className="font-semibold text-[#c89b3c]">Tempo:</span>{" "}
{mutation.data?.duration_ms}ms
</div>
<div>
<span className="font-semibold text-[#c89b3c]">ID:</span>{" "}
{mutation.data?.execution_id?.slice(0, 8)}...
</div>
</div>
</div>
)}
</CardContent>
)}
</Card>
</div>
);
}