remove mock data
This commit is contained in:
@@ -26,27 +26,12 @@ interface ChatPanelProps {
|
||||
}
|
||||
|
||||
const queenColor = "hsl(45,95%,58%)";
|
||||
const workerColorMap: Record<string, string> = {
|
||||
"inbox-management": "hsl(38,80%,55%)",
|
||||
"job-hunter": "hsl(30,85%,58%)",
|
||||
"fitness-coach": "hsl(25,75%,55%)",
|
||||
"vuln-assessment": "hsl(15,70%,52%)",
|
||||
};
|
||||
|
||||
function getColor(_agent: string, role?: "queen" | "worker"): string {
|
||||
if (role === "queen") return queenColor;
|
||||
return workerColorMap[_agent] || "hsl(220,60%,55%)";
|
||||
return "hsl(220,60%,55%)";
|
||||
}
|
||||
|
||||
export const workerList = [
|
||||
{ id: "inbox-management", label: "Inbox Management" },
|
||||
{ id: "job-hunter", label: "Job Hunter" },
|
||||
{ id: "fitness-coach", label: "Fitness Coach" },
|
||||
{ id: "vuln-assessment", label: "Vuln Assessment" },
|
||||
{ id: "content-writer", label: "Content Writer" },
|
||||
{ id: "new-agent", label: "New Agent" },
|
||||
];
|
||||
|
||||
function MessageBubble({ msg }: { msg: ChatMessage }) {
|
||||
const isUser = msg.type === "user";
|
||||
const isQueen = msg.role === "queen";
|
||||
@@ -144,8 +129,7 @@ export default function ChatPanel({ messages, onSend, isWaiting, activeThread, a
|
||||
setInput("");
|
||||
};
|
||||
|
||||
const activeWorkerLabel = workerList.find((w) => w.id === activeThread)?.label
|
||||
|| formatAgentDisplayName(activeThread);
|
||||
const activeWorkerLabel = formatAgentDisplayName(activeThread);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full min-w-0">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { KeyRound, Check, AlertCircle, X, Shield, Loader2, Trash2, ExternalLink } from "lucide-react";
|
||||
import { credentialsApi, type CredentialInfo, type AgentCredentialRequirement } from "@/api/credentials";
|
||||
import { credentialsApi, type AgentCredentialRequirement } from "@/api/credentials";
|
||||
|
||||
export interface Credential {
|
||||
id: string;
|
||||
@@ -11,32 +11,11 @@ export interface Credential {
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export const credentialTemplates: Record<string, Omit<Credential, "connected">[]> = {
|
||||
"inbox-management": [
|
||||
{ id: "gmail", name: "Gmail", description: "Read, send, and archive emails", icon: "\ud83d\udce7", required: true },
|
||||
{ id: "gcal", name: "Google Calendar", description: "Accept invites and create events", icon: "\ud83d\udcc5", required: false },
|
||||
{ id: "gsheets", name: "Google Sheets", description: "Log invoices and expenses", icon: "\ud83d\udcca", required: false },
|
||||
],
|
||||
"job-hunter": [
|
||||
{ id: "linkedin", name: "LinkedIn", description: "Scan jobs and auto-apply", icon: "\ud83d\udcbc", required: true },
|
||||
{ id: "gmail", name: "Gmail", description: "Send cover letters and replies", icon: "\ud83d\udce7", required: true },
|
||||
{ id: "gdrive", name: "Google Drive", description: "Access resume and documents", icon: "\ud83d\udcc1", required: false },
|
||||
],
|
||||
"fitness-coach": [
|
||||
{ id: "apple-health", name: "Apple Health", description: "Sleep, HRV, and recovery data", icon: "\u2764\ufe0f", required: true },
|
||||
{ id: "gcal", name: "Google Calendar", description: "Schedule workouts and meals", icon: "\ud83d\udcc5", required: false },
|
||||
],
|
||||
"vuln-assessment": [
|
||||
{ id: "shodan", name: "Shodan", description: "Port scanning and host discovery", icon: "\ud83d\udd0d", required: true },
|
||||
{ id: "ssl-labs", name: "SSL Labs", description: "SSL certificate analysis", icon: "\ud83d\udd12", required: false },
|
||||
{ id: "gcal", name: "Google Calendar", description: "Set renewal reminders", icon: "\ud83d\udcc5", required: false },
|
||||
],
|
||||
};
|
||||
|
||||
/** Create fresh (disconnected) credentials for an agent type */
|
||||
export function createFreshCredentials(agentType: string): Credential[] {
|
||||
const templates = credentialTemplates[agentType] || [];
|
||||
return templates.map(t => ({ ...t, connected: false }));
|
||||
/** Create fresh (disconnected) credentials for an agent type.
|
||||
* Real credentials are fetched from the backend via agentPath — this returns
|
||||
* an empty list as a safe default until the backend responds. */
|
||||
export function createFreshCredentials(_agentType: string): Credential[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** Clone credentials from an existing set (for new instances of the same agent) */
|
||||
@@ -131,21 +110,11 @@ export default function CredentialsModal({
|
||||
}));
|
||||
setRows(newRows);
|
||||
} else {
|
||||
// No real path — fall back to templates + list
|
||||
setLoading(true);
|
||||
const { credentials: stored } = await credentialsApi.list();
|
||||
const storedIds = new Set(stored.map((c: CredentialInfo) => c.credential_id));
|
||||
const templates = credentialTemplates[agentType] || [];
|
||||
const newRows: CredentialRow[] = templates.map(t => ({
|
||||
...t,
|
||||
connected: storedIds.has(t.id),
|
||||
credentialKey: "api_key",
|
||||
adenSupported: false,
|
||||
}));
|
||||
setRows(newRows);
|
||||
// No real path — no credentials to show
|
||||
setRows([]);
|
||||
}
|
||||
} catch {
|
||||
// Backend unavailable — fall back to legacy props or templates
|
||||
// Backend unavailable — fall back to legacy props or empty
|
||||
if (legacyCredentials) {
|
||||
setRows(legacyCredentials.map(c => ({
|
||||
...c,
|
||||
@@ -153,13 +122,7 @@ export default function CredentialsModal({
|
||||
adenSupported: false,
|
||||
})));
|
||||
} else {
|
||||
const templates = credentialTemplates[agentType] || [];
|
||||
setRows(templates.map(t => ({
|
||||
...t,
|
||||
connected: false,
|
||||
credentialKey: "api_key",
|
||||
adenSupported: false,
|
||||
})));
|
||||
setRows([]);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||
import { Crown, Plus, X, KeyRound, Sparkles, Layers, ChevronLeft, Bot, Loader2, WifiOff } from "lucide-react";
|
||||
import AgentGraph, { type GraphNode } from "@/components/AgentGraph";
|
||||
import ChatPanel, { type ChatMessage, workerList } from "@/components/ChatPanel";
|
||||
import AgentGraph, { type GraphNode, type NodeStatus } from "@/components/AgentGraph";
|
||||
import ChatPanel, { type ChatMessage } from "@/components/ChatPanel";
|
||||
import NodeDetailPanel from "@/components/NodeDetailPanel";
|
||||
import CredentialsModal, { type Credential, createFreshCredentials, cloneCredentials, allRequiredCredentialsMet } from "@/components/CredentialsModal";
|
||||
import { agentsApi } from "@/api/agents";
|
||||
@@ -11,112 +11,12 @@ import { executionApi } from "@/api/execution";
|
||||
import { graphsApi } from "@/api/graphs";
|
||||
import { sessionsApi } from "@/api/sessions";
|
||||
import { useSSE } from "@/hooks/use-sse";
|
||||
import type { Agent, AgentEvent, Message } from "@/api/types";
|
||||
import type { Agent, AgentEvent, DiscoverEntry, Message } from "@/api/types";
|
||||
import { backendMessageToChatMessage, sseEventToChatMessage, formatAgentDisplayName } from "@/lib/chat-helpers";
|
||||
import { topologyToGraphNodes } from "@/lib/graph-converter";
|
||||
|
||||
const makeId = () => Math.random().toString(36).slice(2, 9);
|
||||
|
||||
// --- Graph templates per agent type ---
|
||||
const workerGraphs: Record<string, { nodes: GraphNode[]; title: string }> = {
|
||||
"content-writer": {
|
||||
title: "content_writer_graph",
|
||||
nodes: [
|
||||
{ id: "brief-intake", label: "brief-intake", status: "complete", next: ["research"], iterations: 1 },
|
||||
{ id: "research", label: "research", status: "complete", next: ["outline"], iterations: 1 },
|
||||
{ id: "outline", label: "outline", status: "complete", next: ["draft"], iterations: 1 },
|
||||
{ id: "draft", label: "draft", status: "running", next: ["review"], iterations: 1, statusLabel: "writing..." },
|
||||
{ id: "review", label: "review", status: "pending", next: [], backEdges: ["draft"] },
|
||||
],
|
||||
},
|
||||
"new-agent": {
|
||||
title: "new_agent_graph",
|
||||
nodes: [],
|
||||
},
|
||||
"inbox-management": {
|
||||
title: "inbox_management_graph",
|
||||
nodes: [
|
||||
{ id: "fetch-mail", label: "fetch-mail", status: "complete", next: ["classify"], iterations: 1 },
|
||||
{ id: "classify", label: "classify", status: "complete", next: ["prioritize"], iterations: 1 },
|
||||
{ id: "prioritize", label: "prioritize", status: "running", next: ["draft-replies"], iterations: 1, statusLabel: "sorting..." },
|
||||
{ id: "draft-replies", label: "draft-replies", status: "pending", next: ["send-or-archive"] },
|
||||
{ id: "send-or-archive", label: "send-or-archive", status: "pending", next: [], backEdges: ["fetch-mail"] },
|
||||
],
|
||||
},
|
||||
"job-hunter": {
|
||||
title: "job_hunter_graph",
|
||||
nodes: [
|
||||
{ id: "intake", label: "intake", status: "complete", next: ["job-search"], iterations: 1 },
|
||||
{ id: "job-search", label: "job-search", status: "complete", next: ["job-review"], iterations: 1 },
|
||||
{ id: "job-review", label: "job-review", status: "complete", next: ["customize"], iterations: 1 },
|
||||
{ id: "customize", label: "customize", status: "complete", next: [], iterations: 1 },
|
||||
],
|
||||
},
|
||||
"fitness-coach": {
|
||||
title: "fitness_coach_graph",
|
||||
nodes: [
|
||||
{ id: "intake", label: "intake", status: "complete", next: ["coach"], iterations: 1 },
|
||||
{ id: "coach", label: "coach", status: "running", next: ["meal-checkin", "exercise-reminder"], backEdges: ["coach"], iterations: 2, statusLabel: "coaching..." },
|
||||
{ id: "meal-checkin", label: "meal-checkin", status: "pending", next: [] },
|
||||
{ id: "exercise-reminder", label: "exercise-reminder", status: "pending", next: [] },
|
||||
],
|
||||
},
|
||||
"vuln-assessment": {
|
||||
title: "vuln_assessment_graph",
|
||||
nodes: [
|
||||
{ id: "intake", label: "intake", status: "complete", next: ["passive-recon"], iterations: 1 },
|
||||
{ id: "passive-recon", label: "passive-recon", status: "complete", next: ["risk-scoring"], iterations: 1 },
|
||||
{ id: "risk-scoring", label: "risk-scoring", status: "complete", next: ["findings-review"], backEdges: ["passive-recon"], iterations: 1 },
|
||||
{ id: "findings-review", label: "findings-review", status: "running", next: ["final-report"], iterations: 1, statusLabel: "analyzing..." },
|
||||
{ id: "final-report", label: "final-report", status: "pending", next: [], backEdges: ["intake"] },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// --- Seed messages per agent type ---
|
||||
const seedMessages: Record<string, ChatMessage[]> = {
|
||||
"new-agent": [
|
||||
{
|
||||
id: "na-1", agent: "Queen Bee", agentColor: "",
|
||||
content: "Welcome! \ud83d\udc1d I'm the Queen Bee \u2014 I'll help you set up your new agent.\n\nWould you like to:\n\n**1. Build from scratch** \u2014 Define a custom pipeline and workers tailored to your needs.\n\n**2. Start from an existing agent** \u2014 Clone one of your current agents and modify it.\n\nJust let me know which option you'd prefer, or describe what you'd like your agent to do and I'll suggest a setup.",
|
||||
timestamp: "", role: "queen", thread: "new-agent",
|
||||
},
|
||||
],
|
||||
"inbox-management": [
|
||||
{ id: "im-1", agent: "Queen Bee", agentColor: "", content: "Good morning! Let's start with your inbox. Check for anything urgent.", timestamp: "", role: "queen", thread: "inbox-management" },
|
||||
{ id: "im-2", agent: "inbox-management", agentColor: "", content: "You have 23 unread emails. 4 flagged as urgent:\n\n\u2022 Meeting invite from Sarah (tomorrow 2pm)\n\u2022 Invoice from AWS \u2014 $847.32\n\u2022 2 recruiter messages\n\u2022 Client follow-up from Acme Corp", timestamp: "", role: "worker", thread: "inbox-management" },
|
||||
{ id: "im-3", agent: "Queen Bee", agentColor: "", content: "Accept Sarah's meeting, archive the invoice after logging it, and forward the recruiter messages to Job Hunter.", timestamp: "", role: "queen", thread: "inbox-management" },
|
||||
{ id: "im-4", agent: "inbox-management", agentColor: "", content: "Done \u2713\n\n\u2022 Sarah's meeting accepted \u2014 added to calendar\n\u2022 AWS invoice logged to expenses sheet & archived\n\u2022 2 recruiter messages forwarded to Job Hunter\n\u2022 Drafted reply to Acme Corp \u2014 awaiting your review", timestamp: "", role: "worker", thread: "inbox-management" },
|
||||
{ id: "im-5", agent: "Queen Bee", agentColor: "", content: "Great work. Send the Acme reply as-is. Keep monitoring for anything new.", timestamp: "", role: "queen", thread: "inbox-management" },
|
||||
],
|
||||
"job-hunter": [
|
||||
{ id: "jh-1", agent: "Queen Bee", agentColor: "", content: "I've forwarded 2 recruiter messages from Inbox. Analyze them and scan the boards.", timestamp: "", role: "queen", thread: "job-hunter" },
|
||||
{ id: "jh-2", agent: "job-hunter", agentColor: "", content: "Analyzing recruiter messages + scanning 3 job boards...\n\nFound 5 new matches:\n\u2022 Senior Engineer @ Stripe \u2014 95% match \u2b50\n\u2022 Staff Engineer @ Vercel \u2014 88% match\n\u2022 Platform Lead @ Datadog \u2014 82% match\n\u2022 Senior SWE @ Notion \u2014 79% match\n\u2022 Backend Engineer @ Linear \u2014 76% match", timestamp: "", role: "worker", thread: "job-hunter" },
|
||||
{ id: "jh-3", agent: "Queen Bee", agentColor: "", content: "The Stripe role exceeds the 90% threshold. Auto-apply with the latest resume. Bookmark Vercel for manual review.", timestamp: "", role: "queen", thread: "job-hunter" },
|
||||
{ id: "jh-4", agent: "job-hunter", agentColor: "", content: "Application submitted to Stripe \u2713\n\nDetails:\n\u2022 Applied with resume v4.2\n\u2022 Cover letter auto-generated & personalized\n\u2022 Vercel role bookmarked for your review\n\u2022 Recruiter #1 replied \u2014 they want a call Thursday", timestamp: "", role: "worker", thread: "job-hunter" },
|
||||
{ id: "jh-5", agent: "Queen Bee", agentColor: "", content: "Schedule the recruiter call for Thursday 3pm. Keep scanning \u2014 update me if anything above 85% comes in.", timestamp: "", role: "queen", thread: "job-hunter" },
|
||||
],
|
||||
"fitness-coach": [
|
||||
{ id: "fc-1", agent: "Queen Bee", agentColor: "", content: "What's on the fitness plan today? Check sleep and recovery data.", timestamp: "", role: "queen", thread: "fitness-coach" },
|
||||
{ id: "fc-2", agent: "fitness-coach", agentColor: "", content: "Today is leg day \ud83e\uddb5\n\nSleep: 7.2hrs (good)\nHRV: 58ms (above baseline)\nRecovery score: 82% \u2014 green light for heavy lifting\n\nPlanned workout:\n\u2022 Squats 4\u00d78 @ 225lbs\n\u2022 Romanian deadlifts 3\u00d710 @ 185lbs\n\u2022 Leg press 3\u00d712 @ 360lbs\n\u2022 Walking lunges 3\u00d720 steps", timestamp: "", role: "worker", thread: "fitness-coach" },
|
||||
{ id: "fc-3", agent: "Queen Bee", agentColor: "", content: "Looks solid. Add calf raises \u2014 they've been neglected. Also prep a post-workout meal suggestion.", timestamp: "", role: "queen", thread: "fitness-coach" },
|
||||
{ id: "fc-4", agent: "fitness-coach", agentColor: "", content: "Updated \u2713\n\nAdded: Standing calf raises 4\u00d715\n\nPost-workout meal suggestion:\n\ud83c\udf57 Grilled chicken (40g protein)\n\ud83c\udf5a Jasmine rice (60g carbs)\n\ud83e\udd66 Steamed broccoli\n\ud83e\udd64 Creatine + electrolyte shake\n\nEstimated workout duration: 52 mins", timestamp: "", role: "worker", thread: "fitness-coach" },
|
||||
],
|
||||
"vuln-assessment": [
|
||||
{ id: "va-1", agent: "Queen Bee", agentColor: "", content: "Run a full vulnerability scan on openclaw.ai. Check all subdomains and headers.", timestamp: "", role: "queen", thread: "vuln-assessment" },
|
||||
{ id: "va-2", agent: "vuln-assessment", agentColor: "", content: "Scanning openclaw.ai...\n\nDiscovery phase complete:\n\u2022 3 subdomains: api., docs., staging.\n\u2022 12 open ports detected\n\u2022 staging. subdomain has directory listing enabled \u26a0\ufe0f", timestamp: "", role: "worker", thread: "vuln-assessment" },
|
||||
{ id: "va-3", agent: "Queen Bee", agentColor: "", content: "Critical \u2014 disable that directory listing immediately. Continue with header analysis and SSL check.", timestamp: "", role: "queen", thread: "vuln-assessment" },
|
||||
{ id: "va-4", agent: "vuln-assessment", agentColor: "", content: "Header & SSL analysis complete:\n\n\ud83d\udd34 Critical:\n\u2022 Missing Content-Security-Policy\n\u2022 No X-Frame-Options header\n\u2022 staging. directory listing (flagged)\n\n\ud83d\udfe1 Medium:\n\u2022 SPF record too permissive\n\u2022 No DMARC record\n\n\ud83d\udfe2 Good:\n\u2022 SSL cert valid (expires in 12 days \u2014 renew soon)\n\u2022 HSTS enabled on main domain\n\u2022 X-Content-Type-Options present", timestamp: "", role: "worker", thread: "vuln-assessment" },
|
||||
{ id: "va-5", agent: "Queen Bee", agentColor: "", content: "Good scan. Generate a remediation report with priority order. Flag the SSL renewal as a calendar task.", timestamp: "", role: "queen", thread: "vuln-assessment" },
|
||||
],
|
||||
"content-writer": [
|
||||
{ id: "cw-1", agent: "Queen Bee", agentColor: "", content: "Draft a blog post on the future of AI agents. Tone: professional but accessible. Target length: 800 words.", timestamp: "", role: "queen", thread: "content-writer" },
|
||||
{ id: "cw-2", agent: "content-writer", agentColor: "", content: "Research complete. Here's the outline:\n\n1. Introduction \u2014 The rise of autonomous agents\n2. How AI agents differ from chatbots\n3. Real-world use cases today\n4. What the next 5 years look like\n5. Conclusion \u2014 Humans + agents working together\n\nStarting draft now.", timestamp: "", role: "worker", thread: "content-writer" },
|
||||
{ id: "cw-3", agent: "Queen Bee", agentColor: "", content: "Good outline. Make sure section 3 includes concrete examples \u2014 email, recruiting, and security. Keep the intro punchy.", timestamp: "", role: "queen", thread: "content-writer" },
|
||||
{ id: "cw-4", agent: "content-writer", agentColor: "", content: "Draft ready \u2713\n\n\ud83d\udcdd Word count: 823\n\ud83d\udccc Examples added: Inbox Management, Job Hunter, Vuln Assessment\n\ud83d\udd17 3 internal links suggested\n\nReady for your review before publish.", timestamp: "", role: "worker", thread: "content-writer" },
|
||||
],
|
||||
};
|
||||
|
||||
// --- Session types ---
|
||||
interface Session {
|
||||
id: string;
|
||||
@@ -127,15 +27,13 @@ interface Session {
|
||||
credentials: Credential[];
|
||||
}
|
||||
|
||||
function createSession(agentType: string, index: number, existingCredentials?: Credential[]): Session {
|
||||
const graph = workerGraphs[agentType] || { title: agentType, nodes: [] };
|
||||
const agentLabel = workerList.find(w => w.id === agentType)?.label || formatAgentDisplayName(agentType);
|
||||
function createSession(agentType: string, label: string, existingCredentials?: Credential[]): Session {
|
||||
return {
|
||||
id: makeId(),
|
||||
agentType,
|
||||
label: index === 1 ? agentLabel : `${agentLabel} #${index}`,
|
||||
messages: index === 1 ? (seedMessages[agentType] || []) : [],
|
||||
graphNodes: graph.nodes.map(n => ({ ...n })),
|
||||
label,
|
||||
messages: [],
|
||||
graphNodes: [],
|
||||
credentials: existingCredentials ? cloneCredentials(existingCredentials) : createFreshCredentials(agentType),
|
||||
};
|
||||
}
|
||||
@@ -148,12 +46,13 @@ interface NewTabPopoverProps {
|
||||
onClose: () => void;
|
||||
anchorRef: React.RefObject<HTMLButtonElement | null>;
|
||||
activeWorker: string;
|
||||
discoverAgents: DiscoverEntry[];
|
||||
onNewInstance: () => void;
|
||||
onFromScratch: () => void;
|
||||
onCloneAgent: (agentType: string) => void;
|
||||
onCloneAgent: (agentPath: string, agentName: string) => void;
|
||||
}
|
||||
|
||||
function NewTabPopover({ open, onClose, anchorRef, onNewInstance: _onNewInstance, onFromScratch, onCloneAgent }: NewTabPopoverProps) {
|
||||
function NewTabPopover({ open, onClose, anchorRef, discoverAgents, onNewInstance: _onNewInstance, onFromScratch, onCloneAgent }: NewTabPopoverProps) {
|
||||
void _onNewInstance;
|
||||
const [step, setStep] = useState<PopoverStep>("root");
|
||||
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
|
||||
@@ -192,8 +91,6 @@ function NewTabPopover({ open, onClose, anchorRef, onNewInstance: _onNewInstance
|
||||
|
||||
if (!open || !pos) return null;
|
||||
|
||||
const cloneableAgents = workerList.filter(w => w.id !== "new-agent");
|
||||
|
||||
const optionClass =
|
||||
"flex items-center gap-3 w-full px-3 py-2.5 rounded-lg text-sm text-left transition-colors hover:bg-muted/60 text-foreground";
|
||||
const iconWrap =
|
||||
@@ -215,7 +112,7 @@ function NewTabPopover({ open, onClose, anchorRef, onNewInstance: _onNewInstance
|
||||
</button>
|
||||
)}
|
||||
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{step === "root" ? "Add Tab" : step === "new-agent-choice" ? "New Agent" : "Clone Existing"}
|
||||
{step === "root" ? "Add Tab" : step === "new-agent-choice" ? "New Agent" : "Open Agent"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -260,18 +157,21 @@ function NewTabPopover({ open, onClose, anchorRef, onNewInstance: _onNewInstance
|
||||
|
||||
{step === "clone-pick" && (
|
||||
<div className="flex flex-col">
|
||||
{cloneableAgents.map(agent => (
|
||||
{discoverAgents.map(agent => (
|
||||
<button
|
||||
key={agent.id}
|
||||
onClick={() => { onCloneAgent(agent.id); onClose(); }}
|
||||
key={agent.path}
|
||||
onClick={() => { onCloneAgent(agent.path, agent.name); onClose(); }}
|
||||
className="flex items-center gap-2.5 w-full px-3 py-2 rounded-lg text-left transition-colors hover:bg-muted/60 text-foreground"
|
||||
>
|
||||
<div className="w-6 h-6 rounded-md bg-muted/80 flex items-center justify-center flex-shrink-0">
|
||||
<Bot className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">{agent.label}</span>
|
||||
<span className="text-sm font-medium">{agent.name}</span>
|
||||
</button>
|
||||
))}
|
||||
{discoverAgents.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground px-3 py-2">No agents found</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -280,40 +180,28 @@ function NewTabPopover({ open, onClose, anchorRef, onNewInstance: _onNewInstance
|
||||
);
|
||||
}
|
||||
|
||||
// Map discover paths to existing mock IDs
|
||||
const PATH_TO_MOCK_ID: Record<string, string> = {
|
||||
"examples/templates/email_inbox_management": "inbox-management",
|
||||
"examples/templates/job_hunter": "job-hunter",
|
||||
"examples/templates/vulnerability_assessment": "vuln-assessment",
|
||||
"examples/templates/fitness_coach": "fitness-coach",
|
||||
};
|
||||
|
||||
// Reverse mapping: mock ID → real agent path on disk (for credential detection)
|
||||
const MOCK_ID_TO_PATH: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(PATH_TO_MOCK_ID).map(([path, id]) => [id, path]),
|
||||
);
|
||||
|
||||
function resolveMockId(agentParam: string): string {
|
||||
if (workerGraphs[agentParam]) return agentParam;
|
||||
return PATH_TO_MOCK_ID[agentParam] || agentParam;
|
||||
}
|
||||
|
||||
export default function Workspace() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const rawAgent = searchParams.get("agent") || "inbox-management";
|
||||
const initialAgent = resolveMockId(rawAgent);
|
||||
const rawAgent = searchParams.get("agent") || "new-agent";
|
||||
const initialAgent = rawAgent;
|
||||
const initialPrompt = searchParams.get("prompt") || "";
|
||||
|
||||
// Sessions grouped by agent type
|
||||
// Sessions grouped by agent type — only create one for the initial agent
|
||||
const [sessionsByAgent, setSessionsByAgent] = useState<Record<string, Session[]>>(() => {
|
||||
const initial: Record<string, Session[]> = {};
|
||||
workerList.forEach(w => {
|
||||
const session = createSession(w.id, 1);
|
||||
// If this is the new-agent and there's an initial prompt, append user message + queen reply
|
||||
if (w.id === "new-agent" && w.id === initialAgent && initialPrompt) {
|
||||
session.messages = [
|
||||
...session.messages,
|
||||
|
||||
if (initialAgent === "new-agent") {
|
||||
const session = createSession("new-agent", "New Agent");
|
||||
session.messages = [
|
||||
{
|
||||
id: "na-1", agent: "Queen Bee", agentColor: "",
|
||||
content: "Welcome! I'm the Queen Bee \u2014 I'll help you set up your new agent.\n\nWould you like to:\n\n**1. Build from scratch** \u2014 Define a custom pipeline and workers tailored to your needs.\n\n**2. Start from an existing agent** \u2014 Clone one of your current agents and modify it.\n\nJust let me know which option you'd prefer, or describe what you'd like your agent to do and I'll suggest a setup.",
|
||||
timestamp: "", role: "queen", thread: "new-agent",
|
||||
},
|
||||
];
|
||||
if (initialPrompt) {
|
||||
session.messages.push(
|
||||
{
|
||||
id: makeId(), agent: "You", agentColor: "",
|
||||
content: initialPrompt, timestamp: "", type: "user" as const, thread: "new-agent",
|
||||
@@ -323,22 +211,12 @@ export default function Workspace() {
|
||||
content: `Great idea! Let me think about how to set up an agent for that.\n\nI'll design a pipeline to handle: **"${initialPrompt}"**\n\nGive me a moment to put together the right workers and steps for you.`,
|
||||
timestamp: "", role: "queen" as const, thread: "new-agent",
|
||||
},
|
||||
];
|
||||
);
|
||||
}
|
||||
initial[w.id] = [session];
|
||||
});
|
||||
|
||||
// If the initial agent is not in workerList, create an empty session.
|
||||
// The intro_message will be injected once the backend responds.
|
||||
if (!initial[initialAgent]) {
|
||||
initial[initialAgent] = [{
|
||||
id: makeId(),
|
||||
agentType: initialAgent,
|
||||
label: formatAgentDisplayName(initialAgent),
|
||||
messages: [],
|
||||
graphNodes: [],
|
||||
credentials: [],
|
||||
}];
|
||||
initial["new-agent"] = [session];
|
||||
} else {
|
||||
// Real agent: start empty, backend will populate via intro_message + session history
|
||||
initial[initialAgent] = [createSession(initialAgent, formatAgentDisplayName(initialAgent))];
|
||||
}
|
||||
|
||||
return initial;
|
||||
@@ -346,15 +224,8 @@ export default function Workspace() {
|
||||
|
||||
// Active session ID per agent type
|
||||
const [activeSessionByAgent, setActiveSessionByAgent] = useState<Record<string, string>>(() => {
|
||||
const initial: Record<string, string> = {};
|
||||
workerList.forEach(w => {
|
||||
initial[w.id] = sessionsByAgent[w.id][0].id;
|
||||
});
|
||||
// Also set active session for dynamic agent not in workerList
|
||||
if (sessionsByAgent[initialAgent] && !initial[initialAgent]) {
|
||||
initial[initialAgent] = sessionsByAgent[initialAgent][0].id;
|
||||
}
|
||||
return initial;
|
||||
const sessions = sessionsByAgent[initialAgent];
|
||||
return sessions ? { [initialAgent]: sessions[0].id } : {};
|
||||
});
|
||||
|
||||
const [activeWorker, setActiveWorker] = useState(initialAgent);
|
||||
@@ -370,6 +241,11 @@ export default function Workspace() {
|
||||
// one forever.
|
||||
const streamTurnRef = useRef(0);
|
||||
|
||||
// Ref mirror of sessionsByAgent so SSE callback can read current graph
|
||||
// state without adding sessionsByAgent to its dependency array.
|
||||
const sessionsRef = useRef(sessionsByAgent);
|
||||
sessionsRef.current = sessionsByAgent;
|
||||
|
||||
// --- Backend state ---
|
||||
const [backendAgentId, setBackendAgentId] = useState<string | null>(null);
|
||||
const [backendLoading, setBackendLoading] = useState(true);
|
||||
@@ -381,9 +257,7 @@ export default function Workspace() {
|
||||
|
||||
// Version state per agent type: [major, minor]
|
||||
const [agentVersions, setAgentVersions] = useState<Record<string, [number, number]>>(() => {
|
||||
const init: Record<string, [number, number]> = {};
|
||||
workerList.forEach(w => { init[w.id] = [1, 0]; });
|
||||
return init;
|
||||
return { [initialAgent]: [1, 0] };
|
||||
});
|
||||
|
||||
const handleVersionBump = useCallback((type: "major" | "minor") => {
|
||||
@@ -396,8 +270,23 @@ export default function Workspace() {
|
||||
});
|
||||
}, [activeWorker]);
|
||||
|
||||
// --- Fetch discovered agents for NewTabPopover ---
|
||||
const [discoverAgents, setDiscoverAgents] = useState<DiscoverEntry[]>([]);
|
||||
useEffect(() => {
|
||||
agentsApi.discover().then(result => {
|
||||
const all = Object.values(result).flat();
|
||||
setDiscoverAgents(all);
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// --- Agent loading on mount (Phase 4) ---
|
||||
useEffect(() => {
|
||||
// "new-agent" is a client-side builder concept — no backend to load
|
||||
if (rawAgent === "new-agent") {
|
||||
setBackendLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function loadAgent() {
|
||||
@@ -450,10 +339,7 @@ export default function Workspace() {
|
||||
setBackendAgentId(agent.id);
|
||||
|
||||
// Resolve a human-readable display name for this agent.
|
||||
// Prefer workerList label, then format the backend name / agent id.
|
||||
const displayName =
|
||||
workerList.find((w) => w.id === initialAgent)?.label ||
|
||||
formatAgentDisplayName(agent.name || initialAgent);
|
||||
const displayName = formatAgentDisplayName(agent.name || initialAgent);
|
||||
setAgentDisplayName(displayName);
|
||||
|
||||
// Update the session label to use the display name
|
||||
@@ -571,6 +457,52 @@ export default function Workspace() {
|
||||
return () => { cancelled = true; };
|
||||
}, [backendAgentId, backendReady, initialAgent]);
|
||||
|
||||
// --- Graph node status helpers (live updates) ---
|
||||
const updateGraphNodeStatus = useCallback(
|
||||
(nodeId: string, status: NodeStatus, extra?: Partial<GraphNode>) => {
|
||||
setSessionsByAgent((prev) => {
|
||||
const sessions = prev[activeWorker] || [];
|
||||
return {
|
||||
...prev,
|
||||
[activeWorker]: sessions.map((s) => {
|
||||
const activeId = activeSessionByAgent[activeWorker] || sessions[0]?.id;
|
||||
if (s.id !== activeId) return s;
|
||||
return {
|
||||
...s,
|
||||
graphNodes: s.graphNodes.map((n) =>
|
||||
n.id === nodeId ? { ...n, status, ...extra } : n
|
||||
),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
[activeWorker, activeSessionByAgent],
|
||||
);
|
||||
|
||||
const markAllNodesAs = useCallback(
|
||||
(fromStatus: NodeStatus | NodeStatus[], toStatus: NodeStatus) => {
|
||||
const fromArr = Array.isArray(fromStatus) ? fromStatus : [fromStatus];
|
||||
setSessionsByAgent((prev) => {
|
||||
const sessions = prev[activeWorker] || [];
|
||||
return {
|
||||
...prev,
|
||||
[activeWorker]: sessions.map((s) => {
|
||||
const activeId = activeSessionByAgent[activeWorker] || sessions[0]?.id;
|
||||
if (s.id !== activeId) return s;
|
||||
return {
|
||||
...s,
|
||||
graphNodes: s.graphNodes.map((n) =>
|
||||
fromArr.includes(n.status) ? { ...n, status: toStatus } : n
|
||||
),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
[activeWorker, activeSessionByAgent],
|
||||
);
|
||||
|
||||
// --- SSE event handler (Phase 5) ---
|
||||
const handleSSEEvent = useCallback(
|
||||
(event: AgentEvent) => {
|
||||
@@ -579,11 +511,13 @@ export default function Workspace() {
|
||||
streamTurnRef.current += 1;
|
||||
setIsTyping(true);
|
||||
setAwaitingInput(false);
|
||||
markAllNodesAs(["running", "looping", "complete", "error"], "pending");
|
||||
break;
|
||||
|
||||
case "execution_completed":
|
||||
setIsTyping(false);
|
||||
setAwaitingInput(false);
|
||||
markAllNodesAs(["running", "looping"], "complete");
|
||||
break;
|
||||
|
||||
case "execution_failed":
|
||||
@@ -621,6 +555,8 @@ export default function Workspace() {
|
||||
if (event.type === "execution_failed") {
|
||||
setIsTyping(false);
|
||||
setAwaitingInput(false);
|
||||
if (event.node_id) updateGraphNodeStatus(event.node_id, "error");
|
||||
markAllNodesAs(["running", "looping"], "pending");
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -628,17 +564,46 @@ export default function Workspace() {
|
||||
case "node_loop_started":
|
||||
streamTurnRef.current += 1;
|
||||
setIsTyping(true);
|
||||
if (event.node_id) {
|
||||
const sessions = sessionsRef.current[activeWorker] || [];
|
||||
const activeId = activeSessionByAgent[activeWorker] || sessions[0]?.id;
|
||||
const session = sessions.find((s) => s.id === activeId);
|
||||
const existing = session?.graphNodes.find((n) => n.id === event.node_id);
|
||||
const isRevisit = existing?.status === "complete";
|
||||
updateGraphNodeStatus(event.node_id, isRevisit ? "looping" : "running", {
|
||||
maxIterations: (event.data?.max_iterations as number) ?? undefined,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "node_loop_iteration":
|
||||
streamTurnRef.current += 1;
|
||||
if (event.node_id) {
|
||||
updateGraphNodeStatus(event.node_id, "looping", {
|
||||
iterations: (event.data?.iteration as number) ?? undefined,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "node_loop_completed":
|
||||
if (event.node_id) {
|
||||
updateGraphNodeStatus(event.node_id, "complete");
|
||||
}
|
||||
break;
|
||||
|
||||
case "edge_traversed": {
|
||||
const sourceNode = event.data?.source_node as string | undefined;
|
||||
const targetNode = event.data?.target_node as string | undefined;
|
||||
if (sourceNode) updateGraphNodeStatus(sourceNode, "complete");
|
||||
if (targetNode) updateGraphNodeStatus(targetNode, "running");
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[activeWorker, activeSessionByAgent, agentDisplayName],
|
||||
[activeWorker, activeSessionByAgent, agentDisplayName, updateGraphNodeStatus, markAllNodesAs],
|
||||
);
|
||||
|
||||
// SSE subscription
|
||||
@@ -653,8 +618,8 @@ export default function Workspace() {
|
||||
const activeSession = currentSessions.find(s => s.id === activeSessionId) || currentSessions[0];
|
||||
|
||||
const currentGraph = activeSession
|
||||
? { nodes: activeSession.graphNodes, title: workerGraphs[activeWorker]?.title || "" }
|
||||
: workerGraphs[activeWorker] || workerGraphs["inbox-management"];
|
||||
? { nodes: activeSession.graphNodes, title: agentDisplayName || formatAgentDisplayName(activeWorker) }
|
||||
: { nodes: [] as GraphNode[], title: "" };
|
||||
|
||||
// --- handleSend: real backend call or mock fallback (Phase 6) ---
|
||||
const handleSend = useCallback((text: string, thread: string) => {
|
||||
@@ -711,12 +676,13 @@ export default function Workspace() {
|
||||
setIsTyping(false);
|
||||
});
|
||||
// Response content will arrive via SSE events
|
||||
} else {
|
||||
// Mock fallback when backend isn't available
|
||||
} else if (activeWorker === "new-agent") {
|
||||
// Builder flow — no backend, placeholder response
|
||||
setTimeout(() => {
|
||||
const reply: ChatMessage = {
|
||||
id: makeId(), agent: "Queen Bee", agentColor: "",
|
||||
content: "Acknowledged. Dispatching worker swarm...", timestamp: "", role: "queen" as const, thread,
|
||||
content: "Got it! Let me design a pipeline for that. (Builder mode — backend integration coming soon.)",
|
||||
timestamp: "", role: "queen" as const, thread,
|
||||
};
|
||||
setSessionsByAgent(prev => ({
|
||||
...prev,
|
||||
@@ -726,6 +692,20 @@ export default function Workspace() {
|
||||
}));
|
||||
setIsTyping(false);
|
||||
}, 800);
|
||||
} else {
|
||||
// Backend not connected — show error
|
||||
const errorMsg: ChatMessage = {
|
||||
id: makeId(), agent: "System", agentColor: "",
|
||||
content: "Cannot send message: backend is not connected. Please wait for the agent to load.",
|
||||
timestamp: "", type: "system", thread,
|
||||
};
|
||||
setSessionsByAgent(prev => ({
|
||||
...prev,
|
||||
[activeWorker]: prev[activeWorker].map(s =>
|
||||
s.id === activeSession.id ? { ...s, messages: [...s.messages, errorMsg] } : s
|
||||
),
|
||||
}));
|
||||
setIsTyping(false);
|
||||
}
|
||||
}, [activeWorker, activeSession, backendAgentId, backendReady]);
|
||||
|
||||
@@ -734,7 +714,8 @@ export default function Workspace() {
|
||||
const newIndex = sessions.length + 1;
|
||||
// Auto-fill credentials from the first existing session
|
||||
const existingCreds = sessions.length > 0 ? sessions[0].credentials : undefined;
|
||||
const newSession = createSession(activeWorker, newIndex, existingCreds);
|
||||
const label = `${agentDisplayName || formatAgentDisplayName(activeWorker)} #${newIndex}`;
|
||||
const newSession = createSession(activeWorker, label, existingCreds);
|
||||
// If credentials are missing, add a queen message prompting configuration
|
||||
if (!existingCreds || !allRequiredCredentialsMet(existingCreds)) {
|
||||
const promptMsg: ChatMessage = {
|
||||
@@ -753,7 +734,7 @@ export default function Workspace() {
|
||||
[activeWorker]: [...(prev[activeWorker] || []), newSession],
|
||||
}));
|
||||
setActiveSessionByAgent(prev => ({ ...prev, [activeWorker]: newSession.id }));
|
||||
}, [activeWorker, sessionsByAgent]);
|
||||
}, [activeWorker, sessionsByAgent, agentDisplayName]);
|
||||
|
||||
const closeSession = useCallback((sessionId: string) => {
|
||||
const sessions = sessionsByAgent[activeWorker] || [];
|
||||
@@ -766,36 +747,25 @@ export default function Workspace() {
|
||||
}, [activeWorker, sessionsByAgent, activeSessionId]);
|
||||
|
||||
// Create a new session for any agent type (used by NewTabPopover)
|
||||
const addAgentSession = useCallback((agentType: string, cloned = false) => {
|
||||
const addAgentSession = useCallback((agentType: string, agentLabel?: string, cloned = false) => {
|
||||
const sessions = sessionsByAgent[agentType] || [];
|
||||
const newIndex = sessions.length + 1;
|
||||
const existingCreds = sessions.length > 0 ? sessions[0].credentials : undefined;
|
||||
const newSession = createSession(agentType, newIndex, existingCreds);
|
||||
|
||||
// For cloned sessions: reset animated states so the graph is static
|
||||
if (cloned) {
|
||||
newSession.graphNodes = newSession.graphNodes.map(n => ({
|
||||
...n,
|
||||
status: (n.status === "running" || n.status === "looping") ? "complete" : n.status,
|
||||
statusLabel: undefined,
|
||||
iterations: n.status === "running" || n.status === "looping" ? n.iterations : n.iterations,
|
||||
}));
|
||||
}
|
||||
|
||||
// Build intro message
|
||||
const agentLabel = workerList.find(w => w.id === agentType)?.label || agentType;
|
||||
const displayLabel = agentLabel || formatAgentDisplayName(agentType);
|
||||
const label = newIndex === 1 ? displayLabel : `${displayLabel} #${newIndex}`;
|
||||
const newSession = createSession(agentType, label, existingCreds);
|
||||
|
||||
if (cloned) {
|
||||
newSession.messages = [{
|
||||
id: makeId(), agent: "Queen Bee", agentColor: "",
|
||||
content: `Welcome to a new **${agentLabel}** session. \ud83d\udc1d\n\nThis instance is cloned from the existing agent \u2014 the pipeline is ready to go. Configure any credentials if needed, then kick off a run whenever you're ready.`,
|
||||
content: `Welcome to a new **${displayLabel}** session.\n\nConfigure any credentials if needed, then kick off a run whenever you're ready.`,
|
||||
timestamp: "", role: "queen" as const, thread: agentType,
|
||||
}];
|
||||
} else if (agentType === "new-agent") {
|
||||
// "From scratch" flow -- always show the builder prompt
|
||||
newSession.messages = [{
|
||||
id: makeId(), agent: "Queen Bee", agentColor: "",
|
||||
content: "Hey there! \ud83d\udc1d I'm the Queen Bee \u2014 let's build your new agent together.\n\n**What would you like your agent to do?** Here are a few ideas to get you started:\n\n- \ud83d\udce7 **Email manager** \u2014 triage inboxes, draft replies, auto-archive\n- \ud83d\udcbc **Job hunter** \u2014 scan job boards, match roles, auto-apply\n- \ud83d\udd12 **Security auditor** \u2014 run recon, score risks, generate reports\n- \ud83d\udcdd **Content writer** \u2014 research, outline, and draft long-form content\n- \ud83d\udcca **Data analyst** \u2014 pull metrics, detect anomalies, summarize trends\n- \ud83d\uded2 **E-commerce monitor** \u2014 track prices, restock alerts, competitor analysis\n\nJust describe what you want to automate and I'll design the pipeline for you.",
|
||||
content: "Hey there! I'm the Queen Bee \u2014 let's build your new agent together.\n\n**What would you like your agent to do?** Here are a few ideas to get you started:\n\n- **Email manager** \u2014 triage inboxes, draft replies, auto-archive\n- **Job hunter** \u2014 scan job boards, match roles, auto-apply\n- **Security auditor** \u2014 run recon, score risks, generate reports\n- **Content writer** \u2014 research, outline, and draft long-form content\n- **Data analyst** \u2014 pull metrics, detect anomalies, summarize trends\n- **E-commerce monitor** \u2014 track prices, restock alerts, competitor analysis\n\nJust describe what you want to automate and I'll design the pipeline for you.",
|
||||
timestamp: "", role: "queen" as const, thread: "new-agent",
|
||||
}];
|
||||
}
|
||||
@@ -806,11 +776,12 @@ export default function Workspace() {
|
||||
}));
|
||||
setActiveSessionByAgent(prev => ({ ...prev, [agentType]: newSession.id }));
|
||||
setActiveWorker(agentType);
|
||||
|
||||
// Initialize version tracking if not present
|
||||
setAgentVersions(prev => prev[agentType] ? prev : { ...prev, [agentType]: [1, 0] });
|
||||
}, [sessionsByAgent]);
|
||||
|
||||
const activeWorkerLabel = agentDisplayName
|
||||
|| workerList.find(w => w.id === activeWorker)?.label
|
||||
|| formatAgentDisplayName(activeWorker);
|
||||
const activeWorkerLabel = agentDisplayName || formatAgentDisplayName(activeWorker);
|
||||
|
||||
|
||||
return (
|
||||
@@ -883,9 +854,10 @@ export default function Workspace() {
|
||||
onClose={() => setNewTabOpen(false)}
|
||||
anchorRef={newTabBtnRef}
|
||||
activeWorker={activeWorker}
|
||||
discoverAgents={discoverAgents}
|
||||
onNewInstance={() => { addSession(); }}
|
||||
onFromScratch={() => { addAgentSession("new-agent"); }}
|
||||
onCloneAgent={(agentType) => { addAgentSession(agentType, true); }}
|
||||
onCloneAgent={(agentPath, agentName) => { addAgentSession(agentPath, agentName, true); }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -947,7 +919,7 @@ export default function Workspace() {
|
||||
<CredentialsModal
|
||||
agentType={activeWorker}
|
||||
agentLabel={activeWorkerLabel}
|
||||
agentPath={MOCK_ID_TO_PATH[activeWorker] || rawAgent}
|
||||
agentPath={activeWorker !== "new-agent" ? activeWorker : undefined}
|
||||
open={credentialsOpen}
|
||||
onClose={() => setCredentialsOpen(false)}
|
||||
credentials={activeSession?.credentials || []}
|
||||
|
||||
Reference in New Issue
Block a user