mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
331c750515
commit
b64358df7e
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user