Python: Add Function Approval UI to DevUI (#1401)

* ensure function aproval is parsed correctly

* udpate ui, add deployment guide button, other debug panel fixes

* feat(devui): Implement lazy loading architecture with enhanced security and state management

Major architectural improvements to DevUI for better performance, security, and developer experience:

Performance & Architecture:
- Implement lazy loading for entity discovery - entities loaded on-demand instead of at startup
- Add hot reload capability for development workflow via new reload endpoint
- Reduce startup time and memory footprint by deferring module imports

Security Enhancements:
- Remove remote entity loading capabilities (POST /v1/entities/add, DELETE endpoints)
- DevUI now strictly local development tool - no remote code execution
- Add explicit security documentation and best practices in README

Frontend Improvements:
- Migrate to Zustand for centralized state management (replacing prop drilling)
- Add lightweight zero-dependency markdown renderer with code block copy support
- Improve gallery UX with setup instructions modal instead of direct URL loading
- Enhanced message UI with copy functionality and better token usage display

Testing & Quality:
- Expand test coverage for lazy loading, type detection, and cache invalidation
- Add comprehensive tests for new behaviors (+231 lines of test code)
- Improve type safety and documentation throughout

Breaking Changes:
- Remote entity loading via URLs is no longer supported
- Entities must be loaded from local filesystem only

* update ui issues, uupdate test descripion
This commit is contained in:
Victor Dibia
2025-10-15 14:36:29 -07:00
committed by GitHub
Unverified
parent 331c750515
commit b64358df7e
38 changed files with 3726 additions and 1814 deletions
@@ -14,7 +14,6 @@ interface AppHeaderProps {
workflows: WorkflowInfo[];
selectedItem?: AgentInfo | WorkflowInfo;
onSelect: (item: AgentInfo | WorkflowInfo) => void;
onRemove?: (entityId: string) => void;
onBrowseGallery?: () => void;
isLoading?: boolean;
onSettingsClick?: () => void;
@@ -25,7 +24,6 @@ export function AppHeader({
workflows,
selectedItem,
onSelect,
onRemove,
onBrowseGallery,
isLoading = false,
onSettingsClick,
@@ -66,7 +64,6 @@ export function AppHeader({
workflows={workflows}
selectedItem={selectedItem}
onSelect={onSelect}
onRemove={onRemove}
onBrowseGallery={onBrowseGallery}
isLoading={isLoading}
/>
@@ -24,6 +24,40 @@ import {
} from "lucide-react";
import type { ExtendedResponseStreamEvent } from "@/types";
// Simple visual separator component
function MessageSeparator() {
return (
<div className="flex items-center gap-2 py-3 px-2">
<div className="flex-1 border-t border-border/50" />
</div>
);
}
// Helper to add separators between message rounds
function addSeparatorsToEvents(events: ExtendedResponseStreamEvent[]): (ExtendedResponseStreamEvent | { type: "separator"; id: string })[] {
const result: (ExtendedResponseStreamEvent | { type: "separator"; id: string })[] = [];
let lastWasResponseDone = false;
for (let i = 0; i < events.length; i++) {
const event = events[i];
// Add separator before first event after response.done
if (lastWasResponseDone && event.type !== "response.done") {
result.push({ type: "separator", id: `sep-${i}` });
lastWasResponseDone = false;
}
result.push(event);
// Track when we see response.done
if (event.type === "response.done" || event.type === "response.completed") {
lastWasResponseDone = true;
}
}
return result;
}
// Type definitions for event data structures
interface EventDataBase {
call_id?: string;
@@ -64,7 +98,7 @@ interface DebugPanelProps {
onClose?: () => void;
}
// Helper: Extract function result from DevUI custom format
// Helper: Extract function result from DevUI custom event
function getFunctionResultFromEvent(event: ExtendedResponseStreamEvent): {
call_id: string;
output: string;
@@ -101,9 +135,18 @@ function processEventsForDisplay(
let accumulatedText = "";
for (const event of events) {
// Skip trace events - they belong in the Traces tab only
if (
event.type === "response.trace_event.complete" ||
event.type === "response.trace.complete"
) {
continue;
}
// Handle response.output_item.added - NEW! Extract function call metadata
if (event.type === "response.output_item.added") {
const outputEvent = event as import("@/types").ResponseOutputItemAddedEvent;
const outputEvent =
event as import("@/types").ResponseOutputItemAddedEvent;
const item = outputEvent.item;
// If it's a function call item, extract metadata
@@ -388,15 +431,17 @@ function getEventSummary(event: ExtendedResponseStreamEvent): string {
}
return "Function arguments...";
case "response.function_result.complete": {
const resultEvent =
event as import("@/types").ResponseFunctionResultComplete;
const truncated = resultEvent.output.slice(0, 40);
return `Function result: ${truncated}${
truncated.length >= 40 ? "..." : ""
}`;
}
case "response.output_item.added": {
const result = getFunctionResultFromEvent(event);
if (result) {
const truncated = result.output.slice(0, 40);
return `Tool result: ${truncated}${
truncated.length >= 40 ? "..." : ""
}`;
}
// Could also be a function call
// Could be a function call
const addedEvent =
event as import("@/types").ResponseOutputItemAddedEvent;
if (addedEvent.item.type === "function_call") {
@@ -422,7 +467,8 @@ function getEventSummary(event: ExtendedResponseStreamEvent): string {
case "response.completed":
if ("response" in event && event.response && "usage" in event.response) {
const completedEvent = event as import("@/types").ResponseCompletedEvent;
const completedEvent =
event as import("@/types").ResponseCompletedEvent;
const usage = completedEvent.response.usage;
if (usage) {
return `Response complete (${usage.total_tokens} tokens)`;
@@ -453,6 +499,8 @@ function getEventIcon(type: string) {
case "response.function_call.delta":
case "response.function_call_arguments.delta":
return Wrench;
case "response.function_result.complete":
return CheckCircle2;
case "response.output_item.added":
return CheckCircle2;
case "response.workflow_event.complete":
@@ -479,6 +527,8 @@ function getEventColor(type: string) {
case "response.function_call.delta":
case "response.function_call_arguments.delta":
return "text-blue-600 dark:text-blue-400";
case "response.function_result.complete":
return "text-green-600 dark:text-green-400";
case "response.output_item.added":
return "text-green-600 dark:text-green-400";
case "response.workflow_event.complete":
@@ -509,6 +559,7 @@ function EventItem({ event }: EventItemProps) {
(event.type === "response.function_call.complete" &&
"data" in event &&
event.data) ||
event.type === "response.function_result.complete" ||
(event.type === "response.output_item.added" &&
getFunctionResultFromEvent(event) !== null) ||
(event.type === "response.workflow_event.complete" &&
@@ -681,6 +732,53 @@ function EventExpandedContent({
}
break;
case "response.function_result.complete": {
const resultEvent =
event as import("@/types").ResponseFunctionResultComplete;
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-green-500" />
<span className="font-semibold text-sm">Function Result</span>
</div>
<div className="grid grid-cols-1 gap-2 text-xs">
<div>
<span className="font-medium text-muted-foreground">
Call ID:
</span>
<span className="ml-2 font-mono text-xs">
{resultEvent.call_id}
</span>
</div>
<div>
<span className="font-medium text-muted-foreground">
Status:
</span>
<span
className={`ml-2 px-2 py-1 rounded text-xs font-medium ${
resultEvent.status === "completed"
? "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
: "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
}`}
>
{resultEvent.status}
</span>
</div>
<div>
<span className="font-medium text-muted-foreground">
Output:
</span>
<div className="mt-1 max-h-32 overflow-auto">
<pre className="text-xs bg-background border rounded p-2 whitespace-pre-wrap max-w-full break-all">
{resultEvent.output}
</pre>
</div>
</div>
</div>
</div>
);
}
case "response.output_item.added": {
const result = getFunctionResultFromEvent(event);
if (result) {
@@ -915,7 +1013,8 @@ function EventExpandedContent({
case "response.completed":
if ("response" in event && event.response) {
const completedEvent = event as import("@/types").ResponseCompletedEvent;
const completedEvent =
event as import("@/types").ResponseCompletedEvent;
const response = completedEvent.response;
return (
<div className="space-y-2">
@@ -1006,11 +1105,14 @@ function EventsTab({
// Process events to accumulate tool calls and reduce noise
const processedEvents = processEventsForDisplay(events);
// Add separators between message rounds
const eventsWithSeparators = addSeparatorsToEvents(processedEvents);
// Reverse events so latest appears at top
const reversedEvents = [...processedEvents].reverse();
const reversedEvents = [...eventsWithSeparators].reverse();
return (
<div className="h-full">
<div className="h-full flex flex-col">
<div className="flex items-center justify-between p-3 border-b">
<div className="flex items-center gap-2">
<Activity className="h-4 w-4" />
@@ -1030,7 +1132,7 @@ function EventsTab({
)}
</div>
<ScrollArea ref={scrollRef}>
<ScrollArea ref={scrollRef} className="flex-1">
<div className="p-3">
{processedEvents.length === 0 ? (
<div className="text-center text-muted-foreground text-sm py-8">
@@ -1040,9 +1142,12 @@ function EventsTab({
</div>
) : (
<div className="space-y-2">
{reversedEvents.map((event, index) => (
<EventItem key={`${event.type}-${index}`} event={event} />
))}
{reversedEvents.map((event, index) => {
if ('type' in event && event.type === "separator") {
return <MessageSeparator key={(event as { type: "separator"; id: string }).id} />;
}
return <EventItem key={`${event.type}-${index}`} event={event as ExtendedResponseStreamEvent} />;
})}
</div>
)}
</div>
@@ -1059,18 +1164,21 @@ function TracesTab({ events }: { events: ExtendedResponseStreamEvent[] }) {
e.type === "response.trace.complete"
);
// Add separators between message rounds
const tracesWithSeparators = addSeparatorsToEvents(traceEvents);
// Reverse to show latest traces at the top
const reversedTraceEvents = [...traceEvents].reverse();
const reversedTraceEvents = [...tracesWithSeparators].reverse();
return (
<div className="h-full">
<div className="h-full flex flex-col">
<div className="flex items-center gap-2 p-3 border-b">
<Search className="h-4 w-4" />
<span className="font-medium">Traces</span>
<Badge variant="outline">{traceEvents.length}</Badge>
</div>
<ScrollArea className="">
<ScrollArea className="flex-1">
<div className="p-3">
{traceEvents.length === 0 ? (
<div className="text-center text-muted-foreground text-sm py-8">
@@ -1094,9 +1202,12 @@ function TracesTab({ events }: { events: ExtendedResponseStreamEvent[] }) {
</div>
) : (
<div className="space-y-3">
{reversedTraceEvents.map((event, index) => (
<TraceEventItem key={index} event={event} />
))}
{reversedTraceEvents.map((event, index) => {
if ('type' in event && event.type === "separator") {
return <MessageSeparator key={(event as { type: "separator"; id: string }).id} />;
}
return <TraceEventItem key={index} event={event as ExtendedResponseStreamEvent} />;
})}
</div>
)}
</div>
@@ -1165,7 +1276,7 @@ function TraceEventItem({ event }: { event: ExtendedResponseStreamEvent }) {
<ChevronRight className="h-3 w-3" />
)}
</div>
<div className="text-muted-foreground flex-1">
<div className="text-muted-foreground flex-1 break-all">
<span className="font-medium">{operationName}</span>
{entityId && <span className="ml-2 text-xs">({entityId})</span>}
</div>
@@ -1193,7 +1304,7 @@ function TraceEventItem({ event }: { event: ExtendedResponseStreamEvent }) {
<span className="font-medium text-muted-foreground">
Span ID:
</span>
<span className="ml-2 font-mono text-xs">
<span className="ml-2 font-mono text-xs break-all">
{data.span_id}
</span>
</div>
@@ -1203,7 +1314,7 @@ function TraceEventItem({ event }: { event: ExtendedResponseStreamEvent }) {
<span className="font-medium text-muted-foreground">
Trace ID:
</span>
<span className="ml-2 font-mono text-xs">
<span className="ml-2 font-mono text-xs break-all">
{data.trace_id}
</span>
</div>
@@ -1213,7 +1324,7 @@ function TraceEventItem({ event }: { event: ExtendedResponseStreamEvent }) {
<span className="font-medium text-muted-foreground">
Parent Span:
</span>
<span className="ml-2 font-mono text-xs">
<span className="ml-2 font-mono text-xs break-all">
{data.parent_span_id}
</span>
</div>
@@ -1250,7 +1361,7 @@ function TraceEventItem({ event }: { event: ExtendedResponseStreamEvent }) {
<span className="font-medium text-muted-foreground">
Entity:
</span>
<span className="ml-2 font-mono text-xs">
<span className="ml-2 font-mono text-xs break-all">
{data.entity_id}
</span>
</div>
@@ -1338,18 +1449,21 @@ function ToolsTab({ events }: { events: ExtendedResponseStreamEvent[] }) {
toolEvents.push(result);
});
// Add separators between message rounds
const toolsWithSeparators = addSeparatorsToEvents(toolEvents);
// Reverse to show latest tools at the top
const reversedToolEvents = [...toolEvents].reverse();
const reversedToolEvents = [...toolsWithSeparators].reverse();
return (
<div className="h-full">
<div className="h-full flex flex-col">
<div className="flex items-center gap-2 p-3 border-b">
<Wrench className="h-4 w-4" />
<span className="font-medium">Tools</span>
<Badge variant="outline">{toolEvents.length}</Badge>
</div>
<ScrollArea>
<ScrollArea className="flex-1">
<div className="p-3">
{toolEvents.length === 0 ? (
<div className="text-center text-muted-foreground text-sm py-8">
@@ -1358,9 +1472,12 @@ function ToolsTab({ events }: { events: ExtendedResponseStreamEvent[] }) {
</div>
) : (
<div className="space-y-3">
{reversedToolEvents.map((event, index) => (
<ToolEventItem key={index} event={event} />
))}
{reversedToolEvents.map((event, index) => {
if ('type' in event && event.type === "separator") {
return <MessageSeparator key={(event as { type: "separator"; id: string }).id} />;
}
return <ToolEventItem key={index} event={event as ExtendedResponseStreamEvent} />;
})}
</div>
)}
</div>
@@ -1370,21 +1487,21 @@ function ToolsTab({ events }: { events: ExtendedResponseStreamEvent[] }) {
}
function ToolEventItem({ event }: { event: ExtendedResponseStreamEvent }) {
if (!("data" in event)) {
return null;
}
const data = event.data as EventDataBase;
const timestamp = new Date().toLocaleTimeString();
// Check if this is a function call event
// Check if this is a function call or result event
const isFunctionCall = event.type === "response.function_call.complete";
const isFunctionResult = getFunctionResultFromEvent(event) !== null;
const resultData = getFunctionResultFromEvent(event);
const isFunctionResult = resultData !== null;
if (!isFunctionCall && !isFunctionResult) {
return null;
}
// For function calls: extract data field
const callData =
isFunctionCall && "data" in event ? (event.data as EventDataBase) : null;
return (
<div className="border rounded p-3">
<div className="flex items-center justify-between mb-2">
@@ -1393,9 +1510,9 @@ function ToolEventItem({ event }: { event: ExtendedResponseStreamEvent }) {
<span className="font-medium text-sm">
{isFunctionCall ? "Tool Call" : "Tool Result"}
</span>
{isFunctionCall && data.name !== undefined && (
{isFunctionCall && callData && callData.name !== undefined && (
<span className="text-xs text-muted-foreground">
({String(data.name)})
({String(callData.name)})
</span>
)}
</div>
@@ -1405,7 +1522,7 @@ function ToolEventItem({ event }: { event: ExtendedResponseStreamEvent }) {
</div>
{/* Function Calls */}
{isFunctionCall && (
{isFunctionCall && callData && (
<div className="p-2 bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 rounded">
<div className="flex items-center gap-2 mb-2">
<Wrench className="h-3 w-3 text-blue-600 dark:text-blue-400" />
@@ -1413,19 +1530,19 @@ function ToolEventItem({ event }: { event: ExtendedResponseStreamEvent }) {
CALL
</span>
<span className="font-medium text-sm">
{String(data.name || "unknown")}
{String(callData.name || "unknown")}
</span>
</div>
{data.arguments !== undefined && (
{callData.arguments !== undefined && (
<div className="text-xs">
<span className="text-muted-foreground mb-1 block">
Arguments:
</span>
<pre className="p-2 bg-background border rounded text-xs overflow-auto max-h-32 max-w-full break-all whitespace-pre-wrap">
{typeof data.arguments === "string"
? data.arguments
: JSON.stringify(data.arguments, null, 1)}
{typeof callData.arguments === "string"
? callData.arguments
: JSON.stringify(callData.arguments, null, 1)}
</pre>
</div>
)}
@@ -1433,31 +1550,35 @@ function ToolEventItem({ event }: { event: ExtendedResponseStreamEvent }) {
)}
{/* Function Results */}
{isFunctionResult && (
{isFunctionResult && resultData && (
<div className="p-2 bg-green-50 dark:bg-green-950/50 border border-green-200 dark:border-green-800 rounded">
<div className="flex items-center gap-2 mb-2">
<CheckCircle2 className="h-3 w-3 text-green-600 dark:text-green-400" />
<span className="text-xs font-mono bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-1 rounded">
RESULT
</span>
{/* Only show status badge for non-completed states (errors/incomplete) */}
{resultData.status !== "completed" && (
<span className="ml-auto px-2 py-1 rounded text-xs font-medium bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200">
{resultData.status}
</span>
)}
</div>
<div className="text-xs">
<span className="text-muted-foreground mb-1 block">Result:</span>
<pre className="p-2 bg-background border rounded text-xs overflow-auto max-h-32 max-w-full break-all whitespace-pre-wrap">
{typeof data.result === "string"
? data.result
: JSON.stringify(data.result, null, 1)}
</pre>
</div>
{data.exception !== null && data.exception !== undefined && (
<div className="mt-2 p-2 bg-red-50 dark:bg-red-950/50 border border-red-200 dark:border-red-800 rounded">
<span className="text-xs text-red-600 dark:text-red-400">
Error: {String(data.exception)}
<div className="text-xs space-y-1">
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Call ID:</span>
<span className="font-mono text-xs break-all">
{resultData.call_id}
</span>
</div>
)}
<div>
<span className="text-muted-foreground block mb-1">Output:</span>
<pre className="p-2 bg-background border rounded text-xs overflow-auto max-h-32 break-all whitespace-pre-wrap">
{resultData.output}
</pre>
</div>
</div>
</div>
)}
</div>
@@ -1470,9 +1591,9 @@ export function DebugPanel({
onClose,
}: DebugPanelProps) {
return (
<div className=" overflow-auto h-[calc(100vh-3.7rem)] border-l">
<Tabs defaultValue="events" className="h-full flex flex-col">
<div className="px-3 pt-3 flex items-center gap-2">
<div className="flex-1 border-l flex flex-col min-h-0">
<Tabs defaultValue="events" className="flex-1 flex flex-col min-h-0">
<div className="px-3 pt-3 flex items-center gap-2 flex-shrink-0">
<TabsList className="flex-1">
<TabsTrigger value="events" className="flex-1">
Events
@@ -1497,15 +1618,15 @@ export function DebugPanel({
)}
</div>
<TabsContent value="events" className="flex-1 mt-0">
<TabsContent value="events" className="flex-1 mt-0 overflow-hidden">
<EventsTab events={events} isStreaming={isStreaming} />
</TabsContent>
<TabsContent value="traces" className="flex-1 mt-0">
<TabsContent value="traces" className="flex-1 mt-0 overflow-hidden">
<TracesTab events={events} />
</TabsContent>
<TabsContent value="tools" className="flex-1 mt-0">
<TabsContent value="tools" className="flex-1 mt-0 overflow-hidden">
<ToolsTab events={events} />
</TabsContent>
</Tabs>
@@ -0,0 +1,519 @@
/**
* DeploymentModal - Shows Azure deployment instructions and Docker templates
* Features: Docker setup files, Azure Container Apps deployment guide
*/
import { useState, useEffect, useRef } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogClose,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Rocket,
Container,
Cloud,
Copy,
CheckCircle2,
ExternalLink,
} from "lucide-react";
interface DeploymentModalProps {
open: boolean;
onClose: () => void;
agentName?: string;
}
type Tab = "docker" | "azure";
export function DeploymentModal({
open,
onClose,
agentName = "Agent",
}: DeploymentModalProps) {
const [activeTab, setActiveTab] = useState<Tab>("docker");
const [copiedTemplate, setCopiedTemplate] = useState<string | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
const handleCopy = async (template: string, templateName: string) => {
try {
await navigator.clipboard.writeText(template);
setCopiedTemplate(templateName);
// Clear any existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Set new timeout with cleanup
timeoutRef.current = setTimeout(() => {
setCopiedTemplate(null);
timeoutRef.current = null;
}, 2000);
} catch (err) {
console.error("Failed to copy template:", err);
// Reset state on error
setCopiedTemplate(null);
}
};
const dockerfileTemplate = `# Dockerfile for ${agentName}
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy agent/workflow directories
COPY . .
# Expose DevUI default port
EXPOSE 8080
# Run DevUI server
CMD ["devui", ".", "--port", "8080", "--host", "0.0.0.0"]
`;
const dockerComposeTemplate = `# docker-compose.yml
version: '3.8'
services:
${agentName.toLowerCase().replace(/\s+/g, "-")}:
build: .
environment:
# OpenAI
- OPENAI_API_KEY=\${OPENAI_API_KEY}
- OPENAI_CHAT_MODEL_ID=\${OPENAI_CHAT_MODEL_ID:-gpt-4o-mini}
# Or Azure OpenAI
- AZURE_OPENAI_API_KEY=\${AZURE_OPENAI_API_KEY}
- AZURE_OPENAI_ENDPOINT=\${AZURE_OPENAI_ENDPOINT}
- AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=\${AZURE_OPENAI_CHAT_DEPLOYMENT_NAME}
# Optional: Enable tracing
- ENABLE_OTEL=\${ENABLE_OTEL:-false}
ports:
- "8080:8080"
restart: unless-stopped
`;
const requirementsTemplate = `# requirements.txt
agent-framework-devui>=0.1.0
agent-framework>=0.1.0
# Chat clients (install what you need)
openai>=1.0.0
# azure-openai
# anthropic
`;
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="w-[800px] max-w-[90vw]">
<DialogClose onClose={onClose} />
<DialogHeader className="p-6 pb-2">
<DialogTitle className="flex items-center gap-2">
<Rocket className="h-5 w-5" />
Deploy {agentName}
</DialogTitle>
<p className="text-sm text-muted-foreground pt-1">
Get started with containerizing your agent for deployment.
</p>
</DialogHeader>
{/* Tabs */}
<div className="flex border-b px-6">
<button
onClick={() => setActiveTab("docker")}
className={`px-4 py-2 text-sm font-medium transition-colors relative ${
activeTab === "docker"
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
<Container className="h-4 w-4 mr-2 inline" />
Docker
{activeTab === "docker" && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
)}
</button>
<button
onClick={() => setActiveTab("azure")}
className={`px-4 py-2 text-sm font-medium transition-colors relative ${
activeTab === "azure"
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
<Cloud className="h-4 w-4 mr-2 inline" />
Azure
{activeTab === "azure" && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
)}
</button>
</div>
{/* Tab Content */}
<div className="px-6 pb-6 min-h-[400px]">
<ScrollArea className="h-[500px]">
<div className="pr-4">
{activeTab === "docker" && (
<div className="space-y-4 pt-4">
<div>
<h3 className="font-semibold mb-2">
Containerize with Docker
</h3>
<p className="text-sm text-muted-foreground">
Package your agent as a Docker container for consistent
deployment anywhere.
</p>
</div>
{/* Dockerfile */}
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">Dockerfile</span>
<Button
size="sm"
variant="ghost"
onClick={() =>
handleCopy(dockerfileTemplate, "dockerfile")
}
>
{copiedTemplate === "dockerfile" ? (
<>
<CheckCircle2 className="h-4 w-4 mr-1 text-green-500" />
Copied!
</>
) : (
<>
<Copy className="h-4 w-4 mr-1" />
Copy
</>
)}
</Button>
</div>
<pre className="bg-muted p-3 rounded-md text-xs overflow-x-auto border">
{dockerfileTemplate}
</pre>
</div>
{/* docker-compose.yml */}
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">
docker-compose.yml
</span>
<Button
size="sm"
variant="ghost"
onClick={() =>
handleCopy(dockerComposeTemplate, "compose")
}
>
{copiedTemplate === "compose" ? (
<>
<CheckCircle2 className="h-4 w-4 mr-1 text-green-500" />
Copied!
</>
) : (
<>
<Copy className="h-4 w-4 mr-1" />
Copy
</>
)}
</Button>
</div>
<pre className="bg-muted p-3 rounded-md text-xs overflow-x-auto border">
{dockerComposeTemplate}
</pre>
</div>
{/* requirements.txt */}
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">
requirements.txt
</span>
<Button
size="sm"
variant="ghost"
onClick={() =>
handleCopy(requirementsTemplate, "requirements")
}
>
{copiedTemplate === "requirements" ? (
<>
<CheckCircle2 className="h-4 w-4 mr-1 text-green-500" />
Copied!
</>
) : (
<>
<Copy className="h-4 w-4 mr-1" />
Copy
</>
)}
</Button>
</div>
<pre className="bg-muted p-3 rounded-md text-xs overflow-x-auto border">
{requirementsTemplate}
</pre>
</div>
{/* Quick Start */}
<div className="bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 rounded-md p-3">
<h4 className="text-sm font-semibold mb-2">Quick Start</h4>
<ol className="text-xs space-y-1 list-decimal list-inside text-muted-foreground">
<li>Save the files above to your project directory</li>
<li>
Build:{" "}
<code className="bg-muted px-1 rounded">
docker build -t {agentName.toLowerCase()}-agent .
</code>
</li>
<li>
Run:{" "}
<code className="bg-muted px-1 rounded">
docker-compose up
</code>
</li>
<li>Your agent is now running in a container!</li>
</ol>
</div>
{/* Production Warnings */}
<div className="bg-amber-50 dark:bg-amber-950/50 border border-amber-200 dark:border-amber-800 rounded-md p-3">
<h4 className="text-sm font-semibold mb-2 text-amber-900 dark:text-amber-100">
Production Considerations
</h4>
<ul className="text-xs space-y-1 list-disc list-inside text-amber-800 dark:text-amber-200">
<li>
<strong>In-memory state:</strong> Conversations are lost
when container restarts
</li>
<li>
<strong>No authentication:</strong> Add reverse proxy
(nginx, Caddy) with auth for production
</li>
<li>
<strong>Security:</strong> Use Azure Key Vault for
secrets management
</li>
<li>
<strong>Scaling:</strong> Single instance only due to
in-memory conversation store
</li>
</ul>
</div>
{/* Deployment Checklist */}
<div className="border-t pt-4">
<h4 className="font-semibold text-sm mb-3">
Pre-Deployment Checklist
</h4>
<div className="space-y-2 text-sm">
<div className="flex items-start gap-2">
<CheckCircle2 className="h-4 w-4 mt-0.5 text-muted-foreground flex-shrink-0" />
<span className="text-muted-foreground">
Set environment variables (API keys, secrets)
</span>
</div>
<div className="flex items-start gap-2">
<CheckCircle2 className="h-4 w-4 mt-0.5 text-muted-foreground flex-shrink-0" />
<span className="text-muted-foreground">
Test agent locally in container
</span>
</div>
<div className="flex items-start gap-2">
<CheckCircle2 className="h-4 w-4 mt-0.5 text-muted-foreground flex-shrink-0" />
<span className="text-muted-foreground">
Configure logging and monitoring
</span>
</div>
<div className="flex items-start gap-2">
<CheckCircle2 className="h-4 w-4 mt-0.5 text-muted-foreground flex-shrink-0" />
<span className="text-muted-foreground">
Set up error handling and retries
</span>
</div>
</div>
</div>
</div>
)}
{activeTab === "azure" && (
<div className="space-y-4 pt-4">
<div>
<h3 className="font-semibold mb-2">
Deploy to Azure Container Apps
</h3>
<p className="text-sm text-muted-foreground">
Azure Container Apps provides serverless containers with
auto-scaling and integrated monitoring.
</p>
</div>
{/* Prerequisites */}
<div className="border rounded-lg p-4 space-y-3">
<h4 className="font-medium text-sm">Prerequisites</h4>
<ul className="text-xs space-y-1 list-disc list-inside text-muted-foreground">
<li>Azure subscription</li>
<li>
Azure CLI installed (
<code className="bg-muted px-1 rounded">
az --version
</code>
)
</li>
<li>Docker installed and running</li>
<li>
Logged in to Azure:{" "}
<code className="bg-muted px-1 rounded">az login</code>
</li>
</ul>
</div>
{/* Step-by-step */}
<div className="space-y-3">
<h4 className="font-medium text-sm">Deployment Steps</h4>
<div className="space-y-3">
{/* Step 1 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
1
</div>
<h5 className="font-medium text-sm">
Create Azure Container Registry
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`# Create resource group
az group create --name myResourceGroup --location eastus
# Create container registry
az acr create --resource-group myResourceGroup \\
--name myregistry --sku Basic`}
</pre>
</div>
{/* Step 2 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
2
</div>
<h5 className="font-medium text-sm">
Build and Push Docker Image
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`# Build and push in one command
az acr build --registry myregistry \\
--image ${agentName.toLowerCase()}-agent:latest .`}
</pre>
</div>
{/* Step 3 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
3
</div>
<h5 className="font-medium text-sm">
Create Container Apps Environment
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`az containerapp env create --name myEnvironment \\
--resource-group myResourceGroup \\
--location eastus`}
</pre>
</div>
{/* Step 4 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
4
</div>
<h5 className="font-medium text-sm">
Deploy Container App
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`az containerapp create --name ${agentName.toLowerCase()}-app \\
--resource-group myResourceGroup \\
--environment myEnvironment \\
--image myregistry.azurecr.io/${agentName.toLowerCase()}-agent:latest \\
--target-port 8080 \\
--ingress 'external' \\
--registry-server myregistry.azurecr.io \\
--env-vars OPENAI_API_KEY=secretref:openai-key OPENAI_CHAT_MODEL_ID=gpt-4o-mini`}
</pre>
</div>
{/* Step 5 */}
<div className="border-l-2 border-primary pl-3">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
5
</div>
<h5 className="font-medium text-sm">
Get Application URL
</h5>
</div>
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto border mt-2">
{`az containerapp show --name ${agentName.toLowerCase()}-app \\
--resource-group myResourceGroup \\
--query properties.configuration.ingress.fqdn`}
</pre>
</div>
</div>
</div>
{/* Learn More */}
<div className="bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 rounded-md p-3">
<h4 className="text-sm font-semibold mb-2">Learn More</h4>
<p className="text-xs text-muted-foreground mb-3">
Explore Azure Container Apps documentation for advanced
features like scaling, monitoring, and CI/CD integration.
</p>
<Button
size="sm"
variant="outline"
className="w-full"
asChild
>
<a
href="https://learn.microsoft.com/azure/container-apps/"
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="h-3 w-3 mr-1" />
View Azure Container Apps Documentation
</a>
</Button>
</div>
</div>
)}
</div>
</ScrollArea>
</div>
</DialogContent>
</Dialog>
);
}
@@ -1,6 +1,6 @@
/**
* EntitySelector - High-quality dropdown for selecting agents/workflows
* Features: Type indicators, tool counts, keyboard navigation, search
* EntitySelector - Dropdown for selecting agents/workflows
* Features: Loading states, descriptions, lazy loading indicators
*/
import { useState } from "react";
@@ -13,9 +13,8 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Badge } from "@/components/ui/badge";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { ChevronDown, Bot, Workflow, FolderOpen, Database, Globe, X, Plus } from "lucide-react";
import { ChevronDown, Bot, Workflow, Plus, Loader2 } from "lucide-react";
import type { AgentInfo, WorkflowInfo } from "@/types";
interface EntitySelectorProps {
@@ -23,7 +22,6 @@ interface EntitySelectorProps {
workflows: WorkflowInfo[];
selectedItem?: AgentInfo | WorkflowInfo;
onSelect: (item: AgentInfo | WorkflowInfo) => void;
onRemove?: (entityId: string) => void;
onBrowseGallery?: () => void;
isLoading?: boolean;
}
@@ -32,30 +30,11 @@ const getTypeIcon = (type: "agent" | "workflow") => {
return type === "workflow" ? Workflow : Bot;
};
const getSourceIcon = (source: "directory" | "in_memory" | "remote_gallery") => {
switch (source) {
case "directory": return FolderOpen;
case "in_memory": return Database;
case "remote_gallery": return Globe;
default: return Database;
}
};
const getSourceLabel = (source: "directory" | "in_memory" | "remote_gallery") => {
switch (source) {
case "directory": return "Local";
case "in_memory": return "Memory";
case "remote_gallery": return "Gallery";
default: return "Unknown";
}
};
export function EntitySelector({
agents,
workflows,
selectedItem,
onSelect,
onRemove,
onBrowseGallery,
isLoading = false,
}: EntitySelectorProps) {
@@ -72,11 +51,7 @@ export function EntitySelector({
const TypeIcon = selectedItem ? getTypeIcon(selectedItem.type) : Bot;
const displayName = selectedItem?.name || selectedItem?.id || "Select Agent or Workflow";
const itemCount =
selectedItem?.type === "workflow"
? (selectedItem as WorkflowInfo).executors?.length || 0
: (selectedItem as AgentInfo)?.tools?.length || 0;
const itemLabel = selectedItem?.type === "workflow" ? "executors" : "tools";
const isLoaded = selectedItem?.metadata?.lazy_loaded !== false;
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
@@ -96,10 +71,8 @@ export function EntitySelector({
<div className="flex items-center gap-2 min-w-0">
<TypeIcon className="h-4 w-4 flex-shrink-0" />
<span className="truncate">{displayName}</span>
{selectedItem && (
<Badge variant="secondary" className="ml-auto flex-shrink-0">
{itemCount} {itemLabel}
</Badge>
{selectedItem && !isLoaded && (
<Loader2 className="h-3 w-3 text-muted-foreground animate-spin ml-auto flex-shrink-0" />
)}
</div>
<ChevronDown className="h-4 w-4 opacity-50" />
@@ -116,53 +89,29 @@ export function EntitySelector({
Agents ({agents.length})
</DropdownMenuLabel>
{agents.map((agent) => {
const SourceIcon = getSourceIcon(agent.source);
const isAgentLoaded = agent.metadata?.lazy_loaded !== false;
return (
<DropdownMenuItem
key={agent.id}
className="cursor-pointer group"
>
<div className="flex items-center justify-between w-full">
<div className="flex items-center justify-between w-full gap-2">
<div
className="flex items-center gap-2 min-w-0 flex-1"
onClick={() => handleSelect(agent)}
>
<Bot className="h-4 w-4 flex-shrink-0" />
<div className="min-w-0">
<div className="truncate font-medium">
<div className="min-w-0 flex-1">
<span className="truncate font-medium block">
{agent.name || agent.id}
</div>
{agent.description && (
<div className="text-xs text-muted-foreground truncate">
</span>
{isAgentLoaded && agent.description && (
<div className="text-xs text-muted-foreground line-clamp-2">
{agent.description}
</div>
)}
</div>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<SourceIcon className="h-3 w-3 opacity-60" />
<Badge variant="outline" className="text-xs">
{getSourceLabel(agent.source)}
</Badge>
<Badge variant="outline" className="text-xs ml-1">
{agent.tools.length}
</Badge>
{/* Remove button for gallery entities */}
{agent.source === 'remote_gallery' && onRemove && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100 ml-1"
onClick={(e) => {
e.stopPropagation();
onRemove(agent.id);
}}
>
<X className="h-3 w-3 text-destructive" />
</Button>
)}
</div>
</div>
</DropdownMenuItem>
);
@@ -178,53 +127,29 @@ export function EntitySelector({
Workflows ({workflows.length})
</DropdownMenuLabel>
{workflows.map((workflow) => {
const SourceIcon = getSourceIcon(workflow.source);
const isWorkflowLoaded = workflow.metadata?.lazy_loaded !== false;
return (
<DropdownMenuItem
key={workflow.id}
className="cursor-pointer group"
>
<div className="flex items-center justify-between w-full">
<div className="flex items-center justify-between w-full gap-2">
<div
className="flex items-center gap-2 min-w-0 flex-1"
onClick={() => handleSelect(workflow)}
>
<Workflow className="h-4 w-4 flex-shrink-0" />
<div className="min-w-0">
<div className="truncate font-medium">
<div className="min-w-0 flex-1">
<span className="truncate font-medium block">
{workflow.name || workflow.id}
</div>
{workflow.description && (
<div className="text-xs text-muted-foreground truncate">
</span>
{isWorkflowLoaded && workflow.description && (
<div className="text-xs text-muted-foreground line-clamp-2">
{workflow.description}
</div>
)}
</div>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<SourceIcon className="h-3 w-3 opacity-60" />
<Badge variant="outline" className="text-xs">
{getSourceLabel(workflow.source)}
</Badge>
<Badge variant="outline" className="text-xs ml-1">
{workflow.executors.length}
</Badge>
{/* Remove button for gallery entities */}
{workflow.source === 'remote_gallery' && onRemove && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100 ml-1"
onClick={(e) => {
e.stopPropagation();
onRemove(workflow.id);
}}
>
<X className="h-3 w-3 text-destructive" />
</Button>
)}
</div>
</div>
</DropdownMenuItem>
);
@@ -7,3 +7,4 @@ export { EntitySelector } from "./entity-selector";
export { DebugPanel } from "./debug-panel";
export { SettingsModal } from "./settings-modal";
export { AboutModal } from "./about-modal";
export { DeploymentModal } from "./deployment-modal";
@@ -24,7 +24,7 @@ interface SettingsModalProps {
type Tab = "about" | "settings";
export function SettingsModal({ open, onOpenChange, onBackendUrlChange }: SettingsModalProps) {
const [activeTab, setActiveTab] = useState<Tab>("about");
const [activeTab, setActiveTab] = useState<Tab>("settings");
// Get current backend URL from localStorage or default
const defaultUrl = import.meta.env.VITE_API_BASE_URL || "http://localhost:8080";
@@ -73,19 +73,6 @@ export function SettingsModal({ open, onOpenChange, onBackendUrlChange }: Settin
{/* Tabs */}
<div className="flex border-b px-6">
<button
onClick={() => setActiveTab("about")}
className={`px-4 py-2 text-sm font-medium transition-colors relative ${
activeTab === "about"
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
About
{activeTab === "about" && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
)}
</button>
<button
onClick={() => setActiveTab("settings")}
className={`px-4 py-2 text-sm font-medium transition-colors relative ${
@@ -99,35 +86,23 @@ export function SettingsModal({ open, onOpenChange, onBackendUrlChange }: Settin
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
)}
</button>
<button
onClick={() => setActiveTab("about")}
className={`px-4 py-2 text-sm font-medium transition-colors relative ${
activeTab === "about"
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
About
{activeTab === "about" && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
)}
</button>
</div>
{/* Tab Content */}
<div className="px-6 pb-6 min-h-[240px]">
{activeTab === "about" && (
<div className="space-y-4 pt-4">
<p className="text-sm text-muted-foreground">
DevUI is a sample app for getting started with Agent Framework.
</p>
<div className="flex justify-center pt-2">
<Button
variant="outline"
size="sm"
onClick={() =>
window.open(
"https://github.com/microsoft/agent-framework",
"_blank"
)
}
className="text-xs"
>
<ExternalLink className="h-3 w-3 mr-1" />
Learn More about Agent Framework
</Button>
</div>
</div>
)}
{activeTab === "settings" && (
<div className="space-y-6 pt-4">
{/* Backend URL Setting */}
@@ -188,6 +163,31 @@ export function SettingsModal({ open, onOpenChange, onBackendUrlChange }: Settin
</div>
</div>
)}
{activeTab === "about" && (
<div className="space-y-4 pt-4">
<p className="text-sm text-muted-foreground">
DevUI is a sample app for getting started with Agent Framework.
</p>
<div className="flex justify-center pt-2">
<Button
variant="outline"
size="sm"
onClick={() =>
window.open(
"https://github.com/microsoft/agent-framework",
"_blank"
)
}
className="text-xs"
>
<ExternalLink className="h-3 w-3 mr-1" />
Learn More about Agent Framework
</Button>
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>