mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: DevUI improvements. (#1091)
* enable deeplinking in ui, add agent details to entity info, add usage data, add middleware example in samples and foundry agent. * update ui build * Update python/packages/devui/frontend/src/components/workflow/workflow-input-form.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update python/packages/devui/pyproject.toml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update python/packages/devui/pyproject.toml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * imporove mapping for agent nodes and serialiation for agent run events * lint fixes * update pyproj toml and ui updates --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
61ac6d43b2
commit
01f438d710
@@ -1,11 +1,23 @@
|
||||
# React + TypeScript + Vite
|
||||
# DevUI Frontend
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
## Build Instructions
|
||||
|
||||
Currently, two official plugins are available:
|
||||
```bash
|
||||
cd frontend
|
||||
yarn install
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
# Create .env.local with backend URL
|
||||
echo 'VITE_API_BASE_URL=http://localhost:8000' > .env.local
|
||||
|
||||
# Create .env.production (empty for relative URLs)
|
||||
echo '' > .env.production
|
||||
|
||||
# Development
|
||||
yarn dev
|
||||
|
||||
# Build (copies to backend)
|
||||
yarn build
|
||||
```
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/agentframework.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Agent Framework Dev UI</title>
|
||||
</head>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<svg width="805" height="805" viewBox="0 0 805 805" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_iii_510_1294)">
|
||||
<path d="M402.488 119.713C439.197 119.713 468.955 149.472 468.955 186.18C468.955 192.086 471.708 197.849 476.915 200.635L546.702 237.977C555.862 242.879 566.95 240.96 576.092 236.023C585.476 230.955 596.218 228.078 607.632 228.078C644.341 228.078 674.098 257.836 674.099 294.545C674.099 316.95 663.013 336.765 646.028 348.806C637.861 354.595 631.412 363.24 631.412 373.251V430.818C631.412 440.83 637.861 449.475 646.028 455.264C663.013 467.305 674.099 487.121 674.099 509.526C674.099 546.235 644.341 575.994 607.632 575.994C598.598 575.994 589.985 574.191 582.133 570.926C573.644 567.397 563.91 566.393 555.804 570.731L469.581 616.867C469.193 617.074 468.955 617.479 468.955 617.919C468.955 654.628 439.197 684.386 402.488 684.386C365.779 684.386 336.021 654.628 336.021 617.919C336.021 616.802 335.423 615.765 334.439 615.238L249.895 570C241.61 565.567 231.646 566.713 223.034 570.472C214.898 574.024 205.914 575.994 196.47 575.994C159.761 575.994 130.002 546.235 130.002 509.526C130.002 486.66 141.549 466.49 159.13 454.531C167.604 448.766 174.349 439.975 174.349 429.726V372.538C174.349 362.289 167.604 353.498 159.13 347.734C141.549 335.774 130.002 315.604 130.002 292.738C130.002 256.029 159.761 226.271 196.47 226.271C208.223 226.271 219.263 229.322 228.843 234.674C238.065 239.827 249.351 241.894 258.666 236.91L328.655 199.459C333.448 196.895 336.021 191.616 336.021 186.18C336.021 149.471 365.779 119.713 402.488 119.713ZM475.716 394.444C471.337 396.787 468.955 401.586 468.955 406.552C468.955 429.68 457.142 450.048 439.221 461.954C430.571 467.7 423.653 476.574 423.653 486.959V537.511C423.653 547.896 430.746 556.851 439.379 562.622C449 569.053 461.434 572.052 471.637 566.592L527.264 536.826C536.887 531.677 541.164 520.44 541.164 509.526C541.164 485.968 553.42 465.272 571.904 453.468C580.846 447.757 588.054 438.749 588.054 428.139V371.427C588.054 363.494 582.671 356.676 575.716 352.862C569.342 349.366 561.663 348.454 555.253 351.884L475.716 394.444ZM247.992 349.841C241.997 346.633 234.806 347.465 228.873 350.785C222.524 354.337 217.706 360.639 217.706 367.915V429.162C217.706 439.537 224.611 448.404 233.248 454.152C251.144 466.062 262.937 486.417 262.937 509.526C262.937 519.654 267.026 529.991 275.955 534.769L334.852 566.284C344.582 571.49 356.362 568.81 365.528 562.667C373.735 557.166 380.296 548.643 380.296 538.764V486.305C380.296 476.067 373.564 467.282 365.103 461.516C347.548 449.552 336.021 429.398 336.021 406.552C336.021 400.967 333.389 395.536 328.465 392.902L247.992 349.841ZM270.019 280.008C265.421 282.469 262.936 287.522 262.937 292.738C262.937 293.308 262.929 293.876 262.915 294.443C262.615 306.354 266.961 318.871 277.466 324.492L334.017 354.751C344.13 360.163 356.442 357.269 366.027 350.969C376.495 344.088 389.024 340.085 402.488 340.085C416.203 340.085 428.947 344.239 439.532 351.357C449.163 357.834 461.63 360.861 471.864 355.385L526.625 326.083C537.106 320.474 541.458 307.999 541.182 296.115C541.17 295.593 541.164 295.069 541.164 294.545C541.164 288.551 538.376 282.696 533.091 279.868L463.562 242.664C454.384 237.753 443.274 239.688 434.123 244.65C424.716 249.75 413.941 252.647 402.488 252.647C390.83 252.647 379.873 249.646 370.348 244.373C361.148 239.281 349.917 237.256 340.646 242.217L270.019 280.008Z" fill="url(#paint0_linear_510_1294)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_iii_510_1294" x="103.759" y="93.4694" width="578.735" height="599.314" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="8.39647" dy="8.39647"/>
|
||||
<feGaussianBlur stdDeviation="20.9912"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.835294 0 0 0 0 0.623529 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_510_1294"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-26.2432" dy="-26.2432"/>
|
||||
<feGaussianBlur stdDeviation="20.9912"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.368627 0 0 0 0 0.262745 0 0 0 0 0.564706 0 0 0 0.3 0"/>
|
||||
<feBlend mode="plus-darker" in2="effect1_innerShadow_510_1294" result="effect2_innerShadow_510_1294"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-26.2432" dy="-26.2432"/>
|
||||
<feGaussianBlur stdDeviation="50"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.368627 0 0 0 0 0.262745 0 0 0 0 0.564706 0 0 0 0.1 0"/>
|
||||
<feBlend mode="plus-darker" in2="effect2_innerShadow_510_1294" result="effect3_innerShadow_510_1294"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_510_1294" x1="255.628" y1="-34.3245" x2="618.483" y2="632.032" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D59FFF"/>
|
||||
<stop offset="1" stop-color="#8562C5"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
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 { SettingsModal } from "@/components/shared/settings-modal";
|
||||
@@ -12,8 +11,9 @@ 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 { Toast } from "@/components/ui/toast";
|
||||
import { apiClient } from "@/services/api";
|
||||
import { ChevronLeft, ChevronDown, ServerOff } from "lucide-react";
|
||||
import { PanelRightOpen, ChevronDown, ServerOff } from "lucide-react";
|
||||
import type { SampleEntity } from "@/data/gallery";
|
||||
import type {
|
||||
AgentInfo,
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
AppState,
|
||||
ExtendedResponseStreamEvent,
|
||||
} from "@/types";
|
||||
import { Button } from "./components/ui/button";
|
||||
|
||||
export default function App() {
|
||||
const [appState, setAppState] = useState<AppState>({
|
||||
@@ -29,8 +30,13 @@ export default function App() {
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
const [debugEvents, setDebugEvents] = useState<ExtendedResponseStreamEvent[]>([]);
|
||||
const [debugPanelOpen, setDebugPanelOpen] = useState(true);
|
||||
const [debugEvents, setDebugEvents] = useState<ExtendedResponseStreamEvent[]>(
|
||||
[]
|
||||
);
|
||||
const [showDebugPanel, setShowDebugPanel] = useState(() => {
|
||||
const saved = localStorage.getItem("showDebugPanel");
|
||||
return saved !== null ? saved === "true" : true;
|
||||
});
|
||||
const [debugPanelWidth, setDebugPanelWidth] = useState(() => {
|
||||
const savedWidth = localStorage.getItem("debugPanelWidth");
|
||||
return savedWidth ? parseInt(savedWidth, 10) : 320;
|
||||
@@ -41,6 +47,7 @@ export default function App() {
|
||||
const [addingEntityId, setAddingEntityId] = useState<string | null>(null);
|
||||
const [errorEntityId, setErrorEntityId] = useState<string | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [showEntityNotFoundToast, setShowEntityNotFoundToast] = useState(false);
|
||||
|
||||
// Initialize app - load agents and workflows
|
||||
useEffect(() => {
|
||||
@@ -51,16 +58,51 @@ export default function App() {
|
||||
apiClient.getWorkflows(),
|
||||
]);
|
||||
|
||||
setAppState((prev) => ({
|
||||
...prev,
|
||||
agents,
|
||||
workflows,
|
||||
selectedAgent:
|
||||
// Check if there's an entity_id in the URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const entityId = urlParams.get("entity_id");
|
||||
|
||||
let selectedAgent: AgentInfo | WorkflowInfo | undefined;
|
||||
|
||||
// Try to find entity from URL parameter first
|
||||
if (entityId) {
|
||||
selectedAgent =
|
||||
agents.find((a) => a.id === entityId) ||
|
||||
workflows.find((w) => w.id === entityId);
|
||||
|
||||
// If entity not found but was requested, show notification
|
||||
if (!selectedAgent) {
|
||||
setShowEntityNotFoundToast(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to first available entity if URL entity not found
|
||||
if (!selectedAgent) {
|
||||
selectedAgent =
|
||||
agents.length > 0
|
||||
? agents[0]
|
||||
: workflows.length > 0
|
||||
? workflows[0]
|
||||
: undefined,
|
||||
: undefined;
|
||||
|
||||
// Update URL to match actual selected entity (or clear if none)
|
||||
if (selectedAgent) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("entity_id", selectedAgent.id);
|
||||
window.history.replaceState({}, "", url);
|
||||
} else {
|
||||
// Clear entity_id if no entities available
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete("entity_id");
|
||||
window.history.replaceState({}, "", url);
|
||||
}
|
||||
}
|
||||
|
||||
setAppState((prev) => ({
|
||||
...prev,
|
||||
agents,
|
||||
workflows,
|
||||
selectedAgent,
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
@@ -76,7 +118,11 @@ export default function App() {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
// Save debug panel width to localStorage
|
||||
// Save debug panel state to localStorage
|
||||
useEffect(() => {
|
||||
localStorage.setItem("showDebugPanel", showDebugPanel.toString());
|
||||
}, [showDebugPanel]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("debugPanelWidth", debugPanelWidth.toString());
|
||||
}, [debugPanelWidth]);
|
||||
@@ -111,11 +157,6 @@ export default function App() {
|
||||
[debugPanelWidth]
|
||||
);
|
||||
|
||||
// Handle double-click to collapse
|
||||
const handleDoubleClick = useCallback(() => {
|
||||
setDebugPanelOpen(false);
|
||||
}, []);
|
||||
|
||||
// Handle entity selection
|
||||
const handleEntitySelect = useCallback((item: AgentInfo | WorkflowInfo) => {
|
||||
setAppState((prev) => ({
|
||||
@@ -124,18 +165,26 @@ export default function App() {
|
||||
currentThread: undefined,
|
||||
}));
|
||||
|
||||
// Update URL with selected entity ID
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("entity_id", item.id);
|
||||
window.history.pushState({}, "", url);
|
||||
|
||||
// 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]);
|
||||
}
|
||||
}, []);
|
||||
const handleDebugEvent = useCallback(
|
||||
(event: ExtendedResponseStreamEvent | "clear") => {
|
||||
if (event === "clear") {
|
||||
setDebugEvents([]);
|
||||
} else {
|
||||
setDebugEvents((prev) => [...prev, event]);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Handle adding sample entity
|
||||
const handleAddSample = useCallback(async (sample: SampleEntity) => {
|
||||
@@ -146,9 +195,9 @@ export default function App() {
|
||||
try {
|
||||
// Call backend to fetch and add entity
|
||||
const newEntity = await apiClient.addEntity(sample.url, {
|
||||
source: 'remote_gallery',
|
||||
source: "remote_gallery",
|
||||
originalUrl: sample.url,
|
||||
sampleId: sample.id
|
||||
sampleId: sample.id,
|
||||
});
|
||||
|
||||
// Convert backend entity to frontend format
|
||||
@@ -157,52 +206,67 @@ export default function App() {
|
||||
name: newEntity.name,
|
||||
description: newEntity.description,
|
||||
type: newEntity.type,
|
||||
source: (newEntity.source as "directory" | "in_memory" | "remote_gallery") || 'remote_gallery',
|
||||
source:
|
||||
(newEntity.source as "directory" | "in_memory" | "remote_gallery") ||
|
||||
"remote_gallery",
|
||||
has_env: false,
|
||||
module_path: undefined
|
||||
module_path: undefined,
|
||||
};
|
||||
|
||||
// Update app state
|
||||
if (newEntity.type === 'agent') {
|
||||
if (newEntity.type === "agent") {
|
||||
const agentEntity = {
|
||||
...convertedEntity,
|
||||
tools: (newEntity.tools || []).map(tool =>
|
||||
typeof tool === 'string' ? tool : JSON.stringify(tool)
|
||||
)
|
||||
tools: (newEntity.tools || []).map((tool) =>
|
||||
typeof tool === "string" ? tool : JSON.stringify(tool)
|
||||
),
|
||||
} as AgentInfo;
|
||||
|
||||
setAppState(prev => ({
|
||||
setAppState((prev) => ({
|
||||
...prev,
|
||||
agents: [...prev.agents, agentEntity],
|
||||
selectedAgent: agentEntity
|
||||
selectedAgent: agentEntity,
|
||||
}));
|
||||
|
||||
// Update URL with new entity
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("entity_id", agentEntity.id);
|
||||
window.history.pushState({}, "", url);
|
||||
} else {
|
||||
const workflowEntity = {
|
||||
...convertedEntity,
|
||||
executors: (newEntity.tools || []).map(tool =>
|
||||
typeof tool === 'string' ? tool : JSON.stringify(tool)
|
||||
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"
|
||||
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 => ({
|
||||
setAppState((prev) => ({
|
||||
...prev,
|
||||
workflows: [...prev.workflows, workflowEntity],
|
||||
selectedAgent: workflowEntity
|
||||
selectedAgent: workflowEntity,
|
||||
}));
|
||||
|
||||
// Update URL with new entity
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("entity_id", workflowEntity.id);
|
||||
window.history.pushState({}, "", url);
|
||||
}
|
||||
|
||||
// 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);
|
||||
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 {
|
||||
@@ -216,29 +280,35 @@ export default function App() {
|
||||
}, []);
|
||||
|
||||
// Handle removing entity
|
||||
const handleRemoveEntity = useCallback(async (entityId: string) => {
|
||||
try {
|
||||
await apiClient.removeEntity(entityId);
|
||||
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
|
||||
}));
|
||||
// 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([]);
|
||||
// Update URL - clear entity_id if we removed the selected entity
|
||||
if (appState.selectedAgent?.id === entityId) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete("entity_id");
|
||||
window.history.pushState({}, "", url);
|
||||
setDebugEvents([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to remove entity:", error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to remove entity:', error);
|
||||
}
|
||||
}, [appState.selectedAgent?.id]);
|
||||
},
|
||||
[appState.selectedAgent?.id]
|
||||
);
|
||||
|
||||
// Show loading state while initializing
|
||||
if (appState.isLoading) {
|
||||
@@ -293,24 +363,29 @@ export default function App() {
|
||||
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.
|
||||
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>
|
||||
<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>
|
||||
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>
|
||||
Default:{" "}
|
||||
<span className="font-mono">http://localhost:8080</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -339,10 +414,7 @@ export default function App() {
|
||||
</div>
|
||||
|
||||
{/* Settings Modal */}
|
||||
<SettingsModal
|
||||
open={showAboutModal}
|
||||
onOpenChange={setShowAboutModal}
|
||||
/>
|
||||
<SettingsModal open={showAboutModal} onOpenChange={setShowAboutModal} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -373,7 +445,9 @@ export default function App() {
|
||||
errorMessage={errorMessage}
|
||||
onClearError={handleClearError}
|
||||
onClose={() => setShowGallery(false)}
|
||||
hasExistingEntities={appState.agents.length > 0 || appState.workflows.length > 0}
|
||||
hasExistingEntities={
|
||||
appState.agents.length > 0 || appState.workflows.length > 0
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : appState.agents.length === 0 && appState.workflows.length === 0 ? (
|
||||
@@ -409,51 +483,50 @@ export default function App() {
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
{showDebugPanel ? (
|
||||
<>
|
||||
{/* Resize Handle */}
|
||||
<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}
|
||||
>
|
||||
<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
|
||||
/>
|
||||
{/* Right Panel - Debug */}
|
||||
<div
|
||||
className="flex-shrink-0"
|
||||
style={{ width: `${debugPanelWidth}px` }}
|
||||
>
|
||||
<DebugPanel
|
||||
events={debugEvents}
|
||||
isStreaming={false} // Each view manages its own streaming state
|
||||
onClose={() => setShowDebugPanel(false)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* Button to reopen when closed */
|
||||
<div className="flex-shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowDebugPanel(true)}
|
||||
className="h-full w-10 rounded-none border-l"
|
||||
title="Show debug panel"
|
||||
>
|
||||
<PanelRightOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -461,10 +534,16 @@ export default function App() {
|
||||
</div>
|
||||
|
||||
{/* Settings Modal */}
|
||||
<SettingsModal
|
||||
open={showAboutModal}
|
||||
onOpenChange={setShowAboutModal}
|
||||
/>
|
||||
<SettingsModal open={showAboutModal} onOpenChange={setShowAboutModal} />
|
||||
|
||||
{/* Toast Notification */}
|
||||
{showEntityNotFoundToast && (
|
||||
<Toast
|
||||
message="Entity not found. Showing first available entity instead."
|
||||
type="info"
|
||||
onClose={() => setShowEntityNotFoundToast(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { AgentDetailsModal } from "@/components/shared/agent-details-modal";
|
||||
import {
|
||||
SendHorizontal,
|
||||
User,
|
||||
@@ -28,14 +29,9 @@ import {
|
||||
Plus,
|
||||
AlertCircle,
|
||||
Paperclip,
|
||||
Info,
|
||||
Trash2,
|
||||
FileText,
|
||||
ChevronDown,
|
||||
Package,
|
||||
FolderOpen,
|
||||
Database,
|
||||
Globe,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { apiClient } from "@/services/api";
|
||||
import type {
|
||||
@@ -111,8 +107,33 @@ function MessageBubble({ message }: MessageBubbleProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground font-mono">
|
||||
{new Date(message.timestamp).toLocaleTimeString()}
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground font-mono">
|
||||
<span>{new Date(message.timestamp).toLocaleTimeString()}</span>
|
||||
{!isUser && message.usage && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="text-[11px]">
|
||||
{message.usage.total_tokens >= 1000
|
||||
? `${(message.usage.total_tokens / 1000).toFixed(2)}k`
|
||||
: message.usage.total_tokens}{" "}
|
||||
tokens
|
||||
{message.usage.prompt_tokens > 0 && (
|
||||
<span className="opacity-70">
|
||||
{" "}
|
||||
(
|
||||
{message.usage.prompt_tokens >= 1000
|
||||
? `${(message.usage.prompt_tokens / 1000).toFixed(1)}k`
|
||||
: message.usage.prompt_tokens}{" "}
|
||||
in,{" "}
|
||||
{message.usage.completion_tokens >= 1000
|
||||
? `${(message.usage.completion_tokens / 1000).toFixed(1)}k`
|
||||
: message.usage.completion_tokens}{" "}
|
||||
out)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,12 +175,21 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
const [pasteNotification, setPasteNotification] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [detailsExpanded, setDetailsExpanded] = useState(false);
|
||||
const [detailsModalOpen, setDetailsModalOpen] = useState(false);
|
||||
const [threadUsage, setThreadUsage] = useState<{
|
||||
total_tokens: number;
|
||||
message_count: number;
|
||||
}>({ total_tokens: 0, message_count: 0 });
|
||||
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const accumulatedText = useRef<string>("");
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const currentMessageUsage = useRef<{
|
||||
total_tokens: number;
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
} | null>(null);
|
||||
|
||||
// Auto-scroll to bottom when new messages arrive
|
||||
useEffect(() => {
|
||||
@@ -439,12 +469,78 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
messages: [],
|
||||
isStreaming: false,
|
||||
});
|
||||
setThreadUsage({ total_tokens: 0, message_count: 0 });
|
||||
accumulatedText.current = "";
|
||||
} catch (error) {
|
||||
console.error("Failed to create thread:", error);
|
||||
}
|
||||
}, [selectedAgent]);
|
||||
|
||||
// Handle thread deletion
|
||||
const handleDeleteThread = useCallback(
|
||||
async (threadId: string, e?: React.MouseEvent) => {
|
||||
// Prevent event from bubbling to SelectItem
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Confirm deletion
|
||||
if (!confirm("Delete this thread? This cannot be undone.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await apiClient.deleteThread(threadId);
|
||||
if (success) {
|
||||
// Remove thread from available threads
|
||||
const updatedThreads = availableThreads.filter((t) => t.id !== threadId);
|
||||
setAvailableThreads(updatedThreads);
|
||||
|
||||
// If deleted thread was selected, switch to another thread or clear chat
|
||||
if (currentThread?.id === threadId) {
|
||||
if (updatedThreads.length > 0) {
|
||||
// Select the most recent remaining thread
|
||||
const nextThread = updatedThreads[0];
|
||||
setCurrentThread(nextThread);
|
||||
|
||||
// Load messages for the next thread
|
||||
try {
|
||||
const threadMessages = await apiClient.getThreadMessages(nextThread.id);
|
||||
setChatState({
|
||||
messages: threadMessages,
|
||||
isStreaming: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to load thread messages:", error);
|
||||
setChatState({
|
||||
messages: [],
|
||||
isStreaming: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// No threads left, clear everything
|
||||
setCurrentThread(undefined);
|
||||
setChatState({
|
||||
messages: [],
|
||||
isStreaming: false,
|
||||
});
|
||||
setThreadUsage({ total_tokens: 0, message_count: 0 });
|
||||
accumulatedText.current = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Clear debug panel
|
||||
onDebugEvent("clear");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete thread:", error);
|
||||
alert("Failed to delete thread. Please try again.");
|
||||
}
|
||||
},
|
||||
[availableThreads, currentThread, onDebugEvent]
|
||||
);
|
||||
|
||||
// Handle thread selection
|
||||
const handleThreadSelect = useCallback(
|
||||
async (threadId: string) => {
|
||||
@@ -465,6 +561,16 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
isStreaming: false,
|
||||
});
|
||||
|
||||
// Calculate cumulative usage for this thread
|
||||
const totalTokens = threadMessages.reduce(
|
||||
(sum, msg) => sum + (msg.usage?.total_tokens || 0),
|
||||
0
|
||||
);
|
||||
const messageCount = threadMessages.filter(
|
||||
(msg) => msg.role === "assistant" && msg.usage
|
||||
).length;
|
||||
setThreadUsage({ total_tokens: totalTokens, message_count: messageCount });
|
||||
|
||||
console.log(
|
||||
`Restored ${threadMessages.length} messages for thread ${threadId}`
|
||||
);
|
||||
@@ -602,6 +708,20 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
// Pass all events to debug panel
|
||||
onDebugEvent(openAIEvent);
|
||||
|
||||
// Handle usage events
|
||||
if (openAIEvent.type === "response.usage.complete") {
|
||||
const usageEvent = openAIEvent as import("@/types").ResponseUsageEventComplete;
|
||||
console.log("📊 Usage event received:", usageEvent.data);
|
||||
if (usageEvent.data) {
|
||||
currentMessageUsage.current = {
|
||||
total_tokens: usageEvent.data.total_tokens || 0,
|
||||
prompt_tokens: usageEvent.data.prompt_tokens || 0,
|
||||
completion_tokens: usageEvent.data.completion_tokens || 0,
|
||||
};
|
||||
console.log("📊 Set usage:", currentMessageUsage.current);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle error events from the stream
|
||||
if (openAIEvent.type === "error") {
|
||||
const errorEvent = openAIEvent as ExtendedResponseStreamEvent & {
|
||||
@@ -663,14 +783,35 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
// (Server will close the stream when done, so we'll exit the loop naturally)
|
||||
}
|
||||
|
||||
// Stream ended - mark as complete
|
||||
// Stream ended - mark as complete and attach usage
|
||||
const finalUsage = currentMessageUsage.current;
|
||||
console.log("📊 Stream ended, attaching usage to message:", finalUsage);
|
||||
|
||||
setChatState((prev) => ({
|
||||
...prev,
|
||||
isStreaming: false,
|
||||
messages: prev.messages.map((msg) =>
|
||||
msg.id === assistantMessage.id ? { ...msg, streaming: false } : msg
|
||||
msg.id === assistantMessage.id
|
||||
? {
|
||||
...msg,
|
||||
streaming: false,
|
||||
usage: finalUsage || undefined,
|
||||
}
|
||||
: msg
|
||||
),
|
||||
}));
|
||||
|
||||
// Update thread-level usage stats
|
||||
if (finalUsage) {
|
||||
setThreadUsage((prev) => ({
|
||||
total_tokens: prev.total_tokens + finalUsage.total_tokens,
|
||||
message_count: prev.message_count + 1,
|
||||
}));
|
||||
console.log("📊 Updated thread usage");
|
||||
}
|
||||
|
||||
// Reset usage for next message
|
||||
currentMessageUsage.current = null;
|
||||
} catch (error) {
|
||||
console.error("Streaming error:", error);
|
||||
setChatState((prev) => ({
|
||||
@@ -831,14 +972,11 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setDetailsExpanded(!detailsExpanded)}
|
||||
onClick={() => setDetailsModalOpen(true)}
|
||||
className="h-6 w-6 p-0 flex-shrink-0"
|
||||
title="View agent details"
|
||||
>
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform duration-200 ${
|
||||
detailsExpanded ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
<Info className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -849,7 +987,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
onValueChange={handleThreadSelect}
|
||||
disabled={loadingThreads || isSubmitting}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-48">
|
||||
<SelectTrigger className="w-full sm:w-64">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
loadingThreads
|
||||
@@ -860,7 +998,24 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
? `Thread ${currentThread.id.slice(-8)}`
|
||||
: "Select thread"
|
||||
}
|
||||
/>
|
||||
>
|
||||
{currentThread && (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span>Thread {currentThread.id.slice(-8)}</span>
|
||||
{threadUsage.total_tokens > 0 && (
|
||||
<>
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<span className="text-muted-foreground">
|
||||
{threadUsage.total_tokens >= 1000
|
||||
? `${(threadUsage.total_tokens / 1000).toFixed(1)}k`
|
||||
: threadUsage.total_tokens}{" "}
|
||||
tokens
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableThreads.map((thread) => (
|
||||
@@ -878,6 +1033,16 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => currentThread && handleDeleteThread(currentThread.id)}
|
||||
disabled={!currentThread || isSubmitting}
|
||||
title={currentThread ? `Delete Thread ${currentThread.id.slice(-8)}` : "No thread selected"}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
@@ -896,68 +1061,6 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
{selectedAgent.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Collapsible Details Section */}
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-200 ease-in-out ${
|
||||
detailsExpanded ? "max-h-40 mt-3" : "max-h-0"
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-2 text-xs">
|
||||
{/* Tools */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Tools:</span>
|
||||
<span className="font-mono">
|
||||
{selectedAgent.tools.length > 0
|
||||
? selectedAgent.tools.join(", ")
|
||||
: "No tools"}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
({selectedAgent.tools.length})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Source */}
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedAgent.source === "directory" ? (
|
||||
<FolderOpen className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
) : selectedAgent.source === "in_memory" ? (
|
||||
<Database className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<Globe className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
)}
|
||||
<span className="text-muted-foreground">Source:</span>
|
||||
<span>
|
||||
{selectedAgent.source === "directory"
|
||||
? "Local"
|
||||
: selectedAgent.source === "in_memory"
|
||||
? "In-Memory"
|
||||
: "Gallery"}
|
||||
</span>
|
||||
{selectedAgent.module_path && (
|
||||
<span className="text-muted-foreground font-mono text-[11px]">
|
||||
({selectedAgent.module_path})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Environment */}
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedAgent.has_env ? (
|
||||
<XCircle className="h-3.5 w-3.5 text-orange-500" />
|
||||
) : (
|
||||
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
|
||||
)}
|
||||
<span className="text-muted-foreground">Environment:</span>
|
||||
<span>
|
||||
{selectedAgent.has_env
|
||||
? "Requires environment variables"
|
||||
: "No environment variables required"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
@@ -1077,6 +1180,13 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent Details Modal */}
|
||||
<AgentDetailsModal
|
||||
agent={selectedAgent}
|
||||
open={detailsModalOpen}
|
||||
onOpenChange={setDetailsModalOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* AgentDetailsModal - Responsive grid-based modal for displaying agent metadata
|
||||
*/
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogClose,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Bot,
|
||||
Package,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
Database,
|
||||
Globe,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import type { AgentInfo } from "@/types";
|
||||
|
||||
interface AgentDetailsModalProps {
|
||||
agent: AgentInfo;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
interface DetailCardProps {
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function DetailCard({ title, icon, children, className = "" }: DetailCardProps) {
|
||||
return (
|
||||
<div className={`border rounded-lg p-4 bg-card ${className}`}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
{icon}
|
||||
<h3 className="text-sm font-semibold text-foreground">{title}</h3>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentDetailsModal({
|
||||
agent,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: AgentDetailsModalProps) {
|
||||
const sourceIcon =
|
||||
agent.source === "directory" ? (
|
||||
<FolderOpen className="h-4 w-4 text-muted-foreground" />
|
||||
) : agent.source === "in_memory" ? (
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<Globe className="h-4 w-4 text-muted-foreground" />
|
||||
);
|
||||
|
||||
const sourceLabel =
|
||||
agent.source === "directory"
|
||||
? "Local"
|
||||
: agent.source === "in_memory"
|
||||
? "In-Memory"
|
||||
: "Gallery";
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
|
||||
<DialogHeader className="px-6 pt-6 flex-shrink-0">
|
||||
<DialogTitle>Agent Details</DialogTitle>
|
||||
<DialogClose onClose={() => onOpenChange(false)} />
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-6 pb-6 overflow-y-auto flex-1">
|
||||
{/* Header Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Bot className="h-6 w-6 text-primary" />
|
||||
<h2 className="text-xl font-semibold text-foreground">
|
||||
{agent.name || agent.id}
|
||||
</h2>
|
||||
</div>
|
||||
{agent.description && (
|
||||
<p className="text-muted-foreground">{agent.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-border mb-6" />
|
||||
|
||||
{/* Grid Layout for Metadata */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
{/* Model & Client */}
|
||||
{(agent.model || agent.chat_client_type) && (
|
||||
<DetailCard
|
||||
title="Model & Client"
|
||||
icon={<Bot className="h-4 w-4 text-muted-foreground" />}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{agent.model && (
|
||||
<div className="font-mono text-foreground">{agent.model}</div>
|
||||
)}
|
||||
{agent.chat_client_type && (
|
||||
<div className="text-xs">({agent.chat_client_type})</div>
|
||||
)}
|
||||
</div>
|
||||
</DetailCard>
|
||||
)}
|
||||
|
||||
{/* Source */}
|
||||
<DetailCard title="Source" icon={sourceIcon}>
|
||||
<div className="space-y-1">
|
||||
<div className="text-foreground">{sourceLabel}</div>
|
||||
{agent.module_path && (
|
||||
<div className="font-mono text-xs break-all">
|
||||
{agent.module_path}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DetailCard>
|
||||
|
||||
{/* Environment */}
|
||||
<DetailCard
|
||||
title="Environment"
|
||||
icon={
|
||||
agent.has_env ? (
|
||||
<XCircle className="h-4 w-4 text-orange-500" />
|
||||
) : (
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
)
|
||||
}
|
||||
className="md:col-span-2"
|
||||
>
|
||||
<div
|
||||
className={
|
||||
agent.has_env ? "text-orange-600 dark:text-orange-400" : "text-green-600 dark:text-green-400"
|
||||
}
|
||||
>
|
||||
{agent.has_env
|
||||
? "Requires environment variables"
|
||||
: "No environment variables required"}
|
||||
</div>
|
||||
</DetailCard>
|
||||
</div>
|
||||
|
||||
{/* Full Width Sections */}
|
||||
{agent.instructions && (
|
||||
<DetailCard
|
||||
title="Instructions"
|
||||
icon={<FileText className="h-4 w-4 text-muted-foreground" />}
|
||||
className="mb-4"
|
||||
>
|
||||
<div className="text-sm text-foreground leading-relaxed whitespace-pre-wrap">
|
||||
{agent.instructions}
|
||||
</div>
|
||||
</DetailCard>
|
||||
)}
|
||||
|
||||
{/* Tools and Middleware Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Tools */}
|
||||
<DetailCard
|
||||
title={`Tools (${agent.tools.length})`}
|
||||
icon={<Package className="h-4 w-4 text-muted-foreground" />}
|
||||
>
|
||||
{agent.tools.length > 0 ? (
|
||||
<ul className="space-y-1">
|
||||
{agent.tools.map((tool, index) => (
|
||||
<li key={index} className="font-mono text-xs text-foreground">
|
||||
• {tool}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="text-muted-foreground">No tools configured</div>
|
||||
)}
|
||||
</DetailCard>
|
||||
|
||||
{/* Middleware */}
|
||||
{agent.middleware && agent.middleware.length > 0 && (
|
||||
<DetailCard
|
||||
title={`Middleware (${agent.middleware.length})`}
|
||||
icon={<Package className="h-4 w-4 text-muted-foreground" />}
|
||||
>
|
||||
<ul className="space-y-1">
|
||||
{agent.middleware.map((mw, index) => (
|
||||
<li key={index} className="font-mono text-xs text-foreground">
|
||||
• {mw}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</DetailCard>
|
||||
)}
|
||||
|
||||
{/* Context Providers */}
|
||||
{agent.context_providers && agent.context_providers.length > 0 && (
|
||||
<DetailCard
|
||||
title={`Context Providers (${agent.context_providers.length})`}
|
||||
icon={<Database className="h-4 w-4 text-muted-foreground" />}
|
||||
className={!agent.middleware || agent.middleware.length === 0 ? "md:col-start-2" : ""}
|
||||
>
|
||||
<ul className="space-y-1">
|
||||
{agent.context_providers.map((cp, index) => (
|
||||
<li key={index} className="font-mono text-xs text-foreground">
|
||||
• {cp}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</DetailCard>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -32,7 +32,35 @@ export function AppHeader({
|
||||
}: AppHeaderProps) {
|
||||
return (
|
||||
<header className="flex h-14 items-center gap-4 border-b px-4">
|
||||
<div className="font-semibold">Dev UI</div>
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 805 805"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<path
|
||||
d="M402.488 119.713C439.197 119.713 468.955 149.472 468.955 186.18C468.955 192.086 471.708 197.849 476.915 200.635L546.702 237.977C555.862 242.879 566.95 240.96 576.092 236.023C585.476 230.955 596.218 228.078 607.632 228.078C644.341 228.078 674.098 257.836 674.099 294.545C674.099 316.95 663.013 336.765 646.028 348.806C637.861 354.595 631.412 363.24 631.412 373.251V430.818C631.412 440.83 637.861 449.475 646.028 455.264C663.013 467.305 674.099 487.121 674.099 509.526C674.099 546.235 644.341 575.994 607.632 575.994C598.598 575.994 589.985 574.191 582.133 570.926C573.644 567.397 563.91 566.393 555.804 570.731L469.581 616.867C469.193 617.074 468.955 617.479 468.955 617.919C468.955 654.628 439.197 684.386 402.488 684.386C365.779 684.386 336.021 654.628 336.021 617.919C336.021 616.802 335.423 615.765 334.439 615.238L249.895 570C241.61 565.567 231.646 566.713 223.034 570.472C214.898 574.024 205.914 575.994 196.47 575.994C159.761 575.994 130.002 546.235 130.002 509.526C130.002 486.66 141.549 466.49 159.13 454.531C167.604 448.766 174.349 439.975 174.349 429.726V372.538C174.349 362.289 167.604 353.498 159.13 347.734C141.549 335.774 130.002 315.604 130.002 292.738C130.002 256.029 159.761 226.271 196.47 226.271C208.223 226.271 219.263 229.322 228.843 234.674C238.065 239.827 249.351 241.894 258.666 236.91L328.655 199.459C333.448 196.895 336.021 191.616 336.021 186.18C336.021 149.471 365.779 119.713 402.488 119.713ZM475.716 394.444C471.337 396.787 468.955 401.586 468.955 406.552C468.955 429.68 457.142 450.048 439.221 461.954C430.571 467.7 423.653 476.574 423.653 486.959V537.511C423.653 547.896 430.746 556.851 439.379 562.622C449 569.053 461.434 572.052 471.637 566.592L527.264 536.826C536.887 531.677 541.164 520.44 541.164 509.526C541.164 485.968 553.42 465.272 571.904 453.468C580.846 447.757 588.054 438.749 588.054 428.139V371.427C588.054 363.494 582.671 356.676 575.716 352.862C569.342 349.366 561.663 348.454 555.253 351.884L475.716 394.444ZM247.992 349.841C241.997 346.633 234.806 347.465 228.873 350.785C222.524 354.337 217.706 360.639 217.706 367.915V429.162C217.706 439.537 224.611 448.404 233.248 454.152C251.144 466.062 262.937 486.417 262.937 509.526C262.937 519.654 267.026 529.991 275.955 534.769L334.852 566.284C344.582 571.49 356.362 568.81 365.528 562.667C373.735 557.166 380.296 548.643 380.296 538.764V486.305C380.296 476.067 373.564 467.282 365.103 461.516C347.548 449.552 336.021 429.398 336.021 406.552C336.021 400.967 333.389 395.536 328.465 392.902L247.992 349.841ZM270.019 280.008C265.421 282.469 262.936 287.522 262.937 292.738C262.937 293.308 262.929 293.876 262.915 294.443C262.615 306.354 266.961 318.871 277.466 324.492L334.017 354.751C344.13 360.163 356.442 357.269 366.027 350.969C376.495 344.088 389.024 340.085 402.488 340.085C416.203 340.085 428.947 344.239 439.532 351.357C449.163 357.834 461.63 360.861 471.864 355.385L526.625 326.083C537.106 320.474 541.458 307.999 541.182 296.115C541.17 295.593 541.164 295.069 541.164 294.545C541.164 288.551 538.376 282.696 533.091 279.868L463.562 242.664C454.384 237.753 443.274 239.688 434.123 244.65C424.716 249.75 413.941 252.647 402.488 252.647C390.83 252.647 379.873 249.646 370.348 244.373C361.148 239.281 349.917 237.256 340.646 242.217L270.019 280.008Z"
|
||||
fill="url(#paint0_linear_510_1294)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_510_1294"
|
||||
x1="255.628"
|
||||
y1="-34.3245"
|
||||
x2="618.483"
|
||||
y2="632.032"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#D59FFF" />
|
||||
<stop offset="1" stopColor="#8562C5" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
Dev UI
|
||||
</div>
|
||||
<EntitySelector
|
||||
agents={agents}
|
||||
workflows={workflows}
|
||||
@@ -51,4 +79,4 @@ export function AppHeader({
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useRef, useState } from "react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Activity,
|
||||
Search,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Info,
|
||||
PanelRightClose,
|
||||
} from "lucide-react";
|
||||
import type { ExtendedResponseStreamEvent } from "@/types";
|
||||
|
||||
@@ -65,6 +67,7 @@ interface TraceEventData extends EventDataBase {
|
||||
interface DebugPanelProps {
|
||||
events: ExtendedResponseStreamEvent[];
|
||||
isStreaming?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
// Helper function to accumulate OpenAI events into meaningful units
|
||||
@@ -1359,12 +1362,16 @@ function ToolEventItem({ event }: { event: ExtendedResponseStreamEvent }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function DebugPanel({ events, isStreaming = false }: DebugPanelProps) {
|
||||
export function DebugPanel({
|
||||
events,
|
||||
isStreaming = false,
|
||||
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">
|
||||
<TabsList className="w-full">
|
||||
<div className="px-3 pt-3 flex items-center gap-2">
|
||||
<TabsList className="flex-1">
|
||||
<TabsTrigger value="events" className="flex-1">
|
||||
Events
|
||||
</TabsTrigger>
|
||||
@@ -1375,6 +1382,17 @@ export function DebugPanel({ events, isStreaming = false }: DebugPanelProps) {
|
||||
Tools
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
{onClose && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="h-8 w-8 p-0 flex-shrink-0"
|
||||
title="Hide debug panel"
|
||||
>
|
||||
<PanelRightClose className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TabsContent value="events" className="flex-1 mt-0">
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* WorkflowDetailsModal - Responsive grid-based modal for displaying workflow metadata
|
||||
*/
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogClose,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Workflow as WorkflowIcon,
|
||||
Package,
|
||||
FolderOpen,
|
||||
Database,
|
||||
Globe,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
PlayCircle,
|
||||
} from "lucide-react";
|
||||
import type { WorkflowInfo } from "@/types";
|
||||
|
||||
interface WorkflowDetailsModalProps {
|
||||
workflow: WorkflowInfo;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
interface DetailCardProps {
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function DetailCard({ title, icon, children, className = "" }: DetailCardProps) {
|
||||
return (
|
||||
<div className={`border rounded-lg p-4 bg-card ${className}`}>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
{icon}
|
||||
<h3 className="text-sm font-semibold text-foreground">{title}</h3>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkflowDetailsModal({
|
||||
workflow,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: WorkflowDetailsModalProps) {
|
||||
const sourceIcon =
|
||||
workflow.source === "directory" ? (
|
||||
<FolderOpen className="h-4 w-4 text-muted-foreground" />
|
||||
) : workflow.source === "in_memory" ? (
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<Globe className="h-4 w-4 text-muted-foreground" />
|
||||
);
|
||||
|
||||
const sourceLabel =
|
||||
workflow.source === "directory"
|
||||
? "Local"
|
||||
: workflow.source === "in_memory"
|
||||
? "In-Memory"
|
||||
: "Gallery";
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
|
||||
<DialogHeader className="px-6 pt-6 flex-shrink-0">
|
||||
<DialogTitle>Workflow Details</DialogTitle>
|
||||
<DialogClose onClose={() => onOpenChange(false)} />
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-6 pb-6 overflow-y-auto flex-1">
|
||||
{/* Header Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<WorkflowIcon className="h-6 w-6 text-primary" />
|
||||
<h2 className="text-xl font-semibold text-foreground">
|
||||
{workflow.name || workflow.id}
|
||||
</h2>
|
||||
</div>
|
||||
{workflow.description && (
|
||||
<p className="text-muted-foreground">{workflow.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-border mb-6" />
|
||||
|
||||
{/* Grid Layout for Metadata */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
{/* Start Executor */}
|
||||
<DetailCard
|
||||
title="Start Executor"
|
||||
icon={<PlayCircle className="h-4 w-4 text-muted-foreground" />}
|
||||
>
|
||||
<div className="font-mono text-foreground">
|
||||
{workflow.start_executor_id}
|
||||
</div>
|
||||
</DetailCard>
|
||||
|
||||
{/* Source */}
|
||||
<DetailCard title="Source" icon={sourceIcon}>
|
||||
<div className="space-y-1">
|
||||
<div className="text-foreground">{sourceLabel}</div>
|
||||
{workflow.module_path && (
|
||||
<div className="font-mono text-xs break-all">
|
||||
{workflow.module_path}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DetailCard>
|
||||
|
||||
{/* Environment */}
|
||||
<DetailCard
|
||||
title="Environment"
|
||||
icon={
|
||||
workflow.has_env ? (
|
||||
<XCircle className="h-4 w-4 text-orange-500" />
|
||||
) : (
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
)
|
||||
}
|
||||
className="md:col-span-2"
|
||||
>
|
||||
<div
|
||||
className={
|
||||
workflow.has_env
|
||||
? "text-orange-600 dark:text-orange-400"
|
||||
: "text-green-600 dark:text-green-400"
|
||||
}
|
||||
>
|
||||
{workflow.has_env
|
||||
? "Requires environment variables"
|
||||
: "No environment variables required"}
|
||||
</div>
|
||||
</DetailCard>
|
||||
</div>
|
||||
|
||||
{/* Executors */}
|
||||
<DetailCard
|
||||
title={`Executors (${workflow.executors.length})`}
|
||||
icon={<Package className="h-4 w-4 text-muted-foreground" />}
|
||||
>
|
||||
{workflow.executors.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{workflow.executors.map((executor, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="font-mono text-xs text-foreground bg-muted px-2 py-1 rounded"
|
||||
>
|
||||
{executor}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground">No executors configured</div>
|
||||
)}
|
||||
</DetailCard>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Simple toast notification component
|
||||
* Displays floating notifications in the top-right corner
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
export interface ToastProps {
|
||||
message: string;
|
||||
type?: "info" | "success" | "warning" | "error";
|
||||
duration?: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function Toast({ message, type = "info", duration = 4000, onClose }: ToastProps) {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
setTimeout(onClose, 300); // Wait for fade out animation
|
||||
}, duration);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [duration, onClose]);
|
||||
|
||||
const bgColorClass = {
|
||||
info: "bg-primary/10 border-primary/20",
|
||||
success: "bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800",
|
||||
warning: "bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800",
|
||||
error: "bg-red-50 dark:bg-red-950 border-red-200 dark:border-red-800",
|
||||
}[type];
|
||||
|
||||
const textColorClass = {
|
||||
info: "text-primary",
|
||||
success: "text-green-800 dark:text-green-200",
|
||||
warning: "text-orange-800 dark:text-orange-200",
|
||||
error: "text-red-800 dark:text-red-200",
|
||||
}[type];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed top-4 right-4 z-50 flex items-start gap-3 p-4 rounded-lg border shadow-lg max-w-md transition-all duration-300 ${
|
||||
isVisible ? "opacity-100 translate-x-0" : "opacity-0 translate-x-4"
|
||||
} ${bgColorClass}`}
|
||||
>
|
||||
<p className={`text-sm flex-1 ${textColorClass}`}>{message}</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsVisible(false);
|
||||
setTimeout(onClose, 300);
|
||||
}}
|
||||
className={`flex-shrink-0 hover:opacity-70 transition-opacity ${textColorClass}`}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Toast container for managing multiple toasts
|
||||
export interface ToastData {
|
||||
id: string;
|
||||
message: string;
|
||||
type?: "info" | "success" | "warning" | "error";
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
interface ToastContainerProps {
|
||||
toasts: ToastData[];
|
||||
onRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
export function ToastContainer({ toasts, onRemove }: ToastContainerProps) {
|
||||
return (
|
||||
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2">
|
||||
{toasts.map((toast) => (
|
||||
<Toast
|
||||
key={toast.id}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
duration={toast.duration}
|
||||
onClose={() => onRemove(toast.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
DialogClose,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Send } from "lucide-react";
|
||||
import { Send, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { JSONSchemaProperty } from "@/types";
|
||||
|
||||
@@ -29,22 +29,27 @@ interface FormFieldProps {
|
||||
schema: JSONSchemaProperty;
|
||||
value: unknown;
|
||||
onChange: (value: unknown) => void;
|
||||
isRequired?: boolean;
|
||||
}
|
||||
|
||||
function FormField({ name, schema, value, onChange }: FormFieldProps) {
|
||||
function FormField({ name, schema, value, onChange, isRequired = false }: FormFieldProps) {
|
||||
const { type, description, enum: enumValues, default: defaultValue } = schema;
|
||||
|
||||
// For text/message/content fields, treat as textarea for better UX
|
||||
const isTextContentField = ['text', 'message', 'content', 'query', 'prompt'].includes(name.toLowerCase());
|
||||
|
||||
// Determine if this field should span full width
|
||||
// Only span full if it's a textarea or has very long description
|
||||
const shouldSpanFullWidth =
|
||||
schema.format === "textarea" ||
|
||||
(description && description.length > 100) ||
|
||||
type === "object" ||
|
||||
type === "array";
|
||||
isTextContentField || // text/message fields span full width
|
||||
(description && description.length > 150);
|
||||
|
||||
const shouldSpanTwoColumns =
|
||||
type === "object" ||
|
||||
schema.format === "textarea" ||
|
||||
(description && description.length > 50);
|
||||
isTextContentField ||
|
||||
(description && description.length > 80) ||
|
||||
type === "array"; // Arrays might need more space for comma-separated values
|
||||
|
||||
const fieldContent = (() => {
|
||||
// Handle different field types based on JSON Schema
|
||||
@@ -54,7 +59,10 @@ function FormField({ name, schema, value, onChange }: FormFieldProps) {
|
||||
// Enum select
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={name}>{name}</Label>
|
||||
<Label htmlFor={name}>
|
||||
{name}
|
||||
{isRequired && <span className="text-destructive ml-1">*</span>}
|
||||
</Label>
|
||||
<Select
|
||||
value={
|
||||
typeof value === "string" && value
|
||||
@@ -83,12 +91,16 @@ function FormField({ name, schema, value, onChange }: FormFieldProps) {
|
||||
);
|
||||
} else if (
|
||||
schema.format === "textarea" ||
|
||||
isTextContentField ||
|
||||
(description && description.length > 100)
|
||||
) {
|
||||
// Multi-line text
|
||||
// Multi-line text (including text/message/content fields)
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={name}>{name}</Label>
|
||||
<Label htmlFor={name}>
|
||||
{name}
|
||||
{isRequired && <span className="text-destructive ml-1">*</span>}
|
||||
</Label>
|
||||
<Textarea
|
||||
id={name}
|
||||
value={typeof value === "string" ? value : ""}
|
||||
@@ -98,7 +110,7 @@ function FormField({ name, schema, value, onChange }: FormFieldProps) {
|
||||
? defaultValue
|
||||
: `Enter ${name}`
|
||||
}
|
||||
rows={2}
|
||||
rows={isTextContentField ? 4 : 2}
|
||||
/>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
@@ -109,7 +121,10 @@ function FormField({ name, schema, value, onChange }: FormFieldProps) {
|
||||
// Single-line text
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={name}>{name}</Label>
|
||||
<Label htmlFor={name}>
|
||||
{name}
|
||||
{isRequired && <span className="text-destructive ml-1">*</span>}
|
||||
</Label>
|
||||
<Input
|
||||
id={name}
|
||||
type="text"
|
||||
@@ -131,7 +146,10 @@ function FormField({ name, schema, value, onChange }: FormFieldProps) {
|
||||
case "number":
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={name}>{name}</Label>
|
||||
<Label htmlFor={name}>
|
||||
{name}
|
||||
{isRequired && <span className="text-destructive ml-1">*</span>}
|
||||
</Label>
|
||||
<Input
|
||||
id={name}
|
||||
type="number"
|
||||
@@ -161,7 +179,10 @@ function FormField({ name, schema, value, onChange }: FormFieldProps) {
|
||||
checked={Boolean(value)}
|
||||
onCheckedChange={(checked) => onChange(checked)}
|
||||
/>
|
||||
<Label htmlFor={name}>{name}</Label>
|
||||
<Label htmlFor={name}>
|
||||
{name}
|
||||
{isRequired && <span className="text-destructive ml-1">*</span>}
|
||||
</Label>
|
||||
</div>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
@@ -172,7 +193,10 @@ function FormField({ name, schema, value, onChange }: FormFieldProps) {
|
||||
case "array":
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={name}>{name}</Label>
|
||||
<Label htmlFor={name}>
|
||||
{name}
|
||||
{isRequired && <span className="text-destructive ml-1">*</span>}
|
||||
</Label>
|
||||
<Textarea
|
||||
id={name}
|
||||
value={
|
||||
@@ -203,7 +227,10 @@ function FormField({ name, schema, value, onChange }: FormFieldProps) {
|
||||
// For complex objects or unknown types, use JSON textarea
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={name}>{name}</Label>
|
||||
<Label htmlFor={name}>
|
||||
{name}
|
||||
{isRequired && <span className="text-destructive ml-1">*</span>}
|
||||
</Label>
|
||||
<Textarea
|
||||
id={name}
|
||||
value={
|
||||
@@ -235,7 +262,7 @@ function FormField({ name, schema, value, onChange }: FormFieldProps) {
|
||||
|
||||
// Return the field with appropriate grid column spanning
|
||||
const getColumnSpan = () => {
|
||||
if (shouldSpanFullWidth) return "md:col-span-3 xl:col-span-4";
|
||||
if (shouldSpanFullWidth) return "md:col-span-2 lg:col-span-3 xl:col-span-4";
|
||||
if (shouldSpanTwoColumns) return "xl:col-span-2";
|
||||
return "";
|
||||
};
|
||||
@@ -259,6 +286,7 @@ export function WorkflowInputForm({
|
||||
className,
|
||||
}: WorkflowInputFormProps) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [showAdvancedFields, setShowAdvancedFields] = useState(false);
|
||||
|
||||
// Check if we're in embedded mode (being used inside another modal)
|
||||
const isEmbedded = className?.includes('embedded');
|
||||
@@ -268,10 +296,55 @@ export function WorkflowInputForm({
|
||||
// Determine field info
|
||||
const properties = inputSchema.properties || {};
|
||||
const fieldNames = Object.keys(properties);
|
||||
const requiredFields = inputSchema.required || [];
|
||||
const isSimpleInput = inputSchema.type === "string" && !inputSchema.enum;
|
||||
const primaryField = isSimpleInput ? "value" : fieldNames[0];
|
||||
const canSubmit = primaryField
|
||||
? formData[primaryField] !== undefined && formData[primaryField] !== ""
|
||||
|
||||
// Plan D: Separate required and optional fields first
|
||||
const allOptionalFieldNames = fieldNames.filter(name => !requiredFields.includes(name));
|
||||
|
||||
// Detect ChatMessage-like pattern
|
||||
const isChatMessageLike =
|
||||
requiredFields.includes('role') &&
|
||||
allOptionalFieldNames.some(f => ['text', 'message', 'content'].includes(f)) &&
|
||||
properties['role']?.type === 'string';
|
||||
|
||||
// For ChatMessage: hide 'role' field (will be auto-filled)
|
||||
const requiredFieldNames = fieldNames.filter(name =>
|
||||
requiredFields.includes(name) && !(isChatMessageLike && name === 'role')
|
||||
);
|
||||
const optionalFieldNames = allOptionalFieldNames;
|
||||
|
||||
// For ChatMessage: prioritize text/message/content field to show first
|
||||
const sortedOptionalFields = isChatMessageLike
|
||||
? [...optionalFieldNames].sort((a, b) => {
|
||||
const priority = (name: string) =>
|
||||
['text', 'message', 'content'].includes(name) ? 1 : 0;
|
||||
return priority(b) - priority(a);
|
||||
})
|
||||
: optionalFieldNames;
|
||||
|
||||
// Always show ALL required fields + fill to minimum visible with optional fields
|
||||
// For ChatMessage: show only 1 optional field (text)
|
||||
const MIN_VISIBLE_FIELDS = isChatMessageLike ? 1 : 6;
|
||||
const visibleOptionalCount = Math.max(0, MIN_VISIBLE_FIELDS - requiredFieldNames.length);
|
||||
const visibleOptionalFields = sortedOptionalFields.slice(0, visibleOptionalCount);
|
||||
const collapsedOptionalFields = sortedOptionalFields.slice(visibleOptionalCount);
|
||||
|
||||
const hasCollapsedFields = collapsedOptionalFields.length > 0;
|
||||
const hasRequiredFields = requiredFieldNames.length > 0;
|
||||
|
||||
// Update canSubmit to check required fields properly
|
||||
// For ChatMessage: role is auto-filled, so it's always valid
|
||||
const canSubmit = isSimpleInput
|
||||
? formData.value !== undefined && formData.value !== ""
|
||||
: requiredFields.length > 0
|
||||
? requiredFields.every(fieldName => {
|
||||
// Auto-filled fields are always valid
|
||||
if (isChatMessageLike && fieldName === 'role' && formData['role'] === 'user') {
|
||||
return true;
|
||||
}
|
||||
return formData[fieldName] !== undefined && formData[fieldName] !== "";
|
||||
})
|
||||
: Object.keys(formData).length > 0;
|
||||
|
||||
// Initialize form data
|
||||
@@ -287,9 +360,15 @@ export function WorkflowInputForm({
|
||||
initialData[key] = fieldSchema.enum[0];
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-fill role="user" for ChatMessage-like inputs
|
||||
if (isChatMessageLike && !initialData['role']) {
|
||||
initialData['role'] = 'user';
|
||||
}
|
||||
|
||||
setFormData(initialData);
|
||||
}
|
||||
}, [inputSchema]);
|
||||
}, [inputSchema, isChatMessageLike]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -306,7 +385,16 @@ export function WorkflowInputForm({
|
||||
const fieldName = fieldNames[0];
|
||||
onSubmit({ [fieldName]: formData[fieldName] || "" });
|
||||
} else {
|
||||
onSubmit(formData);
|
||||
// Filter out empty optional fields before submission
|
||||
const filteredData: Record<string, unknown> = {};
|
||||
Object.keys(formData).forEach(key => {
|
||||
const value = formData[key];
|
||||
// Include if: 1) required field, OR 2) has non-empty value
|
||||
if (requiredFields.includes(key) || (value !== undefined && value !== "" && value !== null)) {
|
||||
filteredData[key] = value;
|
||||
}
|
||||
});
|
||||
onSubmit(filteredData);
|
||||
}
|
||||
} else {
|
||||
onSubmit(formData);
|
||||
@@ -330,27 +418,84 @@ export function WorkflowInputForm({
|
||||
if (isEmbedded) {
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={className}>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Simple input */}
|
||||
{isSimpleInput && primaryField && (
|
||||
{isSimpleInput && (
|
||||
<FormField
|
||||
name="Input"
|
||||
schema={inputSchema}
|
||||
value={formData.value}
|
||||
onChange={(value) => updateField("value", value)}
|
||||
isRequired={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Complex form fields */}
|
||||
{/* Complex form fields - Plan D: Required + Optional separation */}
|
||||
{!isSimpleInput && (
|
||||
<>
|
||||
{fieldNames.map((fieldName) => (
|
||||
{/* Required fields section */}
|
||||
{requiredFieldNames.map((fieldName) => (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
name={fieldName}
|
||||
schema={properties[fieldName] as JSONSchemaProperty}
|
||||
value={formData[fieldName]}
|
||||
onChange={(value) => updateField(fieldName, value)}
|
||||
isRequired={true}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Separator between required and optional (only if both exist) */}
|
||||
{hasRequiredFields && optionalFieldNames.length > 0 && (
|
||||
<div className="sm:col-span-2 border-t border-border my-2"></div>
|
||||
)}
|
||||
|
||||
{/* Visible optional fields */}
|
||||
{visibleOptionalFields.map((fieldName) => (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
name={fieldName}
|
||||
schema={properties[fieldName] as JSONSchemaProperty}
|
||||
value={formData[fieldName]}
|
||||
onChange={(value) => updateField(fieldName, value)}
|
||||
isRequired={false}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Collapsed optional fields toggle */}
|
||||
{hasCollapsedFields && (
|
||||
<div className="sm:col-span-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowAdvancedFields(!showAdvancedFields)}
|
||||
className="w-full justify-center gap-2"
|
||||
>
|
||||
{showAdvancedFields ? (
|
||||
<>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
Hide {collapsedOptionalFields.length} optional field{collapsedOptionalFields.length !== 1 ? 's' : ''}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
Show {collapsedOptionalFields.length} optional field{collapsedOptionalFields.length !== 1 ? 's' : ''}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Collapsed optional fields - only show when toggled */}
|
||||
{showAdvancedFields && collapsedOptionalFields.map((fieldName) => (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
name={fieldName}
|
||||
schema={properties[fieldName] as JSONSchemaProperty}
|
||||
value={formData[fieldName]}
|
||||
onChange={(value) => updateField(fieldName, value)}
|
||||
isRequired={false}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
@@ -440,15 +585,16 @@ export function WorkflowInputForm({
|
||||
{/* Scrollable Form Content */}
|
||||
<div className="px-8 py-6 overflow-y-auto flex-1 min-h-0">
|
||||
<form id="workflow-modal-form" onSubmit={handleSubmit}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-4 gap-8 max-w-none">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 md:gap-8 max-w-none">
|
||||
{/* Simple input */}
|
||||
{isSimpleInput && primaryField && (
|
||||
<div className="md:col-span-3 xl:col-span-4">
|
||||
{isSimpleInput && (
|
||||
<div className="md:col-span-2 lg:col-span-3 xl:col-span-4">
|
||||
<FormField
|
||||
name="Input"
|
||||
schema={inputSchema}
|
||||
value={formData.value}
|
||||
onChange={(value) => updateField("value", value)}
|
||||
isRequired={false}
|
||||
/>
|
||||
{inputSchema.description && (
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
@@ -458,16 +604,74 @@ export function WorkflowInputForm({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Complex form fields - Show all */}
|
||||
{/* Complex form fields - Plan D: Required + Optional separation */}
|
||||
{!isSimpleInput && (
|
||||
<>
|
||||
{fieldNames.map((fieldName) => (
|
||||
{/* Required fields section */}
|
||||
{requiredFieldNames.map((fieldName) => (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
name={fieldName}
|
||||
schema={properties[fieldName] as JSONSchemaProperty}
|
||||
value={formData[fieldName]}
|
||||
onChange={(value) => updateField(fieldName, value)}
|
||||
isRequired={true}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Separator between required and optional (only if both exist) */}
|
||||
{hasRequiredFields && optionalFieldNames.length > 0 && (
|
||||
<div className="md:col-span-2 lg:col-span-3 xl:col-span-4">
|
||||
<div className="border-t border-border"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Visible optional fields */}
|
||||
{visibleOptionalFields.map((fieldName) => (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
name={fieldName}
|
||||
schema={properties[fieldName] as JSONSchemaProperty}
|
||||
value={formData[fieldName]}
|
||||
onChange={(value) => updateField(fieldName, value)}
|
||||
isRequired={false}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Collapsed optional fields toggle */}
|
||||
{hasCollapsedFields && (
|
||||
<div className="md:col-span-2 lg:col-span-3 xl:col-span-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowAdvancedFields(!showAdvancedFields)}
|
||||
className="w-full justify-center gap-2"
|
||||
>
|
||||
{showAdvancedFields ? (
|
||||
<>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
Hide {collapsedOptionalFields.length} optional field{collapsedOptionalFields.length !== 1 ? 's' : ''}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
Show {collapsedOptionalFields.length} optional field{collapsedOptionalFields.length !== 1 ? 's' : ''}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Collapsed optional fields - only show when toggled */}
|
||||
{showAdvancedFields && collapsedOptionalFields.map((fieldName) => (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
name={fieldName}
|
||||
schema={properties[fieldName] as JSONSchemaProperty}
|
||||
value={formData[fieldName]}
|
||||
onChange={(value) => updateField(fieldName, value)}
|
||||
isRequired={false}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -11,12 +11,7 @@ import {
|
||||
Play,
|
||||
Settings,
|
||||
RotateCcw,
|
||||
ChevronDown,
|
||||
Package,
|
||||
FolderOpen,
|
||||
Database,
|
||||
Globe,
|
||||
XCircle,
|
||||
Info,
|
||||
Workflow as WorkflowIcon,
|
||||
} from "lucide-react";
|
||||
import { LoadingState } from "@/components/ui/loading-state";
|
||||
@@ -24,6 +19,7 @@ import { WorkflowInputForm } from "@/components/workflow/workflow-input-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { WorkflowFlow } from "@/components/workflow/workflow-flow";
|
||||
import { useWorkflowEventCorrelation } from "@/hooks/useWorkflowEventCorrelation";
|
||||
import { WorkflowDetailsModal } from "@/components/shared/workflow-details-modal";
|
||||
import { apiClient } from "@/services/api";
|
||||
import type {
|
||||
WorkflowInfo,
|
||||
@@ -190,11 +186,12 @@ function RunWorkflowButton({
|
||||
variant={
|
||||
buttonVariant === "destructive" ? "destructive" : "default"
|
||||
}
|
||||
size="icon"
|
||||
className="rounded-l-none border-l-0 w-9"
|
||||
title="Configure inputs"
|
||||
size="default"
|
||||
className="rounded-l-none border-l-0 px-3"
|
||||
title="Configure workflow inputs - customize parameters before running"
|
||||
>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
<Settings className="w-4 h-4" />
|
||||
<span className="ml-1.5">Inputs</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -273,7 +270,7 @@ export function WorkflowView({
|
||||
const [workflowResult, setWorkflowResult] = useState<string>("");
|
||||
const [workflowError, setWorkflowError] = useState<string>("");
|
||||
const accumulatedText = useRef<string>("");
|
||||
const [detailsExpanded, setDetailsExpanded] = useState(false);
|
||||
const [detailsModalOpen, setDetailsModalOpen] = useState(false);
|
||||
|
||||
// Panel resize state
|
||||
const [bottomPanelHeight, setBottomPanelHeight] = useState(() => {
|
||||
@@ -554,14 +551,11 @@ export function WorkflowView({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setDetailsExpanded(!detailsExpanded)}
|
||||
onClick={() => setDetailsModalOpen(true)}
|
||||
className="h-6 w-6 p-0 flex-shrink-0"
|
||||
title="View workflow details"
|
||||
>
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform duration-200 ${
|
||||
detailsExpanded ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
<Info className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -593,78 +587,6 @@ export function WorkflowView({
|
||||
{selectedWorkflow.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Executors - Always visible */}
|
||||
{selectedWorkflow.executors.length > 0 && (
|
||||
<div className="flex items-center gap-2 text-xs mt-2 mb-2">
|
||||
<Package className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Executors:</span>
|
||||
<span className="font-mono">
|
||||
{selectedWorkflow.executors.slice(0, 3).join(", ")}
|
||||
{selectedWorkflow.executors.length > 3 && "..."}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
({selectedWorkflow.executors.length})
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Collapsible Details Section */}
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-200 ease-in-out ${
|
||||
detailsExpanded ? "max-h-40 mt-3" : "max-h-0"
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-2 text-xs">
|
||||
{/* Start Executor */}
|
||||
<div className="flex items-center gap-2">
|
||||
<WorkflowIcon className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Start:</span>
|
||||
<span className="font-mono">
|
||||
{selectedWorkflow.start_executor_id}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Source */}
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedWorkflow.source === "directory" ? (
|
||||
<FolderOpen className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
) : selectedWorkflow.source === "in_memory" ? (
|
||||
<Database className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<Globe className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
)}
|
||||
<span className="text-muted-foreground">Source:</span>
|
||||
<span>
|
||||
{selectedWorkflow.source === "directory"
|
||||
? "Local"
|
||||
: selectedWorkflow.source === "in_memory"
|
||||
? "In-Memory"
|
||||
: "Gallery"}
|
||||
</span>
|
||||
{selectedWorkflow.module_path && (
|
||||
<span className="text-muted-foreground font-mono text-[11px]">
|
||||
({selectedWorkflow.module_path})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Environment */}
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedWorkflow.has_env ? (
|
||||
<XCircle className="h-3.5 w-3.5 text-orange-500" />
|
||||
) : (
|
||||
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
|
||||
)}
|
||||
<span className="text-muted-foreground">Environment:</span>
|
||||
<span>
|
||||
{selectedWorkflow.has_env
|
||||
? "Requires environment variables"
|
||||
: "No environment variables required"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflow Visualization */}
|
||||
@@ -954,6 +876,13 @@ export function WorkflowView({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflow Details Modal */}
|
||||
<WorkflowDetailsModal
|
||||
workflow={selectedWorkflow}
|
||||
open={detailsModalOpen}
|
||||
onOpenChange={setDetailsModalOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,12 +10,14 @@ import type {
|
||||
RunAgentRequest,
|
||||
RunWorkflowRequest,
|
||||
ThreadInfo,
|
||||
WorkflowInfo,
|
||||
} from "@/types";
|
||||
import type { AgentFrameworkRequest } from "@/types/agent-framework";
|
||||
import type { ExtendedResponseStreamEvent } from "@/types/openai";
|
||||
|
||||
// Backend API response types to match Python Pydantic models
|
||||
interface EntityInfo {
|
||||
// Backend API response type - polymorphic entity that can be agent or workflow
|
||||
// This matches the Python Pydantic EntityInfo model which has all fields optional
|
||||
interface BackendEntityInfo {
|
||||
id: string;
|
||||
type: "agent" | "workflow";
|
||||
name: string;
|
||||
@@ -25,6 +27,13 @@ interface EntityInfo {
|
||||
metadata: Record<string, unknown>;
|
||||
source?: string;
|
||||
original_url?: string;
|
||||
// Agent-specific fields (present when type === "agent")
|
||||
instructions?: string;
|
||||
model?: string;
|
||||
chat_client_type?: string;
|
||||
context_providers?: string[];
|
||||
middleware?: string[];
|
||||
// Workflow-specific fields (present when type === "workflow")
|
||||
executors?: string[];
|
||||
workflow_dump?: Record<string, unknown>;
|
||||
input_schema?: Record<string, unknown>;
|
||||
@@ -33,7 +42,7 @@ interface EntityInfo {
|
||||
}
|
||||
|
||||
interface DiscoveryResponse {
|
||||
entities: EntityInfo[];
|
||||
entities: BackendEntityInfo[];
|
||||
}
|
||||
|
||||
interface ThreadApiResponse {
|
||||
@@ -119,15 +128,15 @@ class ApiClient {
|
||||
|
||||
// Entity discovery using new unified endpoint
|
||||
async getEntities(): Promise<{
|
||||
entities: (AgentInfo | import("@/types").WorkflowInfo)[];
|
||||
entities: (AgentInfo | WorkflowInfo)[];
|
||||
agents: AgentInfo[];
|
||||
workflows: import("@/types").WorkflowInfo[];
|
||||
workflows: WorkflowInfo[];
|
||||
}> {
|
||||
const response = await this.request<DiscoveryResponse>("/v1/entities");
|
||||
|
||||
// Separate agents and workflows
|
||||
const agents: AgentInfo[] = [];
|
||||
const workflows: import("@/types").WorkflowInfo[] = [];
|
||||
const workflows: WorkflowInfo[] = [];
|
||||
|
||||
response.entities.forEach((entity) => {
|
||||
if (entity.type === "agent") {
|
||||
@@ -145,6 +154,12 @@ class ApiClient {
|
||||
typeof entity.metadata?.module_path === "string"
|
||||
? entity.metadata.module_path
|
||||
: undefined,
|
||||
// Agent-specific fields
|
||||
instructions: entity.instructions,
|
||||
model: entity.model,
|
||||
chat_client_type: entity.chat_client_type,
|
||||
context_providers: entity.context_providers,
|
||||
middleware: entity.middleware,
|
||||
});
|
||||
} else if (entity.type === "workflow") {
|
||||
const firstTool = entity.tools?.[0];
|
||||
@@ -183,7 +198,7 @@ class ApiClient {
|
||||
return agents;
|
||||
}
|
||||
|
||||
async getWorkflows(): Promise<import("@/types").WorkflowInfo[]> {
|
||||
async getWorkflows(): Promise<WorkflowInfo[]> {
|
||||
const { workflows } = await this.getEntities();
|
||||
return workflows;
|
||||
}
|
||||
@@ -467,8 +482,8 @@ class ApiClient {
|
||||
}
|
||||
|
||||
// Add entity from URL
|
||||
async addEntity(url: string, metadata?: Record<string, unknown>): Promise<EntityInfo> {
|
||||
const response = await this.request<{ success: boolean; entity: EntityInfo }>("/v1/entities/add", {
|
||||
async addEntity(url: string, metadata?: Record<string, unknown>): Promise<BackendEntityInfo> {
|
||||
const response = await this.request<{ success: boolean; entity: BackendEntityInfo }>("/v1/entities/add", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ url, metadata }),
|
||||
});
|
||||
|
||||
@@ -31,6 +31,12 @@ export interface AgentInfo {
|
||||
has_env: boolean;
|
||||
module_path?: string;
|
||||
required_env_vars?: EnvVarRequirement[];
|
||||
// Agent-specific fields
|
||||
instructions?: string;
|
||||
model?: string;
|
||||
chat_client_type?: string;
|
||||
context_providers?: string[];
|
||||
middleware?: string[];
|
||||
}
|
||||
|
||||
// JSON Schema types for workflow input
|
||||
@@ -133,6 +139,11 @@ export interface ChatMessage {
|
||||
author_name?: string;
|
||||
message_id?: string;
|
||||
error?: boolean; // Flag to indicate this is an error message
|
||||
usage?: {
|
||||
total_tokens: number;
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
// UI State types
|
||||
|
||||
Reference in New Issue
Block a user