Files
agent-framework/python/packages/devui/frontend/src/App.tsx
T
Victor Dibia 1ef24d3e91 Python: Add DevUI to AgentFramework (#781)
* add initial backend service code for devui

* add tests

* add frontendcode

* ui updates

* update readme

* ui updates and tweaks

* update ui bundle

* improve ui, add react flow base

* add react flow ui, fix background

* update ui, fix introspection bug

* update readme

* update ui build

* add support for multimodal input - both backend and frontend

* update ui build

* refactor as main framework package

* backend and tests refactor

* ui build update

* ui build update and refactor

* update pyproject.toml, update uv.lock

* update ui build

* ui update to fit oai responses types

* add backend updat and readme update

* mypy and other fixes

* add intial dev guide

* update ui and fix workflow bug

* update ui build, add thread support

* type fixes

* update workflow view

* update uv.lock

* fix workflow iport errors

* lint and other fixes

* mypy fixes

* minor update

* update ui build

* refactor to use oai dependencies directly, update examples to samples, improve typing

* readme update

* update ui and ui build

* fix workflow pyright error

* update ui, fix issues with run workflow placement, miniamp menu, etc

* make samples integrate serve

---------

Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
2025-09-22 23:30:08 +00:00

313 lines
9.7 KiB
TypeScript

/**
* DevUI App - Minimal orchestrator for agent/workflow interactions
* Features: Entity selection, layout management, debug coordination
*/
import { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { AppHeader } from "@/components/shared/app-header";
import { DebugPanel } from "@/components/shared/debug-panel";
import { AboutModal } from "@/components/shared/about-modal";
import { AgentView } from "@/components/agent/agent-view";
import { WorkflowView } from "@/components/workflow/workflow-view";
import { LoadingState } from "@/components/ui/loading-state";
import { apiClient } from "@/services/api";
import { ChevronLeft } from "lucide-react";
import type {
AgentInfo,
WorkflowInfo,
AppState,
ExtendedResponseStreamEvent,
} from "@/types";
export default function App() {
const [appState, setAppState] = useState<AppState>({
agents: [],
workflows: [],
isLoading: true,
});
const [debugEvents, setDebugEvents] = useState<ExtendedResponseStreamEvent[]>(
[]
);
const [debugPanelOpen, setDebugPanelOpen] = useState(true);
const [debugPanelWidth, setDebugPanelWidth] = useState(() => {
// Initialize from localStorage or default to 320
const savedWidth = localStorage.getItem("debugPanelWidth");
return savedWidth ? parseInt(savedWidth, 10) : 320;
});
const [isResizing, setIsResizing] = useState(false);
const [showAboutModal, setShowAboutModal] = useState(false);
// Initialize app - load agents and workflows
useEffect(() => {
const loadData = async () => {
try {
// Load agents and workflows in parallel
const [agents, workflows] = await Promise.all([
apiClient.getAgents(),
apiClient.getWorkflows(),
]);
setAppState((prev) => ({
...prev,
agents,
workflows,
selectedAgent:
agents.length > 0
? agents[0]
: workflows.length > 0
? workflows[0]
: undefined,
isLoading: false,
}));
} catch (error) {
console.error("Failed to load agents/workflows:", error);
setAppState((prev) => ({
...prev,
error: error instanceof Error ? error.message : "Failed to load data",
isLoading: false,
}));
}
};
loadData();
}, []);
// Save debug panel width to localStorage
useEffect(() => {
localStorage.setItem("debugPanelWidth", debugPanelWidth.toString());
}, [debugPanelWidth]);
// Handle resize drag
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
setIsResizing(true);
const startX = e.clientX;
const startWidth = debugPanelWidth;
const handleMouseMove = (e: MouseEvent) => {
const deltaX = startX - e.clientX; // Subtract because we're dragging from right
const newWidth = Math.max(
200,
Math.min(window.innerWidth * 0.5, startWidth + deltaX)
);
setDebugPanelWidth(newWidth);
};
const handleMouseUp = () => {
setIsResizing(false);
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
},
[debugPanelWidth]
);
// Handle double-click to collapse
const handleDoubleClick = useCallback(() => {
setDebugPanelOpen(false);
}, []);
// Handle entity selection
const handleEntitySelect = useCallback((item: AgentInfo | WorkflowInfo) => {
setAppState((prev) => ({
...prev,
selectedAgent: item,
currentThread: undefined,
}));
// Clear debug events when switching entities
setDebugEvents([]);
}, []);
// Handle debug events from active view
const handleDebugEvent = useCallback((event: ExtendedResponseStreamEvent | 'clear') => {
if (event === 'clear') {
setDebugEvents([]);
} else {
setDebugEvents((prev) => [...prev, event]);
}
}, []);
// Show loading state while initializing
if (appState.isLoading) {
return (
<div className="h-screen flex flex-col bg-background">
{/* Top Bar - Skeleton */}
<header className="flex h-14 items-center gap-4 border-b px-4">
<div className="w-64 h-9 bg-muted animate-pulse rounded-md" />
<div className="flex items-center gap-2 ml-auto">
<div className="w-8 h-8 bg-muted animate-pulse rounded-md" />
<div className="w-8 h-8 bg-muted animate-pulse rounded-md" />
</div>
</header>
{/* Loading Content */}
<LoadingState
message="Initializing DevUI..."
description="Loading agents and workflows from your configuration"
fullPage={true}
/>
</div>
);
}
// Show error state if loading failed
if (appState.error) {
return (
<div className="h-screen flex flex-col bg-background">
<AppHeader
agents={[]}
workflows={[]}
selectedItem={undefined}
onSelect={() => {}}
isLoading={false}
/>
{/* Error Content */}
<div className="flex-1 flex items-center justify-center">
<div className="text-center space-y-4 max-w-md">
<div className="text-destructive text-lg font-medium">
Failed to load entities
</div>
<p className="text-muted-foreground text-sm">{appState.error}</p>
<Button onClick={() => window.location.reload()} variant="outline">
Retry
</Button>
</div>
</div>
</div>
);
}
// Show empty state if no agents or workflows are available
if (
!appState.isLoading &&
appState.agents.length === 0 &&
appState.workflows.length === 0
) {
return (
<div className="h-screen flex flex-col bg-background">
<AppHeader
agents={[]}
workflows={[]}
selectedItem={undefined}
onSelect={() => {}}
isLoading={false}
/>
{/* Empty State Content */}
<div className="flex-1 flex items-center justify-center">
<div className="text-center space-y-4 max-w-md">
<div className="text-lg font-medium">No entities configured</div>
<p className="text-muted-foreground text-sm">
No agents or workflows were found in your configuration. Please
check your setup and ensure entities are properly configured.
</p>
<Button onClick={() => window.location.reload()} variant="outline">
Retry
</Button>
</div>
</div>
</div>
);
}
return (
<div className="h-screen flex flex-col bg-background max-h-screen">
<AppHeader
agents={appState.agents}
workflows={appState.workflows}
selectedItem={appState.selectedAgent}
onSelect={handleEntitySelect}
isLoading={appState.isLoading}
onSettingsClick={() => setShowAboutModal(true)}
/>
{/* Main Content - Split Panel */}
<div className="flex flex-1 overflow-hidden">
{/* Left Panel - Main View */}
<div className="flex-1 min-w-0">
{appState.selectedAgent ? (
appState.selectedAgent.type === "agent" ? (
<AgentView
selectedAgent={appState.selectedAgent as AgentInfo}
onDebugEvent={handleDebugEvent}
/>
) : (
<WorkflowView
selectedWorkflow={appState.selectedAgent as WorkflowInfo}
onDebugEvent={handleDebugEvent}
/>
)
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
Select an agent or workflow to get started.
</div>
)}
</div>
{/* Resize Handle */}
{debugPanelOpen && (
<div
className={`w-1 cursor-col-resize flex-shrink-0 relative group transition-colors duration-200 ease-in-out ${
isResizing ? "bg-primary/40" : "bg-border hover:bg-primary/20"
}`}
onMouseDown={handleMouseDown}
onDoubleClick={handleDoubleClick}
>
<div className="absolute inset-y-0 -left-2 -right-2 flex items-center justify-center">
<div
className={`h-12 w-1 rounded-full transition-all duration-200 ease-in-out ${
isResizing
? "bg-primary shadow-lg shadow-primary/25"
: "bg-primary/30 group-hover:bg-primary group-hover:shadow-md group-hover:shadow-primary/20"
}`}
></div>
</div>
</div>
)}
{/* Button to reopen when closed */}
{!debugPanelOpen && (
<div className="flex-shrink-0">
<Button
variant="ghost"
size="sm"
onClick={() => setDebugPanelOpen(true)}
className="h-full w-8 rounded-none border-l"
>
<ChevronLeft className="h-4 w-4" />
</Button>
</div>
)}
{/* Right Panel - Debug */}
{debugPanelOpen && (
<div
className="flex-shrink-0"
style={{ width: `${debugPanelWidth}px` }}
>
<DebugPanel
events={debugEvents}
isStreaming={false} // Each view manages its own streaming state
/>
</div>
)}
</div>
{/* About Modal */}
<AboutModal
open={showAboutModal}
onOpenChange={setShowAboutModal}
/>
</div>
);
}