feat(ms22-p2): add agent selector UI in WebUI (#685)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #685.
This commit is contained in:
128
apps/web/src/components/chat/AgentSelector.tsx
Normal file
128
apps/web/src/components/chat/AgentSelector.tsx
Normal file
@@ -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 (
|
||||
<svg
|
||||
className={`w-3 h-3 ${className ?? ""}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M12 2v4M12 22v-4" />
|
||||
<path d="M2 12h4M22 12h-4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function BuilderIcon({ className }: { className?: string }): React.ReactElement {
|
||||
return (
|
||||
<svg
|
||||
className={`w-3 h-3 ${className ?? ""}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MedicIcon({ className }: { className?: string }): React.ReactElement {
|
||||
return (
|
||||
<svg
|
||||
className={`w-3 h-3 ${className ?? ""}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const AGENT_ICONS: Record<string, React.FC<{ className?: string }>> = {
|
||||
jarvis: JarvisIcon,
|
||||
builder: BuilderIcon,
|
||||
medic: MedicIcon,
|
||||
};
|
||||
|
||||
export function AgentSelector({
|
||||
selectedAgent,
|
||||
onChange,
|
||||
disabled,
|
||||
}: AgentSelectorProps): React.ReactElement {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium" style={{ color: "rgb(var(--text-muted))" }}>
|
||||
Agent
|
||||
</span>
|
||||
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{Object.entries(AGENT_CONFIG).map(([name, config]) => {
|
||||
const Icon = AGENT_ICONS[name];
|
||||
const isSelected = selectedAgent === name;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
onClick={() => onChange?.(isSelected ? null : name)}
|
||||
disabled={disabled}
|
||||
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-lg border transition-all text-xs ${
|
||||
isSelected ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50"
|
||||
} ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
|
||||
style={{
|
||||
borderColor: isSelected
|
||||
? "rgb(var(--accent-primary))"
|
||||
: "rgb(var(--border-default))",
|
||||
color: isSelected ? "rgb(var(--accent-primary))" : "rgb(var(--text-primary))",
|
||||
}}
|
||||
title={`${config.displayName} — ${config.role}`}
|
||||
>
|
||||
<span
|
||||
className="rounded-full"
|
||||
style={{
|
||||
backgroundColor: config.color,
|
||||
width: "8px",
|
||||
height: "8px",
|
||||
}}
|
||||
/>
|
||||
{Icon && <Icon />}
|
||||
<span className="font-medium">{config.displayName}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<ChatRef, ChatProps>(function Chat(
|
||||
const [selectedModel, setSelectedModel] = useState<ModelId>("llama3.2");
|
||||
const [temperature, setTemperature] = useState<number>(DEFAULT_TEMPERATURE);
|
||||
const [maxTokens, setMaxTokens] = useState<number>(DEFAULT_MAX_TOKENS);
|
||||
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);
|
||||
|
||||
// Suggestion fill value: controls ChatInput's textarea content
|
||||
const [suggestionValue, setSuggestionValue] = useState<string | undefined>(undefined);
|
||||
@@ -88,6 +90,7 @@ export const Chat = forwardRef<ChatRef, ChatProps>(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<ChatRef, ChatProps>(function Chat(
|
||||
}}
|
||||
>
|
||||
<div className="mx-auto max-w-4xl px-4 py-4 lg:px-8">
|
||||
<div className="mb-3">
|
||||
<AgentSelector
|
||||
selectedAgent={selectedAgent}
|
||||
onChange={setSelectedAgent}
|
||||
disabled={isChatLoading || isStreaming || !user}
|
||||
/>
|
||||
</div>
|
||||
<ChatInput
|
||||
onSend={handleSendMessage}
|
||||
disabled={isChatLoading || !user}
|
||||
|
||||
Reference in New Issue
Block a user