Python:DevUI Fixes (#1035)

* fix event reset on thread change, enable multiline input, enable pasting of files and screenshots

* UI updates and improved remove discovery

* ui and other fixes

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
This commit is contained in:
Victor Dibia
2025-10-01 00:21:22 +00:00
committed by GitHub
co-authored by Evan Mattson
parent 042099009f
commit fa9f5c1aed
41 changed files with 2949 additions and 814 deletions
+238 -80
View File
@@ -7,12 +7,14 @@ 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 { SettingsModal } from "@/components/shared/settings-modal";
import { GalleryView } from "@/components/gallery";
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 { ChevronLeft, ChevronDown, ServerOff } from "lucide-react";
import type { SampleEntity } from "@/data/gallery";
import type {
AgentInfo,
WorkflowInfo,
@@ -27,23 +29,23 @@ export default function App() {
isLoading: true,
});
const [debugEvents, setDebugEvents] = useState<ExtendedResponseStreamEvent[]>(
[]
);
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);
const [showGallery, setShowGallery] = useState(false);
const [addingEntityId, setAddingEntityId] = useState<string | null>(null);
const [errorEntityId, setErrorEntityId] = useState<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
// 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(),
@@ -135,6 +137,109 @@ export default function App() {
}
}, []);
// Handle adding sample entity
const handleAddSample = useCallback(async (sample: SampleEntity) => {
setAddingEntityId(sample.id);
setErrorEntityId(null);
setErrorMessage(null);
try {
// Call backend to fetch and add entity
const newEntity = await apiClient.addEntity(sample.url, {
source: 'remote_gallery',
originalUrl: sample.url,
sampleId: sample.id
});
// Convert backend entity to frontend format
const convertedEntity = {
id: newEntity.id,
name: newEntity.name,
description: newEntity.description,
type: newEntity.type,
source: (newEntity.source as "directory" | "in_memory" | "remote_gallery") || 'remote_gallery',
has_env: false,
module_path: undefined
};
// Update app state
if (newEntity.type === 'agent') {
const agentEntity = {
...convertedEntity,
tools: (newEntity.tools || []).map(tool =>
typeof tool === 'string' ? tool : JSON.stringify(tool)
)
} as AgentInfo;
setAppState(prev => ({
...prev,
agents: [...prev.agents, agentEntity],
selectedAgent: agentEntity
}));
} else {
const workflowEntity = {
...convertedEntity,
executors: (newEntity.tools || []).map(tool =>
typeof tool === 'string' ? tool : JSON.stringify(tool)
),
input_schema: { type: "string" },
input_type_name: "Input",
start_executor_id: (newEntity.tools && newEntity.tools.length > 0)
? (typeof newEntity.tools[0] === 'string' ? newEntity.tools[0] : JSON.stringify(newEntity.tools[0]))
: "unknown"
} as WorkflowInfo;
setAppState(prev => ({
...prev,
workflows: [...prev.workflows, workflowEntity],
selectedAgent: workflowEntity
}));
}
// Close gallery and clear debug events
setShowGallery(false);
setDebugEvents([]);
} catch (error) {
const errMsg = error instanceof Error ? error.message : 'Failed to add sample entity';
console.error('Failed to add sample entity:', errMsg);
setErrorEntityId(sample.id);
setErrorMessage(errMsg);
} finally {
setAddingEntityId(null);
}
}, []);
const handleClearError = useCallback(() => {
setErrorEntityId(null);
setErrorMessage(null);
}, []);
// Handle removing entity
const handleRemoveEntity = useCallback(async (entityId: string) => {
try {
await apiClient.removeEntity(entityId);
// Update app state
setAppState(prev => ({
...prev,
agents: prev.agents.filter(a => a.id !== entityId),
workflows: prev.workflows.filter(w => w.id !== entityId),
selectedAgent: prev.selectedAgent?.id === entityId
? undefined
: prev.selectedAgent
}));
// Clear debug events if we removed the selected entity
if (appState.selectedAgent?.id === entityId) {
setDebugEvents([]);
}
} catch (error) {
console.error('Failed to remove entity:', error);
}
}, [appState.selectedAgent?.id]);
// Show loading state while initializing
if (appState.isLoading) {
return (
@@ -167,54 +272,77 @@ export default function App() {
workflows={[]}
selectedItem={undefined}
onSelect={() => {}}
onRemove={handleRemoveEntity}
isLoading={false}
onSettingsClick={() => setShowAboutModal(true)}
/>
{/* 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 className="flex-1 flex items-center justify-center p-8">
<div className="text-center space-y-6 max-w-2xl">
{/* Icon */}
<div className="flex justify-center">
<div className="rounded-full bg-muted p-4 animate-pulse">
<ServerOff className="h-12 w-12 text-muted-foreground" />
</div>
</div>
<p className="text-muted-foreground text-sm">{appState.error}</p>
<Button onClick={() => window.location.reload()} variant="outline">
Retry
{/* Heading */}
<div className="space-y-2">
<h2 className="text-2xl font-semibold text-foreground">
Can't Connect to Backend
</h2>
<p className="text-muted-foreground text-base">
No worries! Just start the DevUI backend server and you'll be good to go.
</p>
</div>
{/* Command Instructions */}
<div className="space-y-3">
<div className="text-left bg-muted/50 rounded-lg p-4 space-y-3">
<p className="text-sm font-medium text-foreground">Start the backend:</p>
<code className="block bg-background px-3 py-2 rounded border text-sm font-mono text-foreground">
devui ./agents --port 8080
</code>
<p className="text-xs text-muted-foreground">
Or launch programmatically with <code className="text-xs">serve(entities=[agent])</code>
</p>
</div>
<p className="text-xs text-muted-foreground">
Default: <span className="font-mono">http://localhost:8080</span>
</p>
</div>
{/* Error Details (Collapsible) */}
{appState.error && (
<details className="text-left group">
<summary className="text-sm text-muted-foreground cursor-pointer hover:text-foreground flex items-center gap-2">
<ChevronDown className="h-4 w-4 transition-transform group-open:rotate-180" />
Error details
</summary>
<p className="mt-2 text-xs text-muted-foreground font-mono bg-muted/30 p-3 rounded border">
{appState.error}
</p>
</details>
)}
{/* Retry Button */}
<Button
onClick={() => window.location.reload()}
variant="default"
className="mt-2"
>
Retry Connection
</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}
{/* Settings Modal */}
<SettingsModal
open={showAboutModal}
onOpenChange={setShowAboutModal}
/>
{/* 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>
);
}
@@ -226,35 +354,63 @@ export default function App() {
workflows={appState.workflows}
selectedItem={appState.selectedAgent}
onSelect={handleEntitySelect}
onRemove={handleRemoveEntity}
onBrowseGallery={() => setShowGallery(true)}
isLoading={appState.isLoading}
onSettingsClick={() => setShowAboutModal(true)}
/>
{/* Main Content - Split Panel */}
{/* Main Content - Split Panel or Gallery */}
<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.
{showGallery ? (
// Show gallery full screen (w-full ensures it takes entire width)
<div className="flex-1 w-full">
<GalleryView
variant="route"
onAdd={handleAddSample}
addingEntityId={addingEntityId}
errorEntityId={errorEntityId}
errorMessage={errorMessage}
onClearError={handleClearError}
onClose={() => setShowGallery(false)}
hasExistingEntities={appState.agents.length > 0 || appState.workflows.length > 0}
/>
</div>
) : appState.agents.length === 0 && appState.workflows.length === 0 ? (
// Empty state - show gallery inline (full width, no debug panel)
<GalleryView
variant="inline"
onAdd={handleAddSample}
addingEntityId={addingEntityId}
errorEntityId={errorEntityId}
errorMessage={errorMessage}
onClearError={handleClearError}
/>
) : (
<>
{/* 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>
)}
</div>
{/* Resize Handle */}
{debugPanelOpen && (
{/* 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"
@@ -288,22 +444,24 @@ export default function App() {
</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>
{/* 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
{/* Settings Modal */}
<SettingsModal
open={showAboutModal}
onOpenChange={setShowAboutModal}
/>