From 1de3e5da2b578b09b1f68cf28d6379fdb79e86f0 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Wed, 4 Mar 2026 21:23:32 -0600 Subject: [PATCH] feat(ms22-p2): add agent selector UI in WebUI - Add AgentSelector component with Jarvis/Builder/Medic options - Add agents.ts API client for agent CRUD operations - Update chat.ts to pass agent parameter in stream requests - Update useChat hook to accept and pass agent parameter - Integrate AgentSelector into Chat.tsx with state management Task: MS22-P2-008 Co-Authored-By: Claude Opus 4.6 --- .../web/src/components/chat/AgentSelector.tsx | 128 ++++++++++++++++++ apps/web/src/components/chat/Chat.tsx | 10 ++ apps/web/src/hooks/useChat.ts | 7 + apps/web/src/lib/api/agents.ts | 125 +++++++++++++++++ apps/web/src/lib/api/chat.ts | 13 +- 5 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/chat/AgentSelector.tsx create mode 100644 apps/web/src/lib/api/agents.ts diff --git a/apps/web/src/components/chat/AgentSelector.tsx b/apps/web/src/components/chat/AgentSelector.tsx new file mode 100644 index 0000000..51216d9 --- /dev/null +++ b/apps/web/src/components/chat/AgentSelector.tsx @@ -0,0 +1,128 @@ +"use client"; + +import React from "react"; + +interface AgentSelectorProps { + selectedAgent?: string | null; + onChange?: (agent: string | null) => void; + disabled?: boolean; +} + +const AGENT_CONFIG = { + jarvis: { + displayName: "Jarvis", + role: "Orchestrator", + color: "#3498db", + }, + builder: { + displayName: "Builder", + role: "Coding Agent", + color: "#3b82f6", + }, + medic: { + displayName: "Medic", + role: "Health Monitor", + color: "#10b981", + }, +} as const; + +function JarvisIcon({ className }: { className?: string }): React.ReactElement { + return ( + + + + + + ); +} + +function BuilderIcon({ className }: { className?: string }): React.ReactElement { + return ( + + + + ); +} + +function MedicIcon({ className }: { className?: string }): React.ReactElement { + return ( + + + + ); +} + +const AGENT_ICONS: Record> = { + jarvis: JarvisIcon, + builder: BuilderIcon, + medic: MedicIcon, +}; + +export function AgentSelector({ + selectedAgent, + onChange, + disabled, +}: AgentSelectorProps): React.ReactElement { + return ( +
+ + Agent + + +
+ {Object.entries(AGENT_CONFIG).map(([name, config]) => { + const Icon = AGENT_ICONS[name]; + const isSelected = selectedAgent === name; + + return ( + + ); + })} +
+
+ ); +} diff --git a/apps/web/src/components/chat/Chat.tsx b/apps/web/src/components/chat/Chat.tsx index 4168c42..b760b32 100644 --- a/apps/web/src/components/chat/Chat.tsx +++ b/apps/web/src/components/chat/Chat.tsx @@ -9,6 +9,7 @@ import { useWorkspaceId } from "@/lib/hooks"; import { MessageList } from "./MessageList"; import { ChatInput, type ModelId, DEFAULT_TEMPERATURE, DEFAULT_MAX_TOKENS } from "./ChatInput"; import { ChatEmptyState } from "./ChatEmptyState"; +import { AgentSelector } from "./AgentSelector"; import type { Message } from "@/hooks/useChat"; export interface ChatRef { @@ -66,6 +67,7 @@ export const Chat = forwardRef(function Chat( const [selectedModel, setSelectedModel] = useState("llama3.2"); const [temperature, setTemperature] = useState(DEFAULT_TEMPERATURE); const [maxTokens, setMaxTokens] = useState(DEFAULT_MAX_TOKENS); + const [selectedAgent, setSelectedAgent] = useState(null); // Suggestion fill value: controls ChatInput's textarea content const [suggestionValue, setSuggestionValue] = useState(undefined); @@ -88,6 +90,7 @@ export const Chat = forwardRef(function Chat( temperature, maxTokens, ...(initialProjectId !== undefined && { projectId: initialProjectId }), + ...(selectedAgent !== null && { agent: selectedAgent }), }); // Read workspace ID from localStorage (set by auth-context after session check). @@ -375,6 +378,13 @@ export const Chat = forwardRef(function Chat( }} >
+
+ +
void; } @@ -63,6 +64,7 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn { maxTokens, systemPrompt, projectId, + agent, onError, } = options; @@ -77,6 +79,10 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn { const projectIdRef = useRef(projectId ?? null); projectIdRef.current = projectId ?? null; + // Track agent in ref to prevent stale closures + const agentRef = useRef(agent); + agentRef.current = agent; + // Track messages in ref to prevent stale closures during rapid sends const messagesRef = useRef(messages); messagesRef.current = messages; @@ -209,6 +215,7 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn { ...(temperature !== undefined && { temperature }), ...(maxTokens !== undefined && { maxTokens }), ...(systemPrompt !== undefined && { systemPrompt }), + ...(agentRef.current && { agent: agentRef.current }), }; const controller = new AbortController(); diff --git a/apps/web/src/lib/api/agents.ts b/apps/web/src/lib/api/agents.ts new file mode 100644 index 0000000..04516ac --- /dev/null +++ b/apps/web/src/lib/api/agents.ts @@ -0,0 +1,125 @@ +/** + * Agent API client + * Handles agent-related API interactions + */ + +import { apiGet, apiPost, apiPatch, apiDelete } from "./client"; + +export interface AgentStatus { + id: string; + name: string; + displayName: string; + role: string; + isActive: boolean; + containerStatus?: "running" | "stopped" | "unknown"; +} + +export interface UserAgent { + id: string; + userId: string; + templateId: string | null; + name: string; + displayName: string; + role: string; + personality: string; + primaryModel: string | null; + fallbackModels: string[]; + toolPermissions: string[]; + discordChannel: string | null; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + +export interface CreateUserAgentRequest { + templateId?: string; + name: string; + displayName: string; + role: string; + personality: string; + primaryModel?: string; + fallbackModels?: string[]; + toolPermissions?: string[]; + discordChannel?: string; + isActive?: boolean; +} + +export interface UpdateUserAgentRequest { + name?: string; + displayName?: string; + role?: string; + personality?: string; + primaryModel?: string; + fallbackModels?: string[]; + toolPermissions?: string[]; + discordChannel?: string; + isActive?: boolean; +} + +export interface UpdateUserAgentRequest { + name?: string; + displayName?: string; + role?: string; + personality?: string; + primaryModel?: string; + fallbackModels?: string[]; + toolPermissions?: string[]; + discordChannel?: string; + isActive?: boolean; +} + +/** + * Get all user's agents + */ +export async function getAgents(): Promise { + return apiGet("/api/agents"); +} + +/** + * Get all agent statuses + */ +export async function getAgentStatuses(): Promise { + return apiGet("/api/agents/status"); +} + +/** + * Get a single agent by ID + */ +export async function getAgent(id: string): Promise { + return apiGet(`/api/agents/${id}`); +} + +/** + * Get a single agent's status + */ +export async function getAgentStatus(id: string): Promise { + return apiGet(`/api/agents/${id}/status`); +} + +/** + * Create a new custom agent + */ +export async function createAgent(data: CreateUserAgentRequest): Promise { + return apiPost("/api/agents", data); +} + +/** + * Create an agent from a template + */ +export async function createAgentFromTemplate(templateId: string): Promise { + return apiPost(`/api/agents/from-template/${templateId}`, {}); +} + +/** + * Update an agent + */ +export async function updateAgent(id: string, data: UpdateUserAgentRequest): Promise { + return apiPatch(`/api/agents/${id}`, data); +} + +/** + * Delete an agent + */ +export async function deleteAgent(id: string): Promise { + await apiDelete(`/api/agents/${id}`); +} diff --git a/apps/web/src/lib/api/chat.ts b/apps/web/src/lib/api/chat.ts index 18022c8..2c509f5 100644 --- a/apps/web/src/lib/api/chat.ts +++ b/apps/web/src/lib/api/chat.ts @@ -18,6 +18,7 @@ export interface ChatRequest { temperature?: number; maxTokens?: number; systemPrompt?: string; + agent?: string; } export interface ChatResponse { @@ -117,7 +118,11 @@ export function streamGuestChat( "Content-Type": "application/json", }, credentials: "include", - body: JSON.stringify({ messages: request.messages, stream: true }), + body: JSON.stringify({ + messages: request.messages, + stream: true, + ...(request.agent && { agent: request.agent }), + }), signal: signal ?? null, }); @@ -269,7 +274,11 @@ export function streamChatMessage( "X-CSRF-Token": csrfToken, }, credentials: "include", - body: JSON.stringify({ messages: request.messages, stream: true }), + body: JSON.stringify({ + messages: request.messages, + stream: true, + ...(request.agent && { agent: request.agent }), + }), signal: signal ?? null, });