diff --git a/client/src/App.tsx b/client/src/App.tsx index 57a89c0..b1b4d76 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -9,6 +9,7 @@ import { SoeMotorProvider } from "@/contexts/SoeMotorContext"; import { ProtectedRoute } from "@/lib/protected-route"; import { CommandPalette } from "@/components/CommandPalette"; import { KnowledgeCollectorInit } from "@/components/KnowledgeCollectorInit"; +import { OpenClawWidget } from "@/components/openclaw/OpenClawWidget"; import NotFound from "@/pages/not-found"; import AuthPage from "@/pages/auth-page"; @@ -164,6 +165,7 @@ function App() { + diff --git a/client/src/components/openclaw/OpenClawWidget.tsx b/client/src/components/openclaw/OpenClawWidget.tsx new file mode 100644 index 0000000..e84e3bd --- /dev/null +++ b/client/src/components/openclaw/OpenClawWidget.tsx @@ -0,0 +1,131 @@ +/** + * OpenClawWidget.tsx + * + * Widget flutuante do OpenClaw — aparece quando há sugestões pendentes. + * Posicionado no canto inferior direito, fora das rotas. + * Abre o modal SkillSuggestion para confirmação. + * + * Fase 4: OpenClaw Sprint 2 + */ + +import { useState } from "react"; +import { Sparkles, X, ChevronUp, ChevronDown } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { useAgentEmergence } from "@/hooks/useAgentEmergence"; +import { SkillSuggestion } from "./SkillSuggestion"; +import { useAuth } from "@/hooks/use-auth"; + +export function OpenClawWidget() { + const { user } = useAuth(); + const { + suggestions, + activeSuggestion, + pendingCount, + confirmSkill, + rejectSkill, + openSuggestion, + closeSuggestion, + } = useAgentEmergence(); + + const [collapsed, setCollapsed] = useState(false); + const [dismissed, setDismissed] = useState(false); + + // Não renderiza se não há usuário, não há sugestões, ou foi dispensado + if (!user || pendingCount === 0 || dismissed) return null; + + return ( + <> + {/* Widget flutuante */} +
+ {/* Painel expandido */} + {!collapsed && ( +
+
+
+ + + OpenClaw + + + {pendingCount} + +
+
+ + +
+
+ +
+

+ Detectei padrões de uso que podem virar skills automáticas: +

+ {suggestions.map((s) => ( + + ))} +
+
+ )} + + {/* Botão minimizado */} + {collapsed && ( + + )} +
+ + {/* Modal de detalhes */} + {activeSuggestion && ( + + )} + + ); +} diff --git a/client/src/components/openclaw/SkillSuggestion.tsx b/client/src/components/openclaw/SkillSuggestion.tsx new file mode 100644 index 0000000..77f2671 --- /dev/null +++ b/client/src/components/openclaw/SkillSuggestion.tsx @@ -0,0 +1,101 @@ +/** + * SkillSuggestion.tsx + * + * Modal com preview da skill sugerida pelo OpenClaw. + * Apresenta padrão detectado, nome, descrição e preview de automação. + * Usuário pode confirmar ou rejeitar. + * + * Fase 4: OpenClaw Sprint 2 + */ + +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Sparkles, X, Check, Zap, BarChart2 } from "lucide-react"; +import type { SkillSuggestionData } from "@/hooks/useAgentEmergence"; + +interface SkillSuggestionProps { + suggestion: SkillSuggestionData; + onConfirm: (id: string) => void; + onReject: (id: string) => void; + onClose: () => void; +} + +export function SkillSuggestion({ suggestion, onConfirm, onReject, onClose }: SkillSuggestionProps) { + const confidencePct = Math.round(suggestion.confidence * 100); + + return ( + + + +
+
+ +
+ Nova skill detectada +
+
+ + {/* Padrão detectado */} +
+

Padrão detectado

+

{suggestion.pattern.description || suggestion.pattern.action_type}

+
+ + + {suggestion.pattern.frequency}x detectado + + + + Confiança: {confidencePct}% + +
+
+ + {/* Skill sugerida */} +
+
+

Nome da skill

+
+ + {suggestion.suggested_skill_name} + +
+
+ +
+

Descrição

+

{suggestion.suggested_description}

+
+ +
+

O que será automatizado

+
+ {suggestion.estimated_automation} +
+
+
+ + + + + +
+
+ ); +} diff --git a/client/src/hooks/useAgentEmergence.ts b/client/src/hooks/useAgentEmergence.ts new file mode 100644 index 0000000..0a392a7 --- /dev/null +++ b/client/src/hooks/useAgentEmergence.ts @@ -0,0 +1,143 @@ +/** + * useAgentEmergence.ts + * + * Hook para gerenciar sugestões de skills do OpenClaw. + * Escuta eventos Socket.IO e coordena confirmações/rejeições. + * + * Fase 4: OpenClaw Sprint 2 + */ + +import { useState, useEffect, useCallback } from "react"; +import { useAuth } from "./use-auth"; + +export interface SkillSuggestionData { + id: string; + pattern: { + id: string; + action_type: string; + frequency: number; + confidence: number; + description: string; + }; + suggested_skill_name: string; + suggested_description: string; + estimated_automation: string; + confidence: number; + created_at: string; + status: "pending" | "accepted" | "rejected"; +} + +interface UseAgentEmergenceReturn { + suggestions: SkillSuggestionData[]; + activeSuggestion: SkillSuggestionData | null; + isOpen: boolean; + pendingCount: number; + confirmSkill: (suggestionId: string) => Promise; + rejectSkill: (suggestionId: string) => Promise; + openSuggestion: (suggestion: SkillSuggestionData) => void; + closeSuggestion: () => void; + dismissWidget: () => void; +} + +export function useAgentEmergence(): UseAgentEmergenceReturn { + const { user } = useAuth(); + const [suggestions, setSuggestions] = useState([]); + const [activeSuggestion, setActiveSuggestion] = useState(null); + const [isOpen, setIsOpen] = useState(false); + + // Carrega sugestões pendentes do servidor + const fetchPendingSuggestions = useCallback(async () => { + if (!user) return; + try { + const res = await fetch("/api/openclaw/suggestions"); + if (res.ok) { + const data = await res.json(); + setSuggestions(data.suggestions || []); + } + } catch { + // silencioso — não bloqueia o usuário + } + }, [user]); + + // Escuta eventos Socket.IO via window (padrão da suite) + useEffect(() => { + if (!user) return; + + fetchPendingSuggestions(); + + const handleOpenClawEvent = (event: CustomEvent) => { + const suggestion: SkillSuggestionData = event.detail; + setSuggestions((prev) => { + const exists = prev.find((s) => s.id === suggestion.id); + if (exists) return prev; + return [suggestion, ...prev]; + }); + }; + + window.addEventListener("openclaw:suggestion" as any, handleOpenClawEvent); + return () => { + window.removeEventListener("openclaw:suggestion" as any, handleOpenClawEvent); + }; + }, [user, fetchPendingSuggestions]); + + const confirmSkill = useCallback(async (suggestionId: string) => { + try { + const res = await fetch("/api/openclaw/confirm-skill", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ suggestion_id: suggestionId, action: "confirm" }), + }); + if (res.ok) { + setSuggestions((prev) => prev.filter((s) => s.id !== suggestionId)); + setActiveSuggestion(null); + setIsOpen(false); + } + } catch { + // erro tratado na UI via toast externo + } + }, []); + + const rejectSkill = useCallback(async (suggestionId: string) => { + try { + const res = await fetch("/api/openclaw/confirm-skill", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ suggestion_id: suggestionId, action: "reject" }), + }); + if (res.ok) { + setSuggestions((prev) => prev.filter((s) => s.id !== suggestionId)); + setActiveSuggestion(null); + setIsOpen(false); + } + } catch { + // silencioso + } + }, []); + + const openSuggestion = useCallback((suggestion: SkillSuggestionData) => { + setActiveSuggestion(suggestion); + setIsOpen(true); + }, []); + + const closeSuggestion = useCallback(() => { + setActiveSuggestion(null); + setIsOpen(false); + }, []); + + const dismissWidget = useCallback(() => { + setIsOpen(false); + setActiveSuggestion(null); + }, []); + + return { + suggestions, + activeSuggestion, + isOpen, + pendingCount: suggestions.length, + confirmSkill, + rejectSkill, + openSuggestion, + closeSuggestion, + dismissWidget, + }; +}