From c7145156b11b18062a5bbbd913ce950bb3198fe2 Mon Sep 17 00:00:00 2001 From: Victor Dibia Date: Sat, 14 Dec 2024 15:33:14 -0800 Subject: [PATCH] Add Component Gallery to AGS (#4693) * fix message instance check error * general refactor, enable 3rd party gallery * format fixes * update detail view * improve detail view and test sync capabilities * minor tweaks, version bump * version bump * update uv.lock * update lockfile * update uv.lock * update uv lock * pin uv version * uv version * revert * revert * minor side bar and drag drop layout fixes * revert version numbering. --------- Co-authored-by: Eric Zhu --- .../autogenstudio/web/managers/connection.py | 38 ++- .../frontend/src/components/layout.tsx | 4 + .../frontend/src/components/sidebar.tsx | 61 ++-- .../src/components/types/datamodel.ts | 80 +++-- .../components/views/gallery/create-modal.tsx | 200 +++++++++++ .../src/components/views/gallery/detail.tsx | 315 +++++++++++++++++ .../src/components/views/gallery/manager.tsx | 205 ++++++++++++ .../src/components/views/gallery/sidebar.tsx | 276 +++++++++++++++ .../src/components/views/gallery/store.tsx | 156 +++++++++ .../src/components/views/gallery/types.ts | 44 +++ .../src/components/views/gallery/utils.ts | 194 +++++++++++ .../session/chat/agentflow/agentflow.tsx | 4 +- .../components/views/session/chat/chat.tsx | 9 +- .../views/session/chat/inputrequest.tsx | 2 +- .../views/session/chat/rendermessage.tsx | 4 +- .../components/views/session/chat/runview.tsx | 4 +- .../src/components/views/session/manager.tsx | 54 ++- .../src/components/views/session/sidebar.tsx | 105 +++--- .../components/views/team/builder/builder.tsx | 8 +- .../views/team/builder/components/library.tsx | 316 ------------------ .../components/views/team/builder/library.tsx | 228 +++++++++++++ .../builder/{components => }/node-editor.tsx | 24 +- .../team/builder/{components => }/nodes.tsx | 32 +- .../components/views/team/builder/store.tsx | 36 +- .../team/builder/{components => }/toolbar.tsx | 0 .../components/views/team/builder/types.ts | 7 - .../builder/{utils/converter.ts => utils.ts} | 6 +- .../src/components/views/team/manager.tsx | 10 +- .../src/components/views/team/sidebar.tsx | 229 +++++++++---- .../src/components/views/team/types.ts | 4 +- .../frontend/src/pages/gallery.tsx | 28 ++ .../frontend/src/styles/global.css | 4 +- .../frontend/static/images/bg/layeredbg.svg | 1 + 33 files changed, 2122 insertions(+), 566 deletions(-) create mode 100644 python/packages/autogen-studio/frontend/src/components/views/gallery/create-modal.tsx create mode 100644 python/packages/autogen-studio/frontend/src/components/views/gallery/detail.tsx create mode 100644 python/packages/autogen-studio/frontend/src/components/views/gallery/manager.tsx create mode 100644 python/packages/autogen-studio/frontend/src/components/views/gallery/sidebar.tsx create mode 100644 python/packages/autogen-studio/frontend/src/components/views/gallery/store.tsx create mode 100644 python/packages/autogen-studio/frontend/src/components/views/gallery/types.ts create mode 100644 python/packages/autogen-studio/frontend/src/components/views/gallery/utils.ts delete mode 100644 python/packages/autogen-studio/frontend/src/components/views/team/builder/components/library.tsx create mode 100644 python/packages/autogen-studio/frontend/src/components/views/team/builder/library.tsx rename python/packages/autogen-studio/frontend/src/components/views/team/builder/{components => }/node-editor.tsx (97%) rename python/packages/autogen-studio/frontend/src/components/views/team/builder/{components => }/nodes.tsx (96%) rename python/packages/autogen-studio/frontend/src/components/views/team/builder/{components => }/toolbar.tsx (100%) rename python/packages/autogen-studio/frontend/src/components/views/team/builder/{utils/converter.ts => utils.ts} (97%) create mode 100644 python/packages/autogen-studio/frontend/src/pages/gallery.tsx create mode 100644 python/packages/autogen-studio/frontend/static/images/bg/layeredbg.svg diff --git a/python/packages/autogen-studio/autogenstudio/web/managers/connection.py b/python/packages/autogen-studio/autogenstudio/web/managers/connection.py index 97c2aa0888..23e835ca5c 100644 --- a/python/packages/autogen-studio/autogenstudio/web/managers/connection.py +++ b/python/packages/autogen-studio/autogenstudio/web/managers/connection.py @@ -5,7 +5,16 @@ from typing import Any, Callable, Dict, Optional, Union from uuid import UUID from autogen_agentchat.base._task import TaskResult -from autogen_agentchat.messages import AgentMessage, ChatMessage, MultiModalMessage, TextMessage +from autogen_agentchat.messages import ( + AgentMessage, + ChatMessage, + HandoffMessage, + MultiModalMessage, + StopMessage, + TextMessage, + ToolCallMessage, + ToolCallResultMessage, +) from autogen_core import CancellationToken from autogen_core import Image as AGImage from fastapi import WebSocket, WebSocketDisconnect @@ -91,16 +100,22 @@ class WebSocketManager: if formatted_message: await self._send_message(run_id, formatted_message) - # Save message if it's a content message - if isinstance(message, TextMessage): - await self._save_message(run_id, message) - elif isinstance(message, MultiModalMessage): + # Save messages by concrete type + if isinstance( + message, + ( + TextMessage, + MultiModalMessage, + StopMessage, + HandoffMessage, + ToolCallMessage, + ToolCallResultMessage, + ), + ): await self._save_message(run_id, message) # Capture final result if it's a TeamResult elif isinstance(message, TeamResult): final_result = message.model_dump() - elif isinstance(message, (AgentMessage, ChatMessage)): - await self._save_message(run_id, message) if not cancellation_token.is_cancelled() and run_id not in self._closed_connections: if final_result: await self._update_run(run_id, RunStatus.COMPLETE, team_result=final_result) @@ -301,19 +316,20 @@ class WebSocketManager: ] return {"type": "message", "data": message_dump} - elif isinstance(message, TextMessage): - return {"type": "message", "data": message.model_dump()} - elif isinstance(message, TeamResult): return { "type": "result", "data": message.model_dump(), "status": "complete", } - elif isinstance(message, (AgentMessage, ChatMessage)): + + elif isinstance( + message, (TextMessage, StopMessage, HandoffMessage, ToolCallMessage, ToolCallResultMessage) + ): return {"type": "message", "data": message.model_dump()} return None + except Exception as e: logger.error(f"Message formatting error: {e}") return None diff --git a/python/packages/autogen-studio/frontend/src/components/layout.tsx b/python/packages/autogen-studio/frontend/src/components/layout.tsx index 67cd4f9cac..d3216d3407 100644 --- a/python/packages/autogen-studio/frontend/src/components/layout.tsx +++ b/python/packages/autogen-studio/frontend/src/components/layout.tsx @@ -99,6 +99,10 @@ const Layout = ({ { const IconComponent = item.icon; const navLink = ( - handleNavClick(item)} - className={classNames( - // Base styles - "group flex gap-x-3 rounded-md mr-2 p-2 text-sm font-medium", - !showFull && "justify-center", - // Color states - isActive - ? "bg-tertiary text-accent " - : "text-secondary hover:bg-tertiary hover:text-accent" +
+ {isActive && ( +
+ {" "} +
)} - > - handleNavClick(item)} className={classNames( - "h-6 w-6 shrink-0", + // Base styles + "group ml-1 flex gap-x-3 rounded-md mr-2 p-2 text-sm font-medium", + !showFull && "justify-center", + // Color states isActive - ? "text-accent" - : "text-secondary group-hover:text-accent" + ? "bg-secondary text-primary " + : "text-secondary hover:bg-tertiary hover:text-accent" )} - /> - {showFull && item.name} - + > + {" "} + + {showFull && item.name} + +
); return ( diff --git a/python/packages/autogen-studio/frontend/src/components/types/datamodel.ts b/python/packages/autogen-studio/frontend/src/components/types/datamodel.ts index b8871f9e5b..b502f04c89 100644 --- a/python/packages/autogen-studio/frontend/src/components/types/datamodel.ts +++ b/python/packages/autogen-studio/frontend/src/components/types/datamodel.ts @@ -98,6 +98,7 @@ export interface SessionRuns { export interface BaseConfig { component_type: string; version?: string; + description?: string; } export interface WebSocketMessage { @@ -119,7 +120,6 @@ export type ModelTypes = export type AgentTypes = | "AssistantAgent" - | "CodingAssistantAgent" | "UserProxyAgent" | "MultimodalWebSurfer" | "FileSurfer" @@ -132,12 +132,6 @@ export type TeamTypes = | "SelectorGroupChat" | "MagenticOneGroupChat"; -// class ComponentType(str, Enum): -// TEAM = "team" -// AGENT = "agent" -// MODEL = "model" -// TOOL = "tool" -// TERMINATION = "termination" export type TerminationTypes = | "MaxMessageTermination" | "StopMessageTermination" @@ -153,11 +147,11 @@ export type ComponentTypes = | "termination"; export type ComponentConfigTypes = - | TeamConfigTypes + | TeamConfig | AgentConfig - | ModelConfigTypes + | ModelConfig | ToolConfig - | TerminationConfigTypes; + | TerminationConfig; export interface BaseModelConfig extends BaseConfig { model: string; @@ -178,22 +172,57 @@ export interface OpenAIModelConfig extends BaseModelConfig { model_type: "OpenAIChatCompletionClient"; } -export type ModelConfigTypes = AzureOpenAIModelConfig | OpenAIModelConfig; +export type ModelConfig = AzureOpenAIModelConfig | OpenAIModelConfig; -export interface ToolConfig extends BaseConfig { +export interface BaseToolConfig extends BaseConfig { name: string; description: string; content: string; tool_type: ToolTypes; } -export interface AgentConfig extends BaseConfig { + +export interface PythonFunctionToolConfig extends BaseToolConfig { + tool_type: "PythonFunction"; +} + +export type ToolConfig = PythonFunctionToolConfig; + +export interface BaseAgentConfig extends BaseConfig { name: string; agent_type: AgentTypes; system_message?: string; - model_client?: ModelConfigTypes; + model_client?: ModelConfig; tools?: ToolConfig[]; description?: string; } + +export interface AssistantAgentConfig extends BaseAgentConfig { + agent_type: "AssistantAgent"; +} + +export interface UserProxyAgentConfig extends BaseAgentConfig { + agent_type: "UserProxyAgent"; +} + +export interface MultimodalWebSurferAgentConfig extends BaseAgentConfig { + agent_type: "MultimodalWebSurfer"; +} + +export interface FileSurferAgentConfig extends BaseAgentConfig { + agent_type: "FileSurfer"; +} + +export interface MagenticOneCoderAgentConfig extends BaseAgentConfig { + agent_type: "MagenticOneCoderAgent"; +} + +export type AgentConfig = + | AssistantAgentConfig + | UserProxyAgentConfig + | MultimodalWebSurferAgentConfig + | FileSurferAgentConfig + | MagenticOneCoderAgentConfig; + // export interface TerminationConfig extends BaseConfig { // termination_type: TerminationTypes; // max_messages?: number; @@ -217,28 +246,19 @@ export interface TextMentionTerminationConfig extends BaseTerminationConfig { export interface CombinationTerminationConfig extends BaseTerminationConfig { termination_type: "CombinationTermination"; operator: "and" | "or"; - conditions: TerminationConfigTypes[]; + conditions: TerminationConfig[]; } -export type TerminationConfigTypes = +export type TerminationConfig = | MaxMessageTerminationConfig | TextMentionTerminationConfig | CombinationTerminationConfig; -// export interface TeamConfig extends BaseConfig { -// name: string; -// participants: AgentConfig[]; -// team_type: TeamTypes; -// model_client?: ModelConfig; -// termination_condition?: TerminationConfig; -// selector_prompt?: string; -// } - export interface BaseTeamConfig extends BaseConfig { name: string; participants: AgentConfig[]; team_type: TeamTypes; - termination_condition?: TerminationConfigTypes; + termination_condition?: TerminationConfig; } export interface RoundRobinGroupChatConfig extends BaseTeamConfig { @@ -248,15 +268,13 @@ export interface RoundRobinGroupChatConfig extends BaseTeamConfig { export interface SelectorGroupChatConfig extends BaseTeamConfig { team_type: "SelectorGroupChat"; selector_prompt: string; - model_client: ModelConfigTypes; + model_client: ModelConfig; } -export type TeamConfigTypes = - | RoundRobinGroupChatConfig - | SelectorGroupChatConfig; +export type TeamConfig = RoundRobinGroupChatConfig | SelectorGroupChatConfig; export interface Team extends DBModel { - config: TeamConfigTypes; + config: TeamConfig; } export interface TeamResult { diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/create-modal.tsx b/python/packages/autogen-studio/frontend/src/components/views/gallery/create-modal.tsx new file mode 100644 index 0000000000..80ff323add --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/create-modal.tsx @@ -0,0 +1,200 @@ +import React, { useState, useRef } from "react"; +import { Modal, Tabs, Input, Button, Alert, Upload } from "antd"; +import { Globe, Upload as UploadIcon, Code } from "lucide-react"; +import { MonacoEditor } from "../monaco"; +import type { InputRef, UploadFile, UploadProps } from "antd"; +import { Gallery } from "./types"; +import { defaultGallery } from "./utils"; + +interface GalleryCreateModalProps { + open: boolean; + onCancel: () => void; + onCreateGallery: (gallery: Gallery) => void; +} + +export const GalleryCreateModal: React.FC = ({ + open, + onCancel, + onCreateGallery, +}) => { + const [activeTab, setActiveTab] = useState("url"); + const [url, setUrl] = useState(""); + const [jsonContent, setJsonContent] = useState( + JSON.stringify(defaultGallery, null, 2) + ); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const editorRef = useRef(null); + + const handleUrlImport = async () => { + setIsLoading(true); + setError(""); + try { + const response = await fetch(url); + const data = await response.json(); + // TODO: Validate against Gallery schema + onCreateGallery(data); + onCancel(); + } catch (err) { + setError("Failed to fetch or parse gallery from URL"); + } finally { + setIsLoading(false); + } + }; + + const handleFileUpload = (info: { file: UploadFile }) => { + const { status, originFileObj } = info.file; + if (status === "done" && originFileObj instanceof File) { + const reader = new FileReader(); + reader.onload = (e: ProgressEvent) => { + try { + const content = JSON.parse(e.target?.result as string); + // TODO: Validate against Gallery schema + onCreateGallery(content); + onCancel(); + } catch (err) { + setError("Invalid JSON file"); + } + }; + reader.readAsText(originFileObj); + } else if (status === "error") { + setError("File upload failed"); + } + }; + + const handlePasteImport = () => { + try { + const content = JSON.parse(jsonContent); + // TODO: Validate against Gallery schema + onCreateGallery(content); + onCancel(); + } catch (err) { + setError("Invalid JSON format"); + } + }; + + const uploadProps: UploadProps = { + accept: ".json", + showUploadList: false, + customRequest: ({ file, onSuccess }) => { + setTimeout(() => { + onSuccess && onSuccess("ok"); + }, 0); + }, + onChange: handleFileUpload, + }; + + const inputRef = useRef(null); + + const items = [ + { + key: "url", + label: ( + + URL Import + + ), + children: ( + + ), + }, + { + key: "file", + label: ( + + File Upload + + ), + children: ( +
+ +

+ +

+

+ Click or drag JSON file to this area +

+
+
+ ), + }, + { + key: "paste", + label: ( + + Paste JSON + + ), + children: ( +
+
+ +
+ +
+ ), + }, + ]; + + return ( + +
+ + + {error && ( + + )} +
+
+ ); +}; + +export default GalleryCreateModal; diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/detail.tsx b/python/packages/autogen-studio/frontend/src/components/views/gallery/detail.tsx new file mode 100644 index 0000000000..00e9fe331b --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/detail.tsx @@ -0,0 +1,315 @@ +import React, { useState, useRef } from "react"; +import { Button, message, Tooltip } from "antd"; +import { + Package, + Users, + Bot, + Globe, + RefreshCw, + Edit2, + X, + Wrench, + Brain, + Timer, + Save, + ChevronUp, + ChevronDown, + Edit, +} from "lucide-react"; +import type { Gallery } from "./types"; +import { useGalleryStore } from "./store"; +import { MonacoEditor } from "../monaco"; +import { ComponentConfigTypes } from "../../types/datamodel"; +import { getRelativeTimeString, TruncatableText } from "../atoms"; + +const ComponentGrid: React.FC<{ + title: string; + icon: React.ReactNode; + items: ComponentConfigTypes[]; +}> = ({ title, icon, items }) => { + const [isExpanded, setIsExpanded] = useState(true); + + return ( +
+
setIsExpanded(!isExpanded)} + > +
+ {icon} + + {items.length} {items.length === 1 ? title : `${title}s`} + +
+ {isExpanded ? ( + + ) : ( + + )} +
+ +
+ {items.map((item, idx) => ( +
+
+ {item.component_type} +
+ {item.description && ( +

+ +

+ )} +
+ ))} +
+
+ ); +}; + +interface GalleryDetailProps { + gallery: Gallery; + onSave: (updates: Partial) => void; + onDirtyStateChange: (isDirty: boolean) => void; +} + +export const GalleryDetail: React.FC = ({ + gallery, + onSave, + onDirtyStateChange, +}) => { + const [isEditing, setIsEditing] = useState(false); + const [isSyncing, setIsSyncing] = useState(false); + const [jsonValue, setJsonValue] = useState(JSON.stringify(gallery, null, 2)); + const editorRef = useRef(null); + const { syncGallery, getLastSyncTime } = useGalleryStore(); + + const handleSync = async () => { + if (!gallery.url) return; + + setIsSyncing(true); + try { + await syncGallery(gallery.id); + message.success("Gallery synced successfully"); + } catch (error) { + message.error("Failed to sync gallery"); + } finally { + setIsSyncing(false); + } + }; + + const handleJsonChange = (value: string) => { + setJsonValue(value); + onDirtyStateChange(true); + }; + + const handleSave = async () => { + try { + const parsedGallery = JSON.parse(jsonValue); + const updatedGallery = { + ...parsedGallery, + id: gallery.id, + metadata: { + ...parsedGallery.metadata, + updated_at: new Date().toISOString(), + }, + }; + await onSave(updatedGallery); + onDirtyStateChange(false); + setIsEditing(false); + message.success("Gallery updated successfully"); + } catch (error) { + message.error("Invalid JSON format"); + } + }; + + const gridItems = [ + { + icon: , + title: "team", + items: gallery.items.teams, + }, + { + icon: , + title: "agent", + items: gallery.items.components.agents, + }, + { + icon: , + title: "tool", + items: gallery.items.components.tools, + }, + { + icon: , + title: "model", + items: gallery.items.components.models, + }, + { + icon: , + title: "termination", + items: gallery.items.components.terminations, + }, + ]; + + return ( +
+ {/* Banner Section - Kept unchanged */} +
+ Gallery Banner +
+
+
+

+ {gallery.name} +

+ {gallery.url && ( + + + + )} +
+

+ {gallery.metadata.description} +

+

+ {gallery.metadata.author} +

+
+ +
+
+ + + {Object.values(gallery.items.components).reduce( + (sum, arr) => sum + arr.length, + 0 + )}{" "} + components + +
+
+ v{gallery.metadata.version} +
+ {gallery.metadata.tags?.map((tag) => ( +
+ {tag} +
+ ))} +
+
+
+ + {/* Action Buttons */} +
+ {gallery.url && ( + + + + )} + {!isEditing ? ( + + ) : ( + <> + + + + )} +
+ + {/* Grid Layout */} +
+ {gridItems.map((item) => ( + + ))} +
+ + {/* Editor Section */} + {isEditing && ( +
+
+

+ Edit Gallery Configuration +

+
+ +
+
+
+ +
+
+ )} +
+ ); +}; diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/manager.tsx b/python/packages/autogen-studio/frontend/src/components/views/gallery/manager.tsx new file mode 100644 index 0000000000..95562b7dd9 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/manager.tsx @@ -0,0 +1,205 @@ +import React, { useEffect, useState } from "react"; +import { message, Modal } from "antd"; +import { ChevronRight } from "lucide-react"; +import { useGalleryStore } from "./store"; +import { GallerySidebar } from "./sidebar"; +import { GalleryDetail } from "./detail"; +import { GalleryCreateModal } from "./create-modal"; +import type { Gallery } from "./types"; + +export const GalleryManager: React.FC = () => { + const [isLoading, setIsLoading] = useState(false); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [isSidebarOpen, setIsSidebarOpen] = useState(() => { + if (typeof window !== "undefined") { + const stored = localStorage.getItem("gallerySidebar"); + return stored !== null ? JSON.parse(stored) : true; + } + return true; + }); + + const { + galleries, + selectedGalleryId, + selectGallery, + addGallery, + updateGallery, + removeGallery, + setDefaultGallery, + getSelectedGallery, + getDefaultGallery, + } = useGalleryStore(); + + const [messageApi, contextHolder] = message.useMessage(); + const currentGallery = getSelectedGallery(); + + // Persist sidebar state + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem("gallerySidebar", JSON.stringify(isSidebarOpen)); + } + }, [isSidebarOpen]); + + // Handle URL params + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const galleryId = params.get("galleryId"); + + if (galleryId && !selectedGalleryId) { + handleSelectGallery(galleryId); + } + }, []); + + // Update URL when gallery changes + useEffect(() => { + if (selectedGalleryId) { + window.history.pushState({}, "", `?galleryId=${selectedGalleryId}`); + } + }, [selectedGalleryId]); + + const handleSelectGallery = async (galleryId: string) => { + if (hasUnsavedChanges) { + Modal.confirm({ + title: "Unsaved Changes", + content: "You have unsaved changes. Do you want to discard them?", + okText: "Discard", + cancelText: "Go Back", + onOk: () => { + selectGallery(galleryId); + setHasUnsavedChanges(false); + }, + }); + } else { + selectGallery(galleryId); + } + }; + + const handleCreateGallery = async (galleryData: Gallery) => { + const newGallery: Gallery = { + id: `gallery_${Date.now()}`, + name: galleryData.name || "New Gallery", + url: galleryData.url, + metadata: { + ...galleryData.metadata, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + items: galleryData.items || { + teams: [], + components: { + agents: [], + models: [], + tools: [], + terminations: [], + }, + }, + }; + + try { + setIsLoading(true); + await addGallery(newGallery); + messageApi.success("Gallery created successfully"); + selectGallery(newGallery.id); + } catch (error) { + messageApi.error("Failed to create gallery"); + console.error(error); + } finally { + setIsLoading(false); + } + }; + + const handleDeleteGallery = async (galleryId: string) => { + try { + await removeGallery(galleryId); + messageApi.success("Gallery deleted successfully"); + } catch (error) { + messageApi.error("Failed to delete gallery"); + console.error(error); + } + }; + + const handleUpdateGallery = async ( + galleryId: string, + updates: Partial + ) => { + try { + await updateGallery(galleryId, updates); + setHasUnsavedChanges(false); + messageApi.success("Gallery updated successfully"); + } catch (error) { + messageApi.error("Failed to update gallery"); + console.error(error); + } + }; + + return ( +
+ {contextHolder} + + {/* Create Modal */} + setIsCreateModalOpen(false)} + onCreateGallery={handleCreateGallery} + /> + + {/* Sidebar */} +
+ setIsSidebarOpen(!isSidebarOpen)} + onSelectGallery={(gallery) => handleSelectGallery(gallery.id)} + onCreateGallery={() => setIsCreateModalOpen(true)} + onDeleteGallery={handleDeleteGallery} + defaultGalleryId={getDefaultGallery()?.id} + onSetDefault={setDefaultGallery} + isLoading={isLoading} + /> +
+ + {/* Main Content */} +
+
+ {/* Breadcrumb */} +
+ Galleries + {currentGallery && ( + <> + + {currentGallery.name} + + )} +
+ + {/* Content Area */} + {currentGallery ? ( + + handleUpdateGallery(currentGallery.id, updates) + } + onDirtyStateChange={setHasUnsavedChanges} + /> + ) : ( +
+ Select a gallery from the sidebar or create a new one +
+ )} +
+
+
+ ); +}; + +export default GalleryManager; diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/sidebar.tsx b/python/packages/autogen-studio/frontend/src/components/views/gallery/sidebar.tsx new file mode 100644 index 0000000000..d375677158 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/sidebar.tsx @@ -0,0 +1,276 @@ +import React from "react"; +import { Button, Tooltip, Tag } from "antd"; +import { + Plus, + Trash2, + PanelLeftClose, + PanelLeftOpen, + Pin, + Package, + RefreshCw, + Globe, + Info, +} from "lucide-react"; +import type { Gallery } from "./types"; +import { getRelativeTimeString } from "../atoms"; +import { useGalleryStore } from "./store"; + +interface GallerySidebarProps { + isOpen: boolean; + galleries: Gallery[]; + currentGallery: Gallery | null; + onToggle: () => void; + onSelectGallery: (gallery: Gallery) => void; + onCreateGallery: () => void; + onDeleteGallery: (galleryId: string) => void; + onSetDefault: (galleryId: string) => void; + isLoading?: boolean; + defaultGalleryId: string; +} + +export const GallerySidebar: React.FC = ({ + isOpen, + galleries, + currentGallery, + onToggle, + onSelectGallery, + onCreateGallery, + onDeleteGallery, + onSetDefault, + defaultGalleryId, + isLoading = false, +}) => { + const { syncGallery, getLastSyncTime } = useGalleryStore(); + + // Render collapsed state + if (!isOpen) { + return ( +
+
+ + + +
+ +
+ +
+
+ ); + } + + // Render expanded state + return ( +
+ {/* Header */} +
+
+ Galleries + + {galleries.length} + +
+ + + +
+ + {/* Create Gallery Button */} +
+
+ + + +
+
+ + {/* Section Label */} +
All Galleries
+ + {/* Galleries List */} + {isLoading ? ( +
Loading...
+ ) : galleries.length === 0 ? ( +
+ No galleries found +
+ ) : ( +
+ <> + {galleries.map((gallery) => ( +
+
+
onSelectGallery(gallery)} + > + {/* Gallery Name and Actions Row */} +
+ {" "} + {/* Added min-w-0 */} +
+ {" "} + {/* Added min-w-0 and flex-1 */} +
+ {" "} + {/* Wrapped name in div with truncate and flex-1 */} + {gallery.name} +
+ {gallery.url && ( + + {" "} + {/* Added flex-shrink-0 */} + + )} +
+
+ {gallery.url && ( + +
+
+ + {/* Rest of the content remains the same */} +
+ + v{gallery.metadata.version} + +
+ + + {Object.values(gallery.items.components).reduce( + (sum, arr) => sum + arr.length, + 0 + )}{" "} + components + +
+
+ + {/* Updated Timestamp */} +
+ + {getRelativeTimeString(gallery.metadata.updated_at)} + {defaultGalleryId === gallery.id ? ( + + default + + ) : ( + "" + )} + +
+
+
+ ))} + + +
+ Gallery items marked as default ( + ) are available in + the builder by default. +
+
+ )} +
+ ); +}; diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/store.tsx b/python/packages/autogen-studio/frontend/src/components/views/gallery/store.tsx new file mode 100644 index 0000000000..c09096f73c --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/store.tsx @@ -0,0 +1,156 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { Gallery } from "./types"; +import { + AgentConfig, + ModelConfig, + TeamConfig, + TerminationConfig, + ToolConfig, +} from "../../types/datamodel"; +import { defaultGallery } from "./utils"; + +interface GalleryStore { + galleries: Gallery[]; + defaultGalleryId: string; + selectedGalleryId: string | null; + + addGallery: (gallery: Gallery) => void; + updateGallery: (id: string, gallery: Partial) => void; + removeGallery: (id: string) => void; + setDefaultGallery: (id: string) => void; + selectGallery: (id: string) => void; + getDefaultGallery: () => Gallery; + getSelectedGallery: () => Gallery | null; + syncGallery: (id: string) => Promise; + getLastSyncTime: (id: string) => string | null; + getGalleryComponents: () => { + teams: TeamConfig[]; + components: { + agents: AgentConfig[]; + models: ModelConfig[]; + tools: ToolConfig[]; + terminations: TerminationConfig[]; + }; + }; +} + +export const useGalleryStore = create()( + persist( + (set, get) => ({ + galleries: [defaultGallery], + defaultGalleryId: defaultGallery.id, + selectedGalleryId: defaultGallery.id, + + addGallery: (gallery) => + set((state) => { + if (state.galleries.find((g) => g.id === gallery.id)) return state; + return { + galleries: [gallery, ...state.galleries], + defaultGalleryId: state.defaultGalleryId || gallery.id, + selectedGalleryId: state.selectedGalleryId || gallery.id, + }; + }), + + updateGallery: (id, updates) => + set((state) => ({ + galleries: state.galleries.map((gallery) => + gallery.id === id + ? { + ...gallery, + ...updates, + metadata: { + ...gallery.metadata, + ...updates.metadata, + updated_at: new Date().toISOString(), + }, + } + : gallery + ), + })), + + removeGallery: (id) => + set((state) => { + if (state.galleries.length <= 1) return state; + + const newGalleries = state.galleries.filter((g) => g.id !== id); + const updates: Partial = { + galleries: newGalleries, + }; + + if (id === state.defaultGalleryId) { + updates.defaultGalleryId = newGalleries[0].id; + } + + if (id === state.selectedGalleryId) { + updates.selectedGalleryId = newGalleries[0].id; + } + + return updates; + }), + + setDefaultGallery: (id) => + set((state) => { + const gallery = state.galleries.find((g) => g.id === id); + if (!gallery) return state; + return { defaultGalleryId: id }; + }), + + selectGallery: (id) => + set((state) => { + const gallery = state.galleries.find((g) => g.id === id); + if (!gallery) return state; + return { selectedGalleryId: id }; + }), + + getDefaultGallery: () => { + const { galleries, defaultGalleryId } = get(); + return galleries.find((g) => g.id === defaultGalleryId)!; + }, + + getSelectedGallery: () => { + const { galleries, selectedGalleryId } = get(); + if (!selectedGalleryId) return null; + return galleries.find((g) => g.id === selectedGalleryId) || null; + }, + + syncGallery: async (id) => { + const gallery = get().galleries.find((g) => g.id === id); + if (!gallery?.url) return; + + try { + const response = await fetch(gallery.url); + const remoteGallery = await response.json(); + + get().updateGallery(id, { + ...remoteGallery, + id, // preserve local id + metadata: { + ...remoteGallery.metadata, + lastSynced: new Date().toISOString(), + }, + }); + } catch (error) { + console.error("Failed to sync gallery:", error); + throw error; + } + }, + + getLastSyncTime: (id) => { + const gallery = get().galleries.find((g) => g.id === id); + return gallery?.metadata.lastSynced ?? null; + }, + + getGalleryComponents: () => { + const defaultGallery = get().getDefaultGallery(); + return { + teams: defaultGallery.items.teams, + components: defaultGallery.items.components, + }; + }, + }), + { + name: "gallery-storage", + } + ) +); diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/types.ts b/python/packages/autogen-studio/frontend/src/components/views/gallery/types.ts new file mode 100644 index 0000000000..015eb961c9 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/types.ts @@ -0,0 +1,44 @@ +import { + AgentConfig, + ModelConfig, + TeamConfig, + TerminationConfig, + ToolConfig, +} from "../../types/datamodel"; + +export interface GalleryMetadata { + author: string; + created_at: string; + updated_at: string; + version: string; + description?: string; + tags?: string[]; + license?: string; + homepage?: string; + category?: string; + lastSynced?: string; +} + +export interface Gallery { + id: string; + name: string; + url?: string; + metadata: GalleryMetadata; + items: { + teams: TeamConfig[]; + components: { + agents: AgentConfig[]; + models: ModelConfig[]; + tools: ToolConfig[]; + terminations: TerminationConfig[]; + }; + }; +} + +export interface GalleryAPI { + listGalleries: () => Promise; + getGallery: (id: string) => Promise; + createGallery: (gallery: Gallery) => Promise; + updateGallery: (gallery: Gallery) => Promise; + deleteGallery: (id: string) => Promise; +} diff --git a/python/packages/autogen-studio/frontend/src/components/views/gallery/utils.ts b/python/packages/autogen-studio/frontend/src/components/views/gallery/utils.ts new file mode 100644 index 0000000000..2b0e4ea83e --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/gallery/utils.ts @@ -0,0 +1,194 @@ +import { + AssistantAgentConfig, + CombinationTerminationConfig, + MaxMessageTerminationConfig, + OpenAIModelConfig, + PythonFunctionToolConfig, + RoundRobinGroupChatConfig, + TextMentionTerminationConfig, + UserProxyAgentConfig, +} from "../../types/datamodel"; + +export const defaultGallery = { + id: "gallery_default", + name: "Default Component Gallery", + metadata: { + author: "AutoGen Team", + created_at: "2024-12-12T00:00:00Z", + updated_at: "2024-12-12T00:00:00Z", + version: "1.0.0", + description: + "A default gallery containing basic components for human-in-loop conversations", + tags: ["human-in-loop", "assistant"], + license: "MIT", + category: "conversation", + }, + items: { + teams: [ + { + component_type: "team", + description: + "A team with an assistant agent and a user agent to enable human-in-loop task completion in a round-robin fashion", + name: "huma_in_loop_team", + participants: [ + { + component_type: "agent", + description: + "An assistant agent that can help users complete tasks", + name: "assistant_agent", + agent_type: "AssistantAgent", + system_message: + "You are a helpful assistant. Solve tasks carefully. You also have a calculator tool which you can use if needed. When the task is done respond with TERMINATE.", + model_client: { + component_type: "model", + description: "A GPT-4o mini model", + model: "gpt-4o-mini", + model_type: "OpenAIChatCompletionClient", + }, + tools: [ + { + component_type: "tool", + name: "calculator", + description: + "A simple calculator that performs basic arithmetic operations between two numbers", + content: + "def calculator(a: float, b: float, operator: str) -> str:\n try:\n if operator == '+':\n return str(a + b)\n elif operator == '-':\n return str(a - b)\n elif operator == '*':\n return str(a * b)\n elif operator == '/':\n if b == 0:\n return 'Error: Division by zero'\n return str(a / b)\n else:\n return 'Error: Invalid operator. Please use +, -, *, or /'\n except Exception as e:\n return f'Error: {str(e)}'", + tool_type: "PythonFunction", + }, + ], + }, + { + component_type: "agent", + description: "A user agent that is driven by a human user", + name: "user_agent", + agent_type: "UserProxyAgent", + tools: [], + }, + ], + team_type: "RoundRobinGroupChat", + termination_condition: { + description: + "Terminate the conversation when the user mentions 'TERMINATE' or after 10 messages", + component_type: "termination", + termination_type: "CombinationTermination", + operator: "or", + conditions: [ + { + component_type: "termination", + description: + "Terminate the conversation when the user mentions 'TERMINATE'", + termination_type: "TextMentionTermination", + text: "TERMINATE", + }, + { + component_type: "termination", + description: "Terminate the conversation after 10 messages", + termination_type: "MaxMessageTermination", + max_messages: 10, + }, + ], + }, + } as RoundRobinGroupChatConfig, + ], + components: { + agents: [ + { + component_type: "agent", + description: "An assistant agent that can help users complete tasks", + name: "assistant_agent", + agent_type: "AssistantAgent", + system_message: + "You are a helpful assistant. Solve tasks carefully. You also have a calculator tool which you can use if needed. When the task is done respond with TERMINATE.", + model_client: { + component_type: "model", + description: "A GPT-4o mini model", + model: "gpt-4o-mini", + model_type: "OpenAIChatCompletionClient", + }, + tools: [ + { + component_type: "tool", + name: "calculator", + description: + "A simple calculator that performs basic arithmetic operations between two numbers", + content: + "def calculator(a: float, b: float, operator: str) -> str:\n try:\n if operator == '+':\n return str(a + b)\n elif operator == '-':\n return str(a - b)\n elif operator == '*':\n return str(a * b)\n elif operator == '/':\n if b == 0:\n return 'Error: Division by zero'\n return str(a / b)\n else:\n return 'Error: Invalid operator. Please use +, -, *, or /'\n except Exception as e:\n return f'Error: {str(e)}'", + tool_type: "PythonFunction", + }, + ], + } as AssistantAgentConfig, + { + component_type: "agent", + description: "A user agent that is driven by a human user", + name: "user_agent", + agent_type: "UserProxyAgent", + tools: [], + } as UserProxyAgentConfig, + ], + models: [ + { + component_type: "model", + description: "A GPT-4o mini model", + model: "gpt-4o-mini", + model_type: "OpenAIChatCompletionClient", + } as OpenAIModelConfig, + ], + tools: [ + { + component_type: "tool", + name: "calculator", + description: + "A simple calculator that performs basic arithmetic operations between two numbers", + content: + "def calculator(a: float, b: float, operator: str) -> str:\n try:\n if operator == '+':\n return str(a + b)\n elif operator == '-':\n return str(a - b)\n elif operator == '*':\n return str(a * b)\n elif operator == '/':\n if b == 0:\n return 'Error: Division by zero'\n return str(a / b)\n else:\n return 'Error: Invalid operator. Please use +, -, *, or /'\n except Exception as e:\n return f'Error: {str(e)}'", + tool_type: "PythonFunction", + } as PythonFunctionToolConfig, + { + component_type: "tool", + name: "fetch_website", + description: "Fetch and return the content of a website URL", + content: + "async def fetch_website(url: str) -> str:\n try:\n import requests\n from urllib.parse import urlparse\n \n # Validate URL format\n parsed = urlparse(url)\n if not parsed.scheme or not parsed.netloc:\n return \"Error: Invalid URL format. Please include http:// or https://\"\n \n # Add scheme if not present\n if not url.startswith(('http://', 'https://')): \n url = 'https://' + url\n \n # Set headers to mimic a browser request\n headers = {\n 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'\n }\n \n # Make the request with a timeout\n response = requests.get(url, headers=headers, timeout=10)\n response.raise_for_status()\n \n # Return the text content\n return response.text\n \n except requests.exceptions.Timeout:\n return \"Error: Request timed out\"\n except requests.exceptions.ConnectionError:\n return \"Error: Failed to connect to the website\"\n except requests.exceptions.HTTPError as e:\n return f\"Error: HTTP {e.response.status_code} - {e.response.reason}\"\n except Exception as e:\n return f\"Error: {str(e)}\"", + tool_type: "PythonFunction", + } as PythonFunctionToolConfig, + ], + terminations: [ + { + component_type: "termination", + description: + "Terminate the conversation when the user mentions 'TERMINATE'", + termination_type: "TextMentionTermination", + text: "TERMINATE", + } as TextMentionTerminationConfig, + { + component_type: "termination", + description: "Terminate the conversation after 10 messages", + termination_type: "MaxMessageTermination", + max_messages: 10, + } as MaxMessageTerminationConfig, + { + component_type: "termination", + description: + "Terminate the conversation when the user mentions 'TERMINATE' or after 10 messages", + termination_type: "CombinationTermination", + operator: "or", + conditions: [ + { + component_type: "termination", + description: + "Terminate the conversation when the user mentions 'TERMINATE'", + termination_type: "TextMentionTermination", + text: "TERMINATE", + }, + { + component_type: "termination", + description: "Terminate the conversation after 10 messages", + termination_type: "MaxMessageTermination", + max_messages: 10, + }, + ], + } as CombinationTerminationConfig, + ], + }, + }, +}; diff --git a/python/packages/autogen-studio/frontend/src/components/views/session/chat/agentflow/agentflow.tsx b/python/packages/autogen-studio/frontend/src/components/views/session/chat/agentflow/agentflow.tsx index 5d76b6d0a5..9b18038c61 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/session/chat/agentflow/agentflow.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/session/chat/agentflow/agentflow.tsx @@ -23,7 +23,7 @@ import AgentNode from "./agentnode"; import { AgentMessageConfig, AgentConfig, - TeamConfigTypes, + TeamConfig, Run, } from "../../../../types/datamodel"; import { CustomEdge, CustomEdgeData } from "./edge"; @@ -32,7 +32,7 @@ import { AgentFlowToolbar } from "./toolbar"; import { EdgeMessageModal } from "./edgemessagemodal"; interface AgentFlowProps { - teamConfig: TeamConfigTypes; + teamConfig: TeamConfig; run: Run; } diff --git a/python/packages/autogen-studio/frontend/src/components/views/session/chat/chat.tsx b/python/packages/autogen-studio/frontend/src/components/views/session/chat/chat.tsx index fc3b9a336d..65d9452bbf 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/session/chat/chat.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/session/chat/chat.tsx @@ -6,7 +6,7 @@ import { Run, Message, WebSocketMessage, - TeamConfigTypes, + TeamConfig, AgentMessageConfig, RunStatus, TeamResult, @@ -46,9 +46,7 @@ export default function ChatView({ session }: ChatViewProps) { const [activeSocket, setActiveSocket] = React.useState( null ); - const [teamConfig, setTeamConfig] = React.useState( - null - ); + const [teamConfig, setTeamConfig] = React.useState(null); const inputTimeoutRef = React.useRef(null); const activeSocketRef = React.useRef(null); @@ -83,6 +81,7 @@ export default function ChatView({ session }: ChatViewProps) { React.useEffect(() => { if (session?.id) { loadSessionRuns(); + setCurrentRun(null); } else { setExistingRuns([]); setCurrentRun(null); @@ -99,7 +98,7 @@ export default function ChatView({ session }: ChatViewProps) { }) .catch((error) => { console.error("Error loading team config:", error); - messageApi.error("Failed to load team config"); + // messageApi.error("Failed to load team config"); setTeamConfig(null); }); } diff --git a/python/packages/autogen-studio/frontend/src/components/views/session/chat/inputrequest.tsx b/python/packages/autogen-studio/frontend/src/components/views/session/chat/inputrequest.tsx index d18b1262a9..1526e47885 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/session/chat/inputrequest.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/session/chat/inputrequest.tsx @@ -123,7 +123,7 @@ const InputRequestView: React.FC = ({ onChange={handleInputChange} onKeyDown={handleKeyDown} disabled={disabled || isSubmitting} - className="flex-1 px-3 py-2 rounded bg-background border border-secondary focus:border-accent focus:ring-1 focus:ring-accent outline-none disabled:opacity-50" + className="text-primary flex-1 px-3 py-2 rounded bg-tertiary border border-secondary focus:border-accent focus:ring-1 focus:ring-accent outline-none disabled:opacity-50" placeholder={ disabled ? "Input timeout - please restart the conversation" diff --git a/python/packages/autogen-studio/frontend/src/components/views/session/chat/rendermessage.tsx b/python/packages/autogen-studio/frontend/src/components/views/session/chat/rendermessage.tsx index 203f994d8a..d5c6bc7a21 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/session/chat/rendermessage.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/session/chat/rendermessage.tsx @@ -45,7 +45,7 @@ const RenderMultiModal: React.FC<{ content: (string | ImageContent)[] }> = ({ const RenderToolCall: React.FC<{ content: FunctionCall[] }> = ({ content }) => (
{content.map((call) => ( -
+
Function: {call.name}
= ({
Result ID: {result.call_id}
))} diff --git a/python/packages/autogen-studio/frontend/src/components/views/session/chat/runview.tsx b/python/packages/autogen-studio/frontend/src/components/views/session/chat/runview.tsx index 30f780b50f..67c1ad8bea 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/session/chat/runview.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/session/chat/runview.tsx @@ -11,7 +11,7 @@ import { ChevronUp, Bot, } from "lucide-react"; -import { Run, Message, TeamConfigTypes } from "../../../types/datamodel"; +import { Run, Message, TeamConfig } from "../../../types/datamodel"; import AgentFlow from "./agentflow/agentflow"; import { RenderMessage } from "./rendermessage"; import InputRequestView from "./inputrequest"; @@ -24,7 +24,7 @@ import { interface RunViewProps { run: Run; - teamConfig?: TeamConfigTypes; + teamConfig?: TeamConfig; onInputResponse?: (response: string) => void; onCancel?: () => void; isFirstRun?: boolean; diff --git a/python/packages/autogen-studio/frontend/src/components/views/session/manager.tsx b/python/packages/autogen-studio/frontend/src/components/views/session/manager.tsx index 4a70f9ab17..c42ad26927 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/session/manager.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/session/manager.tsx @@ -37,7 +37,11 @@ export const SessionManager: React.FC = () => { setIsLoading(true); const data = await sessionAPI.listSessions(user.email); setSessions(data); - if (!session && data.length > 0) { + + // Only set first session if there's no sessionId in URL + const params = new URLSearchParams(window.location.search); + const sessionId = params.get("sessionId"); + if (!session && data.length > 0 && !sessionId) { setSession(data[0]); } } catch (error) { @@ -48,6 +52,31 @@ export const SessionManager: React.FC = () => { } }, [user?.email, setSessions, session, setSession]); + // Handle initial URL params + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const sessionId = params.get("sessionId"); + + if (sessionId && !session) { + handleSelectSession({ id: parseInt(sessionId) } as Session); + } + }, []); + + // Handle browser back/forward + useEffect(() => { + const handleLocationChange = () => { + const params = new URLSearchParams(window.location.search); + const sessionId = params.get("sessionId"); + + if (!sessionId && session) { + setSession(null); + } + }; + + window.addEventListener("popstate", handleLocationChange); + return () => window.removeEventListener("popstate", handleLocationChange); + }, [session]); + const handleSaveSession = async (sessionData: Partial) => { if (!user?.email) return; @@ -80,10 +109,10 @@ export const SessionManager: React.FC = () => { try { const response = await sessionAPI.deleteSession(sessionId, user.email); - console.log("response", response); setSessions(sessions.filter((s) => s.id !== sessionId)); if (session?.id === sessionId || sessions.length === 0) { setSession(sessions[0] || null); + window.history.pushState({}, "", window.location.pathname); // Clear URL params } messageApi.success("Session deleted"); } catch (error) { @@ -98,10 +127,28 @@ export const SessionManager: React.FC = () => { try { setIsLoading(true); const data = await sessionAPI.getSession(selectedSession.id, user.email); + if (!data) { + // Session not found + messageApi.error("Session not found"); + window.history.pushState({}, "", window.location.pathname); // Clear URL + if (sessions.length > 0) { + setSession(sessions[0]); // Fall back to first session + } else { + setSession(null); + } + return; + } setSession(data); + window.history.pushState({}, "", `?sessionId=${selectedSession.id}`); } catch (error) { console.error("Error loading session:", error); messageApi.error("Error loading session"); + window.history.pushState({}, "", window.location.pathname); // Clear invalid URL + if (sessions.length > 0) { + setSession(sessions[0]); // Fall back to first session + } else { + setSession(null); + } } finally { setIsLoading(false); } @@ -130,11 +177,12 @@ export const SessionManager: React.FC = () => { setIsEditorOpen(true); }} onDeleteSession={handleDeleteSession} + isLoading={isLoading} />
diff --git a/python/packages/autogen-studio/frontend/src/components/views/session/sidebar.tsx b/python/packages/autogen-studio/frontend/src/components/views/session/sidebar.tsx index daa83fd821..c4087698e5 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/session/sidebar.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/session/sidebar.tsx @@ -6,6 +6,8 @@ import { Trash2, PanelLeftClose, PanelLeftOpen, + InfoIcon, + RefreshCcw, } from "lucide-react"; import type { Session } from "../../types/datamodel"; import { getRelativeTimeString } from "../atoms"; @@ -18,6 +20,7 @@ interface SidebarProps { onSelectSession: (session: Session) => void; onEditSession: (session?: Session) => void; onDeleteSession: (sessionId: number) => void; + isLoading?: boolean; } export const Sidebar: React.FC = ({ @@ -28,6 +31,7 @@ export const Sidebar: React.FC = ({ onSelectSession, onEditSession, onDeleteSession, + isLoading = false, }) => { if (!isOpen) { return ( @@ -95,55 +99,72 @@ export const Sidebar: React.FC = ({
-
Recents
+
+ Recents{" "} + {isLoading && ( + + )} +
{/* no sessions found */} - {sessions.length === 0 && ( -
No sessions found
+ {!isLoading && sessions.length === 0 && ( +
+ + No recent sessions found +
)}
{sessions.map((s) => ( -
onSelectSession(s)} - > - {s.name} - - {getRelativeTimeString(s.updated_at || "")} - -
- -
))} diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx index c400f3c4c0..6af11cd3c4 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/team/builder/builder.tsx @@ -20,16 +20,16 @@ import "@xyflow/react/dist/style.css"; import { Button, Layout, message, Modal, Switch, Tooltip } from "antd"; import { Cable, Code2, Save } from "lucide-react"; import { useTeamBuilderStore } from "./store"; -import { ComponentLibrary } from "./components/library"; +import { ComponentLibrary } from "./library"; import { ComponentTypes, Team } from "../../../types/datamodel"; import { CustomNode, CustomEdge, DragItem } from "./types"; -import { edgeTypes, nodeTypes } from "./components/nodes"; +import { edgeTypes, nodeTypes } from "./nodes"; // import builder css import "./builder.css"; -import TeamBuilderToolbar from "./components/toolbar"; +import TeamBuilderToolbar from "./toolbar"; import { MonacoEditor } from "../../monaco"; -import { NodeEditor } from "./components/node-editor"; +import { NodeEditor } from "./node-editor"; const { Sider, Content } = Layout; diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/components/library.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/builder/components/library.tsx deleted file mode 100644 index a45fa10d07..0000000000 --- a/python/packages/autogen-studio/frontend/src/components/views/team/builder/components/library.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import React from "react"; -import { Input, Collapse, type CollapseProps } from "antd"; -import { useDraggable } from "@dnd-kit/core"; -import { CSS } from "@dnd-kit/utilities"; -import { - Brain, - Settings, - FoldVertical, - ChevronDown, - Bot, - Wrench, - Timer, - Maximize2, - MinimizeIcon, - Minimize2, -} from "lucide-react"; -import { - AgentConfig, - ModelConfig, - TerminationConfig, - ToolConfig, -} from "../../../../types/datamodel"; -import Sider from "antd/es/layout/Sider"; - -// Types -interface ComponentConfigTypes { - [key: string]: any; -} - -type ComponentTypes = "agent" | "model" | "tool" | "termination"; - -interface LibraryProps {} - -interface PresetItemProps { - id: string; - type: ComponentTypes; - config: ComponentConfigTypes; - label: string; - icon: React.ReactNode; -} - -interface SectionItem { - label: string; - config: ComponentConfigTypes; -} - -const PresetItem: React.FC = ({ - id, - type, - config, - label, - icon, -}) => { - const { attributes, listeners, setNodeRef, transform, isDragging } = - useDraggable({ - id, - data: { - current: { - type, - config, - label, - }, - }, - }); - - const style = { - transform: CSS.Transform.toString(transform), - opacity: isDragging ? 0.5 : undefined, - }; - - return ( -
-
- {icon} - {label} -
-
- ); -}; - -export const ComponentLibrary: React.FC = () => { - const [searchTerm, setSearchTerm] = React.useState(""); - const [isMinimized, setIsMinimized] = React.useState(false); - - const sections: Array<{ - title: string; - items: SectionItem[]; - icon: React.ReactNode; - type: ComponentTypes; - }> = [ - { - title: "Agents", - type: "agent", - items: [ - { - label: "Assistant Agent", - config: { - name: "assistant_agent", - agent_type: "AssistantAgent", - system_message: - "You are a helpful assistant. Solve tasks carefully.", - model_client: { - component_type: "model", - model: "gpt-4o-mini", - model_type: "OpenAIChatCompletionClient", - }, - } as AgentConfig, - }, - { - label: "User Proxy Agent", - config: { - name: "user_agent", - agent_type: "UserProxyAgent", - } as AgentConfig, - }, - ], - icon: , - }, - { - title: "Models", - type: "model", - items: [ - { - label: "OpenAI GPT4o-mini", - config: { - model: "gpt-4o-mini", - model_type: "OpenAIChatCompletionClient", - } as ModelConfig, - }, - { - label: "OpenAI GPT4o", - config: { - model: "gpt-4o", - model_type: "OpenAIChatCompletionClient", - } as ModelConfig, - }, - ], - icon: , - }, - { - title: "Tools", - type: "tool", - items: [ - { - label: "Calculator Tool", - config: { - component_type: "tool", - name: "calculator", - description: - "Perform basic arithmetic operations (+, -, *, /) between two numbers", - content: - "async def calculator(num1: float, num2: float, operation: str) -> str:\n operations = {\n '+': lambda x, y: x + y,\n '-': lambda x, y: x - y,\n '*': lambda x, y: x * y,\n '/': lambda x, y: x / y if y != 0 else 'Error: Division by zero'\n }\n \n if operation not in operations:\n return f\"Error: Invalid operation. Please use one of: {', '.join(operations.keys())}\"\n \n try:\n result = operations[operation](num1, num2)\n return f\"{num1} {operation} {num2} = {result}\"\n except Exception as e:\n return f\"Error: {str(e)}\"", - tool_type: "PythonFunction", - } as ToolConfig, - }, - { - label: "Fetch Website", - config: { - component_type: "tool", - name: "fetch_website", - description: "Fetch and return the content of a website URL", - content: - "async def fetch_website(url: str) -> str:\n try:\n import requests\n from urllib.parse import urlparse\n \n # Validate URL format\n parsed = urlparse(url)\n if not parsed.scheme or not parsed.netloc:\n return \"Error: Invalid URL format. Please include http:// or https://\"\n \n # Add scheme if not present\n if not url.startswith(('http://', 'https://')): \n url = 'https://' + url\n \n # Set headers to mimic a browser request\n headers = {\n 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'\n }\n \n # Make the request with a timeout\n response = requests.get(url, headers=headers, timeout=10)\n response.raise_for_status()\n \n # Return the text content\n return response.text\n \n except requests.exceptions.Timeout:\n return \"Error: Request timed out\"\n except requests.exceptions.ConnectionError:\n return \"Error: Failed to connect to the website\"\n except requests.exceptions.HTTPError as e:\n return f\"Error: HTTP {e.response.status_code} - {e.response.reason}\"\n except Exception as e:\n return f\"Error: {str(e)}\"", - tool_type: "PythonFunction", - } as ToolConfig, - }, - ], - icon: , - }, - { - title: "Terminations", - type: "termination", - items: [ - { - label: "10 Max Message Termination", - config: { - termination_type: "MaxMessageTermination", - max_messages: 10, - } as TerminationConfig, - }, - { - label: "Text Message Termination", - config: { - termination_type: "TextMentionTermination", - text: "TERMINATE", - } as TerminationConfig, - }, - { - label: "Max or Text Termination", - config: { - component_type: "termination", - termination_type: "CombinationTermination", - operator: "or", - conditions: [ - { - component_type: "termination", - termination_type: "TextMentionTermination", - text: "TERMINATE", - }, - { - component_type: "termination", - termination_type: "MaxMessageTermination", - max_messages: 10, - }, - ], - } as TerminationConfig, - }, - ], - icon: , - }, - ]; - - const items: CollapseProps["items"] = sections.map((section, index) => { - const filteredItems = section.items.filter((item) => - item.label.toLowerCase().includes(searchTerm.toLowerCase()) - ); - - return { - key: section.title, - label: ( -
- {section.icon} - {section.title} - - ({filteredItems.length}) - -
- ), - children: ( -
- {filteredItems.map((item, itemIndex) => ( - - ))} -
- ), - }; - }); - - if (isMinimized) { - return ( -
setIsMinimized(false)} - className="absolute group top-4 left-4 bg-primary shadow-md rounded px-4 pr-2 py-2 cursor-pointer transition-all duration-300 z-50 flex items-center gap-2" - > - Show Component Library - - -
- ); - } - - return ( - -
-
-
Component Library
- -
- -
- Drag a component to add it to the team -
- -
- setSearchTerm(e.target.value)} - className="flex-1 p-2" - /> -
- - ( - - )} - /> -
-
- ); -}; - -export default ComponentLibrary; diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/library.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/builder/library.tsx new file mode 100644 index 0000000000..c5bdfceb12 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/components/views/team/builder/library.tsx @@ -0,0 +1,228 @@ +import React from "react"; +import { Input, Collapse, type CollapseProps } from "antd"; +import { useDraggable } from "@dnd-kit/core"; +import { CSS } from "@dnd-kit/utilities"; +import { + Brain, + ChevronDown, + Bot, + Wrench, + Timer, + Maximize2, + Minimize2, +} from "lucide-react"; +import type { + AgentConfig, + ModelConfig, + TerminationConfig, + ToolConfig, +} from "../../../types/datamodel"; +import Sider from "antd/es/layout/Sider"; +import { useGalleryStore } from "../../gallery/store"; + +interface ComponentConfigTypes { + [key: string]: any; +} + +type ComponentTypes = "agent" | "model" | "tool" | "termination"; + +interface LibraryProps {} + +interface PresetItemProps { + id: string; + type: ComponentTypes; + config: ComponentConfigTypes; + label: string; + icon: React.ReactNode; +} + +const PresetItem: React.FC = ({ + id, + type, + config, + label, + icon, +}) => { + const { attributes, listeners, setNodeRef, transform, isDragging } = + useDraggable({ + id, + data: { + current: { + type, + config, + label, + }, + }, + }); + + const style = { + transform: CSS.Transform.toString(transform), + opacity: isDragging ? 0.5 : undefined, + }; + + return ( +
+
+ {icon} + {label} +
+
+ ); +}; + +export const ComponentLibrary: React.FC = () => { + const [searchTerm, setSearchTerm] = React.useState(""); + const [isMinimized, setIsMinimized] = React.useState(false); + const defaultGallery = useGalleryStore((state) => state.getDefaultGallery()); + + if (!defaultGallery) { + return null; + } + // Map gallery components to sections format + const sections = React.useMemo( + () => [ + { + title: "Agents", + type: "agent" as ComponentTypes, + items: defaultGallery.items.components.agents.map((agent) => ({ + label: agent.name, + config: agent, + })), + icon: , + }, + { + title: "Models", + type: "model" as ComponentTypes, + items: defaultGallery.items.components.models.map((model) => ({ + label: `${model.model_type} - ${model.model}`, + config: model, + })), + icon: , + }, + { + title: "Tools", + type: "tool" as ComponentTypes, + items: defaultGallery.items.components.tools.map((tool) => ({ + label: tool.name, + config: tool, + })), + icon: , + }, + { + title: "Terminations", + type: "termination" as ComponentTypes, + items: defaultGallery.items.components.terminations.map( + (termination) => ({ + label: `${termination.termination_type}`, + config: termination, + }) + ), + icon: , + }, + ], + [defaultGallery] + ); + + const items: CollapseProps["items"] = sections.map((section) => { + const filteredItems = section.items.filter((item) => + item.label.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return { + key: section.title, + label: ( +
+ {section.icon} + {section.title} + + ({filteredItems.length}) + +
+ ), + children: ( +
+ {filteredItems.map((item, itemIndex) => ( + + ))} +
+ ), + }; + }); + + if (isMinimized) { + return ( +
setIsMinimized(false)} + className="absolute group top-4 left-4 bg-primary shadow-md rounded px-4 pr-2 py-2 cursor-pointer transition-all duration-300 z-50 flex items-center gap-2" + > + Show Component Library + +
+ ); + } + + return ( + +
+
+
Component Library
+ +
+ +
+ Drag a component to add it to the team +
+ +
+ setSearchTerm(e.target.value)} + className="flex-1 p-2" + /> +
+ + ( + + )} + /> +
+
+ ); +}; + +export default ComponentLibrary; diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/components/node-editor.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/builder/node-editor.tsx similarity index 97% rename from python/packages/autogen-studio/frontend/src/components/views/team/builder/components/node-editor.tsx rename to python/packages/autogen-studio/frontend/src/components/views/team/builder/node-editor.tsx index ea205279f2..07520e47a2 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/builder/components/node-editor.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/team/builder/node-editor.tsx @@ -1,15 +1,15 @@ import React, { useEffect, useState } from "react"; import { Drawer, Button, Space, message, Select, Input } from "antd"; -import { NodeEditorProps } from "../types"; -import { useTeamBuilderStore } from "../store"; +import { NodeEditorProps } from "./types"; +import { useTeamBuilderStore } from "./store"; import { - TeamConfigTypes, + TeamConfig, ComponentTypes, TeamTypes, ModelTypes, SelectorGroupChatConfig, RoundRobinGroupChatConfig, - ModelConfigTypes, + ModelConfig, AzureOpenAIModelConfig, OpenAIModelConfig, ComponentConfigTypes, @@ -17,12 +17,12 @@ import { ToolConfig, AgentTypes, ToolTypes, - TerminationConfigTypes, + TerminationConfig, TerminationTypes, MaxMessageTerminationConfig, TextMentionTerminationConfig, CombinationTerminationConfig, -} from "../../../../types/datamodel"; +} from "../../../types/datamodel"; const { TextArea } = Input; @@ -32,7 +32,7 @@ interface EditorProps { disabled?: boolean; } -const TeamEditor: React.FC> = ({ +const TeamEditor: React.FC> = ({ value, onChange, disabled, @@ -119,7 +119,7 @@ const TeamEditor: React.FC> = ({ ); }; -const ModelEditor: React.FC> = ({ +const ModelEditor: React.FC> = ({ value, onChange, disabled, @@ -358,7 +358,7 @@ const ToolEditor: React.FC> = ({ ); }; -const TerminationEditor: React.FC> = ({ +const TerminationEditor: React.FC> = ({ value, onChange, disabled, @@ -413,7 +413,7 @@ const TerminationEditor: React.FC> = ({ const handleUpdateCondition = ( index: number, - newCondition: TerminationConfigTypes + newCondition: TerminationConfig ) => { if (value.termination_type === "CombinationTermination") { const newConditions = [...value.conditions]; @@ -632,7 +632,7 @@ export const NodeEditor: React.FC = ({ node, onUpdate }) => { function validateConfig(config: ComponentConfigTypes): void { switch (config.component_type) { case "team": { - const teamConfig = config as TeamConfigTypes; + const teamConfig = config as TeamConfig; if ("selector_prompt" in teamConfig) { // Type guard for SelectorGroupChatConfig if (!teamConfig.selector_prompt) { @@ -646,7 +646,7 @@ function validateConfig(config: ComponentConfigTypes): void { } case "model": - const modelConfig = config as ModelConfigTypes; + const modelConfig = config as ModelConfig; if ("AzureOpenAIChatCompletionClient" in modelConfig) { const azureConfig = config as AzureOpenAIModelConfig; if ( diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/components/nodes.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/builder/nodes.tsx similarity index 96% rename from python/packages/autogen-studio/frontend/src/components/views/team/builder/components/nodes.tsx rename to python/packages/autogen-studio/frontend/src/components/views/team/builder/nodes.tsx index 9551f108cc..7701e11d26 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/builder/components/nodes.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/team/builder/nodes.tsx @@ -16,26 +16,27 @@ import { Timer, Trash2Icon, Edit, + Bot, } from "lucide-react"; -import { NodeData, CustomNode } from "../types"; +import { NodeData, CustomNode } from "./types"; import { AgentConfig, - TeamConfigTypes, - ModelConfigTypes, + TeamConfig, + ModelConfig, ToolConfig, - TerminationConfigTypes, + TerminationConfig, ComponentTypes, -} from "../../../../types/datamodel"; +} from "../../../types/datamodel"; import { useDroppable } from "@dnd-kit/core"; -import { TruncatableText } from "../../../atoms"; -import { useTeamBuilderStore } from "../store"; +import { TruncatableText } from "../../atoms"; +import { useTeamBuilderStore } from "./store"; // Icon mapping for different node types const iconMap: Record = { team: Users, - agent: Brain, + agent: Bot, tool: Wrench, - model: Settings, + model: Brain, termination: Timer, }; @@ -152,9 +153,12 @@ const BaseNode: React.FC = ({ {headerContent}
- {descriptionContent && ( + {data.config.description && (
- {descriptionContent} +
)} @@ -190,7 +194,7 @@ const ConnectionBadge: React.FC<{ // Team Node export const TeamNode: React.FC> = (props) => { - const config = props.data.config as TeamConfigTypes; + const config = props.data.config as TeamConfig; const hasModel = config.team_type === "SelectorGroupChat" && !!config.model_client; const participantCount = config.participants?.length || 0; @@ -410,7 +414,7 @@ export const AgentNode: React.FC> = (props) => { // Model Node export const ModelNode: React.FC> = (props) => { - const config = props.data.config as ModelConfigTypes; + const config = props.data.config as ModelConfig; return ( > = (props) => { // First, let's add the Termination Node component export const TerminationNode: React.FC> = (props) => { - const config = props.data.config as TerminationConfigTypes; + const config = props.data.config as TerminationConfig; return ( { +const isTeamConfig = (config: any): config is TeamConfig => { return "team_type" in config; }; @@ -31,7 +28,7 @@ const isAgentConfig = (config: any): config is AgentConfig => { return "agent_type" in config; }; -const isModelConfig = (config: any): config is ModelConfigTypes => { +const isModelConfig = (config: any): config is ModelConfig => { return "model_type" in config; }; @@ -39,7 +36,7 @@ const isToolConfig = (config: any): config is ToolConfig => { return "tool_type" in config; }; -const isTerminationConfig = (config: any): config is TerminationConfigTypes => { +const isTerminationConfig = (config: any): config is TerminationConfig => { return "termination_type" in config; }; @@ -49,7 +46,7 @@ export interface TeamBuilderState { selectedNodeId: string | null; history: Array<{ nodes: CustomNode[]; edges: CustomEdge[] }>; currentHistoryIndex: number; - originalConfig: TeamConfigTypes | null; + originalConfig: TeamConfig | null; addNode: ( type: ComponentTypes, position: Position, @@ -69,8 +66,8 @@ export interface TeamBuilderState { redo: () => void; // Sync with JSON - syncToJson: () => TeamConfigTypes | null; - loadFromJson: (config: TeamConfigTypes) => GraphState; + syncToJson: () => TeamConfig | null; + loadFromJson: (config: TeamConfig) => GraphState; layoutNodes: () => void; resetHistory: () => void; } @@ -79,7 +76,7 @@ const buildTeamConfig = ( teamNode: CustomNode, nodes: CustomNode[], edges: CustomEdge[] -): TeamConfigTypes | null => { +): TeamConfig | null => { if (!isTeamConfig(teamNode.data.config)) return null; const config = { ...teamNode.data.config }; @@ -343,12 +340,15 @@ export const useTeamBuilderStore = create((set, get) => ({ newNodes.push(newNode); } + const { nodes: layoutedNodes, edges: layoutedEdges } = + getLayoutedElements(newNodes, newEdges); + return { - nodes: newNodes, - edges: newEdges, + nodes: layoutedNodes, + edges: layoutedEdges, history: [ ...state.history.slice(0, state.currentHistoryIndex + 1), - { nodes: newNodes, edges: newEdges }, + { nodes: layoutedNodes, edges: layoutedEdges }, ].slice(-MAX_HISTORY), currentHistoryIndex: state.currentHistoryIndex + 1, }; @@ -644,7 +644,7 @@ export const useTeamBuilderStore = create((set, get) => ({ }); }, - loadFromJson: (config: TeamConfigTypes) => { + loadFromJson: (config: TeamConfig) => { const { nodes, edges } = convertTeamConfigToGraph(config); const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( nodes, diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/components/toolbar.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/builder/toolbar.tsx similarity index 100% rename from python/packages/autogen-studio/frontend/src/components/views/team/builder/components/toolbar.tsx rename to python/packages/autogen-studio/frontend/src/components/views/team/builder/toolbar.tsx diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/types.ts b/python/packages/autogen-studio/frontend/src/components/views/team/builder/types.ts index fb3541e5f7..ca96204c95 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/builder/types.ts +++ b/python/packages/autogen-studio/frontend/src/components/views/team/builder/types.ts @@ -1,13 +1,6 @@ import { Node, Edge } from "@xyflow/react"; import { ComponentConfigTypes, ComponentTypes } from "../../../types/datamodel"; -interface NodeConnections { - modelClient: string | null; - tools: string[]; - participants: string[]; - termination: string | null; -} - export interface NodeData extends Record { label: string; type: ComponentTypes; diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/builder/utils/converter.ts b/python/packages/autogen-studio/frontend/src/components/views/team/builder/utils.ts similarity index 97% rename from python/packages/autogen-studio/frontend/src/components/views/team/builder/utils/converter.ts rename to python/packages/autogen-studio/frontend/src/components/views/team/builder/utils.ts index c4b8001e7c..6e5c709310 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/builder/utils/converter.ts +++ b/python/packages/autogen-studio/frontend/src/components/views/team/builder/utils.ts @@ -1,5 +1,5 @@ import dagre from "@dagrejs/dagre"; -import { CustomNode, CustomEdge } from "../types"; +import { CustomNode, CustomEdge } from "./types"; import { nanoid } from "nanoid"; import { TeamConfig, @@ -8,7 +8,7 @@ import { ToolConfig, ComponentTypes, TerminationConfig, -} from "../../../../types/datamodel"; +} from "../../../types/datamodel"; interface ConversionResult { nodes: CustomNode[]; @@ -100,7 +100,7 @@ export const convertTeamConfigToGraph = ( nodes.push(teamNode); // Add model client if present - if (config.model_client) { + if (config.team_type === "SelectorGroupChat" && config.model_client) { const modelNode = createNode( "model", { x: 200, y: 50 }, diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/manager.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/manager.tsx index 360dc6090f..dca1fafe6b 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/manager.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/team/manager.tsx @@ -99,7 +99,7 @@ export const TeamManager: React.FC = () => { if (!teamId || !user?.email) return; setIsLoading(true); try { - const data = await teamAPI.getTeam(teamId, user.email!); // We can assert user.email exists since we checked above + const data = await teamAPI.getTeam(teamId, user.email!); setCurrentTeam(data); window.history.pushState({}, "", `?teamId=${teamId}`); } catch (error) { @@ -126,9 +126,7 @@ export const TeamManager: React.FC = () => { } }; - const handleCreateTeam = () => { - const newTeam = Object.assign({}, defaultTeam); - newTeam.config.name = "new_team_" + new Date().getTime(); + const handleCreateTeam = (newTeam: Team) => { setCurrentTeam(newTeam); // also save it to db @@ -191,7 +189,7 @@ export const TeamManager: React.FC = () => { {/* Main Content */}
@@ -222,7 +220,7 @@ export const TeamManager: React.FC = () => { onDirtyStateChange={setHasUnsavedChanges} /> ) : ( -
+
Select a team from the sidebar or create a new one
)} diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/sidebar.tsx b/python/packages/autogen-studio/frontend/src/components/views/team/sidebar.tsx index 1b57e99586..ddb7f50dba 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/sidebar.tsx +++ b/python/packages/autogen-studio/frontend/src/components/views/team/sidebar.tsx @@ -8,9 +8,15 @@ import { PanelLeftClose, PanelLeftOpen, Calendar, + Copy, + GalleryHorizontalEnd, + InfoIcon, + RefreshCcw, } from "lucide-react"; import type { Team } from "../../types/datamodel"; import { getRelativeTimeString } from "../atoms"; +import { defaultTeam } from "./types"; +import { useGalleryStore } from "../gallery/store"; interface TeamSidebarProps { isOpen: boolean; @@ -18,7 +24,7 @@ interface TeamSidebarProps { currentTeam: Team | null; onToggle: () => void; onSelectTeam: (team: Team) => void; - onCreateTeam: () => void; + onCreateTeam: (team: Team) => void; onEditTeam: (team: Team) => void; onDeleteTeam: (teamId: number) => void; isLoading?: boolean; @@ -35,6 +41,13 @@ export const TeamSidebar: React.FC = ({ onDeleteTeam, isLoading = false, }) => { + const defaultGallery = useGalleryStore((state) => state.getDefaultGallery()); + + const createTeam = () => { + const newTeam = Object.assign({}, defaultTeam); + newTeam.config.name = "new_team_" + new Date().getTime(); + onCreateTeam(newTeam); + }; // Render collapsed state if (!isOpen) { return ( @@ -55,7 +68,7 @@ export const TeamSidebar: React.FC = ({ @@ -103,32 +116,59 @@ export const TeamSidebar: React.FC = ({
{/* Section Label */} -
Recents
+
+ Recents + {isLoading && ( + + )} +
{/* Teams List */} - {isLoading ? ( -
Loading...
- ) : teams.length === 0 ? ( -
- No teams found + + {!isLoading && teams.length === 0 && ( +
+ + No recent teams found
- ) : ( -
- {teams.map((team) => ( + )} + +
+ <> + {teams.length > 0 && (
onSelectTeam(team)} > - {/* Team Name and Actions Row */} -
- {team.config.name} -
- {/* + {" "} + {teams.map((team) => ( +
+ { +
+ {" "} +
+ } +
onSelectTeam(team)} + > + {/* Team Name and Actions Row */} +
+ + {team.config.name} + +
+ {/*
-
+ +
+
- {/* Team Metadata Row */} -
- - {team.config.team_type} - -
- - - {team.config.participants.length}{" "} - {team.config.participants.length === 1 ? "agent" : "agents"} - -
-
+ {/* Team Metadata Row */} +
+ + {team.config.team_type} + +
+ + + {team.config.participants.length}{" "} + {team.config.participants.length === 1 + ? "agent" + : "agents"} + +
+
- {/* Updated Timestamp */} - {team.updated_at && ( -
- {/* */} - {getRelativeTimeString(team.updated_at)} + {/* Updated Timestamp */} + {team.updated_at && ( +
+ {/* */} + {getRelativeTimeString(team.updated_at)} +
+ )} +
- )} + ))}
- ))} -
- )} + )} + + {/* Gallery Teams Section */} +
+ + From Gallery +
+
+ {defaultGallery?.items.teams.map((galleryTeam) => ( +
+
+
+ {/* Team Name and Use Template Action */} +
+ + {galleryTeam.name} + +
+ +
+
+ + {/* Team Metadata Row */} +
+ + {galleryTeam.team_type} + +
+ + + {galleryTeam.participants.length}{" "} + {galleryTeam.participants.length === 1 + ? "agent" + : "agents"} + +
+
+
+
+ ))} +
+ +
); }; diff --git a/python/packages/autogen-studio/frontend/src/components/views/team/types.ts b/python/packages/autogen-studio/frontend/src/components/views/team/types.ts index 9dab65b567..fd29035eb3 100644 --- a/python/packages/autogen-studio/frontend/src/components/views/team/types.ts +++ b/python/packages/autogen-studio/frontend/src/components/views/team/types.ts @@ -1,4 +1,4 @@ -import type { Team, TeamConfigTypes } from "../../types/datamodel"; +import type { Team, TeamConfig } from "../../types/datamodel"; export interface TeamEditorProps { team?: Team; @@ -16,7 +16,7 @@ export interface TeamListProps { isLoading?: boolean; } -export const defaultTeamConfig: TeamConfigTypes = { +export const defaultTeamConfig: TeamConfig = { version: "1.0.0", component_type: "team", name: "default_team", diff --git a/python/packages/autogen-studio/frontend/src/pages/gallery.tsx b/python/packages/autogen-studio/frontend/src/pages/gallery.tsx new file mode 100644 index 0000000000..3fc7725953 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/pages/gallery.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import Layout from "../components/layout"; +import { graphql } from "gatsby"; +import GalleryManager from "../components/views/gallery/manager"; + +// markup +const GalleryPage = ({ data }: any) => { + return ( + +
+ +
+
+ ); +}; + +export const query = graphql` + query HomePageQuery { + site { + siteMetadata { + description + title + } + } + } +`; + +export default GalleryPage; diff --git a/python/packages/autogen-studio/frontend/src/styles/global.css b/python/packages/autogen-studio/frontend/src/styles/global.css index c925e92d18..f3dea477de 100644 --- a/python/packages/autogen-studio/frontend/src/styles/global.css +++ b/python/packages/autogen-studio/frontend/src/styles/global.css @@ -105,7 +105,7 @@ body { border: grey; } -.ant-modal-content { +/* .ant-modal-content { @apply dark:bg-primary dark:text-primary; } .ant-modal-footer { @@ -118,7 +118,7 @@ body { .ant-modal-title, .ant-modal-header { @apply bg-primary text-primary; -} +} */ a:hover { @apply text-accent; } diff --git a/python/packages/autogen-studio/frontend/static/images/bg/layeredbg.svg b/python/packages/autogen-studio/frontend/static/images/bg/layeredbg.svg new file mode 100644 index 0000000000..05fba68a6a --- /dev/null +++ b/python/packages/autogen-studio/frontend/static/images/bg/layeredbg.svg @@ -0,0 +1 @@ + \ No newline at end of file