From c341ee7ed256196c224ad7413d1ca77f9e049866 Mon Sep 17 00:00:00 2001 From: Victor Dibia Date: Wed, 8 Oct 2025 12:34:30 -0700 Subject: [PATCH] =?UTF-8?q?Python:=20DevUI=20-=20Internal=20Refactor,=20Co?= =?UTF-8?q?nversations=20API=20support,=20and=20per=E2=80=A6=20(#1235)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Python: DevUI - Internal Refactor, Conversations API support, and performance improvements Comprehensive refactor of DevUI package including samples relocation, frontend reorganization, OpenAI Conversations API support, and critical performance and code quality improvements. Key Changes: Architecture & Organization - Moved DevUI samples to python/samples/getting_started/devui/ - Consolidated with other framework samples for better discoverability - Added .env.example files and comprehensive README - Restructured frontend components into feature-based folders (agent, workflow, gallery, layout) - Created new OpenAI-compliant message renderers (devui should render oai responses types primarily) New Features - Added _conversations.py (467 lines) - Full conversation storage abstraction, replaces the /threads endpoint to better match oai conversations api - Implements OpenAI Conversations API for thread management, Supports in-memory and extensible storage backends API Simplification - Use 'model' field as entity_id (agent/workflow name) instead of extra_body - Use standard OpenAI 'conversation' field for conversation context. Performance & Quality Improvements - Improved context management in MessageMapper with bounded memory (~500KB max) - Implemented hybrid LRU + cleanup approach to prevent unbounded memory growth - General QOL improvement - Eliminated ~150 lines of dead/duplicate code, Consolidated helper functions into _utils.py, Extracted magic numbers to module-level constants, Optimized conversation item lookups with index-based approach Testing - Added test_conversations.py (13 tests) - Added test_performance_fixes.py (9 tests) - Updated existing tests for code consolidation - 53 tests passing Impact: 76 files changed: +4,106 insertions, -2,373 deletions All linting and formatting checks passing. No breaking changes - backward compatible. Migration: Samples moved to python/samples/getting_started/devui/ * readme lint fixes * initial support for function approval and minor ui fixes --- python/packages/devui/README.md | 119 ++- .../agent_framework_devui/_conversations.py | 473 ++++++++++ .../devui/agent_framework_devui/_discovery.py | 136 +-- .../devui/agent_framework_devui/_executor.py | 334 ++----- .../devui/agent_framework_devui/_mapper.py | 343 +++++-- .../devui/agent_framework_devui/_server.py | 259 ++--- .../devui/agent_framework_devui/_utils.py | 127 +++ .../agent_framework_devui/models/__init__.py | 25 +- .../models/_openai_custom.py | 128 +-- .../ui/assets/index-BhFnsoso.css | 1 + .../ui/assets/index-D0SfShuZ.js | 445 --------- .../ui/assets/index-WsCIE0bH.css | 1 - .../ui/assets/index-ZIs_B0ln.js | 455 +++++++++ .../devui/agent_framework_devui/ui/index.html | 4 +- python/packages/devui/dev.md | 98 +- python/packages/devui/frontend/src/App.tsx | 10 +- .../agent}/agent-details-modal.tsx | 0 .../{ => features}/agent/agent-view.tsx | 882 +++++++++++------- .../src/components/features/agent/index.ts | 7 + .../OpenAIContentRenderer.tsx | 289 ++++++ .../OpenAIMessageRenderer.tsx | 77 ++ .../features/agent/message-renderers/index.ts | 7 + .../{ => features}/gallery/gallery-view.tsx | 0 .../{ => features}/gallery/index.ts | 0 .../{ => features}/workflow/executor-node.tsx | 0 .../src/components/features/workflow/index.ts | 9 + .../workflow}/workflow-details-modal.tsx | 0 .../{ => features}/workflow/workflow-flow.tsx | 10 +- .../workflow/workflow-input-form.tsx | 0 .../{ => features}/workflow/workflow-view.tsx | 451 +++++++-- .../{shared => layout}/about-modal.tsx | 0 .../{shared => layout}/app-header.tsx | 4 +- .../{shared => layout}/debug-panel.tsx | 324 ++++--- .../{shared => layout}/entity-selector.tsx | 0 .../frontend/src/components/layout/index.ts | 9 + .../{shared => layout}/settings-modal.tsx | 0 .../message_renderer/ContentRenderer.tsx | 331 ------- .../message_renderer/MessageRenderer.tsx | 38 - .../message_renderer/StreamingRenderer.tsx | 114 --- .../src/components/message_renderer/index.ts | 8 - .../src/components/message_renderer/types.ts | 48 - .../src/data/gallery/sample-entities.ts | 38 +- .../devui/frontend/src/services/api.ts | 194 ++-- .../frontend/src/types/agent-framework.ts | 27 +- .../devui/frontend/src/types/index.ts | 37 +- .../devui/frontend/src/types/openai.ts | 182 +++- .../devui/frontend/src/types/workflow.ts | 8 + .../devui/frontend/src/utils/simple-layout.ts | 2 +- .../frontend/src/utils/workflow-utils.ts | 21 +- python/packages/devui/samples/README.md | 38 + .../packages/devui/samples/in_memory_mode.py | 71 -- .../devui/samples/weather_agent/agent.py | 69 -- .../samples/weather_agent_azure/__init__.py | 7 - .../packages/devui/tests/capture_messages.py | 85 +- .../devui/tests/test_conversations.py | 335 +++++++ python/packages/devui/tests/test_discovery.py | 57 +- python/packages/devui/tests/test_execution.py | 87 +- python/packages/devui/tests/test_mapper.py | 42 +- python/packages/devui/tests/test_server.py | 32 +- python/samples/README.md | 2 + .../samples/getting_started/devui/README.md | 159 ++++ .../devui}/fanout_workflow/__init__.py | 0 .../devui}/fanout_workflow/workflow.py | 11 +- .../devui/foundry_agent/.env.example | 6 + .../devui}/foundry_agent/__init__.py | 0 .../devui}/foundry_agent/agent.py | 26 +- .../getting_started/devui/in_memory_mode.py | 120 +++ .../devui}/spam_workflow/__init__.py | 0 .../devui}/spam_workflow/workflow.py | 9 +- .../devui/weather_agent_azure/.env.example | 6 + .../devui/weather_agent_azure}/__init__.py | 0 .../devui}/weather_agent_azure/agent.py | 2 +- .../devui/workflow_agents/.env.example | 7 + .../devui}/workflow_agents/__init__.py | 0 .../devui}/workflow_agents/workflow.py | 5 +- 75 files changed, 4605 insertions(+), 2646 deletions(-) create mode 100644 python/packages/devui/agent_framework_devui/_conversations.py create mode 100644 python/packages/devui/agent_framework_devui/ui/assets/index-BhFnsoso.css delete mode 100644 python/packages/devui/agent_framework_devui/ui/assets/index-D0SfShuZ.js delete mode 100644 python/packages/devui/agent_framework_devui/ui/assets/index-WsCIE0bH.css create mode 100644 python/packages/devui/agent_framework_devui/ui/assets/index-ZIs_B0ln.js rename python/packages/devui/frontend/src/components/{shared => features/agent}/agent-details-modal.tsx (100%) rename python/packages/devui/frontend/src/components/{ => features}/agent/agent-view.tsx (52%) create mode 100644 python/packages/devui/frontend/src/components/features/agent/index.ts create mode 100644 python/packages/devui/frontend/src/components/features/agent/message-renderers/OpenAIContentRenderer.tsx create mode 100644 python/packages/devui/frontend/src/components/features/agent/message-renderers/OpenAIMessageRenderer.tsx create mode 100644 python/packages/devui/frontend/src/components/features/agent/message-renderers/index.ts rename python/packages/devui/frontend/src/components/{ => features}/gallery/gallery-view.tsx (100%) rename python/packages/devui/frontend/src/components/{ => features}/gallery/index.ts (100%) rename python/packages/devui/frontend/src/components/{ => features}/workflow/executor-node.tsx (100%) create mode 100644 python/packages/devui/frontend/src/components/features/workflow/index.ts rename python/packages/devui/frontend/src/components/{shared => features/workflow}/workflow-details-modal.tsx (100%) rename python/packages/devui/frontend/src/components/{ => features}/workflow/workflow-flow.tsx (98%) rename python/packages/devui/frontend/src/components/{ => features}/workflow/workflow-input-form.tsx (100%) rename python/packages/devui/frontend/src/components/{ => features}/workflow/workflow-view.tsx (62%) rename python/packages/devui/frontend/src/components/{shared => layout}/about-modal.tsx (100%) rename python/packages/devui/frontend/src/components/{shared => layout}/app-header.tsx (96%) rename python/packages/devui/frontend/src/components/{shared => layout}/debug-panel.tsx (85%) rename python/packages/devui/frontend/src/components/{shared => layout}/entity-selector.tsx (100%) create mode 100644 python/packages/devui/frontend/src/components/layout/index.ts rename python/packages/devui/frontend/src/components/{shared => layout}/settings-modal.tsx (100%) delete mode 100644 python/packages/devui/frontend/src/components/message_renderer/ContentRenderer.tsx delete mode 100644 python/packages/devui/frontend/src/components/message_renderer/MessageRenderer.tsx delete mode 100644 python/packages/devui/frontend/src/components/message_renderer/StreamingRenderer.tsx delete mode 100644 python/packages/devui/frontend/src/components/message_renderer/index.ts delete mode 100644 python/packages/devui/frontend/src/components/message_renderer/types.ts create mode 100644 python/packages/devui/samples/README.md delete mode 100644 python/packages/devui/samples/in_memory_mode.py delete mode 100644 python/packages/devui/samples/weather_agent/agent.py delete mode 100644 python/packages/devui/samples/weather_agent_azure/__init__.py create mode 100644 python/packages/devui/tests/test_conversations.py create mode 100644 python/samples/getting_started/devui/README.md rename python/{packages/devui/samples => samples/getting_started/devui}/fanout_workflow/__init__.py (100%) rename python/{packages/devui/samples => samples/getting_started/devui}/fanout_workflow/workflow.py (98%) create mode 100644 python/samples/getting_started/devui/foundry_agent/.env.example rename python/{packages/devui/samples => samples/getting_started/devui}/foundry_agent/__init__.py (100%) rename python/{packages/devui/samples => samples/getting_started/devui}/foundry_agent/agent.py (76%) create mode 100644 python/samples/getting_started/devui/in_memory_mode.py rename python/{packages/devui/samples => samples/getting_started/devui}/spam_workflow/__init__.py (100%) rename python/{packages/devui/samples => samples/getting_started/devui}/spam_workflow/workflow.py (97%) create mode 100644 python/samples/getting_started/devui/weather_agent_azure/.env.example rename python/{packages/devui/samples/weather_agent => samples/getting_started/devui/weather_agent_azure}/__init__.py (100%) rename python/{packages/devui/samples => samples/getting_started/devui}/weather_agent_azure/agent.py (99%) create mode 100644 python/samples/getting_started/devui/workflow_agents/.env.example rename python/{packages/devui/samples => samples/getting_started/devui}/workflow_agents/__init__.py (100%) rename python/{packages/devui/samples => samples/getting_started/devui}/workflow_agents/workflow.py (96%) diff --git a/python/packages/devui/README.md b/python/packages/devui/README.md index a76e1e5e4e..8c70a71f39 100644 --- a/python/packages/devui/README.md +++ b/python/packages/devui/README.md @@ -78,21 +78,65 @@ devui ./agents --tracing framework ## OpenAI-Compatible API -For convenience, you can interact with the agents/workflows using the standard OpenAI API format. Just specify the `entity_id` in the `extra_body` field. This can be an `agent_id` or `workflow_id`. +For convenience, DevUI provides an OpenAI Responses backend API. This means you can run the backend and also use the OpenAI client sdk to connect to it. Use **agent/workflow name as the model**, and set streaming to `True` as needed. ```bash -# Standard OpenAI format +# Simple - use your entity name as the model curl -X POST http://localhost:8080/v1/responses \ -H "Content-Type: application/json" \ -d @- << 'EOF' { - "model": "agent-framework", - "input": "Hello world", - "extra_body": {"entity_id": "weather_agent"} + "model": "weather_agent", + "input": "Hello world" } - ``` +Or use the OpenAI Python SDK: + +```python +from openai import OpenAI + +client = OpenAI( + base_url="http://localhost:8080/v1", + api_key="not-needed" # API key not required for local DevUI +) + +response = client.responses.create( + model="weather_agent", # Your agent/workflow name + input="What's the weather in Seattle?" +) + +# Extract text from response +print(response.output[0].content[0].text) +# Supports streaming with stream=True +``` + +### Multi-turn Conversations + +Use the standard OpenAI `conversation` parameter for multi-turn conversations: + +```python +# Create a conversation +conversation = client.conversations.create( + metadata={"agent_id": "weather_agent"} +) + +# Use it across multiple turns +response1 = client.responses.create( + model="weather_agent", + input="What's the weather in Seattle?", + conversation=conversation.id +) + +response2 = client.responses.create( + model="weather_agent", + input="How about tomorrow?", + conversation=conversation.id # Continues the conversation! +) +``` + +**How it works:** DevUI automatically retrieves the conversation's message history from the stored thread and passes it to the agent. You don't need to manually manage message history - just provide the same `conversation` ID for follow-up requests. + ## CLI Options ```bash @@ -109,30 +153,79 @@ Options: ## Key Endpoints +## API Mapping + +Given that DevUI offers an OpenAI Responses API, it internally maps messages and events from Agent Framework to OpenAI Responses API events (in `_mapper.py`). For transparency, this mapping is shown below: + +| Agent Framework Content | OpenAI Event/Type | Status | +| ------------------------------- | ---------------------------------------- | -------- | +| `TextContent` | `response.output_text.delta` | Standard | +| `TextReasoningContent` | `response.reasoning.delta` | Standard | +| `FunctionCallContent` (initial) | `response.output_item.added` | Standard | +| `FunctionCallContent` (args) | `response.function_call_arguments.delta` | Standard | +| `FunctionResultContent` | `response.function_result.complete` | DevUI | +| `ErrorContent` | `response.error` | Standard | +| `UsageContent` | Final `Response.usage` field (not streamed) | Standard | +| `WorkflowEvent` | `response.workflow_event.complete` | DevUI | +| `DataContent`, `UriContent` | `response.trace.complete` | DevUI | + +- **Standard** = OpenAI Responses API spec +- **DevUI** = Custom extensions for Agent Framework features (workflows, traces, function results) + +### OpenAI Responses API Compliance + +DevUI follows the OpenAI Responses API specification for maximum compatibility: + +**Standard OpenAI Types Used:** +- `ResponseOutputItemAddedEvent` - Output item notifications (function calls) +- `Response.usage` - Token usage (in final response, not streamed) +- All standard text, reasoning, and function call events + +**Custom DevUI Extensions:** +- `response.function_result.complete` - Function execution results (DevUI executes functions, OpenAI doesn't) +- `response.workflow_event.complete` - Agent Framework workflow events +- `response.trace.complete` - Execution traces for debugging + +These custom extensions are clearly namespaced and can be safely ignored by standard OpenAI clients. + +### Entity Management + - `GET /v1/entities` - List discovered agents/workflows - `GET /v1/entities/{entity_id}/info` - Get detailed entity information - `POST /v1/entities/add` - Add entity from URL (for gallery samples) - `DELETE /v1/entities/{entity_id}` - Remove remote entity + +### Execution (OpenAI Responses API) + - `POST /v1/responses` - Execute agent/workflow (streaming or sync) + +### Conversations (OpenAI Standard) + +- `POST /v1/conversations` - Create conversation +- `GET /v1/conversations/{id}` - Get conversation +- `POST /v1/conversations/{id}` - Update conversation metadata +- `DELETE /v1/conversations/{id}` - Delete conversation +- `GET /v1/conversations?agent_id={id}` - List conversations _(DevUI extension)_ +- `POST /v1/conversations/{id}/items` - Add items to conversation +- `GET /v1/conversations/{id}/items` - List conversation items +- `GET /v1/conversations/{id}/items/{item_id}` - Get conversation item + +### Health + - `GET /health` - Health check -- `POST /v1/threads` - Create thread for agent (optional) -- `GET /v1/threads?agent_id={id}` - List threads for agent -- `GET /v1/threads/{thread_id}` - Get thread info -- `DELETE /v1/threads/{thread_id}` - Delete thread -- `GET /v1/threads/{thread_id}/messages` - Get thread messages ## Implementation - **Discovery**: `agent_framework_devui/_discovery.py` - **Execution**: `agent_framework_devui/_executor.py` - **Message Mapping**: `agent_framework_devui/_mapper.py` -- **Session Management**: `agent_framework_devui/_session.py` +- **Conversations**: `agent_framework_devui/_conversations.py` - **API Server**: `agent_framework_devui/_server.py` - **CLI**: `agent_framework_devui/_cli.py` ## Examples -See `samples/` for working agent and workflow implementations. +See working implementations in `python/samples/getting_started/devui/` ## License diff --git a/python/packages/devui/agent_framework_devui/_conversations.py b/python/packages/devui/agent_framework_devui/_conversations.py new file mode 100644 index 0000000000..5b892c8f35 --- /dev/null +++ b/python/packages/devui/agent_framework_devui/_conversations.py @@ -0,0 +1,473 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Conversation storage abstraction for OpenAI Conversations API. + +This module provides a clean abstraction layer for managing conversations +while wrapping AgentFramework's AgentThread underneath. +""" + +import time +import uuid +from abc import ABC, abstractmethod +from typing import Any, Literal, cast + +from agent_framework import AgentThread, ChatMessage +from openai.types.conversations import Conversation, ConversationDeletedResource +from openai.types.conversations.conversation_item import ConversationItem +from openai.types.conversations.message import Message +from openai.types.conversations.text_content import TextContent +from openai.types.responses import ( + ResponseFunctionToolCallItem, + ResponseFunctionToolCallOutputItem, + ResponseInputFile, + ResponseInputImage, +) + +# Type alias for OpenAI Message role literals +MessageRole = Literal["unknown", "user", "assistant", "system", "critic", "discriminator", "developer", "tool"] + + +class ConversationStore(ABC): + """Abstract base class for conversation storage. + + Provides OpenAI Conversations API interface while managing + AgentThread instances underneath. + """ + + @abstractmethod + def create_conversation(self, metadata: dict[str, str] | None = None) -> Conversation: + """Create a new conversation (wraps AgentThread creation). + + Args: + metadata: Optional metadata dict (e.g., {"agent_id": "weather_agent"}) + + Returns: + Conversation object with generated ID + """ + pass + + @abstractmethod + def get_conversation(self, conversation_id: str) -> Conversation | None: + """Retrieve conversation metadata. + + Args: + conversation_id: Conversation ID + + Returns: + Conversation object or None if not found + """ + pass + + @abstractmethod + def update_conversation(self, conversation_id: str, metadata: dict[str, str]) -> Conversation: + """Update conversation metadata. + + Args: + conversation_id: Conversation ID + metadata: New metadata dict + + Returns: + Updated Conversation object + + Raises: + ValueError: If conversation not found + """ + pass + + @abstractmethod + def delete_conversation(self, conversation_id: str) -> ConversationDeletedResource: + """Delete conversation (including AgentThread). + + Args: + conversation_id: Conversation ID + + Returns: + ConversationDeletedResource object + + Raises: + ValueError: If conversation not found + """ + pass + + @abstractmethod + async def add_items(self, conversation_id: str, items: list[dict[str, Any]]) -> list[ConversationItem]: + """Add items to conversation (syncs to AgentThread.message_store). + + Args: + conversation_id: Conversation ID + items: List of conversation items to add + + Returns: + List of added ConversationItem objects + + Raises: + ValueError: If conversation not found + """ + pass + + @abstractmethod + async def list_items( + self, conversation_id: str, limit: int = 100, after: str | None = None, order: str = "asc" + ) -> tuple[list[ConversationItem], bool]: + """List conversation items from AgentThread.message_store. + + Args: + conversation_id: Conversation ID + limit: Maximum number of items to return + after: Cursor for pagination (item_id) + order: Sort order ("asc" or "desc") + + Returns: + Tuple of (items list, has_more boolean) + + Raises: + ValueError: If conversation not found + """ + pass + + @abstractmethod + def get_item(self, conversation_id: str, item_id: str) -> ConversationItem | None: + """Get specific conversation item. + + Args: + conversation_id: Conversation ID + item_id: Item ID + + Returns: + ConversationItem or None if not found + """ + pass + + @abstractmethod + def get_thread(self, conversation_id: str) -> AgentThread | None: + """Get underlying AgentThread for execution (internal use). + + This is the critical method that allows the executor to get the + AgentThread for running agents with conversation context. + + Args: + conversation_id: Conversation ID + + Returns: + AgentThread object or None if not found + """ + pass + + @abstractmethod + def list_conversations_by_metadata(self, metadata_filter: dict[str, str]) -> list[Conversation]: + """Filter conversations by metadata (e.g., agent_id). + + Args: + metadata_filter: Metadata key-value pairs to match + + Returns: + List of matching Conversation objects + """ + pass + + +class InMemoryConversationStore(ConversationStore): + """In-memory conversation storage wrapping AgentThread. + + This implementation stores conversations in memory with their + underlying AgentThread instances for execution. + """ + + def __init__(self) -> None: + """Initialize in-memory conversation storage. + + Storage structure maps conversation IDs to conversation data including + the underlying AgentThread, metadata, and cached ConversationItems. + """ + self._conversations: dict[str, dict[str, Any]] = {} + + # Item index for O(1) lookup: {conversation_id: {item_id: ConversationItem}} + self._item_index: dict[str, dict[str, ConversationItem]] = {} + + def create_conversation(self, metadata: dict[str, str] | None = None) -> Conversation: + """Create a new conversation with underlying AgentThread.""" + conv_id = f"conv_{uuid.uuid4().hex}" + created_at = int(time.time()) + + # Create AgentThread with default ChatMessageStore + thread = AgentThread() + + self._conversations[conv_id] = { + "id": conv_id, + "thread": thread, + "metadata": metadata or {}, + "created_at": created_at, + "items": [], + } + + # Initialize item index for this conversation + self._item_index[conv_id] = {} + + return Conversation(id=conv_id, object="conversation", created_at=created_at, metadata=metadata) + + def get_conversation(self, conversation_id: str) -> Conversation | None: + """Retrieve conversation metadata.""" + conv_data = self._conversations.get(conversation_id) + if not conv_data: + return None + + return Conversation( + id=conv_data["id"], + object="conversation", + created_at=conv_data["created_at"], + metadata=conv_data.get("metadata"), + ) + + def update_conversation(self, conversation_id: str, metadata: dict[str, str]) -> Conversation: + """Update conversation metadata.""" + conv_data = self._conversations.get(conversation_id) + if not conv_data: + raise ValueError(f"Conversation {conversation_id} not found") + + conv_data["metadata"] = metadata + + return Conversation( + id=conv_data["id"], + object="conversation", + created_at=conv_data["created_at"], + metadata=metadata, + ) + + def delete_conversation(self, conversation_id: str) -> ConversationDeletedResource: + """Delete conversation and its AgentThread.""" + if conversation_id not in self._conversations: + raise ValueError(f"Conversation {conversation_id} not found") + + del self._conversations[conversation_id] + # Cleanup item index + self._item_index.pop(conversation_id, None) + + return ConversationDeletedResource(id=conversation_id, object="conversation.deleted", deleted=True) + + async def add_items(self, conversation_id: str, items: list[dict[str, Any]]) -> list[ConversationItem]: + """Add items to conversation and sync to AgentThread.""" + conv_data = self._conversations.get(conversation_id) + if not conv_data: + raise ValueError(f"Conversation {conversation_id} not found") + + thread: AgentThread = conv_data["thread"] + + # Convert items to ChatMessages and add to thread + chat_messages = [] + for item in items: + # Simple conversion - assume text content for now + role = item.get("role", "user") + content = item.get("content", []) + text = content[0].get("text", "") if content else "" + + chat_msg = ChatMessage(role=role, contents=[{"type": "text", "text": text}]) + chat_messages.append(chat_msg) + + # Add messages to AgentThread + await thread.on_new_messages(chat_messages) + + # Create Message objects (ConversationItem is a Union - use concrete Message type) + conv_items: list[ConversationItem] = [] + for msg in chat_messages: + item_id = f"item_{uuid.uuid4().hex}" + + # Extract role - handle both string and enum + role_str = msg.role.value if hasattr(msg.role, "value") else str(msg.role) + role = cast(MessageRole, role_str) # Safe: Agent Framework roles match OpenAI roles + + # Convert ChatMessage contents to OpenAI TextContent format + message_content = [] + for content_item in msg.contents: + if hasattr(content_item, "type") and content_item.type == "text": + # Extract text from TextContent object + text_value = getattr(content_item, "text", "") + message_content.append(TextContent(type="text", text=text_value)) + + # Create Message object (concrete type from ConversationItem union) + message = Message( + id=item_id, + type="message", # Required discriminator for union + role=role, + content=message_content, + status="completed", # Required field + ) + conv_items.append(message) + + # Cache items + conv_data["items"].extend(conv_items) + + # Update item index for O(1) lookup + if conversation_id not in self._item_index: + self._item_index[conversation_id] = {} + + for conv_item in conv_items: + if conv_item.id: # Guard against None + self._item_index[conversation_id][conv_item.id] = conv_item + + return conv_items + + async def list_items( + self, conversation_id: str, limit: int = 100, after: str | None = None, order: str = "asc" + ) -> tuple[list[ConversationItem], bool]: + """List conversation items from AgentThread message store. + + Converts AgentFramework ChatMessages to proper OpenAI ConversationItem types: + - Messages with text/images/files → Message + - Function calls → ResponseFunctionToolCallItem + - Function results → ResponseFunctionToolCallOutputItem + """ + conv_data = self._conversations.get(conversation_id) + if not conv_data: + raise ValueError(f"Conversation {conversation_id} not found") + + thread: AgentThread = conv_data["thread"] + + # Get messages from thread's message store + items: list[ConversationItem] = [] + if thread.message_store: + af_messages = await thread.message_store.list_messages() + + # Convert each AgentFramework ChatMessage to appropriate ConversationItem type(s) + for i, msg in enumerate(af_messages): + item_id = f"item_{i}" + role_str = msg.role.value if hasattr(msg.role, "value") else str(msg.role) + role = cast(MessageRole, role_str) # Safe: Agent Framework roles match OpenAI roles + + # Process each content item in the message + # A single ChatMessage may produce multiple ConversationItems + # (e.g., a message with both text and a function call) + message_contents: list[TextContent | ResponseInputImage | ResponseInputFile] = [] + function_calls = [] + function_results = [] + + for content in msg.contents: + content_type = getattr(content, "type", None) + + if content_type == "text": + # Text content for Message + text_value = getattr(content, "text", "") + message_contents.append(TextContent(type="text", text=text_value)) + + elif content_type == "data": + # Data content (images, files, PDFs) + uri = getattr(content, "uri", "") + media_type = getattr(content, "media_type", None) + + if media_type and media_type.startswith("image/"): + # Convert to ResponseInputImage + message_contents.append( + ResponseInputImage(type="input_image", image_url=uri, detail="auto") + ) + else: + # Convert to ResponseInputFile + # Extract filename from URI if possible + filename = None + if media_type == "application/pdf": + filename = "document.pdf" + + message_contents.append( + ResponseInputFile(type="input_file", file_url=uri, filename=filename) + ) + + elif content_type == "function_call": + # Function call - create separate ConversationItem + call_id = getattr(content, "call_id", None) + name = getattr(content, "name", "") + arguments = getattr(content, "arguments", "") + + if call_id and name: + function_calls.append( + ResponseFunctionToolCallItem( + id=f"{item_id}_call_{call_id}", + call_id=call_id, + name=name, + arguments=arguments, + type="function_call", + status="completed", + ) + ) + + elif content_type == "function_result": + # Function result - create separate ConversationItem + call_id = getattr(content, "call_id", None) + # Output is stored in additional_properties + output = "" + if hasattr(content, "additional_properties"): + output = content.additional_properties.get("output", "") + + if call_id: + function_results.append( + ResponseFunctionToolCallOutputItem( + id=f"{item_id}_result_{call_id}", + call_id=call_id, + output=output, + type="function_call_output", + status="completed", + ) + ) + + # Create ConversationItems based on what we found + # If message has text/images/files, create a Message item + if message_contents: + message = Message( + id=item_id, + type="message", + role=role, # type: ignore + content=message_contents, # type: ignore + status="completed", + ) + items.append(message) + + # Add function call items + items.extend(function_calls) + + # Add function result items + items.extend(function_results) + + # Apply pagination + if order == "desc": + items = items[::-1] + + start_idx = 0 + if after: + # Find the index after the cursor + for i, item in enumerate(items): + if item.id == after: + start_idx = i + 1 + break + + paginated_items = items[start_idx : start_idx + limit] + has_more = len(items) > start_idx + limit + + return paginated_items, has_more + + def get_item(self, conversation_id: str, item_id: str) -> ConversationItem | None: + """Get specific conversation item - O(1) lookup via index.""" + # Use index for O(1) lookup instead of linear search + conv_items = self._item_index.get(conversation_id) + if not conv_items: + return None + + return conv_items.get(item_id) + + def get_thread(self, conversation_id: str) -> AgentThread | None: + """Get AgentThread for execution - CRITICAL for agent.run_stream().""" + conv_data = self._conversations.get(conversation_id) + return conv_data["thread"] if conv_data else None + + def list_conversations_by_metadata(self, metadata_filter: dict[str, str]) -> list[Conversation]: + """Filter conversations by metadata (e.g., agent_id).""" + results = [] + for conv_data in self._conversations.values(): + conv_meta = conv_data.get("metadata", {}) + # Check if all filter items match + if all(conv_meta.get(k) == v for k, v in metadata_filter.items()): + results.append( + Conversation( + id=conv_data["id"], + object="conversation", + created_at=conv_data["created_at"], + metadata=conv_meta, + ) + ) + return results diff --git a/python/packages/devui/agent_framework_devui/_discovery.py b/python/packages/devui/agent_framework_devui/_discovery.py index 0ddc370d68..631ba60412 100644 --- a/python/packages/devui/agent_framework_devui/_discovery.py +++ b/python/packages/devui/agent_framework_devui/_discovery.py @@ -20,6 +20,10 @@ from .models._discovery_models import EntityInfo logger = logging.getLogger(__name__) +# Constants for remote entity fetching +REMOTE_FETCH_TIMEOUT_SECONDS = 30.0 +REMOTE_FETCH_MAX_SIZE_MB = 10 + class EntityDiscovery: """Discovery for Agent Framework entities - agents and workflows.""" @@ -116,16 +120,9 @@ class EntityDiscovery: # Extract metadata with improved fallback naming name = getattr(entity_object, "name", None) if not name: - # In-memory entities: use ID with entity type prefix since no directory name available - entity_id_raw = getattr(entity_object, "id", None) - if entity_id_raw: - # Truncate UUID to first 8 characters for readability - short_id = str(entity_id_raw)[:8] if len(str(entity_id_raw)) > 8 else str(entity_id_raw) - name = f"{entity_type.title()} {short_id}" - else: - # Fallback to class name with entity type - class_name = entity_object.__class__.__name__ - name = f"{entity_type.title()} {class_name}" + # In-memory entities: use class name as it's more readable than UUID + class_name = entity_object.__class__.__name__ + name = f"{entity_type.title()} {class_name}" description = getattr(entity_object, "description", "") # Generate entity ID using Agent Framework specific naming @@ -142,43 +139,27 @@ class EntityDiscovery: middleware_list = None if entity_type == "agent": - # Try to get instructions - if hasattr(entity_object, "chat_options") and hasattr(entity_object.chat_options, "instructions"): - instructions = entity_object.chat_options.instructions + from ._utils import extract_agent_metadata - # Try to get model - check both chat_options and chat_client - if ( - hasattr(entity_object, "chat_options") - and hasattr(entity_object.chat_options, "model_id") - and entity_object.chat_options.model_id - ): - model = entity_object.chat_options.model_id - elif hasattr(entity_object, "chat_client") and hasattr(entity_object.chat_client, "model_id"): - model = entity_object.chat_client.model_id + agent_meta = extract_agent_metadata(entity_object) + instructions = agent_meta["instructions"] + model = agent_meta["model"] + chat_client_type = agent_meta["chat_client_type"] + context_providers_list = agent_meta["context_providers"] + middleware_list = agent_meta["middleware"] - # Try to get chat client type - if hasattr(entity_object, "chat_client"): - chat_client_type = entity_object.chat_client.__class__.__name__ + # Log helpful info about agent capabilities (before creating EntityInfo) + if entity_type == "agent": + has_run_stream = hasattr(entity_object, "run_stream") + has_run = hasattr(entity_object, "run") - # Try to get context providers - if ( - hasattr(entity_object, "context_provider") - and entity_object.context_provider - and hasattr(entity_object.context_provider, "__class__") - ): - context_providers_list = [entity_object.context_provider.__class__.__name__] - - # Try to get middleware - if hasattr(entity_object, "middleware") and entity_object.middleware: - middleware_list = [] - for m in entity_object.middleware: - # Try multiple ways to get a good name for middleware - if hasattr(m, "__name__"): # Function or callable - middleware_list.append(m.__name__) - elif hasattr(m, "__class__"): # Class instance - middleware_list.append(m.__class__.__name__) - else: - middleware_list.append(str(m)) + if not has_run_stream and has_run: + logger.info( + f"Agent '{entity_id}' only has run() (non-streaming). " + "DevUI will automatically convert to streaming." + ) + elif not has_run_stream and not has_run: + logger.warning(f"Agent '{entity_id}' lacks both run() and run_stream() methods. May not work.") # Create EntityInfo with Agent Framework specifics return EntityInfo( @@ -444,7 +425,9 @@ class EntityDiscovery: pass # Fallback to duck typing for agent protocol - if hasattr(obj, "run_stream") and hasattr(obj, "id") and hasattr(obj, "name"): + # Agent must have either run_stream() or run() method, plus id and name + has_execution_method = hasattr(obj, "run_stream") or hasattr(obj, "run") + if has_execution_method and hasattr(obj, "id") and hasattr(obj, "name"): return True except (TypeError, AttributeError): @@ -482,13 +465,9 @@ class EntityDiscovery: # Extract metadata from the live object with improved fallback naming name = getattr(obj, "name", None) if not name: - entity_id_raw = getattr(obj, "id", None) - if entity_id_raw: - # Truncate UUID to first 8 characters for readability - short_id = str(entity_id_raw)[:8] if len(str(entity_id_raw)) > 8 else str(entity_id_raw) - name = f"{obj_type.title()} {short_id}" - else: - name = f"{obj_type.title()} {obj.__class__.__name__}" + # Use class name as it's more readable than UUID + class_name = obj.__class__.__name__ + name = f"{obj_type.title()} {class_name}" description = getattr(obj, "description", None) tools = await self._extract_tools_from_object(obj, obj_type) @@ -505,39 +484,14 @@ class EntityDiscovery: middleware_list = None if obj_type == "agent": - # Try to get instructions - if hasattr(obj, "chat_options") and hasattr(obj.chat_options, "instructions"): - instructions = obj.chat_options.instructions + from ._utils import extract_agent_metadata - # Try to get model - check both chat_options and chat_client - if hasattr(obj, "chat_options") and hasattr(obj.chat_options, "model_id") and obj.chat_options.model_id: - model = obj.chat_options.model_id - elif hasattr(obj, "chat_client") and hasattr(obj.chat_client, "model_id"): - model = obj.chat_client.model_id - - # Try to get chat client type - if hasattr(obj, "chat_client"): - chat_client_type = obj.chat_client.__class__.__name__ - - # Try to get context providers - if ( - hasattr(obj, "context_provider") - and obj.context_provider - and hasattr(obj.context_provider, "__class__") - ): - context_providers_list = [obj.context_provider.__class__.__name__] - - # Try to get middleware - if hasattr(obj, "middleware") and obj.middleware: - middleware_list = [] - for m in obj.middleware: - # Try multiple ways to get a good name for middleware - if hasattr(m, "__name__"): # Function or callable - middleware_list.append(m.__name__) - elif hasattr(m, "__class__"): # Class instance - middleware_list.append(m.__class__.__name__) - else: - middleware_list.append(str(m)) + agent_meta = extract_agent_metadata(obj) + instructions = agent_meta["instructions"] + model = agent_meta["model"] + chat_client_type = agent_meta["chat_client_type"] + context_providers_list = agent_meta["context_providers"] + middleware_list = agent_meta["middleware"] entity_info = EntityInfo( id=entity_id, @@ -628,7 +582,7 @@ class EntityDiscovery: source: Source of entity (directory, in_memory, remote) Returns: - Unique entity ID with format: {type}_{source}_{name}_{uuid8} + Unique entity ID with format: {type}_{source}_{name}_{uuid} """ import re @@ -644,10 +598,10 @@ class EntityDiscovery: else: base_name = "entity" - # Generate short UUID (8 chars = 4 billion combinations) - short_uuid = uuid.uuid4().hex[:8] + # Generate full UUID for guaranteed uniqueness + full_uuid = uuid.uuid4().hex - return f"{entity_type}_{source}_{base_name}_{short_uuid}" + return f"{entity_type}_{source}_{base_name}_{full_uuid}" async def fetch_remote_entity( self, url: str, metadata: dict[str, Any] | None = None @@ -722,12 +676,10 @@ class EntityDiscovery: return url - async def _fetch_url_content(self, url: str, max_size_mb: int = 10) -> str | None: + async def _fetch_url_content(self, url: str, max_size_mb: int = REMOTE_FETCH_MAX_SIZE_MB) -> str | None: """Fetch content from URL with size and timeout limits.""" try: - timeout = 30.0 # 30 second timeout - - async with httpx.AsyncClient(timeout=timeout) as client: + async with httpx.AsyncClient(timeout=REMOTE_FETCH_TIMEOUT_SECONDS) as client: response = await client.get(url) if response.status_code != 200: diff --git a/python/packages/devui/agent_framework_devui/_executor.py b/python/packages/devui/agent_framework_devui/_executor.py index 73edfde74f..68740732e9 100644 --- a/python/packages/devui/agent_framework_devui/_executor.py +++ b/python/packages/devui/agent_framework_devui/_executor.py @@ -5,12 +5,12 @@ import json import logging import os -import uuid from collections.abc import AsyncGenerator -from typing import Any, get_origin +from typing import Any -from agent_framework import AgentThread +from agent_framework import AgentProtocol +from ._conversations import ConversationStore, InMemoryConversationStore from ._discovery import EntityDiscovery from ._mapper import MessageMapper from ._tracing import capture_traces @@ -29,21 +29,26 @@ class EntityNotFoundError(Exception): class AgentFrameworkExecutor: """Executor for Agent Framework entities - agents and workflows.""" - def __init__(self, entity_discovery: EntityDiscovery, message_mapper: MessageMapper): + def __init__( + self, + entity_discovery: EntityDiscovery, + message_mapper: MessageMapper, + conversation_store: ConversationStore | None = None, + ): """Initialize Agent Framework executor. Args: entity_discovery: Entity discovery instance message_mapper: Message mapper instance + conversation_store: Optional conversation store (defaults to in-memory) """ self.entity_discovery = entity_discovery self.message_mapper = message_mapper self._setup_tracing_provider() self._setup_agent_framework_tracing() - # Minimal thread storage - no metadata needed - self.thread_storage: dict[str, AgentThread] = {} - self.agent_threads: dict[str, list[str]] = {} # agent_id -> thread_ids + # Use provided conversation store or default to in-memory + self.conversation_store = conversation_store or InMemoryConversationStore() def _setup_tracing_provider(self) -> None: """Set up our own TracerProvider so we can add processors.""" @@ -83,199 +88,6 @@ class AgentFrameworkExecutor: else: logger.debug("ENABLE_OTEL not set, skipping observability setup") - # Thread Management Methods - def create_thread(self, agent_id: str) -> str: - """Create new thread for agent.""" - thread_id = f"thread_{uuid.uuid4().hex[:8]}" - thread = AgentThread() - - self.thread_storage[thread_id] = thread - - if agent_id not in self.agent_threads: - self.agent_threads[agent_id] = [] - self.agent_threads[agent_id].append(thread_id) - - return thread_id - - def get_thread(self, thread_id: str) -> AgentThread | None: - """Get AgentThread by ID.""" - return self.thread_storage.get(thread_id) - - def list_threads_for_agent(self, agent_id: str) -> list[str]: - """List thread IDs for agent.""" - return self.agent_threads.get(agent_id, []) - - def get_agent_for_thread(self, thread_id: str) -> str | None: - """Find which agent owns this thread.""" - for agent_id, thread_ids in self.agent_threads.items(): - if thread_id in thread_ids: - return agent_id - return None - - def delete_thread(self, thread_id: str) -> bool: - """Delete thread.""" - if thread_id not in self.thread_storage: - return False - - for _agent_id, thread_ids in self.agent_threads.items(): - if thread_id in thread_ids: - thread_ids.remove(thread_id) - break - - del self.thread_storage[thread_id] - return True - - async def get_thread_messages(self, thread_id: str) -> list[dict[str, Any]]: - """Get messages from a thread's message store, preserving all content types for UI display.""" - thread = self.get_thread(thread_id) - if not thread or not thread.message_store: - return [] - - try: - # Get AgentFramework ChatMessage objects from thread - af_messages = await thread.message_store.list_messages() - - ui_messages = [] - for i, af_msg in enumerate(af_messages): - # Extract role value (handle enum) - role = af_msg.role.value if hasattr(af_msg.role, "value") else str(af_msg.role) - - # Skip tool/function messages - only show user and assistant messages - if role not in ["user", "assistant"]: - continue - - # Extract all user-facing content (text, images, files, etc.) - display_contents = self._extract_display_contents(af_msg.contents) - - # Skip messages with no displayable content - if not display_contents: - continue - - # Extract usage information if present - usage_data = None - for content in af_msg.contents: - content_type = getattr(content, "type", None) - if content_type == "usage": - details = getattr(content, "details", None) - if details: - usage_data = { - "total_tokens": getattr(details, "total_token_count", 0) or 0, - "prompt_tokens": getattr(details, "input_token_count", 0) or 0, - "completion_tokens": getattr(details, "output_token_count", 0) or 0, - } - break - - ui_message = { - "id": af_msg.message_id or f"restored-{i}", - "role": role, - "contents": display_contents, - "timestamp": __import__("datetime").datetime.now().isoformat(), - "author_name": af_msg.author_name, - "message_id": af_msg.message_id, - } - - # Add usage data if available - if usage_data: - ui_message["usage"] = usage_data - - ui_messages.append(ui_message) - - logger.info(f"Restored {len(ui_messages)} display messages for thread {thread_id}") - return ui_messages - - except Exception as e: - logger.error(f"Error getting thread messages: {e}") - import traceback - - logger.error(traceback.format_exc()) - return [] - - def _extract_display_contents(self, contents: list[Any]) -> list[dict[str, Any]]: - """Extract all user-facing content (text, images, files, etc.) from message contents. - - Filters out internal mechanics like function calls/results while preserving - all content types that should be displayed in the UI. - """ - display_contents = [] - - for content in contents: - content_type = getattr(content, "type", None) - - # Text content - if content_type == "text": - text = getattr(content, "text", "") - - # Handle double-encoded JSON from user messages - if text.startswith('{"role":'): - try: - import json - - parsed = json.loads(text) - if parsed.get("contents"): - for sub_content in parsed["contents"]: - if sub_content.get("type") == "text": - display_contents.append({"type": "text", "text": sub_content.get("text", "")}) - except Exception: - display_contents.append({"type": "text", "text": text}) - else: - display_contents.append({"type": "text", "text": text}) - - # Data content (images, files, PDFs, etc.) - elif content_type == "data": - display_contents.append({ - "type": "data", - "uri": getattr(content, "uri", ""), - "media_type": getattr(content, "media_type", None), - }) - - # URI content (external links to images/files) - elif content_type == "uri": - display_contents.append({ - "type": "uri", - "uri": getattr(content, "uri", ""), - "media_type": getattr(content, "media_type", None), - }) - - # Skip function_call, function_result, and other internal content types - - return display_contents - - async def serialize_thread(self, thread_id: str) -> dict[str, Any] | None: - """Serialize thread state for persistence.""" - thread = self.get_thread(thread_id) - if not thread: - return None - - try: - # Use AgentThread's built-in serialization - serialized_state = await thread.serialize() - - # Add our metadata - agent_id = self.get_agent_for_thread(thread_id) - serialized_state["metadata"] = {"agent_id": agent_id, "thread_id": thread_id} - - return serialized_state - - except Exception as e: - logger.error(f"Error serializing thread {thread_id}: {e}") - return None - - async def deserialize_thread(self, thread_id: str, agent_id: str, serialized_state: dict[str, Any]) -> bool: - """Deserialize thread state from persistence.""" - try: - thread = await AgentThread.deserialize(serialized_state) - # Store the restored thread - self.thread_storage[thread_id] = thread - if agent_id not in self.agent_threads: - self.agent_threads[agent_id] = [] - self.agent_threads[agent_id].append(thread_id) - - return True - - except Exception as e: - logger.error(f"Error deserializing thread {thread_id}: {e}") - return False - async def discover_entities(self) -> list[EntityInfo]: """Discover all available entities. @@ -390,7 +202,7 @@ class AgentFrameworkExecutor: yield {"type": "error", "message": str(e), "entity_id": entity_id} async def _execute_agent( - self, agent: Any, request: AgentFrameworkRequest, trace_collector: Any + self, agent: AgentProtocol, request: AgentFrameworkRequest, trace_collector: Any ) -> AsyncGenerator[Any, None]: """Execute Agent Framework agent with trace collection and optional thread support. @@ -406,34 +218,51 @@ class AgentFrameworkExecutor: # Convert input to proper ChatMessage or string user_message = self._convert_input_to_chat_message(request.input) - # Get thread if provided in extra_body + # Get thread from conversation parameter (OpenAI standard!) thread = None - if request.extra_body and hasattr(request.extra_body, "thread_id") and request.extra_body.thread_id: - thread_id = request.extra_body.thread_id - thread = self.get_thread(thread_id) + conversation_id = request.get_conversation_id() + if conversation_id: + thread = self.conversation_store.get_thread(conversation_id) if thread: - logger.debug(f"Using existing thread: {thread_id}") + logger.debug(f"Using existing conversation: {conversation_id}") else: - logger.warning(f"Thread {thread_id} not found, proceeding without thread") + logger.warning(f"Conversation {conversation_id} not found, proceeding without thread") if isinstance(user_message, str): logger.debug(f"Executing agent with text input: {user_message[:100]}...") else: logger.debug(f"Executing agent with multimodal ChatMessage: {type(user_message)}") + # Check if agent supports streaming + if hasattr(agent, "run_stream") and callable(agent.run_stream): + # Use Agent Framework's native streaming with optional thread + if thread: + async for update in agent.run_stream(user_message, thread=thread): + for trace_event in trace_collector.get_pending_events(): + yield trace_event - # Use Agent Framework's native streaming with optional thread - if thread: - async for update in agent.run_stream(user_message, thread=thread): - for trace_event in trace_collector.get_pending_events(): - yield trace_event + yield update + else: + async for update in agent.run_stream(user_message): + for trace_event in trace_collector.get_pending_events(): + yield trace_event - yield update + yield update + elif hasattr(agent, "run") and callable(agent.run): + # Non-streaming agent - use run() and yield complete response + logger.info("Agent lacks run_stream(), using run() method (non-streaming)") + if thread: + response = await agent.run(user_message, thread=thread) + else: + response = await agent.run(user_message) + + # Yield trace events before response + for trace_event in trace_collector.get_pending_events(): + yield trace_event + + # Yield the complete response (mapper will convert to streaming events) + yield response else: - async for update in agent.run_stream(user_message): - for trace_event in trace_collector.get_pending_events(): - yield trace_event - - yield update + raise ValueError("Agent must implement either run() or run_stream() method") except Exception as e: logger.error(f"Error in agent execution: {e}") @@ -455,8 +284,8 @@ class AgentFrameworkExecutor: try: # Get input data - prefer structured data from extra_body input_data: str | list[Any] | dict[str, Any] - if request.extra_body and hasattr(request.extra_body, "input_data") and request.extra_body.input_data: - input_data = request.extra_body.input_data + if request.extra_body and isinstance(request.extra_body, dict) and request.extra_body.get("input_data"): + input_data = request.extra_body.get("input_data") # type: ignore logger.debug(f"Using structured input_data from extra_body: {type(input_data)}") else: input_data = request.input @@ -483,6 +312,9 @@ class AgentFrameworkExecutor: def _convert_input_to_chat_message(self, input_data: Any) -> Any: """Convert OpenAI Responses API input to Agent Framework ChatMessage or string. + Handles various input formats including text, images, files, and multimodal content. + Falls back to string extraction for simple cases. + Args: input_data: OpenAI ResponseInputParam (List[ResponseInputItemParam]) @@ -512,6 +344,9 @@ class AgentFrameworkExecutor: ) -> Any: """Convert OpenAI ResponseInputParam to Agent Framework ChatMessage. + Processes text, images, files, and other content types from OpenAI format + to Agent Framework ChatMessage with appropriate content objects. + Args: input_items: List of OpenAI ResponseInputItemParam objects (dicts or objects) ChatMessage: ChatMessage class for creating chat messages @@ -597,6 +432,40 @@ class AgentFrameworkExecutor: elif file_url: contents.append(DataContent(uri=file_url, media_type=media_type)) + elif content_type == "function_approval_response": + # Handle function approval response (DevUI extension) + try: + from agent_framework import FunctionApprovalResponseContent, FunctionCallContent + + request_id = content_item.get("request_id", "") + approved = content_item.get("approved", False) + function_call_data = content_item.get("function_call", {}) + + # Create FunctionCallContent from the function_call data + function_call = FunctionCallContent( + call_id=function_call_data.get("id", ""), + name=function_call_data.get("name", ""), + arguments=function_call_data.get("arguments", {}), + ) + + # Create FunctionApprovalResponseContent with correct signature + approval_response = FunctionApprovalResponseContent( + approved, # positional argument + id=request_id, # keyword argument 'id', NOT 'request_id' + function_call=function_call, # FunctionCallContent object + ) + contents.append(approval_response) + logger.info( + f"Added FunctionApprovalResponseContent: id={request_id}, " + f"approved={approved}, call_id={function_call.call_id}" + ) + except ImportError: + logger.warning( + "FunctionApprovalResponseContent not available in agent_framework" + ) + except Exception as e: + logger.error(f"Failed to create FunctionApprovalResponseContent: {e}") + # Handle other OpenAI input item types as needed # (tool calls, function results, etc.) @@ -687,23 +556,6 @@ class AgentFrameworkExecutor: return start_executor, message_types - def _select_primary_input_type(self, message_types: list[Any]) -> Any | None: - """Choose the most user-friendly input type for workflow kick-off.""" - if not message_types: - return None - - preferred = (str, dict) - - for candidate in preferred: - for message_type in message_types: - if message_type is candidate: - return candidate - origin = get_origin(message_type) - if origin is candidate: - return candidate - - return message_types[0] - def _parse_structured_workflow_input(self, workflow: Any, input_data: dict[str, Any]) -> Any: """Parse structured input data for workflow execution. @@ -728,7 +580,9 @@ class AgentFrameworkExecutor: return input_data # Get the first (primary) input type - input_type = self._select_primary_input_type(message_types) + from ._utils import select_primary_input_type + + input_type = select_primary_input_type(message_types) if input_type is None: logger.debug("Could not select primary input type for workflow - using raw dict") return input_data @@ -764,7 +618,9 @@ class AgentFrameworkExecutor: return raw_input # Get the first (primary) input type - input_type = self._select_primary_input_type(message_types) + from ._utils import select_primary_input_type + + input_type = select_primary_input_type(message_types) if input_type is None: logger.debug("Could not select primary input type for workflow - using raw string") return raw_input diff --git a/python/packages/devui/agent_framework_devui/_mapper.py b/python/packages/devui/agent_framework_devui/_mapper.py index 4866e68230..e950216ae2 100644 --- a/python/packages/devui/agent_framework_devui/_mapper.py +++ b/python/packages/devui/agent_framework_devui/_mapper.py @@ -5,6 +5,7 @@ import json import logging import uuid +from collections import OrderedDict from collections.abc import Sequence from datetime import datetime from typing import Any, Union @@ -17,6 +18,8 @@ from .models import ( ResponseErrorEvent, ResponseFunctionCallArgumentsDeltaEvent, ResponseFunctionResultComplete, + ResponseFunctionToolCall, + ResponseOutputItemAddedEvent, ResponseOutputMessage, ResponseOutputText, ResponseReasoningTextDeltaEvent, @@ -24,7 +27,6 @@ from .models import ( ResponseTextDeltaEvent, ResponseTraceEventComplete, ResponseUsage, - ResponseUsageEventComplete, ResponseWorkflowEventComplete, ) @@ -34,19 +36,26 @@ logger = logging.getLogger(__name__) EventType = Union[ ResponseStreamEvent, ResponseWorkflowEventComplete, - ResponseFunctionResultComplete, + ResponseOutputItemAddedEvent, ResponseTraceEventComplete, - ResponseUsageEventComplete, ] class MessageMapper: """Maps Agent Framework messages/responses to OpenAI format.""" - def __init__(self) -> None: - """Initialize Agent Framework message mapper.""" + def __init__(self, max_contexts: int = 1000) -> None: + """Initialize Agent Framework message mapper. + + Args: + max_contexts: Maximum number of contexts to keep in memory (default: 1000) + """ self.sequence_counter = 0 - self._conversion_contexts: dict[int, dict[str, Any]] = {} + self._conversion_contexts: OrderedDict[int, dict[str, Any]] = OrderedDict() + self._max_contexts = max_contexts + + # Track usage per request for final Response.usage (OpenAI standard) + self._usage_accumulator: dict[str, dict[str, int]] = {} # Register content type mappers for all 12 Agent Framework content types self.content_mappers = { @@ -95,7 +104,7 @@ class MessageMapper: # Import Agent Framework types for proper isinstance checks try: - from agent_framework import AgentRunResponseUpdate, WorkflowEvent + from agent_framework import AgentRunResponse, AgentRunResponseUpdate, WorkflowEvent from agent_framework._workflows._events import AgentRunUpdateEvent # Handle AgentRunUpdateEvent - workflow event wrapping AgentRunResponseUpdate @@ -107,6 +116,10 @@ class MessageMapper: # If no data, treat as generic workflow event return await self._convert_workflow_event(raw_event, context) + # Handle complete agent response (AgentRunResponse) - for non-streaming agent execution + if isinstance(raw_event, AgentRunResponse): + return await self._convert_agent_response(raw_event, context) + # Handle agent updates (AgentRunResponseUpdate) - for direct agent execution if isinstance(raw_event, AgentRunResponseUpdate): return await self._convert_agent_update(raw_event, context) @@ -159,17 +172,31 @@ class MessageMapper: status="completed", ) - # Create usage object - input_token_count = len(str(request.input)) // 4 if request.input else 0 - output_token_count = len(full_content) // 4 + # Get usage from accumulator (OpenAI standard) + request_id = str(id(request)) + usage_data = self._usage_accumulator.get(request_id) - usage = ResponseUsage( - input_tokens=input_token_count, - output_tokens=output_token_count, - total_tokens=input_token_count + output_token_count, - input_tokens_details=InputTokensDetails(cached_tokens=0), - output_tokens_details=OutputTokensDetails(reasoning_tokens=0), - ) + if usage_data: + usage = ResponseUsage( + input_tokens=usage_data["input_tokens"], + output_tokens=usage_data["output_tokens"], + total_tokens=usage_data["total_tokens"], + input_tokens_details=InputTokensDetails(cached_tokens=0), + output_tokens_details=OutputTokensDetails(reasoning_tokens=0), + ) + # Cleanup accumulator + del self._usage_accumulator[request_id] + else: + # Fallback: estimate if no usage was tracked + input_token_count = len(str(request.input)) // 4 if request.input else 0 + output_token_count = len(full_content) // 4 + usage = ResponseUsage( + input_tokens=input_token_count, + output_tokens=output_token_count, + total_tokens=input_token_count + output_token_count, + input_tokens_details=InputTokensDetails(cached_tokens=0), + output_tokens_details=OutputTokensDetails(reasoning_tokens=0), + ) return OpenAIResponse( id=f"resp_{uuid.uuid4().hex[:12]}", @@ -186,10 +213,18 @@ class MessageMapper: except Exception as e: logger.exception(f"Error aggregating response: {e}") return await self._create_error_response(str(e), request) + finally: + # Cleanup: Remove context after aggregation to prevent memory leak + # This handles the common case where streaming completes successfully + request_key = id(request) + if self._conversion_contexts.pop(request_key, None): + logger.debug(f"Cleaned up context for request {request_key} after aggregation") def _get_or_create_context(self, request: AgentFrameworkRequest) -> dict[str, Any]: """Get or create conversion context for this request. + Uses LRU eviction when max_contexts is reached to prevent unbounded memory growth. + Args: request: Request to get context for @@ -197,13 +232,26 @@ class MessageMapper: Conversion context dictionary """ request_key = id(request) + if request_key not in self._conversion_contexts: + # Evict oldest context if at capacity (LRU eviction) + if len(self._conversion_contexts) >= self._max_contexts: + evicted_key, _ = self._conversion_contexts.popitem(last=False) + logger.debug(f"Evicted oldest context (key={evicted_key}) - at max capacity ({self._max_contexts})") + self._conversion_contexts[request_key] = { "sequence_counter": 0, "item_id": f"msg_{uuid.uuid4().hex[:8]}", "content_index": 0, "output_index": 0, + "request_id": str(request_key), # For usage accumulation + # Track active function calls: {call_id: {name, item_id, args_chunks}} + "active_function_calls": {}, } + else: + # Move to end (mark as recently used for LRU) + self._conversion_contexts.move_to_end(request_key) + return self._conversion_contexts[request_key] def _next_sequence(self, context: dict[str, Any]) -> int: @@ -240,10 +288,11 @@ class MessageMapper: if content_type in self.content_mappers: mapped_events = await self.content_mappers[content_type](content, context) - if isinstance(mapped_events, list): - events.extend(mapped_events) - else: - events.append(mapped_events) + if mapped_events is not None: # Handle None returns (e.g., UsageContent) + if isinstance(mapped_events, list): + events.extend(mapped_events) + else: + events.append(mapped_events) else: # Graceful fallback for unknown content types events.append(await self._create_unknown_content_event(content, context)) @@ -256,6 +305,59 @@ class MessageMapper: return events + async def _convert_agent_response(self, response: Any, context: dict[str, Any]) -> Sequence[Any]: + """Convert complete AgentRunResponse to OpenAI events. + + This handles non-streaming agent execution where agent.run() returns + a complete AgentRunResponse instead of streaming AgentRunResponseUpdate objects. + + Args: + response: Agent run response (AgentRunResponse) + context: Conversion context + + Returns: + List of OpenAI response stream events + """ + events: list[Any] = [] + + try: + # Extract all messages from the response + messages = getattr(response, "messages", []) + + # Convert each message's contents to streaming events + for message in messages: + if hasattr(message, "contents") and message.contents: + for content in message.contents: + content_type = content.__class__.__name__ + + if content_type in self.content_mappers: + mapped_events = await self.content_mappers[content_type](content, context) + if mapped_events is not None: # Handle None returns (e.g., UsageContent) + if isinstance(mapped_events, list): + events.extend(mapped_events) + else: + events.append(mapped_events) + else: + # Graceful fallback for unknown content types + events.append(await self._create_unknown_content_event(content, context)) + + context["content_index"] += 1 + + # Add usage information if present + usage_details = getattr(response, "usage_details", None) + if usage_details: + from agent_framework import UsageContent + + usage_content = UsageContent(details=usage_details) + await self._map_usage_content(usage_content, context) + # Note: _map_usage_content returns None - it accumulates usage for final Response.usage + + except Exception as e: + logger.warning(f"Error converting agent response: {e}") + events.append(await self._create_error_event(str(e), context)) + + return events + async def _convert_workflow_event(self, event: Any, context: dict[str, Any]) -> Sequence[Any]: """Convert workflow event to structured OpenAI events. @@ -317,41 +419,143 @@ class MessageMapper: async def _map_function_call_content( self, content: Any, context: dict[str, Any] - ) -> list[ResponseFunctionCallArgumentsDeltaEvent]: - """Map FunctionCallContent to ResponseFunctionCallArgumentsDeltaEvent(s).""" - events = [] + ) -> list[ResponseFunctionCallArgumentsDeltaEvent | ResponseOutputItemAddedEvent]: + """Map FunctionCallContent to OpenAI events following Responses API spec. - # For streaming, need to chunk the arguments JSON - args_str = json.dumps(content.arguments) if hasattr(content, "arguments") and content.arguments else "{}" + Agent Framework emits FunctionCallContent in two patterns: + 1. First event: call_id + name + empty/no arguments + 2. Subsequent events: empty call_id/name + argument chunks - # Chunk the JSON string for streaming - for chunk in self._chunk_json_string(args_str): + We emit: + 1. response.output_item.added (with full metadata) for the first event + 2. response.function_call_arguments.delta (referencing item_id) for chunks + """ + events: list[ResponseFunctionCallArgumentsDeltaEvent | ResponseOutputItemAddedEvent] = [] + + # CASE 1: New function call (has call_id and name) + # This is the first event that establishes the function call + if content.call_id and content.name: + # Use call_id as item_id (simpler, and call_id uniquely identifies the call) + item_id = content.call_id + + # Track this function call for later argument deltas + context["active_function_calls"][content.call_id] = { + "item_id": item_id, + "name": content.name, + "arguments_chunks": [], + } + + logger.debug(f"New function call: {content.name} (call_id={content.call_id})") + + # Emit response.output_item.added event per OpenAI spec events.append( - ResponseFunctionCallArgumentsDeltaEvent( - type="response.function_call_arguments.delta", - delta=chunk, - item_id=context["item_id"], + ResponseOutputItemAddedEvent( + type="response.output_item.added", + item=ResponseFunctionToolCall( + id=content.call_id, # Use call_id as the item id + call_id=content.call_id, + name=content.name, + arguments="", # Empty initially, will be filled by deltas + type="function_call", + status="in_progress", + ), output_index=context["output_index"], sequence_number=self._next_sequence(context), ) ) + # CASE 2: Argument deltas (content has arguments, possibly without call_id/name) + if content.arguments: + # Find the active function call for these arguments + active_call = self._get_active_function_call(content, context) + + if active_call: + item_id = active_call["item_id"] + + # Convert arguments to string if it's a dict (Agent Framework may send either) + delta_str = content.arguments if isinstance(content.arguments, str) else json.dumps(content.arguments) + + # Emit argument delta referencing the item_id + events.append( + ResponseFunctionCallArgumentsDeltaEvent( + type="response.function_call_arguments.delta", + delta=delta_str, + item_id=item_id, + output_index=context["output_index"], + sequence_number=self._next_sequence(context), + ) + ) + + # Track chunk for debugging + active_call["arguments_chunks"].append(delta_str) + else: + logger.warning(f"Received function call arguments without active call: {content.arguments[:50]}...") + return events + def _get_active_function_call(self, content: Any, context: dict[str, Any]) -> dict[str, Any] | None: + """Find the active function call for this content. + + Uses call_id if present, otherwise falls back to most recent call. + Necessary because Agent Framework may send argument chunks without call_id. + + Args: + content: FunctionCallContent with possible call_id + context: Conversion context with active_function_calls + + Returns: + Active call dict or None + """ + active_calls: dict[str, dict[str, Any]] = context["active_function_calls"] + + # If content has call_id, use it to find the exact call + if hasattr(content, "call_id") and content.call_id: + result = active_calls.get(content.call_id) + return result if result is not None else None + + # Otherwise, use the most recent call (last one added) + # This handles the case where Agent Framework sends argument chunks + # without call_id in subsequent events + if active_calls: + return list(active_calls.values())[-1] + + return None + async def _map_function_result_content( self, content: Any, context: dict[str, Any] ) -> ResponseFunctionResultComplete: - """Map FunctionResultContent to structured event.""" + """Map FunctionResultContent to custom DevUI event. + + This is a DevUI extension - OpenAI doesn't stream function execution results + because in their model, applications execute functions, not the API. + Agent Framework executes functions, so we emit this event for debugging visibility. + + IMPORTANT: Always use Agent Framework's call_id from the content. + Do NOT generate a new call_id - it must match the one from the function call event. + """ + # Get call_id from content - this MUST match the call_id from the function call + call_id = getattr(content, "call_id", None) + + if not call_id: + logger.warning("FunctionResultContent missing call_id - this will break call/result pairing") + call_id = f"call_{uuid.uuid4().hex[:8]}" # Fallback only if truly missing + + # Extract result + result = getattr(content, "result", None) + exception = getattr(content, "exception", None) + + # Convert result to string + output = result if isinstance(result, str) else json.dumps(result) if result is not None else "" + + # Determine status + status = "incomplete" if exception else "completed" + + # Return custom DevUI event return ResponseFunctionResultComplete( type="response.function_result.complete", - data={ - "call_id": getattr(content, "call_id", f"call_{uuid.uuid4().hex[:8]}"), - "result": getattr(content, "result", None), - "status": "completed" if not getattr(content, "exception", None) else "failed", - "exception": str(getattr(content, "exception", None)) if getattr(content, "exception", None) else None, - "timestamp": datetime.now().isoformat(), - }, - call_id=getattr(content, "call_id", f"call_{uuid.uuid4().hex[:8]}"), + call_id=call_id, + output=output, + status=status, item_id=context["item_id"], output_index=context["output_index"], sequence_number=self._next_sequence(context), @@ -367,37 +571,34 @@ class MessageMapper: sequence_number=self._next_sequence(context), ) - async def _map_usage_content(self, content: Any, context: dict[str, Any]) -> ResponseUsageEventComplete: - """Map UsageContent to structured usage event.""" - # Store usage data in context for aggregation - if "usage_data" not in context: - context["usage_data"] = [] - context["usage_data"].append(content) + async def _map_usage_content(self, content: Any, context: dict[str, Any]) -> None: + """Accumulate usage data for final Response.usage field. + OpenAI does NOT stream usage events. Usage appears only in final Response. + This method accumulates usage data per request for later inclusion in Response.usage. + + Returns: + None - no event emitted (usage goes in final Response.usage) + """ # Extract usage from UsageContent.details (UsageDetails object) details = getattr(content, "details", None) - total_tokens = 0 - prompt_tokens = 0 - completion_tokens = 0 + total_tokens = getattr(details, "total_token_count", 0) or 0 + prompt_tokens = getattr(details, "input_token_count", 0) or 0 + completion_tokens = getattr(details, "output_token_count", 0) or 0 - if details: - total_tokens = getattr(details, "total_token_count", 0) or 0 - prompt_tokens = getattr(details, "input_token_count", 0) or 0 - completion_tokens = getattr(details, "output_token_count", 0) or 0 + # Accumulate for final Response.usage + request_id = context.get("request_id", "default") + if request_id not in self._usage_accumulator: + self._usage_accumulator[request_id] = {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0} - return ResponseUsageEventComplete( - type="response.usage.complete", - data={ - "usage_data": details.to_dict() if details and hasattr(details, "to_dict") else {}, - "total_tokens": total_tokens, - "completion_tokens": completion_tokens, - "prompt_tokens": prompt_tokens, - "timestamp": datetime.now().isoformat(), - }, - item_id=context["item_id"], - output_index=context["output_index"], - sequence_number=self._next_sequence(context), - ) + self._usage_accumulator[request_id]["input_tokens"] += prompt_tokens + self._usage_accumulator[request_id]["output_tokens"] += completion_tokens + self._usage_accumulator[request_id]["total_tokens"] += total_tokens + + logger.debug(f"Accumulated usage for {request_id}: {self._usage_accumulator[request_id]}") + + # NO EVENT RETURNED - usage goes in final Response only + return async def _map_data_content(self, content: Any, context: dict[str, Any]) -> ResponseTraceEventComplete: """Map DataContent to structured trace event.""" @@ -510,19 +711,15 @@ class MessageMapper: async def _create_unknown_event(self, event_data: Any, context: dict[str, Any]) -> ResponseStreamEvent: """Create event for unknown event types.""" - text = f"Unknown event: {event_data!s}\\n" + text = f"Unknown event: {event_data!s}\n" return self._create_text_delta_event(text, context) async def _create_unknown_content_event(self, content: Any, context: dict[str, Any]) -> ResponseStreamEvent: """Create event for unknown content types.""" content_type = content.__class__.__name__ - text = f"⚠️ Unknown content type: {content_type}\\n" + text = f"⚠️ Unknown content type: {content_type}\n" return self._create_text_delta_event(text, context) - def _chunk_json_string(self, json_str: str, chunk_size: int = 50) -> list[str]: - """Chunk JSON string for streaming.""" - return [json_str[i : i + chunk_size] for i in range(0, len(json_str), chunk_size)] - async def _create_error_response(self, error_message: str, request: AgentFrameworkRequest) -> OpenAIResponse: """Create error response.""" error_text = f"Error: {error_message}" diff --git a/python/packages/devui/agent_framework_devui/_server.py b/python/packages/devui/agent_framework_devui/_server.py index daffdc9688..e0c4f2a565 100644 --- a/python/packages/devui/agent_framework_devui/_server.py +++ b/python/packages/devui/agent_framework_devui/_server.py @@ -7,7 +7,7 @@ import json import logging from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -from typing import Any, get_origin +from typing import Any from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware @@ -23,47 +23,6 @@ from .models._discovery_models import DiscoveryResponse, EntityInfo logger = logging.getLogger(__name__) -def _extract_executor_message_types(executor: Any) -> list[Any]: - """Return declared input types for the given executor.""" - message_types: list[Any] = [] - - try: - input_types = getattr(executor, "input_types", None) - except Exception as exc: # pragma: no cover - defensive logging path - logger.debug(f"Failed to access executor input_types: {exc}") - else: - if input_types: - message_types = list(input_types) - - if not message_types and hasattr(executor, "_handlers"): - try: - handlers = executor._handlers - if isinstance(handlers, dict): - message_types = list(handlers.keys()) - except Exception as exc: # pragma: no cover - defensive logging path - logger.debug(f"Failed to read executor handlers: {exc}") - - return message_types - - -def _select_primary_input_type(message_types: list[Any]) -> Any | None: - """Choose the most user-friendly input type for rendering workflow inputs.""" - if not message_types: - return None - - preferred = (str, dict) - - for candidate in preferred: - for message_type in message_types: - if message_type is candidate: - return candidate - origin = get_origin(message_type) - if origin is candidate: - return candidate - - return message_types[0] - - class DevServer: """Development Server - OpenAI compatible API server for debugging agents.""" @@ -263,7 +222,11 @@ class DevServer: start_executor_id = "" try: - from ._utils import generate_input_schema + from ._utils import ( + extract_executor_message_types, + generate_input_schema, + select_primary_input_type, + ) start_executor = entity_obj.get_start_executor() except Exception as e: @@ -274,8 +237,8 @@ class DevServer: start_executor, "id", "" ) - message_types = _extract_executor_message_types(start_executor) - input_type = _select_primary_input_type(message_types) + message_types = extract_executor_message_types(start_executor) + input_type = select_primary_input_type(message_types) if input_type: input_type_name = getattr(input_type, "__name__", str(input_type)) @@ -421,112 +384,161 @@ class DevServer: error = OpenAIError.create(f"Execution failed: {e!s}") return JSONResponse(status_code=500, content=error.to_dict()) - @app.post("/v1/threads") - async def create_thread(request_data: dict[str, Any]) -> dict[str, Any]: - """Create a new thread for an agent.""" - try: - agent_id = request_data.get("agent_id") - if not agent_id: - raise HTTPException(status_code=400, detail="agent_id is required") + # ======================================== + # OpenAI Conversations API (Standard) + # ======================================== + @app.post("/v1/conversations") + async def create_conversation(request_data: dict[str, Any]) -> dict[str, Any]: + """Create a new conversation - OpenAI standard.""" + try: + metadata = request_data.get("metadata") executor = await self._ensure_executor() - thread_id = executor.create_thread(agent_id) + conversation = executor.conversation_store.create_conversation(metadata=metadata) + return conversation.model_dump() + except HTTPException: + raise + except Exception as e: + logger.error(f"Error creating conversation: {e}") + raise HTTPException(status_code=500, detail=f"Failed to create conversation: {e!s}") from e + + @app.get("/v1/conversations") + async def list_conversations(agent_id: str | None = None) -> dict[str, Any]: + """List conversations, optionally filtered by agent_id.""" + try: + executor = await self._ensure_executor() + + if agent_id: + # Filter by agent_id metadata + conversations = executor.conversation_store.list_conversations_by_metadata({"agent_id": agent_id}) + else: + # Return all conversations (for InMemoryStore, list all) + # Note: This assumes list_conversations_by_metadata({}) returns all + conversations = executor.conversation_store.list_conversations_by_metadata({}) return { - "id": thread_id, - "object": "thread", - "created_at": int(__import__("time").time()), - "metadata": {"agent_id": agent_id}, + "object": "list", + "data": [conv.model_dump() for conv in conversations], + "has_more": False, } except HTTPException: raise except Exception as e: - logger.error(f"Error creating thread: {e}") - raise HTTPException(status_code=500, detail=f"Failed to create thread: {e!s}") from e + logger.error(f"Error listing conversations: {e}") + raise HTTPException(status_code=500, detail=f"Failed to list conversations: {e!s}") from e - @app.get("/v1/threads") - async def list_threads(agent_id: str) -> dict[str, Any]: - """List threads for an agent.""" + @app.get("/v1/conversations/{conversation_id}") + async def retrieve_conversation(conversation_id: str) -> dict[str, Any]: + """Get conversation - OpenAI standard.""" try: executor = await self._ensure_executor() - thread_ids = executor.list_threads_for_agent(agent_id) - - # Convert thread IDs to thread objects - threads = [] - for thread_id in thread_ids: - threads.append({"id": thread_id, "object": "thread", "agent_id": agent_id}) - - return {"object": "list", "data": threads} - except Exception as e: - logger.error(f"Error listing threads: {e}") - raise HTTPException(status_code=500, detail=f"Failed to list threads: {e!s}") from e - - @app.get("/v1/threads/{thread_id}") - async def get_thread(thread_id: str) -> dict[str, Any]: - """Get thread information.""" - try: - executor = await self._ensure_executor() - - # Check if thread exists - thread = executor.get_thread(thread_id) - if not thread: - raise HTTPException(status_code=404, detail="Thread not found") - - # Get the agent that owns this thread - agent_id = executor.get_agent_for_thread(thread_id) - - return {"id": thread_id, "object": "thread", "agent_id": agent_id} + conversation = executor.conversation_store.get_conversation(conversation_id) + if not conversation: + raise HTTPException(status_code=404, detail="Conversation not found") + return conversation.model_dump() except HTTPException: raise except Exception as e: - logger.error(f"Error getting thread {thread_id}: {e}") - raise HTTPException(status_code=500, detail=f"Failed to get thread: {e!s}") from e + logger.error(f"Error getting conversation {conversation_id}: {e}") + raise HTTPException(status_code=500, detail=f"Failed to get conversation: {e!s}") from e - @app.delete("/v1/threads/{thread_id}") - async def delete_thread(thread_id: str) -> dict[str, Any]: - """Delete a thread.""" + @app.post("/v1/conversations/{conversation_id}") + async def update_conversation(conversation_id: str, request_data: dict[str, Any]) -> dict[str, Any]: + """Update conversation metadata - OpenAI standard.""" try: executor = await self._ensure_executor() - success = executor.delete_thread(thread_id) - - if not success: - raise HTTPException(status_code=404, detail="Thread not found") - - return {"id": thread_id, "object": "thread.deleted", "deleted": True} + metadata = request_data.get("metadata", {}) + conversation = executor.conversation_store.update_conversation(conversation_id, metadata=metadata) + return conversation.model_dump() + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) from e except HTTPException: raise except Exception as e: - logger.error(f"Error deleting thread {thread_id}: {e}") - raise HTTPException(status_code=500, detail=f"Failed to delete thread: {e!s}") from e + logger.error(f"Error updating conversation {conversation_id}: {e}") + raise HTTPException(status_code=500, detail=f"Failed to update conversation: {e!s}") from e - @app.get("/v1/threads/{thread_id}/messages") - async def get_thread_messages(thread_id: str) -> dict[str, Any]: - """Get messages from a thread.""" + @app.delete("/v1/conversations/{conversation_id}") + async def delete_conversation(conversation_id: str) -> dict[str, Any]: + """Delete conversation - OpenAI standard.""" try: executor = await self._ensure_executor() - - # Check if thread exists - thread = executor.get_thread(thread_id) - if not thread: - raise HTTPException(status_code=404, detail="Thread not found") - - # Get messages from thread - messages = await executor.get_thread_messages(thread_id) - - return {"object": "list", "data": messages, "thread_id": thread_id} + result = executor.conversation_store.delete_conversation(conversation_id) + return result.model_dump() + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) from e except HTTPException: raise except Exception as e: - logger.error(f"Error getting messages for thread {thread_id}: {e}") - raise HTTPException(status_code=500, detail=f"Failed to get thread messages: {e!s}") from e + logger.error(f"Error deleting conversation {conversation_id}: {e}") + raise HTTPException(status_code=500, detail=f"Failed to delete conversation: {e!s}") from e + + @app.post("/v1/conversations/{conversation_id}/items") + async def create_conversation_items(conversation_id: str, request_data: dict[str, Any]) -> dict[str, Any]: + """Add items to conversation - OpenAI standard.""" + try: + executor = await self._ensure_executor() + items = request_data.get("items", []) + conv_items = await executor.conversation_store.add_items(conversation_id, items=items) + return {"object": "list", "data": [item.model_dump() for item in conv_items]} + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) from e + except HTTPException: + raise + except Exception as e: + logger.error(f"Error adding items to conversation {conversation_id}: {e}") + raise HTTPException(status_code=500, detail=f"Failed to add items: {e!s}") from e + + @app.get("/v1/conversations/{conversation_id}/items") + async def list_conversation_items( + conversation_id: str, limit: int = 100, after: str | None = None, order: str = "asc" + ) -> dict[str, Any]: + """List conversation items - OpenAI standard.""" + try: + executor = await self._ensure_executor() + items, has_more = await executor.conversation_store.list_items( + conversation_id, limit=limit, after=after, order=order + ) + return { + "object": "list", + "data": [item.model_dump() for item in items], + "has_more": has_more, + } + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) from e + except HTTPException: + raise + except Exception as e: + logger.error(f"Error listing items for conversation {conversation_id}: {e}") + raise HTTPException(status_code=500, detail=f"Failed to list items: {e!s}") from e + + @app.get("/v1/conversations/{conversation_id}/items/{item_id}") + async def retrieve_conversation_item(conversation_id: str, item_id: str) -> dict[str, Any]: + """Get specific conversation item - OpenAI standard.""" + try: + executor = await self._ensure_executor() + item = executor.conversation_store.get_item(conversation_id, item_id) + if not item: + raise HTTPException(status_code=404, detail="Item not found") + return item.model_dump() + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting item {item_id} from conversation {conversation_id}: {e}") + raise HTTPException(status_code=500, detail=f"Failed to get item: {e!s}") from e async def _stream_execution( self, executor: AgentFrameworkExecutor, request: AgentFrameworkRequest ) -> AsyncGenerator[str, None]: """Stream execution directly through executor.""" try: - # Direct call to executor - simple and clean + # Collect events for final response.completed event + events = [] + + # Stream all events async for event in executor.execute_streaming(request): + events.append(event) + # IMPORTANT: Check model_dump_json FIRST because to_json() can have newlines (pretty-printing) # which breaks SSE format. model_dump_json() returns single-line JSON. if hasattr(event, "model_dump_json"): @@ -544,6 +556,17 @@ class DevServer: payload = json.dumps(str(event)) yield f"data: {payload}\n\n" + # Aggregate to final response and emit response.completed event (OpenAI standard) + from .models import ResponseCompletedEvent + + final_response = await executor.message_mapper.aggregate_to_response(events, request) + completed_event = ResponseCompletedEvent( + type="response.completed", + response=final_response, + sequence_number=len(events), + ) + yield f"data: {completed_event.model_dump_json()}\n\n" + # Send final done event yield "data: [DONE]\n\n" diff --git a/python/packages/devui/agent_framework_devui/_utils.py b/python/packages/devui/agent_framework_devui/_utils.py index a36e5da8de..58aedbd2f3 100644 --- a/python/packages/devui/agent_framework_devui/_utils.py +++ b/python/packages/devui/agent_framework_devui/_utils.py @@ -10,6 +10,133 @@ from typing import Any, get_args, get_origin logger = logging.getLogger(__name__) +# ============================================================================ +# Agent Metadata Extraction +# ============================================================================ + + +def extract_agent_metadata(entity_object: Any) -> dict[str, Any]: + """Extract agent-specific metadata from an entity object. + + Args: + entity_object: Agent Framework agent object + + Returns: + Dictionary with agent metadata: instructions, model, chat_client_type, + context_providers, and middleware + """ + metadata = { + "instructions": None, + "model": None, + "chat_client_type": None, + "context_providers": None, + "middleware": None, + } + + # Try to get instructions + if hasattr(entity_object, "chat_options") and hasattr(entity_object.chat_options, "instructions"): + metadata["instructions"] = entity_object.chat_options.instructions + + # Try to get model - check both chat_options and chat_client + if ( + hasattr(entity_object, "chat_options") + and hasattr(entity_object.chat_options, "model_id") + and entity_object.chat_options.model_id + ): + metadata["model"] = entity_object.chat_options.model_id + elif hasattr(entity_object, "chat_client") and hasattr(entity_object.chat_client, "model_id"): + metadata["model"] = entity_object.chat_client.model_id + + # Try to get chat client type + if hasattr(entity_object, "chat_client"): + metadata["chat_client_type"] = entity_object.chat_client.__class__.__name__ + + # Try to get context providers + if ( + hasattr(entity_object, "context_provider") + and entity_object.context_provider + and hasattr(entity_object.context_provider, "__class__") + ): + metadata["context_providers"] = [entity_object.context_provider.__class__.__name__] # type: ignore + + # Try to get middleware + if hasattr(entity_object, "middleware") and entity_object.middleware: + middleware_list: list[str] = [] + for m in entity_object.middleware: + # Try multiple ways to get a good name for middleware + if hasattr(m, "__name__"): # Function or callable + middleware_list.append(m.__name__) + elif hasattr(m, "__class__"): # Class instance + middleware_list.append(m.__class__.__name__) + else: + middleware_list.append(str(m)) + metadata["middleware"] = middleware_list # type: ignore + + return metadata + + +# ============================================================================ +# Workflow Input Type Utilities +# ============================================================================ + + +def extract_executor_message_types(executor: Any) -> list[Any]: + """Extract declared input types for the given executor. + + Args: + executor: Workflow executor object + + Returns: + List of message types that the executor accepts + """ + message_types: list[Any] = [] + + try: + input_types = getattr(executor, "input_types", None) + except Exception as exc: # pragma: no cover - defensive logging path + logger.debug(f"Failed to access executor input_types: {exc}") + else: + if input_types: + message_types = list(input_types) + + if not message_types and hasattr(executor, "_handlers"): + try: + handlers = executor._handlers + if isinstance(handlers, dict): + message_types = list(handlers.keys()) + except Exception as exc: # pragma: no cover - defensive logging path + logger.debug(f"Failed to read executor handlers: {exc}") + + return message_types + + +def select_primary_input_type(message_types: list[Any]) -> Any | None: + """Choose the most user-friendly input type for workflow inputs. + + Prefers str and dict types for better user experience. + + Args: + message_types: List of possible message types + + Returns: + Selected primary input type, or None if list is empty + """ + if not message_types: + return None + + preferred = (str, dict) + + for candidate in preferred: + for message_type in message_types: + if message_type is candidate: + return candidate + origin = get_origin(message_type) + if origin is candidate: + return candidate + + return message_types[0] + + # ============================================================================ # Type System Utilities # ============================================================================ diff --git a/python/packages/devui/agent_framework_devui/models/__init__.py b/python/packages/devui/agent_framework_devui/models/__init__.py index d4c2d0da24..3db699beff 100644 --- a/python/packages/devui/agent_framework_devui/models/__init__.py +++ b/python/packages/devui/agent_framework_devui/models/__init__.py @@ -4,11 +4,18 @@ # Import discovery models # Import all OpenAI types directly from the openai package +from openai.types.conversations import Conversation, ConversationDeletedResource +from openai.types.conversations.conversation_item import ConversationItem from openai.types.responses import ( Response, + ResponseCompletedEvent, ResponseErrorEvent, ResponseFunctionCallArgumentsDeltaEvent, + ResponseFunctionToolCall, + ResponseFunctionToolCallOutputItem, ResponseInputParam, + ResponseOutputItemAddedEvent, + ResponseOutputItemDoneEvent, ResponseOutputMessage, ResponseOutputText, ResponseReasoningTextDeltaEvent, @@ -25,14 +32,9 @@ from ._openai_custom import ( AgentFrameworkRequest, OpenAIError, ResponseFunctionResultComplete, - ResponseFunctionResultDelta, ResponseTraceEvent, ResponseTraceEventComplete, - ResponseTraceEventDelta, - ResponseUsageEventComplete, - ResponseUsageEventDelta, ResponseWorkflowEventComplete, - ResponseWorkflowEventDelta, ) # Type alias for compatibility @@ -41,6 +43,9 @@ OpenAIResponse = Response # Export all types for easy importing __all__ = [ "AgentFrameworkRequest", + "Conversation", + "ConversationDeletedResource", + "ConversationItem", "DiscoveryResponse", "EntityInfo", "InputTokensDetails", @@ -49,11 +54,15 @@ __all__ = [ "OpenAIResponse", "OutputTokensDetails", "Response", + "ResponseCompletedEvent", "ResponseErrorEvent", "ResponseFunctionCallArgumentsDeltaEvent", "ResponseFunctionResultComplete", - "ResponseFunctionResultDelta", + "ResponseFunctionToolCall", + "ResponseFunctionToolCallOutputItem", "ResponseInputParam", + "ResponseOutputItemAddedEvent", + "ResponseOutputItemDoneEvent", "ResponseOutputMessage", "ResponseOutputText", "ResponseReasoningTextDeltaEvent", @@ -61,12 +70,8 @@ __all__ = [ "ResponseTextDeltaEvent", "ResponseTraceEvent", "ResponseTraceEventComplete", - "ResponseTraceEventDelta", "ResponseUsage", - "ResponseUsageEventComplete", - "ResponseUsageEventDelta", "ResponseWorkflowEventComplete", - "ResponseWorkflowEventDelta", "ResponsesModel", "ToolParam", ] diff --git a/python/packages/devui/agent_framework_devui/models/_openai_custom.py b/python/packages/devui/agent_framework_devui/models/_openai_custom.py index 91aae0eb5f..aa41ea2522 100644 --- a/python/packages/devui/agent_framework_devui/models/_openai_custom.py +++ b/python/packages/devui/agent_framework_devui/models/_openai_custom.py @@ -3,7 +3,7 @@ """Custom OpenAI-compatible event types for Agent Framework extensions. These are custom event types that extend beyond the standard OpenAI Responses API -to support Agent Framework specific features like workflows, traces, and function results. +to support Agent Framework specific features like workflows and traces. """ from __future__ import annotations @@ -15,18 +15,6 @@ from pydantic import BaseModel, ConfigDict # Custom Agent Framework OpenAI event types for structured data -class ResponseWorkflowEventDelta(BaseModel): - """Structured workflow event with completion tracking.""" - - type: Literal["response.workflow_event.delta"] = "response.workflow_event.delta" - delta: dict[str, Any] - executor_id: str | None = None - is_complete: bool = False # Track if this is the final part - item_id: str - output_index: int = 0 - sequence_number: int - - class ResponseWorkflowEventComplete(BaseModel): """Complete workflow event data.""" @@ -38,41 +26,6 @@ class ResponseWorkflowEventComplete(BaseModel): sequence_number: int -class ResponseFunctionResultDelta(BaseModel): - """Structured function result with completion tracking.""" - - type: Literal["response.function_result.delta"] = "response.function_result.delta" - delta: dict[str, Any] - call_id: str - is_complete: bool = False - item_id: str - output_index: int = 0 - sequence_number: int - - -class ResponseFunctionResultComplete(BaseModel): - """Complete function result data.""" - - type: Literal["response.function_result.complete"] = "response.function_result.complete" - data: dict[str, Any] # Complete function result data, not delta - call_id: str - item_id: str - output_index: int = 0 - sequence_number: int - - -class ResponseTraceEventDelta(BaseModel): - """Structured trace event with completion tracking.""" - - type: Literal["response.trace.delta"] = "response.trace.delta" - delta: dict[str, Any] - span_id: str | None = None - is_complete: bool = False - item_id: str - output_index: int = 0 - sequence_number: int - - class ResponseTraceEventComplete(BaseModel): """Complete trace event data.""" @@ -84,22 +37,18 @@ class ResponseTraceEventComplete(BaseModel): sequence_number: int -class ResponseUsageEventDelta(BaseModel): - """Structured usage event with completion tracking.""" +class ResponseFunctionResultComplete(BaseModel): + """Custom DevUI event for function execution results. - type: Literal["response.usage.delta"] = "response.usage.delta" - delta: dict[str, Any] - is_complete: bool = False - item_id: str - output_index: int = 0 - sequence_number: int + This is a DevUI extension - OpenAI doesn't stream function execution results + because in their model, the application executes functions, not the API. + Agent Framework executes functions, so we emit this event for debugging visibility. + """ - -class ResponseUsageEventComplete(BaseModel): - """Complete usage event data.""" - - type: Literal["response.usage.complete"] = "response.usage.complete" - data: dict[str, Any] # Complete usage data, not delta + type: Literal["response.function_result.complete"] = "response.function_result.complete" + call_id: str + output: str + status: Literal["in_progress", "completed", "incomplete"] item_id: str output_index: int = 0 sequence_number: int @@ -110,7 +59,6 @@ class AgentFrameworkExtraBody(BaseModel): """Agent Framework specific routing fields for OpenAI requests.""" entity_id: str - thread_id: str | None = None input_data: dict[str, Any] | None = None model_config = ConfigDict(extra="allow") @@ -118,17 +66,21 @@ class AgentFrameworkExtraBody(BaseModel): # Agent Framework Request Model - Extending real OpenAI types class AgentFrameworkRequest(BaseModel): - """OpenAI ResponseCreateParams with Agent Framework extensions. + """OpenAI ResponseCreateParams with Agent Framework routing. - This properly extends the real OpenAI API request format while adding - our custom routing fields in extra_body. + This properly extends the real OpenAI API request format. + - Uses 'model' field as entity_id (agent/workflow name) + - Uses 'conversation' field for conversation context (OpenAI standard) """ # All OpenAI fields from ResponseCreateParams - model: str + model: str # Used as entity_id in DevUI! input: str | list[Any] # ResponseInputParam stream: bool | None = False + # OpenAI conversation parameter (standard!) + conversation: str | dict[str, Any] | None = None # Union[str, {"id": str}] + # Common OpenAI optional fields instructions: str | None = None metadata: dict[str, Any] | None = None @@ -136,32 +88,35 @@ class AgentFrameworkRequest(BaseModel): max_output_tokens: int | None = None tools: list[dict[str, Any]] | None = None - # Agent Framework extension - strongly typed - extra_body: AgentFrameworkExtraBody | None = None - - entity_id: str | None = None # Allow entity_id as top-level field + # Optional extra_body for advanced use cases + extra_body: dict[str, Any] | None = None model_config = ConfigDict(extra="allow") - def get_entity_id(self) -> str | None: - """Get entity_id from either top-level field or extra_body.""" - # Priority 1: Top-level entity_id field - if self.entity_id: - return self.entity_id + def get_entity_id(self) -> str: + """Get entity_id from model field. - # Priority 2: entity_id in extra_body - if self.extra_body and hasattr(self.extra_body, "entity_id"): - return self.extra_body.entity_id + In DevUI, model IS the entity_id (agent/workflow name). + Simple and clean! + """ + return self.model + def get_conversation_id(self) -> str | None: + """Extract conversation_id from conversation parameter. + + Supports both string and object forms: + - conversation: "conv_123" + - conversation: {"id": "conv_123"} + """ + if isinstance(self.conversation, str): + return self.conversation + if isinstance(self.conversation, dict): + return self.conversation.get("id") return None def to_openai_params(self) -> dict[str, Any]: """Convert to dict for OpenAI client compatibility.""" - data = self.model_dump(exclude={"extra_body", "entity_id"}, exclude_none=True) - if self.extra_body: - # Don't merge extra_body into main params to keep them separate - data["extra_body"] = self.extra_body - return data + return self.model_dump(exclude_none=True) # Error handling @@ -198,12 +153,7 @@ __all__ = [ "AgentFrameworkRequest", "OpenAIError", "ResponseFunctionResultComplete", - "ResponseFunctionResultDelta", "ResponseTraceEvent", "ResponseTraceEventComplete", - "ResponseTraceEventDelta", - "ResponseUsageEventComplete", - "ResponseUsageEventDelta", "ResponseWorkflowEventComplete", - "ResponseWorkflowEventDelta", ] diff --git a/python/packages/devui/agent_framework_devui/ui/assets/index-BhFnsoso.css b/python/packages/devui/agent_framework_devui/ui/assets/index-BhFnsoso.css new file mode 100644 index 0000000000..41a2f21902 --- /dev/null +++ b/python/packages/devui/agent_framework_devui/ui/assets/index-BhFnsoso.css @@ -0,0 +1 @@ +/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-800:oklch(47% .157 37.304);--color-orange-900:oklch(40.8% .123 38.172);--color-orange-950:oklch(26.6% .079 36.259);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-green-950:oklch(26.6% .065 152.934);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-emerald-900:oklch(37.8% .077 168.94);--color-emerald-950:oklch(26.2% .051 172.552);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-blue-950:oklch(28.2% .091 267.935);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-900:oklch(38.1% .176 304.987);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-lg:32rem;--container-2xl:42rem;--container-4xl:56rem;--container-5xl:64rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-medium:500;--font-weight-semibold:600;--tracking-widest:.1em;--leading-tight:1.25;--leading-relaxed:1.625;--drop-shadow-lg:0 4px 4px #00000026;--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--animate-bounce:bounce 1s infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border);outline-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){*{outline-color:color-mix(in oklab,var(--ring)50%,transparent)}}body{background-color:var(--background);color:var(--foreground)}}@layer components;@layer utilities{.\@container\/card-header{container:card-header/inline-size}.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing)*0)}.inset-2{inset:calc(var(--spacing)*2)}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.inset-y-0{inset-block:calc(var(--spacing)*0)}.-top-1{top:calc(var(--spacing)*-1)}.-top-2{top:calc(var(--spacing)*-2)}.top-4{top:calc(var(--spacing)*4)}.-right-1{right:calc(var(--spacing)*-1)}.-right-2{right:calc(var(--spacing)*-2)}.right-0{right:calc(var(--spacing)*0)}.right-2{right:calc(var(--spacing)*2)}.right-3{right:calc(var(--spacing)*3)}.right-4{right:calc(var(--spacing)*4)}.-bottom-2{bottom:calc(var(--spacing)*-2)}.bottom-0{bottom:calc(var(--spacing)*0)}.bottom-3{bottom:calc(var(--spacing)*3)}.bottom-14{bottom:calc(var(--spacing)*14)}.bottom-24{bottom:calc(var(--spacing)*24)}.-left-2{left:calc(var(--spacing)*-2)}.left-0{left:calc(var(--spacing)*0)}.left-1\/2{left:50%}.left-2{left:calc(var(--spacing)*2)}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.col-start-2{grid-column-start:2}.row-span-2{grid-row:span 2/span 2}.row-start-1{grid-row-start:1}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.-mx-1{margin-inline:calc(var(--spacing)*-1)}.mx-auto{margin-inline:auto}.my-1{margin-block:calc(var(--spacing)*1)}.my-2{margin-block:calc(var(--spacing)*2)}.mt-0{margin-top:calc(var(--spacing)*0)}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-12{margin-top:calc(var(--spacing)*12)}.mr-1{margin-right:calc(var(--spacing)*1)}.mr-1\.5{margin-right:calc(var(--spacing)*1.5)}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-1\.5{margin-left:calc(var(--spacing)*1.5)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-3{margin-left:calc(var(--spacing)*3)}.ml-4{margin-left:calc(var(--spacing)*4)}.ml-5{margin-left:calc(var(--spacing)*5)}.ml-auto{margin-left:auto}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.field-sizing-content{field-sizing:content}.size-2{width:calc(var(--spacing)*2);height:calc(var(--spacing)*2)}.size-3\.5{width:calc(var(--spacing)*3.5);height:calc(var(--spacing)*3.5)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-9{width:calc(var(--spacing)*9);height:calc(var(--spacing)*9)}.\!h-2{height:calc(var(--spacing)*2)!important}.h-0{height:calc(var(--spacing)*0)}.h-0\.5{height:calc(var(--spacing)*.5)}.h-1{height:calc(var(--spacing)*1)}.h-2{height:calc(var(--spacing)*2)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-3{height:calc(var(--spacing)*3)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-7{height:calc(var(--spacing)*7)}.h-8{height:calc(var(--spacing)*8)}.h-9{height:calc(var(--spacing)*9)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-14{height:calc(var(--spacing)*14)}.h-16{height:calc(var(--spacing)*16)}.h-32{height:calc(var(--spacing)*32)}.h-96{height:calc(var(--spacing)*96)}.h-\[1\.2rem\]{height:1.2rem}.h-\[calc\(100vh-3\.5rem\)\]{height:calc(100vh - 3.5rem)}.h-\[calc\(100vh-3\.7rem\)\]{height:calc(100vh - 3.7rem)}.h-\[var\(--radix-select-trigger-height\)\]{height:var(--radix-select-trigger-height)}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-\(--radix-dropdown-menu-content-available-height\){max-height:var(--radix-dropdown-menu-content-available-height)}.max-h-\(--radix-select-content-available-height\){max-height:var(--radix-select-content-available-height)}.max-h-20{max-height:calc(var(--spacing)*20)}.max-h-24{max-height:calc(var(--spacing)*24)}.max-h-32{max-height:calc(var(--spacing)*32)}.max-h-64{max-height:calc(var(--spacing)*64)}.max-h-96{max-height:calc(var(--spacing)*96)}.max-h-\[90vh\]{max-height:90vh}.max-h-\[200px\]{max-height:200px}.max-h-none{max-height:none}.max-h-screen{max-height:100vh}.\!min-h-0{min-height:calc(var(--spacing)*0)!important}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-16{min-height:calc(var(--spacing)*16)}.min-h-\[36px\]{min-height:36px}.min-h-\[40px\]{min-height:40px}.min-h-\[50vh\]{min-height:50vh}.min-h-\[240px\]{min-height:240px}.min-h-screen{min-height:100vh}.\!w-2{width:calc(var(--spacing)*2)!important}.w-1{width:calc(var(--spacing)*1)}.w-2{width:calc(var(--spacing)*2)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-3{width:calc(var(--spacing)*3)}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-16{width:calc(var(--spacing)*16)}.w-56{width:calc(var(--spacing)*56)}.w-64{width:calc(var(--spacing)*64)}.w-80{width:calc(var(--spacing)*80)}.w-\[1\.2rem\]{width:1.2rem}.w-\[600px\]{width:600px}.w-fit{width:fit-content}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[80\%\]{max-width:80%}.max-w-\[90vw\]{max-width:90vw}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-none{max-width:none}.\!min-w-0{min-width:calc(var(--spacing)*0)!important}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-\[8rem\]{min-width:8rem}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.origin-\(--radix-dropdown-menu-content-transform-origin\){transform-origin:var(--radix-dropdown-menu-content-transform-origin)}.origin-\(--radix-select-content-transform-origin\){transform-origin:var(--radix-select-content-transform-origin)}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-0{--tw-translate-x:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-4{--tw-translate-x:calc(var(--spacing)*4);translate:var(--tw-translate-x)var(--tw-translate-y)}.scale-0{--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-75{--tw-scale-x:75%;--tw-scale-y:75%;--tw-scale-z:75%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-100{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.rotate-0{rotate:none}.rotate-90{rotate:90deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-bounce{animation:var(--animate-bounce)}.animate-in{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-col-resize{cursor:col-resize}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.cursor-row-resize{cursor:row-resize}.touch-none{touch-action:none}.resize{resize:both}.resize-none{resize:none}.scroll-my-1{scroll-margin-block:calc(var(--spacing)*1)}.list-none{list-style-type:none}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-rows-\[auto_auto\]{grid-template-rows:auto auto}.flex-col{flex-direction:column}.flex-row-reverse{flex-direction:row-reverse}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}.self-start{align-self:flex-start}.justify-self-end{justify-self:flex-end}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.\!rounded-full{border-radius:3.40282e38px!important}.rounded{border-radius:.25rem}.rounded-\[4px\]{border-radius:4px}.rounded-\[inherit\]{border-radius:inherit}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-none{border-radius:0}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.\!border{border-style:var(--tw-border-style)!important;border-width:1px!important}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-0{border-left-style:var(--tw-border-style);border-left-width:0}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.\!border-gray-600{border-color:var(--color-gray-600)!important}.border-\[\#643FB2\]{border-color:#643fb2}.border-\[\#643FB2\]\/30{border-color:#643fb24d}.border-\[\#643FB2\]\/40{border-color:#643fb266}.border-amber-200{border-color:var(--color-amber-200)}.border-blue-200{border-color:var(--color-blue-200)}.border-blue-300{border-color:var(--color-blue-300)}.border-blue-400{border-color:var(--color-blue-400)}.border-blue-500\/30{border-color:#3080ff4d}@supports (color:color-mix(in lab,red,red)){.border-blue-500\/30{border-color:color-mix(in oklab,var(--color-blue-500)30%,transparent)}}.border-blue-500\/40{border-color:#3080ff66}@supports (color:color-mix(in lab,red,red)){.border-blue-500\/40{border-color:color-mix(in oklab,var(--color-blue-500)40%,transparent)}}.border-border,.border-border\/50{border-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.border-border\/50{border-color:color-mix(in oklab,var(--border)50%,transparent)}}.border-destructive,.border-destructive\/20{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/20{border-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.border-destructive\/30{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/30{border-color:color-mix(in oklab,var(--destructive)30%,transparent)}}.border-destructive\/50{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/50{border-color:color-mix(in oklab,var(--destructive)50%,transparent)}}.border-destructive\/70{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/70{border-color:color-mix(in oklab,var(--destructive)70%,transparent)}}.border-emerald-300{border-color:var(--color-emerald-300)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-green-200{border-color:var(--color-green-200)}.border-green-500{border-color:var(--color-green-500)}.border-green-500\/30{border-color:#00c7584d}@supports (color:color-mix(in lab,red,red)){.border-green-500\/30{border-color:color-mix(in oklab,var(--color-green-500)30%,transparent)}}.border-green-500\/40{border-color:#00c75866}@supports (color:color-mix(in lab,red,red)){.border-green-500\/40{border-color:color-mix(in oklab,var(--color-green-500)40%,transparent)}}.border-input{border-color:var(--input)}.border-muted{border-color:var(--muted)}.border-orange-200{border-color:var(--color-orange-200)}.border-orange-500{border-color:var(--color-orange-500)}.border-primary\/20{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.border-primary\/20{border-color:color-mix(in oklab,var(--primary)20%,transparent)}}.border-red-200{border-color:var(--color-red-200)}.border-red-500{border-color:var(--color-red-500)}.border-transparent{border-color:#0000}.border-yellow-200{border-color:var(--color-yellow-200)}.border-t-transparent{border-top-color:#0000}.border-l-transparent{border-left-color:#0000}.bg-\[\#643FB2\]{background-color:#643fb2}.bg-\[\#643FB2\]\/5{background-color:#643fb20d}.bg-\[\#643FB2\]\/10{background-color:#643fb21a}.bg-accent\/10{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.bg-accent\/10{background-color:color-mix(in oklab,var(--accent)10%,transparent)}}.bg-amber-50{background-color:var(--color-amber-50)}.bg-background,.bg-background\/80{background-color:var(--background)}@supports (color:color-mix(in lab,red,red)){.bg-background\/80{background-color:color-mix(in oklab,var(--background)80%,transparent)}}.bg-black{background-color:var(--color-black)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black)60%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-50\/80{background-color:#eff6ffcc}@supports (color:color-mix(in lab,red,red)){.bg-blue-50\/80{background-color:color-mix(in oklab,var(--color-blue-50)80%,transparent)}}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-500\/5{background-color:#3080ff0d}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/5{background-color:color-mix(in oklab,var(--color-blue-500)5%,transparent)}}.bg-blue-500\/10{background-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/10{background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.bg-border{background-color:var(--border)}.bg-card{background-color:var(--card)}.bg-current{background-color:currentColor}.bg-destructive,.bg-destructive\/5{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/5{background-color:color-mix(in oklab,var(--destructive)5%,transparent)}}.bg-destructive\/10{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/10{background-color:color-mix(in oklab,var(--destructive)10%,transparent)}}.bg-destructive\/80{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/80{background-color:color-mix(in oklab,var(--destructive)80%,transparent)}}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-400{background-color:var(--color-gray-400)}.bg-gray-900\/90{background-color:#101828e6}@supports (color:color-mix(in lab,red,red)){.bg-gray-900\/90{background-color:color-mix(in oklab,var(--color-gray-900)90%,transparent)}}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-500{background-color:var(--color-green-500)}.bg-green-500\/5{background-color:#00c7580d}@supports (color:color-mix(in lab,red,red)){.bg-green-500\/5{background-color:color-mix(in oklab,var(--color-green-500)5%,transparent)}}.bg-green-500\/10{background-color:#00c7581a}@supports (color:color-mix(in lab,red,red)){.bg-green-500\/10{background-color:color-mix(in oklab,var(--color-green-500)10%,transparent)}}.bg-muted,.bg-muted\/30{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.bg-muted\/30{background-color:color-mix(in oklab,var(--muted)30%,transparent)}}.bg-muted\/50{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.bg-muted\/50{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}.bg-orange-50{background-color:var(--color-orange-50)}.bg-orange-100{background-color:var(--color-orange-100)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-popover{background-color:var(--popover)}.bg-primary,.bg-primary\/10{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/10{background-color:color-mix(in oklab,var(--primary)10%,transparent)}}.bg-primary\/30{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/30{background-color:color-mix(in oklab,var(--primary)30%,transparent)}}.bg-primary\/40{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/40{background-color:color-mix(in oklab,var(--primary)40%,transparent)}}.bg-purple-50{background-color:var(--color-purple-50)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-500{background-color:var(--color-red-500)}.bg-secondary{background-color:var(--secondary)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/90{background-color:#ffffffe6}@supports (color:color-mix(in lab,red,red)){.bg-white\/90{background-color:color-mix(in oklab,var(--color-white)90%,transparent)}}.bg-yellow-100{background-color:var(--color-yellow-100)}.fill-current{fill:currentColor}.object-cover{object-fit:cover}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.p-\[1px\]{padding:1px}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-0{padding-block:calc(var(--spacing)*0)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.py-8{padding-block:calc(var(--spacing)*8)}.pt-0{padding-top:calc(var(--spacing)*0)}.pt-2{padding-top:calc(var(--spacing)*2)}.pt-3{padding-top:calc(var(--spacing)*3)}.pt-4{padding-top:calc(var(--spacing)*4)}.pt-6{padding-top:calc(var(--spacing)*6)}.pt-8{padding-top:calc(var(--spacing)*8)}.pr-2{padding-right:calc(var(--spacing)*2)}.pr-8{padding-right:calc(var(--spacing)*8)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-3{padding-bottom:calc(var(--spacing)*3)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.pb-6{padding-bottom:calc(var(--spacing)*6)}.pb-12{padding-bottom:calc(var(--spacing)*12)}.pl-2{padding-left:calc(var(--spacing)*2)}.pl-3{padding-left:calc(var(--spacing)*3)}.pl-8{padding-left:calc(var(--spacing)*8)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#643FB2\]{color:#643fb2}.text-amber-500{color:var(--color-amber-500)}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-900{color:var(--color-amber-900)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-card-foreground{color:var(--card-foreground)}.text-current{color:currentColor}.text-destructive{color:var(--destructive)}.text-emerald-600{color:var(--color-emerald-600)}.text-emerald-700{color:var(--color-emerald-700)}.text-emerald-800{color:var(--color-emerald-800)}.text-foreground{color:var(--foreground)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-900{color:var(--color-gray-900)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-muted-foreground,.text-muted-foreground\/80{color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.text-muted-foreground\/80{color:color-mix(in oklab,var(--muted-foreground)80%,transparent)}}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-orange-800{color:var(--color-orange-800)}.text-popover-foreground{color:var(--popover-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-purple-500{color:var(--color-purple-500)}.text-purple-600{color:var(--color-purple-600)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-secondary-foreground{color:var(--secondary-foreground)}.text-white{color:var(--color-white)}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-700{color:var(--color-yellow-700)}.capitalize{text-transform:capitalize}.underline-offset-4{text-underline-offset:4px}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[\#643FB2\]\/20{--tw-shadow-color:#643fb233}@supports (color:color-mix(in lab,red,red)){.shadow-\[\#643FB2\]\/20{--tw-shadow-color:color-mix(in oklab,oklab(47.4316% .069152 -.159147/.2) var(--tw-shadow-alpha),transparent)}}.shadow-green-500\/20{--tw-shadow-color:#00c75833}@supports (color:color-mix(in lab,red,red)){.shadow-green-500\/20{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-green-500)20%,transparent)var(--tw-shadow-alpha),transparent)}}.shadow-orange-500\/20{--tw-shadow-color:#fe6e0033}@supports (color:color-mix(in lab,red,red)){.shadow-orange-500\/20{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-orange-500)20%,transparent)var(--tw-shadow-alpha),transparent)}}.shadow-primary\/25{--tw-shadow-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.shadow-primary\/25{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--primary)25%,transparent)var(--tw-shadow-alpha),transparent)}}.shadow-red-500\/20{--tw-shadow-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){.shadow-red-500\/20{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-red-500)20%,transparent)var(--tw-shadow-alpha),transparent)}}.ring-blue-500{--tw-ring-color:var(--color-blue-500)}.ring-offset-2{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-background{--tw-ring-offset-color:var(--background)}.outline-hidden{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.outline-hidden{outline-offset:2px;outline:2px solid #0000}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.drop-shadow-lg{--tw-drop-shadow-size:drop-shadow(0 4px 4px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-lg));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,visibility,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[color\,box-shadow\]{transition-property:color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-none{transition-property:none}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.\[animation-delay\:-0\.3s\]{animation-delay:-.3s}.\[animation-delay\:-0\.15s\]{animation-delay:-.15s}.fade-in{--tw-enter-opacity:0}.running{animation-play-state:running}.slide-in-from-bottom-2{--tw-enter-translate-y:calc(2*var(--spacing))}.group-open\:rotate-180:is(:where(.group):is([open],:popover-open,:open) *){rotate:180deg}@media (hover:hover){.group-hover\:bg-primary:is(:where(.group):hover *){background-color:var(--primary)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.group-hover\:shadow-md:is(:where(.group):hover *){--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.group-hover\:shadow-primary\/20:is(:where(.group):hover *){--tw-shadow-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.group-hover\:shadow-primary\/20:is(:where(.group):hover *){--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--primary)20%,transparent)var(--tw-shadow-alpha),transparent)}}}.group-data-\[disabled\=true\]\:pointer-events-none:is(:where(.group)[data-disabled=true] *){pointer-events:none}.group-data-\[disabled\=true\]\:opacity-50:is(:where(.group)[data-disabled=true] *){opacity:.5}.peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled~*){cursor:not-allowed}.peer-disabled\:opacity-50:is(:where(.peer):disabled~*){opacity:.5}.selection\:bg-primary ::selection{background-color:var(--primary)}.selection\:bg-primary::selection{background-color:var(--primary)}.selection\:text-primary-foreground ::selection{color:var(--primary-foreground)}.selection\:text-primary-foreground::selection{color:var(--primary-foreground)}.file\:inline-flex::file-selector-button{display:inline-flex}.file\:h-7::file-selector-button{height:calc(var(--spacing)*7)}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-transparent::file-selector-button{background-color:#0000}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.file\:text-foreground::file-selector-button{color:var(--foreground)}.placeholder\:text-muted-foreground::placeholder{color:var(--muted-foreground)}@media (hover:hover){.hover\:border-border:hover{border-color:var(--border)}.hover\:bg-\[\#643FB2\]\/10:hover{background-color:#643fb21a}.hover\:bg-accent:hover{background-color:var(--accent)}.hover\:bg-amber-100:hover{background-color:var(--color-amber-100)}.hover\:bg-background:hover{background-color:var(--background)}.hover\:bg-blue-500\/10:hover{background-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-blue-500\/10:hover{background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.hover\:bg-destructive\/20:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-destructive\/20:hover{background-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.hover\:bg-destructive\/80:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-destructive\/80:hover{background-color:color-mix(in oklab,var(--destructive)80%,transparent)}}.hover\:bg-destructive\/90:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-destructive\/90:hover{background-color:color-mix(in oklab,var(--destructive)90%,transparent)}}.hover\:bg-green-500\/10:hover{background-color:#00c7581a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-green-500\/10:hover{background-color:color-mix(in oklab,var(--color-green-500)10%,transparent)}}.hover\:bg-muted:hover,.hover\:bg-muted\/30:hover{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-muted\/30:hover{background-color:color-mix(in oklab,var(--muted)30%,transparent)}}.hover\:bg-muted\/50:hover{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}.hover\:bg-primary\/20:hover{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/20:hover{background-color:color-mix(in oklab,var(--primary)20%,transparent)}}.hover\:bg-primary\/80:hover{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/80:hover{background-color:color-mix(in oklab,var(--primary)80%,transparent)}}.hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--primary)90%,transparent)}}.hover\:bg-secondary\/80:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab,var(--secondary)80%,transparent)}}.hover\:bg-white:hover{background-color:var(--color-white)}.hover\:text-accent-foreground:hover{color:var(--accent-foreground)}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-70:hover{opacity:.7}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:bg-accent:focus{background-color:var(--accent)}.focus\:text-accent-foreground:focus{color:var(--accent-foreground)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-ring:focus{--tw-ring-color:var(--ring)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:border-ring:focus-visible{border-color:var(--ring)}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-\[3px\]:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.focus-visible\:ring-ring:focus-visible,.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.has-data-\[slot\=card-action\]\:grid-cols-\[1fr_auto\]:has([data-slot=card-action]){grid-template-columns:1fr auto}.has-\[\>svg\]\:px-2\.5:has(>svg){padding-inline:calc(var(--spacing)*2.5)}.has-\[\>svg\]\:px-3:has(>svg){padding-inline:calc(var(--spacing)*3)}.has-\[\>svg\]\:px-4:has(>svg){padding-inline:calc(var(--spacing)*4)}.aria-invalid\:border-destructive[aria-invalid=true]{border-color:var(--destructive)}.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[inset\]\:pl-8[data-inset]{padding-left:calc(var(--spacing)*8)}.data-\[placeholder\]\:text-muted-foreground[data-placeholder]{color:var(--muted-foreground)}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y:calc(2*var(--spacing)*-1)}.data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x:calc(2*var(--spacing))}.data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x:calc(2*var(--spacing)*-1)}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y:calc(2*var(--spacing))}.data-\[size\=default\]\:h-9[data-size=default]{height:calc(var(--spacing)*9)}.data-\[size\=sm\]\:h-8[data-size=sm]{height:calc(var(--spacing)*8)}:is(.\*\:data-\[slot\=select-value\]\:line-clamp-1>*)[data-slot=select-value]{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}:is(.\*\:data-\[slot\=select-value\]\:flex>*)[data-slot=select-value]{display:flex}:is(.\*\:data-\[slot\=select-value\]\:items-center>*)[data-slot=select-value]{align-items:center}:is(.\*\:data-\[slot\=select-value\]\:gap-2>*)[data-slot=select-value]{gap:calc(var(--spacing)*2)}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:var(--background)}.data-\[state\=active\]\:text-foreground[data-state=active]{color:var(--foreground)}.data-\[state\=active\]\:shadow[data-state=active]{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=checked\]\:border-primary[data-state=checked]{border-color:var(--primary)}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:var(--primary)}.data-\[state\=checked\]\:text-primary-foreground[data-state=checked]{color:var(--primary-foreground)}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity:0}.data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale:.95}.data-\[state\=open\]\:animate-in[data-state=open]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-\[state\=open\]\:bg-accent[data-state=open]{background-color:var(--accent)}.data-\[state\=open\]\:text-accent-foreground[data-state=open]{color:var(--accent-foreground)}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity:0}.data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale:.95}.data-\[variant\=destructive\]\:text-destructive[data-variant=destructive]{color:var(--destructive)}.data-\[variant\=destructive\]\:focus\:bg-destructive\/10[data-variant=destructive]:focus{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.data-\[variant\=destructive\]\:focus\:bg-destructive\/10[data-variant=destructive]:focus{background-color:color-mix(in oklab,var(--destructive)10%,transparent)}}.data-\[variant\=destructive\]\:focus\:text-destructive[data-variant=destructive]:focus{color:var(--destructive)}@media (min-width:40rem){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:w-64{width:calc(var(--spacing)*64)}.sm\:max-w-lg{max-width:var(--container-lg)}.sm\:flex-none{flex:none}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}}@media (min-width:48rem){.md\:col-span-2{grid-column:span 2/span 2}.md\:col-start-2{grid-column-start:2}.md\:inline{display:inline}.md\:max-w-2xl{max-width:var(--container-2xl)}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:gap-8{gap:calc(var(--spacing)*8)}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (min-width:64rem){.lg\:col-span-3{grid-column:span 3/span 3}.lg\:max-w-4xl{max-width:var(--container-4xl)}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:justify-between{justify-content:space-between}}@media (min-width:80rem){.xl\:col-span-2{grid-column:span 2/span 2}.xl\:col-span-4{grid-column:span 4/span 4}.xl\:max-w-5xl{max-width:var(--container-5xl)}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.dark\:scale-0:is(.dark *){--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}.dark\:scale-100:is(.dark *){--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.dark\:-rotate-90:is(.dark *){rotate:-90deg}.dark\:rotate-0:is(.dark *){rotate:none}.dark\:\!border-gray-500:is(.dark *){border-color:var(--color-gray-500)!important}.dark\:\!border-gray-600:is(.dark *){border-color:var(--color-gray-600)!important}.dark\:border-\[\#8B5CF6\]:is(.dark *){border-color:#8b5cf6}.dark\:border-\[\#8B5CF6\]\/30:is(.dark *){border-color:#8b5cf64d}.dark\:border-\[\#8B5CF6\]\/40:is(.dark *){border-color:#8b5cf666}.dark\:border-amber-800:is(.dark *){border-color:var(--color-amber-800)}.dark\:border-amber-900:is(.dark *){border-color:var(--color-amber-900)}.dark\:border-blue-500:is(.dark *){border-color:var(--color-blue-500)}.dark\:border-blue-500\/30:is(.dark *){border-color:#3080ff4d}@supports (color:color-mix(in lab,red,red)){.dark\:border-blue-500\/30:is(.dark *){border-color:color-mix(in oklab,var(--color-blue-500)30%,transparent)}}.dark\:border-blue-500\/40:is(.dark *){border-color:#3080ff66}@supports (color:color-mix(in lab,red,red)){.dark\:border-blue-500\/40:is(.dark *){border-color:color-mix(in oklab,var(--color-blue-500)40%,transparent)}}.dark\:border-blue-600:is(.dark *){border-color:var(--color-blue-600)}.dark\:border-blue-800:is(.dark *){border-color:var(--color-blue-800)}.dark\:border-emerald-600:is(.dark *){border-color:var(--color-emerald-600)}.dark\:border-gray-600:is(.dark *){border-color:var(--color-gray-600)}.dark\:border-gray-700:is(.dark *){border-color:var(--color-gray-700)}.dark\:border-green-400:is(.dark *){border-color:var(--color-green-400)}.dark\:border-green-400\/30:is(.dark *){border-color:#05df724d}@supports (color:color-mix(in lab,red,red)){.dark\:border-green-400\/30:is(.dark *){border-color:color-mix(in oklab,var(--color-green-400)30%,transparent)}}.dark\:border-green-400\/40:is(.dark *){border-color:#05df7266}@supports (color:color-mix(in lab,red,red)){.dark\:border-green-400\/40:is(.dark *){border-color:color-mix(in oklab,var(--color-green-400)40%,transparent)}}.dark\:border-green-800:is(.dark *){border-color:var(--color-green-800)}.dark\:border-input:is(.dark *){border-color:var(--input)}.dark\:border-orange-400:is(.dark *){border-color:var(--color-orange-400)}.dark\:border-orange-800:is(.dark *){border-color:var(--color-orange-800)}.dark\:border-red-400:is(.dark *){border-color:var(--color-red-400)}.dark\:border-red-800:is(.dark *){border-color:var(--color-red-800)}.dark\:\!bg-gray-800\/90:is(.dark *){background-color:#1e2939e6!important}@supports (color:color-mix(in lab,red,red)){.dark\:\!bg-gray-800\/90:is(.dark *){background-color:color-mix(in oklab,var(--color-gray-800)90%,transparent)!important}}.dark\:bg-\[\#8B5CF6\]:is(.dark *){background-color:#8b5cf6}.dark\:bg-\[\#8B5CF6\]\/5:is(.dark *){background-color:#8b5cf60d}.dark\:bg-\[\#8B5CF6\]\/10:is(.dark *){background-color:#8b5cf61a}.dark\:bg-amber-950\/20:is(.dark *){background-color:#46190133}@supports (color:color-mix(in lab,red,red)){.dark\:bg-amber-950\/20:is(.dark *){background-color:color-mix(in oklab,var(--color-amber-950)20%,transparent)}}.dark\:bg-background:is(.dark *){background-color:var(--background)}.dark\:bg-blue-500\/5:is(.dark *){background-color:#3080ff0d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-500\/5:is(.dark *){background-color:color-mix(in oklab,var(--color-blue-500)5%,transparent)}}.dark\:bg-blue-500\/10:is(.dark *){background-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-500\/10:is(.dark *){background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.dark\:bg-blue-900:is(.dark *){background-color:var(--color-blue-900)}.dark\:bg-blue-900\/50:is(.dark *){background-color:#1c398e80}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-900\/50:is(.dark *){background-color:color-mix(in oklab,var(--color-blue-900)50%,transparent)}}.dark\:bg-blue-950\/20:is(.dark *){background-color:#16245633}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-950\/20:is(.dark *){background-color:color-mix(in oklab,var(--color-blue-950)20%,transparent)}}.dark\:bg-blue-950\/40:is(.dark *){background-color:#16245666}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-950\/40:is(.dark *){background-color:color-mix(in oklab,var(--color-blue-950)40%,transparent)}}.dark\:bg-blue-950\/50:is(.dark *){background-color:#16245680}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-950\/50:is(.dark *){background-color:color-mix(in oklab,var(--color-blue-950)50%,transparent)}}.dark\:bg-card:is(.dark *){background-color:var(--card)}.dark\:bg-destructive\/20:is(.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-destructive\/20:is(.dark *){background-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.dark\:bg-destructive\/60:is(.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-destructive\/60:is(.dark *){background-color:color-mix(in oklab,var(--destructive)60%,transparent)}}.dark\:bg-emerald-900\/50:is(.dark *){background-color:#004e3b80}@supports (color:color-mix(in lab,red,red)){.dark\:bg-emerald-900\/50:is(.dark *){background-color:color-mix(in oklab,var(--color-emerald-900)50%,transparent)}}.dark\:bg-emerald-950\/50:is(.dark *){background-color:#002c2280}@supports (color:color-mix(in lab,red,red)){.dark\:bg-emerald-950\/50:is(.dark *){background-color:color-mix(in oklab,var(--color-emerald-950)50%,transparent)}}.dark\:bg-gray-500:is(.dark *){background-color:var(--color-gray-500)}.dark\:bg-gray-800:is(.dark *){background-color:var(--color-gray-800)}.dark\:bg-gray-800\/90:is(.dark *){background-color:#1e2939e6}@supports (color:color-mix(in lab,red,red)){.dark\:bg-gray-800\/90:is(.dark *){background-color:color-mix(in oklab,var(--color-gray-800)90%,transparent)}}.dark\:bg-gray-900:is(.dark *){background-color:var(--color-gray-900)}.dark\:bg-green-400:is(.dark *){background-color:var(--color-green-400)}.dark\:bg-green-400\/5:is(.dark *){background-color:#05df720d}@supports (color:color-mix(in lab,red,red)){.dark\:bg-green-400\/5:is(.dark *){background-color:color-mix(in oklab,var(--color-green-400)5%,transparent)}}.dark\:bg-green-400\/10:is(.dark *){background-color:#05df721a}@supports (color:color-mix(in lab,red,red)){.dark\:bg-green-400\/10:is(.dark *){background-color:color-mix(in oklab,var(--color-green-400)10%,transparent)}}.dark\:bg-green-900:is(.dark *){background-color:var(--color-green-900)}.dark\:bg-green-950:is(.dark *){background-color:var(--color-green-950)}.dark\:bg-green-950\/20:is(.dark *){background-color:#032e1533}@supports (color:color-mix(in lab,red,red)){.dark\:bg-green-950\/20:is(.dark *){background-color:color-mix(in oklab,var(--color-green-950)20%,transparent)}}.dark\:bg-green-950\/50:is(.dark *){background-color:#032e1580}@supports (color:color-mix(in lab,red,red)){.dark\:bg-green-950\/50:is(.dark *){background-color:color-mix(in oklab,var(--color-green-950)50%,transparent)}}.dark\:bg-input\/30:is(.dark *){background-color:var(--input)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-input\/30:is(.dark *){background-color:color-mix(in oklab,var(--input)30%,transparent)}}.dark\:bg-orange-400:is(.dark *){background-color:var(--color-orange-400)}.dark\:bg-orange-900:is(.dark *){background-color:var(--color-orange-900)}.dark\:bg-orange-950:is(.dark *){background-color:var(--color-orange-950)}.dark\:bg-orange-950\/50:is(.dark *){background-color:#44130680}@supports (color:color-mix(in lab,red,red)){.dark\:bg-orange-950\/50:is(.dark *){background-color:color-mix(in oklab,var(--color-orange-950)50%,transparent)}}.dark\:bg-purple-900:is(.dark *){background-color:var(--color-purple-900)}.dark\:bg-red-400:is(.dark *){background-color:var(--color-red-400)}.dark\:bg-red-900:is(.dark *){background-color:var(--color-red-900)}.dark\:bg-red-950:is(.dark *){background-color:var(--color-red-950)}.dark\:bg-red-950\/20:is(.dark *){background-color:#46080933}@supports (color:color-mix(in lab,red,red)){.dark\:bg-red-950\/20:is(.dark *){background-color:color-mix(in oklab,var(--color-red-950)20%,transparent)}}.dark\:bg-red-950\/50:is(.dark *){background-color:#46080980}@supports (color:color-mix(in lab,red,red)){.dark\:bg-red-950\/50:is(.dark *){background-color:color-mix(in oklab,var(--color-red-950)50%,transparent)}}.dark\:text-\[\#8B5CF6\]:is(.dark *){color:#8b5cf6}.dark\:text-amber-100:is(.dark *){color:var(--color-amber-100)}.dark\:text-amber-300:is(.dark *){color:var(--color-amber-300)}.dark\:text-amber-400:is(.dark *){color:var(--color-amber-400)}.dark\:text-amber-500:is(.dark *){color:var(--color-amber-500)}.dark\:text-blue-200:is(.dark *){color:var(--color-blue-200)}.dark\:text-blue-300:is(.dark *){color:var(--color-blue-300)}.dark\:text-blue-400:is(.dark *){color:var(--color-blue-400)}.dark\:text-blue-500:is(.dark *){color:var(--color-blue-500)}.dark\:text-emerald-200:is(.dark *){color:var(--color-emerald-200)}.dark\:text-emerald-300:is(.dark *){color:var(--color-emerald-300)}.dark\:text-emerald-400:is(.dark *){color:var(--color-emerald-400)}.dark\:text-gray-100:is(.dark *){color:var(--color-gray-100)}.dark\:text-gray-300:is(.dark *){color:var(--color-gray-300)}.dark\:text-gray-400:is(.dark *){color:var(--color-gray-400)}.dark\:text-green-200:is(.dark *){color:var(--color-green-200)}.dark\:text-green-300:is(.dark *){color:var(--color-green-300)}.dark\:text-green-400:is(.dark *){color:var(--color-green-400)}.dark\:text-orange-200:is(.dark *){color:var(--color-orange-200)}.dark\:text-orange-400:is(.dark *){color:var(--color-orange-400)}.dark\:text-purple-400:is(.dark *){color:var(--color-purple-400)}.dark\:text-red-200:is(.dark *){color:var(--color-red-200)}.dark\:text-red-400:is(.dark *){color:var(--color-red-400)}.dark\:text-yellow-400:is(.dark *){color:var(--color-yellow-400)}.dark\:opacity-30:is(.dark *){opacity:.3}@media (hover:hover){.dark\:hover\:bg-\[\#8B5CF6\]\/10:is(.dark *):hover{background-color:#8b5cf61a}.dark\:hover\:bg-accent\/50:is(.dark *):hover{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-accent\/50:is(.dark *):hover{background-color:color-mix(in oklab,var(--accent)50%,transparent)}}.dark\:hover\:bg-amber-950\/30:is(.dark *):hover{background-color:#4619014d}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-amber-950\/30:is(.dark *):hover{background-color:color-mix(in oklab,var(--color-amber-950)30%,transparent)}}.dark\:hover\:bg-blue-500\/10:is(.dark *):hover{background-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-blue-500\/10:is(.dark *):hover{background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.dark\:hover\:bg-destructive\/30:is(.dark *):hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-destructive\/30:is(.dark *):hover{background-color:color-mix(in oklab,var(--destructive)30%,transparent)}}.dark\:hover\:bg-gray-800:is(.dark *):hover{background-color:var(--color-gray-800)}.dark\:hover\:bg-green-400\/10:is(.dark *):hover{background-color:#05df721a}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-green-400\/10:is(.dark *):hover{background-color:color-mix(in oklab,var(--color-green-400)10%,transparent)}}.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:var(--input)}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:color-mix(in oklab,var(--input)50%,transparent)}}}.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:color-mix(in oklab,var(--destructive)40%,transparent)}}.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:color-mix(in oklab,var(--destructive)40%,transparent)}}.dark\:data-\[state\=checked\]\:bg-primary:is(.dark *)[data-state=checked]{background-color:var(--primary)}.dark\:data-\[variant\=destructive\]\:focus\:bg-destructive\/20:is(.dark *)[data-variant=destructive]:focus{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:data-\[variant\=destructive\]\:focus\:bg-destructive\/20:is(.dark *)[data-variant=destructive]:focus{background-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*=size-]){width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.\[\&_svg\:not\(\[class\*\=\'text-\'\]\)\]\:text-muted-foreground svg:not([class*=text-]){color:var(--muted-foreground)}.\[\.border-b\]\:pb-6.border-b{padding-bottom:calc(var(--spacing)*6)}.\[\.border-t\]\:pt-6.border-t{padding-top:calc(var(--spacing)*6)}:is(.\*\:\[span\]\:last\:flex>*):is(span):last-child{display:flex}:is(.\*\:\[span\]\:last\:items-center>*):is(span):last-child{align-items:center}:is(.\*\:\[span\]\:last\:gap-2>*):is(span):last-child{gap:calc(var(--spacing)*2)}:is(.data-\[variant\=destructive\]\:\*\:\[svg\]\:\!text-destructive[data-variant=destructive]>*):is(svg){color:var(--destructive)!important}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}:root{--radius:.625rem;--background:oklch(100% 0 0);--foreground:oklch(14.5% 0 0);--card:oklch(100% 0 0);--card-foreground:oklch(14.5% 0 0);--popover:oklch(100% 0 0);--popover-foreground:oklch(14.5% 0 0);--primary:oklch(48% .18 290);--primary-foreground:oklch(98.5% 0 0);--secondary:oklch(97% 0 0);--secondary-foreground:oklch(20.5% 0 0);--muted:oklch(97% 0 0);--muted-foreground:oklch(55.6% 0 0);--accent:oklch(97% 0 0);--accent-foreground:oklch(20.5% 0 0);--destructive:oklch(57.7% .245 27.325);--border:oklch(92.2% 0 0);--input:oklch(92.2% 0 0);--ring:oklch(70.8% 0 0);--chart-1:oklch(64.6% .222 41.116);--chart-2:oklch(60% .118 184.704);--chart-3:oklch(39.8% .07 227.392);--chart-4:oklch(82.8% .189 84.429);--chart-5:oklch(76.9% .188 70.08);--sidebar:oklch(98.5% 0 0);--sidebar-foreground:oklch(14.5% 0 0);--sidebar-primary:oklch(20.5% 0 0);--sidebar-primary-foreground:oklch(98.5% 0 0);--sidebar-accent:oklch(97% 0 0);--sidebar-accent-foreground:oklch(20.5% 0 0);--sidebar-border:oklch(92.2% 0 0);--sidebar-ring:oklch(70.8% 0 0)}.dark{--background:oklch(14.5% 0 0);--foreground:oklch(98.5% 0 0);--card:oklch(20.5% 0 0);--card-foreground:oklch(98.5% 0 0);--popover:oklch(20.5% 0 0);--popover-foreground:oklch(98.5% 0 0);--primary:oklch(62% .2 290);--primary-foreground:oklch(98.5% 0 0);--secondary:oklch(26.9% 0 0);--secondary-foreground:oklch(98.5% 0 0);--muted:oklch(26.9% 0 0);--muted-foreground:oklch(70.8% 0 0);--accent:oklch(26.9% 0 0);--accent-foreground:oklch(98.5% 0 0);--destructive:oklch(70.4% .191 22.216);--border:oklch(100% 0 0/.1);--input:oklch(100% 0 0/.15);--ring:oklch(55.6% 0 0);--chart-1:oklch(48.8% .243 264.376);--chart-2:oklch(69.6% .17 162.48);--chart-3:oklch(76.9% .188 70.08);--chart-4:oklch(62.7% .265 303.9);--chart-5:oklch(64.5% .246 16.439);--sidebar:oklch(20.5% 0 0);--sidebar-foreground:oklch(98.5% 0 0);--sidebar-primary:oklch(48.8% .243 264.376);--sidebar-primary-foreground:oklch(98.5% 0 0);--sidebar-accent:oklch(26.9% 0 0);--sidebar-accent-foreground:oklch(98.5% 0 0);--sidebar-border:oklch(100% 0 0/.1);--sidebar-ring:oklch(55.6% 0 0)}.workflow-chat-view .border-green-200{border-color:var(--color-emerald-200)}.workflow-chat-view .bg-green-50{background-color:var(--color-emerald-50)}.workflow-chat-view .bg-green-100{background-color:var(--color-emerald-100)}.workflow-chat-view .text-green-600{color:var(--color-emerald-600)}.workflow-chat-view .text-green-700{color:var(--color-emerald-700)}.workflow-chat-view .text-green-800{color:var(--color-emerald-800)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0));filter:blur(var(--tw-enter-blur,0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity,1);transform:translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0)scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1))rotate(var(--tw-exit-rotate,0));filter:blur(var(--tw-exit-blur,0))}}.react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #777;--xy-background-pattern-lines-color-default: #777;--xy-background-pattern-cross-color-default: #777;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))} diff --git a/python/packages/devui/agent_framework_devui/ui/assets/index-D0SfShuZ.js b/python/packages/devui/agent_framework_devui/ui/assets/index-D0SfShuZ.js deleted file mode 100644 index ba82315af1..0000000000 --- a/python/packages/devui/agent_framework_devui/ui/assets/index-D0SfShuZ.js +++ /dev/null @@ -1,445 +0,0 @@ -function v_(e,r){for(var o=0;os[i]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const r=document.createElement("link").relList;if(r&&r.supports&&r.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const u of i)if(u.type==="childList")for(const d of u.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&s(d)}).observe(document,{childList:!0,subtree:!0});function o(i){const u={};return i.integrity&&(u.integrity=i.integrity),i.referrerPolicy&&(u.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?u.credentials="include":i.crossOrigin==="anonymous"?u.credentials="omit":u.credentials="same-origin",u}function s(i){if(i.ep)return;i.ep=!0;const u=o(i);fetch(i.href,u)}})();function Rm(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ph={exports:{}},ui={};/** - * @license React - * react-jsx-runtime.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var cy;function b_(){if(cy)return ui;cy=1;var e=Symbol.for("react.transitional.element"),r=Symbol.for("react.fragment");function o(s,i,u){var d=null;if(u!==void 0&&(d=""+u),i.key!==void 0&&(d=""+i.key),"key"in i){u={};for(var f in i)f!=="key"&&(u[f]=i[f])}else u=i;return i=u.ref,{$$typeof:e,type:s,key:d,ref:i!==void 0?i:null,props:u}}return ui.Fragment=r,ui.jsx=o,ui.jsxs=o,ui}var uy;function w_(){return uy||(uy=1,ph.exports=b_()),ph.exports}var c=w_(),gh={exports:{}},Re={};/** - * @license React - * react.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var dy;function S_(){if(dy)return Re;dy=1;var e=Symbol.for("react.transitional.element"),r=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),u=Symbol.for("react.consumer"),d=Symbol.for("react.context"),f=Symbol.for("react.forward_ref"),m=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),g=Symbol.for("react.lazy"),v=Symbol.iterator;function y(k){return k===null||typeof k!="object"?null:(k=v&&k[v]||k["@@iterator"],typeof k=="function"?k:null)}var w={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},N=Object.assign,S={};function _(k,B,K){this.props=k,this.context=B,this.refs=S,this.updater=K||w}_.prototype.isReactComponent={},_.prototype.setState=function(k,B){if(typeof k!="object"&&typeof k!="function"&&k!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,k,B,"setState")},_.prototype.forceUpdate=function(k){this.updater.enqueueForceUpdate(this,k,"forceUpdate")};function A(){}A.prototype=_.prototype;function R(k,B,K){this.props=k,this.context=B,this.refs=S,this.updater=K||w}var E=R.prototype=new A;E.constructor=R,N(E,_.prototype),E.isPureReactComponent=!0;var M=Array.isArray,O={H:null,A:null,T:null,S:null,V:null},H=Object.prototype.hasOwnProperty;function I(k,B,K,Z,te,he){return K=he.ref,{$$typeof:e,type:k,key:B,ref:K!==void 0?K:null,props:he}}function U(k,B){return I(k.type,B,void 0,void 0,void 0,k.props)}function Y(k){return typeof k=="object"&&k!==null&&k.$$typeof===e}function Q(k){var B={"=":"=0",":":"=2"};return"$"+k.replace(/[=:]/g,function(K){return B[K]})}var ee=/\/+/g;function q(k,B){return typeof k=="object"&&k!==null&&k.key!=null?Q(""+k.key):B.toString(36)}function X(){}function z(k){switch(k.status){case"fulfilled":return k.value;case"rejected":throw k.reason;default:switch(typeof k.status=="string"?k.then(X,X):(k.status="pending",k.then(function(B){k.status==="pending"&&(k.status="fulfilled",k.value=B)},function(B){k.status==="pending"&&(k.status="rejected",k.reason=B)})),k.status){case"fulfilled":return k.value;case"rejected":throw k.reason}}throw k}function $(k,B,K,Z,te){var he=typeof k;(he==="undefined"||he==="boolean")&&(k=null);var fe=!1;if(k===null)fe=!0;else switch(he){case"bigint":case"string":case"number":fe=!0;break;case"object":switch(k.$$typeof){case e:case r:fe=!0;break;case g:return fe=k._init,$(fe(k._payload),B,K,Z,te)}}if(fe)return te=te(k),fe=Z===""?"."+q(k,0):Z,M(te)?(K="",fe!=null&&(K=fe.replace(ee,"$&/")+"/"),$(te,B,K,"",function(me){return me})):te!=null&&(Y(te)&&(te=U(te,K+(te.key==null||k&&k.key===te.key?"":(""+te.key).replace(ee,"$&/")+"/")+fe)),B.push(te)),1;fe=0;var ne=Z===""?".":Z+":";if(M(k))for(var ae=0;ae>>1,k=C[P];if(0>>1;Pi(Z,L))tei(he,Z)?(C[P]=he,C[te]=L,P=te):(C[P]=Z,C[K]=L,P=K);else if(tei(he,L))C[P]=he,C[te]=L,P=te;else break e}}return D}function i(C,D){var L=C.sortIndex-D.sortIndex;return L!==0?L:C.id-D.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var u=performance;e.unstable_now=function(){return u.now()}}else{var d=Date,f=d.now();e.unstable_now=function(){return d.now()-f}}var m=[],p=[],g=1,v=null,y=3,w=!1,N=!1,S=!1,_=!1,A=typeof setTimeout=="function"?setTimeout:null,R=typeof clearTimeout=="function"?clearTimeout:null,E=typeof setImmediate<"u"?setImmediate:null;function M(C){for(var D=o(p);D!==null;){if(D.callback===null)s(p);else if(D.startTime<=C)s(p),D.sortIndex=D.expirationTime,r(m,D);else break;D=o(p)}}function O(C){if(S=!1,M(C),!N)if(o(m)!==null)N=!0,H||(H=!0,q());else{var D=o(p);D!==null&&$(O,D.startTime-C)}}var H=!1,I=-1,U=5,Y=-1;function Q(){return _?!0:!(e.unstable_now()-YC&&Q());){var P=v.callback;if(typeof P=="function"){v.callback=null,y=v.priorityLevel;var k=P(v.expirationTime<=C);if(C=e.unstable_now(),typeof k=="function"){v.callback=k,M(C),D=!0;break t}v===o(m)&&s(m),M(C)}else s(m);v=o(m)}if(v!==null)D=!0;else{var B=o(p);B!==null&&$(O,B.startTime-C),D=!1}}break e}finally{v=null,y=L,w=!1}D=void 0}}finally{D?q():H=!1}}}var q;if(typeof E=="function")q=function(){E(ee)};else if(typeof MessageChannel<"u"){var X=new MessageChannel,z=X.port2;X.port1.onmessage=ee,q=function(){z.postMessage(null)}}else q=function(){A(ee,0)};function $(C,D){I=A(function(){C(e.unstable_now())},D)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(C){C.callback=null},e.unstable_forceFrameRate=function(C){0>C||125P?(C.sortIndex=L,r(p,C),o(m)===null&&C===o(p)&&(S?(R(I),I=-1):S=!0,$(O,L-P))):(C.sortIndex=k,r(m,C),N||w||(N=!0,H||(H=!0,q()))),C},e.unstable_shouldYield=Q,e.unstable_wrapCallback=function(C){var D=y;return function(){var L=y;y=D;try{return C.apply(this,arguments)}finally{y=L}}}})(vh)),vh}var my;function E_(){return my||(my=1,yh.exports=N_()),yh.exports}var bh={exports:{}},kt={};/** - * @license React - * react-dom.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var py;function __(){if(py)return kt;py=1;var e=Bi();function r(m){var p="https://react.dev/errors/"+m;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(r){console.error(r)}}return e(),bh.exports=__(),bh.exports}/** - * @license React - * react-dom-client.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var xy;function j_(){if(xy)return di;xy=1;var e=E_(),r=Bi(),o=fb();function s(t){var n="https://react.dev/errors/"+t;if(1k||(t.current=P[k],P[k]=null,k--)}function Z(t,n){k++,P[k]=t.current,t.current=n}var te=B(null),he=B(null),fe=B(null),ne=B(null);function ae(t,n){switch(Z(fe,n),Z(he,t),Z(te,null),n.nodeType){case 9:case 11:t=(t=n.documentElement)&&(t=t.namespaceURI)?H0(t):0;break;default:if(t=n.tagName,n=n.namespaceURI)n=H0(n),t=I0(n,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}K(te),Z(te,t)}function me(){K(te),K(he),K(fe)}function ge(t){t.memoizedState!==null&&Z(ne,t);var n=te.current,a=I0(n,t.type);n!==a&&(Z(he,t),Z(te,a))}function se(t){he.current===t&&(K(te),K(he)),ne.current===t&&(K(ne),ai._currentValue=L)}var ie=Object.prototype.hasOwnProperty,de=e.unstable_scheduleCallback,pe=e.unstable_cancelCallback,we=e.unstable_shouldYield,Ne=e.unstable_requestPaint,Se=e.unstable_now,Ie=e.unstable_getCurrentPriorityLevel,St=e.unstable_ImmediatePriority,lt=e.unstable_UserBlockingPriority,ke=e.unstable_NormalPriority,Qe=e.unstable_LowPriority,xt=e.unstable_IdlePriority,hn=e.log,tn=e.unstable_setDisableYieldValue,nn=null,ht=null;function mn(t){if(typeof hn=="function"&&tn(t),ht&&typeof ht.setStrictMode=="function")try{ht.setStrictMode(nn,t)}catch{}}var Tt=Math.clz32?Math.clz32:nd,as=Math.log,td=Math.LN2;function nd(t){return t>>>=0,t===0?32:31-(as(t)/td|0)|0}var Yo=256,qo=4194304;function Gn(t){var n=t&42;if(n!==0)return n;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t&4194048;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Go(t,n,a){var l=t.pendingLanes;if(l===0)return 0;var h=0,x=t.suspendedLanes,j=t.pingedLanes;t=t.warmLanes;var T=l&134217727;return T!==0?(l=T&~x,l!==0?h=Gn(l):(j&=T,j!==0?h=Gn(j):a||(a=T&~t,a!==0&&(h=Gn(a))))):(T=l&~x,T!==0?h=Gn(T):j!==0?h=Gn(j):a||(a=l&~t,a!==0&&(h=Gn(a)))),h===0?0:n!==0&&n!==h&&(n&x)===0&&(x=h&-h,a=n&-n,x>=a||x===32&&(a&4194048)!==0)?n:h}function io(t,n){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&n)===0}function rd(t,n){switch(t){case 1:case 2:case 4:case 8:case 64:return n+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return n+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function al(){var t=Yo;return Yo<<=1,(Yo&4194048)===0&&(Yo=256),t}function sl(){var t=qo;return qo<<=1,(qo&62914560)===0&&(qo=4194304),t}function ss(t){for(var n=[],a=0;31>a;a++)n.push(t);return n}function lo(t,n){t.pendingLanes|=n,n!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function od(t,n,a,l,h,x){var j=t.pendingLanes;t.pendingLanes=a,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=a,t.entangledLanes&=a,t.errorRecoveryDisabledLanes&=a,t.shellSuspendCounter=0;var T=t.entanglements,V=t.expirationTimes,J=t.hiddenUpdates;for(a=j&~a;0)":-1h||V[l]!==J[h]){var le=` -`+V[l].replace(" at new "," at ");return t.displayName&&le.includes("")&&(le=le.replace("",t.displayName)),le}while(1<=l&&0<=h);break}}}finally{ms=!1,Error.prepareStackTrace=a}return(a=t?t.displayName||t.name:"")?Kn(a):""}function ud(t){switch(t.tag){case 26:case 27:case 5:return Kn(t.type);case 16:return Kn("Lazy");case 13:return Kn("Suspense");case 19:return Kn("SuspenseList");case 0:case 15:return ps(t.type,!1);case 11:return ps(t.type.render,!1);case 1:return ps(t.type,!0);case 31:return Kn("Activity");default:return""}}function ml(t){try{var n="";do n+=ud(t),t=t.return;while(t);return n}catch(a){return` -Error generating stack: `+a.message+` -`+a.stack}}function zt(t){switch(typeof t){case"bigint":case"boolean":case"number":case"string":case"undefined":return t;case"object":return t;default:return""}}function pl(t){var n=t.type;return(t=t.nodeName)&&t.toLowerCase()==="input"&&(n==="checkbox"||n==="radio")}function dd(t){var n=pl(t)?"checked":"value",a=Object.getOwnPropertyDescriptor(t.constructor.prototype,n),l=""+t[n];if(!t.hasOwnProperty(n)&&typeof a<"u"&&typeof a.get=="function"&&typeof a.set=="function"){var h=a.get,x=a.set;return Object.defineProperty(t,n,{configurable:!0,get:function(){return h.call(this)},set:function(j){l=""+j,x.call(this,j)}}),Object.defineProperty(t,n,{enumerable:a.enumerable}),{getValue:function(){return l},setValue:function(j){l=""+j},stopTracking:function(){t._valueTracker=null,delete t[n]}}}}function Zo(t){t._valueTracker||(t._valueTracker=dd(t))}function gs(t){if(!t)return!1;var n=t._valueTracker;if(!n)return!0;var a=n.getValue(),l="";return t&&(l=pl(t)?t.checked?"true":"false":t.value),t=l,t!==a?(n.setValue(t),!0):!1}function Ko(t){if(t=t||(typeof document<"u"?document:void 0),typeof t>"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var fd=/[\n"\\]/g;function Lt(t){return t.replace(fd,function(n){return"\\"+n.charCodeAt(0).toString(16)+" "})}function uo(t,n,a,l,h,x,j,T){t.name="",j!=null&&typeof j!="function"&&typeof j!="symbol"&&typeof j!="boolean"?t.type=j:t.removeAttribute("type"),n!=null?j==="number"?(n===0&&t.value===""||t.value!=n)&&(t.value=""+zt(n)):t.value!==""+zt(n)&&(t.value=""+zt(n)):j!=="submit"&&j!=="reset"||t.removeAttribute("value"),n!=null?xs(t,j,zt(n)):a!=null?xs(t,j,zt(a)):l!=null&&t.removeAttribute("value"),h==null&&x!=null&&(t.defaultChecked=!!x),h!=null&&(t.checked=h&&typeof h!="function"&&typeof h!="symbol"),T!=null&&typeof T!="function"&&typeof T!="symbol"&&typeof T!="boolean"?t.name=""+zt(T):t.removeAttribute("name")}function gl(t,n,a,l,h,x,j,T){if(x!=null&&typeof x!="function"&&typeof x!="symbol"&&typeof x!="boolean"&&(t.type=x),n!=null||a!=null){if(!(x!=="submit"&&x!=="reset"||n!=null))return;a=a!=null?""+zt(a):"",n=n!=null?""+zt(n):a,T||n===t.value||(t.value=n),t.defaultValue=n}l=l??h,l=typeof l!="function"&&typeof l!="symbol"&&!!l,t.checked=T?t.checked:!!l,t.defaultChecked=!!l,j!=null&&typeof j!="function"&&typeof j!="symbol"&&typeof j!="boolean"&&(t.name=j)}function xs(t,n,a){n==="number"&&Ko(t.ownerDocument)===t||t.defaultValue===""+a||(t.defaultValue=""+a)}function Wn(t,n,a,l){if(t=t.options,n){n={};for(var h=0;h"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),xd=!1;if(Qn)try{var vs={};Object.defineProperty(vs,"passive",{get:function(){xd=!0}}),window.addEventListener("test",vs,vs),window.removeEventListener("test",vs,vs)}catch{xd=!1}var jr=null,yd=null,yl=null;function Pp(){if(yl)return yl;var t,n=yd,a=n.length,l,h="value"in jr?jr.value:jr.textContent,x=h.length;for(t=0;t=Ss),Xp=" ",Fp=!1;function Zp(t,n){switch(t){case"keyup":return VN.indexOf(n.keyCode)!==-1;case"keydown":return n.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Kp(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var ea=!1;function qN(t,n){switch(t){case"compositionend":return Kp(n);case"keypress":return n.which!==32?null:(Fp=!0,Xp);case"textInput":return t=n.data,t===Xp&&Fp?null:t;default:return null}}function GN(t,n){if(ea)return t==="compositionend"||!Nd&&Zp(t,n)?(t=Pp(),yl=yd=jr=null,ea=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(n.ctrlKey||n.altKey||n.metaKey)||n.ctrlKey&&n.altKey){if(n.char&&1=n)return{node:a,offset:n-t};t=l}e:{for(;a;){if(a.nextSibling){a=a.nextSibling;break e}a=a.parentNode}a=void 0}a=og(a)}}function sg(t,n){return t&&n?t===n?!0:t&&t.nodeType===3?!1:n&&n.nodeType===3?sg(t,n.parentNode):"contains"in t?t.contains(n):t.compareDocumentPosition?!!(t.compareDocumentPosition(n)&16):!1:!1}function ig(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var n=Ko(t.document);n instanceof t.HTMLIFrameElement;){try{var a=typeof n.contentWindow.location.href=="string"}catch{a=!1}if(a)t=n.contentWindow;else break;n=Ko(t.document)}return n}function jd(t){var n=t&&t.nodeName&&t.nodeName.toLowerCase();return n&&(n==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||n==="textarea"||t.contentEditable==="true")}var eE=Qn&&"documentMode"in document&&11>=document.documentMode,ta=null,Cd=null,js=null,Ad=!1;function lg(t,n,a){var l=a.window===a?a.document:a.nodeType===9?a:a.ownerDocument;Ad||ta==null||ta!==Ko(l)||(l=ta,"selectionStart"in l&&jd(l)?l={start:l.selectionStart,end:l.selectionEnd}:(l=(l.ownerDocument&&l.ownerDocument.defaultView||window).getSelection(),l={anchorNode:l.anchorNode,anchorOffset:l.anchorOffset,focusNode:l.focusNode,focusOffset:l.focusOffset}),js&&_s(js,l)||(js=l,l=lc(Cd,"onSelect"),0>=j,h-=j,er=1<<32-Tt(n)+h|a<x?x:8;var j=C.T,T={};C.T=T,mf(t,!1,n,a);try{var V=h(),J=C.S;if(J!==null&&J(T,V),V!==null&&typeof V=="object"&&typeof V.then=="function"){var le=cE(V,l);Ps(t,n,le,Zt(t))}else Ps(t,n,l,Zt(t))}catch(ue){Ps(t,n,{then:function(){},status:"rejected",reason:ue},Zt())}finally{D.p=x,C.T=j}}function mE(){}function ff(t,n,a,l){if(t.tag!==5)throw Error(s(476));var h=cx(t).queue;lx(t,h,n,L,a===null?mE:function(){return ux(t),a(l)})}function cx(t){var n=t.memoizedState;if(n!==null)return n;n={memoizedState:L,baseState:L,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:or,lastRenderedState:L},next:null};var a={};return n.next={memoizedState:a,baseState:a,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:or,lastRenderedState:a},next:null},t.memoizedState=n,t=t.alternate,t!==null&&(t.memoizedState=n),n}function ux(t){var n=cx(t).next.queue;Ps(t,n,{},Zt())}function hf(){return Mt(ai)}function dx(){return dt().memoizedState}function fx(){return dt().memoizedState}function pE(t){for(var n=t.return;n!==null;){switch(n.tag){case 24:case 3:var a=Zt();t=Mr(a);var l=kr(n,t,a);l!==null&&(Kt(l,n,a),zs(l,n,a)),n={cache:$d()},t.payload=n;return}n=n.return}}function gE(t,n,a){var l=Zt();a={lane:l,revertLane:0,action:a,hasEagerState:!1,eagerState:null,next:null},$l(t)?mx(n,a):(a=Rd(t,n,a,l),a!==null&&(Kt(a,t,l),px(a,n,l)))}function hx(t,n,a){var l=Zt();Ps(t,n,a,l)}function Ps(t,n,a,l){var h={lane:l,revertLane:0,action:a,hasEagerState:!1,eagerState:null,next:null};if($l(t))mx(n,h);else{var x=t.alternate;if(t.lanes===0&&(x===null||x.lanes===0)&&(x=n.lastRenderedReducer,x!==null))try{var j=n.lastRenderedState,T=x(j,a);if(h.hasEagerState=!0,h.eagerState=T,Yt(T,j))return _l(t,n,h,0),Je===null&&El(),!1}catch{}finally{}if(a=Rd(t,n,h,l),a!==null)return Kt(a,t,l),px(a,n,l),!0}return!1}function mf(t,n,a,l){if(l={lane:2,revertLane:Gf(),action:l,hasEagerState:!1,eagerState:null,next:null},$l(t)){if(n)throw Error(s(479))}else n=Rd(t,a,l,2),n!==null&&Kt(n,t,2)}function $l(t){var n=t.alternate;return t===De||n!==null&&n===De}function mx(t,n){da=Ll=!0;var a=t.pending;a===null?n.next=n:(n.next=a.next,a.next=n),t.pending=n}function px(t,n,a){if((a&4194048)!==0){var l=n.lanes;l&=t.pendingLanes,a|=l,n.lanes=a,is(t,a)}}var Vl={readContext:Mt,use:Il,useCallback:st,useContext:st,useEffect:st,useImperativeHandle:st,useLayoutEffect:st,useInsertionEffect:st,useMemo:st,useReducer:st,useRef:st,useState:st,useDebugValue:st,useDeferredValue:st,useTransition:st,useSyncExternalStore:st,useId:st,useHostTransitionStatus:st,useFormState:st,useActionState:st,useOptimistic:st,useMemoCache:st,useCacheRefresh:st},gx={readContext:Mt,use:Il,useCallback:function(t,n){return It().memoizedState=[t,n===void 0?null:n],t},useContext:Mt,useEffect:Jg,useImperativeHandle:function(t,n,a){a=a!=null?a.concat([t]):null,Pl(4194308,4,rx.bind(null,n,t),a)},useLayoutEffect:function(t,n){return Pl(4194308,4,t,n)},useInsertionEffect:function(t,n){Pl(4,2,t,n)},useMemo:function(t,n){var a=It();n=n===void 0?null:n;var l=t();if(No){mn(!0);try{t()}finally{mn(!1)}}return a.memoizedState=[l,n],l},useReducer:function(t,n,a){var l=It();if(a!==void 0){var h=a(n);if(No){mn(!0);try{a(n)}finally{mn(!1)}}}else h=n;return l.memoizedState=l.baseState=h,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:h},l.queue=t,t=t.dispatch=gE.bind(null,De,t),[l.memoizedState,t]},useRef:function(t){var n=It();return t={current:t},n.memoizedState=t},useState:function(t){t=lf(t);var n=t.queue,a=hx.bind(null,De,n);return n.dispatch=a,[t.memoizedState,a]},useDebugValue:uf,useDeferredValue:function(t,n){var a=It();return df(a,t,n)},useTransition:function(){var t=lf(!1);return t=lx.bind(null,De,t.queue,!0,!1),It().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,n,a){var l=De,h=It();if(Ye){if(a===void 0)throw Error(s(407));a=a()}else{if(a=n(),Je===null)throw Error(s(349));(Ue&124)!==0||Hg(l,n,a)}h.memoizedState=a;var x={value:a,getSnapshot:n};return h.queue=x,Jg(Bg.bind(null,l,x,t),[t]),l.flags|=2048,ha(9,Ul(),Ig.bind(null,l,x,a,n),null),a},useId:function(){var t=It(),n=Je.identifierPrefix;if(Ye){var a=tr,l=er;a=(l&~(1<<32-Tt(l)-1)).toString(32)+a,n="«"+n+"R"+a,a=Hl++,0Ae?(wt=Ee,Ee=null):wt=Ee.sibling;var $e=re(F,Ee,W[Ae],ce);if($e===null){Ee===null&&(Ee=wt);break}t&&Ee&&$e.alternate===null&&n(F,Ee),G=x($e,G,Ae),ze===null?xe=$e:ze.sibling=$e,ze=$e,Ee=wt}if(Ae===W.length)return a(F,Ee),Ye&&xo(F,Ae),xe;if(Ee===null){for(;AeAe?(wt=Ee,Ee=null):wt=Ee.sibling;var Xr=re(F,Ee,$e.value,ce);if(Xr===null){Ee===null&&(Ee=wt);break}t&&Ee&&Xr.alternate===null&&n(F,Ee),G=x(Xr,G,Ae),ze===null?xe=Xr:ze.sibling=Xr,ze=Xr,Ee=wt}if($e.done)return a(F,Ee),Ye&&xo(F,Ae),xe;if(Ee===null){for(;!$e.done;Ae++,$e=W.next())$e=ue(F,$e.value,ce),$e!==null&&(G=x($e,G,Ae),ze===null?xe=$e:ze.sibling=$e,ze=$e);return Ye&&xo(F,Ae),xe}for(Ee=l(Ee);!$e.done;Ae++,$e=W.next())$e=oe(Ee,F,Ae,$e.value,ce),$e!==null&&(t&&$e.alternate!==null&&Ee.delete($e.key===null?Ae:$e.key),G=x($e,G,Ae),ze===null?xe=$e:ze.sibling=$e,ze=$e);return t&&Ee.forEach(function(y_){return n(F,y_)}),Ye&&xo(F,Ae),xe}function Ke(F,G,W,ce){if(typeof W=="object"&&W!==null&&W.type===N&&W.key===null&&(W=W.props.children),typeof W=="object"&&W!==null){switch(W.$$typeof){case y:e:{for(var xe=W.key;G!==null;){if(G.key===xe){if(xe=W.type,xe===N){if(G.tag===7){a(F,G.sibling),ce=h(G,W.props.children),ce.return=F,F=ce;break e}}else if(G.elementType===xe||typeof xe=="object"&&xe!==null&&xe.$$typeof===U&&yx(xe)===G.type){a(F,G.sibling),ce=h(G,W.props),Vs(ce,W),ce.return=F,F=ce;break e}a(F,G);break}else n(F,G);G=G.sibling}W.type===N?(ce=po(W.props.children,F.mode,ce,W.key),ce.return=F,F=ce):(ce=Cl(W.type,W.key,W.props,null,F.mode,ce),Vs(ce,W),ce.return=F,F=ce)}return j(F);case w:e:{for(xe=W.key;G!==null;){if(G.key===xe)if(G.tag===4&&G.stateNode.containerInfo===W.containerInfo&&G.stateNode.implementation===W.implementation){a(F,G.sibling),ce=h(G,W.children||[]),ce.return=F,F=ce;break e}else{a(F,G);break}else n(F,G);G=G.sibling}ce=zd(W,F.mode,ce),ce.return=F,F=ce}return j(F);case U:return xe=W._init,W=xe(W._payload),Ke(F,G,W,ce)}if($(W))return Me(F,G,W,ce);if(q(W)){if(xe=q(W),typeof xe!="function")throw Error(s(150));return W=xe.call(W),Ce(F,G,W,ce)}if(typeof W.then=="function")return Ke(F,G,Yl(W),ce);if(W.$$typeof===E)return Ke(F,G,Tl(F,W),ce);ql(F,W)}return typeof W=="string"&&W!==""||typeof W=="number"||typeof W=="bigint"?(W=""+W,G!==null&&G.tag===6?(a(F,G.sibling),ce=h(G,W),ce.return=F,F=ce):(a(F,G),ce=Od(W,F.mode,ce),ce.return=F,F=ce),j(F)):a(F,G)}return function(F,G,W,ce){try{$s=0;var xe=Ke(F,G,W,ce);return ma=null,xe}catch(Ee){if(Ee===Ds||Ee===Dl)throw Ee;var ze=qt(29,Ee,null,F.mode);return ze.lanes=ce,ze.return=F,ze}finally{}}}var pa=vx(!0),bx=vx(!1),ln=B(null),Mn=null;function Rr(t){var n=t.alternate;Z(pt,pt.current&1),Z(ln,t),Mn===null&&(n===null||ua.current!==null||n.memoizedState!==null)&&(Mn=t)}function wx(t){if(t.tag===22){if(Z(pt,pt.current),Z(ln,t),Mn===null){var n=t.alternate;n!==null&&n.memoizedState!==null&&(Mn=t)}}else Dr()}function Dr(){Z(pt,pt.current),Z(ln,ln.current)}function ar(t){K(ln),Mn===t&&(Mn=null),K(pt)}var pt=B(0);function Gl(t){for(var n=t;n!==null;){if(n.tag===13){var a=n.memoizedState;if(a!==null&&(a=a.dehydrated,a===null||a.data==="$?"||oh(a)))return n}else if(n.tag===19&&n.memoizedProps.revealOrder!==void 0){if((n.flags&128)!==0)return n}else if(n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return null;n=n.return}n.sibling.return=n.return,n=n.sibling}return null}function pf(t,n,a,l){n=t.memoizedState,a=a(l,n),a=a==null?n:g({},n,a),t.memoizedState=a,t.lanes===0&&(t.updateQueue.baseState=a)}var gf={enqueueSetState:function(t,n,a){t=t._reactInternals;var l=Zt(),h=Mr(l);h.payload=n,a!=null&&(h.callback=a),n=kr(t,h,l),n!==null&&(Kt(n,t,l),zs(n,t,l))},enqueueReplaceState:function(t,n,a){t=t._reactInternals;var l=Zt(),h=Mr(l);h.tag=1,h.payload=n,a!=null&&(h.callback=a),n=kr(t,h,l),n!==null&&(Kt(n,t,l),zs(n,t,l))},enqueueForceUpdate:function(t,n){t=t._reactInternals;var a=Zt(),l=Mr(a);l.tag=2,n!=null&&(l.callback=n),n=kr(t,l,a),n!==null&&(Kt(n,t,a),zs(n,t,a))}};function Sx(t,n,a,l,h,x,j){return t=t.stateNode,typeof t.shouldComponentUpdate=="function"?t.shouldComponentUpdate(l,x,j):n.prototype&&n.prototype.isPureReactComponent?!_s(a,l)||!_s(h,x):!0}function Nx(t,n,a,l){t=n.state,typeof n.componentWillReceiveProps=="function"&&n.componentWillReceiveProps(a,l),typeof n.UNSAFE_componentWillReceiveProps=="function"&&n.UNSAFE_componentWillReceiveProps(a,l),n.state!==t&&gf.enqueueReplaceState(n,n.state,null)}function Eo(t,n){var a=n;if("ref"in n){a={};for(var l in n)l!=="ref"&&(a[l]=n[l])}if(t=t.defaultProps){a===n&&(a=g({},a));for(var h in t)a[h]===void 0&&(a[h]=t[h])}return a}var Xl=typeof reportError=="function"?reportError:function(t){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var n=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof t=="object"&&t!==null&&typeof t.message=="string"?String(t.message):String(t),error:t});if(!window.dispatchEvent(n))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",t);return}console.error(t)};function Ex(t){Xl(t)}function _x(t){console.error(t)}function jx(t){Xl(t)}function Fl(t,n){try{var a=t.onUncaughtError;a(n.value,{componentStack:n.stack})}catch(l){setTimeout(function(){throw l})}}function Cx(t,n,a){try{var l=t.onCaughtError;l(a.value,{componentStack:a.stack,errorBoundary:n.tag===1?n.stateNode:null})}catch(h){setTimeout(function(){throw h})}}function xf(t,n,a){return a=Mr(a),a.tag=3,a.payload={element:null},a.callback=function(){Fl(t,n)},a}function Ax(t){return t=Mr(t),t.tag=3,t}function Mx(t,n,a,l){var h=a.type.getDerivedStateFromError;if(typeof h=="function"){var x=l.value;t.payload=function(){return h(x)},t.callback=function(){Cx(n,a,l)}}var j=a.stateNode;j!==null&&typeof j.componentDidCatch=="function"&&(t.callback=function(){Cx(n,a,l),typeof h!="function"&&(Br===null?Br=new Set([this]):Br.add(this));var T=l.stack;this.componentDidCatch(l.value,{componentStack:T!==null?T:""})})}function yE(t,n,a,l,h){if(a.flags|=32768,l!==null&&typeof l=="object"&&typeof l.then=="function"){if(n=a.alternate,n!==null&&ks(n,a,h,!0),a=ln.current,a!==null){switch(a.tag){case 13:return Mn===null?Pf():a.alternate===null&&at===0&&(at=3),a.flags&=-257,a.flags|=65536,a.lanes=h,l===qd?a.flags|=16384:(n=a.updateQueue,n===null?a.updateQueue=new Set([l]):n.add(l),Vf(t,l,h)),!1;case 22:return a.flags|=65536,l===qd?a.flags|=16384:(n=a.updateQueue,n===null?(n={transitions:null,markerInstances:null,retryQueue:new Set([l])},a.updateQueue=n):(a=n.retryQueue,a===null?n.retryQueue=new Set([l]):a.add(l)),Vf(t,l,h)),!1}throw Error(s(435,a.tag))}return Vf(t,l,h),Pf(),!1}if(Ye)return n=ln.current,n!==null?((n.flags&65536)===0&&(n.flags|=256),n.flags|=65536,n.lanes=h,l!==Id&&(t=Error(s(422),{cause:l}),Ms(rn(t,a)))):(l!==Id&&(n=Error(s(423),{cause:l}),Ms(rn(n,a))),t=t.current.alternate,t.flags|=65536,h&=-h,t.lanes|=h,l=rn(l,a),h=xf(t.stateNode,l,h),Fd(t,h),at!==4&&(at=2)),!1;var x=Error(s(520),{cause:l});if(x=rn(x,a),Ks===null?Ks=[x]:Ks.push(x),at!==4&&(at=2),n===null)return!0;l=rn(l,a),a=n;do{switch(a.tag){case 3:return a.flags|=65536,t=h&-h,a.lanes|=t,t=xf(a.stateNode,l,t),Fd(a,t),!1;case 1:if(n=a.type,x=a.stateNode,(a.flags&128)===0&&(typeof n.getDerivedStateFromError=="function"||x!==null&&typeof x.componentDidCatch=="function"&&(Br===null||!Br.has(x))))return a.flags|=65536,h&=-h,a.lanes|=h,h=Ax(h),Mx(h,t,a,l),Fd(a,h),!1}a=a.return}while(a!==null);return!1}var kx=Error(s(461)),vt=!1;function Nt(t,n,a,l){n.child=t===null?bx(n,null,a,l):pa(n,t.child,a,l)}function Tx(t,n,a,l,h){a=a.render;var x=n.ref;if("ref"in l){var j={};for(var T in l)T!=="ref"&&(j[T]=l[T])}else j=l;return wo(n),l=Jd(t,n,a,j,x,h),T=ef(),t!==null&&!vt?(tf(t,n,h),sr(t,n,h)):(Ye&&T&&Ld(n),n.flags|=1,Nt(t,n,l,h),n.child)}function Rx(t,n,a,l,h){if(t===null){var x=a.type;return typeof x=="function"&&!Dd(x)&&x.defaultProps===void 0&&a.compare===null?(n.tag=15,n.type=x,Dx(t,n,x,l,h)):(t=Cl(a.type,null,l,n,n.mode,h),t.ref=n.ref,t.return=n,n.child=t)}if(x=t.child,!_f(t,h)){var j=x.memoizedProps;if(a=a.compare,a=a!==null?a:_s,a(j,l)&&t.ref===n.ref)return sr(t,n,h)}return n.flags|=1,t=Jn(x,l),t.ref=n.ref,t.return=n,n.child=t}function Dx(t,n,a,l,h){if(t!==null){var x=t.memoizedProps;if(_s(x,l)&&t.ref===n.ref)if(vt=!1,n.pendingProps=l=x,_f(t,h))(t.flags&131072)!==0&&(vt=!0);else return n.lanes=t.lanes,sr(t,n,h)}return yf(t,n,a,l,h)}function Ox(t,n,a){var l=n.pendingProps,h=l.children,x=t!==null?t.memoizedState:null;if(l.mode==="hidden"){if((n.flags&128)!==0){if(l=x!==null?x.baseLanes|a:a,t!==null){for(h=n.child=t.child,x=0;h!==null;)x=x|h.lanes|h.childLanes,h=h.sibling;n.childLanes=x&~l}else n.childLanes=0,n.child=null;return zx(t,n,l,a)}if((a&536870912)!==0)n.memoizedState={baseLanes:0,cachePool:null},t!==null&&Rl(n,x!==null?x.cachePool:null),x!==null?Dg(n,x):Kd(),wx(n);else return n.lanes=n.childLanes=536870912,zx(t,n,x!==null?x.baseLanes|a:a,a)}else x!==null?(Rl(n,x.cachePool),Dg(n,x),Dr(),n.memoizedState=null):(t!==null&&Rl(n,null),Kd(),Dr());return Nt(t,n,h,a),n.child}function zx(t,n,a,l){var h=Yd();return h=h===null?null:{parent:mt._currentValue,pool:h},n.memoizedState={baseLanes:a,cachePool:h},t!==null&&Rl(n,null),Kd(),wx(n),t!==null&&ks(t,n,l,!0),null}function Zl(t,n){var a=n.ref;if(a===null)t!==null&&t.ref!==null&&(n.flags|=4194816);else{if(typeof a!="function"&&typeof a!="object")throw Error(s(284));(t===null||t.ref!==a)&&(n.flags|=4194816)}}function yf(t,n,a,l,h){return wo(n),a=Jd(t,n,a,l,void 0,h),l=ef(),t!==null&&!vt?(tf(t,n,h),sr(t,n,h)):(Ye&&l&&Ld(n),n.flags|=1,Nt(t,n,a,h),n.child)}function Lx(t,n,a,l,h,x){return wo(n),n.updateQueue=null,a=zg(n,l,a,h),Og(t),l=ef(),t!==null&&!vt?(tf(t,n,x),sr(t,n,x)):(Ye&&l&&Ld(n),n.flags|=1,Nt(t,n,a,x),n.child)}function Hx(t,n,a,l,h){if(wo(n),n.stateNode===null){var x=aa,j=a.contextType;typeof j=="object"&&j!==null&&(x=Mt(j)),x=new a(l,x),n.memoizedState=x.state!==null&&x.state!==void 0?x.state:null,x.updater=gf,n.stateNode=x,x._reactInternals=n,x=n.stateNode,x.props=l,x.state=n.memoizedState,x.refs={},Gd(n),j=a.contextType,x.context=typeof j=="object"&&j!==null?Mt(j):aa,x.state=n.memoizedState,j=a.getDerivedStateFromProps,typeof j=="function"&&(pf(n,a,j,l),x.state=n.memoizedState),typeof a.getDerivedStateFromProps=="function"||typeof x.getSnapshotBeforeUpdate=="function"||typeof x.UNSAFE_componentWillMount!="function"&&typeof x.componentWillMount!="function"||(j=x.state,typeof x.componentWillMount=="function"&&x.componentWillMount(),typeof x.UNSAFE_componentWillMount=="function"&&x.UNSAFE_componentWillMount(),j!==x.state&&gf.enqueueReplaceState(x,x.state,null),Hs(n,l,x,h),Ls(),x.state=n.memoizedState),typeof x.componentDidMount=="function"&&(n.flags|=4194308),l=!0}else if(t===null){x=n.stateNode;var T=n.memoizedProps,V=Eo(a,T);x.props=V;var J=x.context,le=a.contextType;j=aa,typeof le=="object"&&le!==null&&(j=Mt(le));var ue=a.getDerivedStateFromProps;le=typeof ue=="function"||typeof x.getSnapshotBeforeUpdate=="function",T=n.pendingProps!==T,le||typeof x.UNSAFE_componentWillReceiveProps!="function"&&typeof x.componentWillReceiveProps!="function"||(T||J!==j)&&Nx(n,x,l,j),Ar=!1;var re=n.memoizedState;x.state=re,Hs(n,l,x,h),Ls(),J=n.memoizedState,T||re!==J||Ar?(typeof ue=="function"&&(pf(n,a,ue,l),J=n.memoizedState),(V=Ar||Sx(n,a,V,l,re,J,j))?(le||typeof x.UNSAFE_componentWillMount!="function"&&typeof x.componentWillMount!="function"||(typeof x.componentWillMount=="function"&&x.componentWillMount(),typeof x.UNSAFE_componentWillMount=="function"&&x.UNSAFE_componentWillMount()),typeof x.componentDidMount=="function"&&(n.flags|=4194308)):(typeof x.componentDidMount=="function"&&(n.flags|=4194308),n.memoizedProps=l,n.memoizedState=J),x.props=l,x.state=J,x.context=j,l=V):(typeof x.componentDidMount=="function"&&(n.flags|=4194308),l=!1)}else{x=n.stateNode,Xd(t,n),j=n.memoizedProps,le=Eo(a,j),x.props=le,ue=n.pendingProps,re=x.context,J=a.contextType,V=aa,typeof J=="object"&&J!==null&&(V=Mt(J)),T=a.getDerivedStateFromProps,(J=typeof T=="function"||typeof x.getSnapshotBeforeUpdate=="function")||typeof x.UNSAFE_componentWillReceiveProps!="function"&&typeof x.componentWillReceiveProps!="function"||(j!==ue||re!==V)&&Nx(n,x,l,V),Ar=!1,re=n.memoizedState,x.state=re,Hs(n,l,x,h),Ls();var oe=n.memoizedState;j!==ue||re!==oe||Ar||t!==null&&t.dependencies!==null&&kl(t.dependencies)?(typeof T=="function"&&(pf(n,a,T,l),oe=n.memoizedState),(le=Ar||Sx(n,a,le,l,re,oe,V)||t!==null&&t.dependencies!==null&&kl(t.dependencies))?(J||typeof x.UNSAFE_componentWillUpdate!="function"&&typeof x.componentWillUpdate!="function"||(typeof x.componentWillUpdate=="function"&&x.componentWillUpdate(l,oe,V),typeof x.UNSAFE_componentWillUpdate=="function"&&x.UNSAFE_componentWillUpdate(l,oe,V)),typeof x.componentDidUpdate=="function"&&(n.flags|=4),typeof x.getSnapshotBeforeUpdate=="function"&&(n.flags|=1024)):(typeof x.componentDidUpdate!="function"||j===t.memoizedProps&&re===t.memoizedState||(n.flags|=4),typeof x.getSnapshotBeforeUpdate!="function"||j===t.memoizedProps&&re===t.memoizedState||(n.flags|=1024),n.memoizedProps=l,n.memoizedState=oe),x.props=l,x.state=oe,x.context=V,l=le):(typeof x.componentDidUpdate!="function"||j===t.memoizedProps&&re===t.memoizedState||(n.flags|=4),typeof x.getSnapshotBeforeUpdate!="function"||j===t.memoizedProps&&re===t.memoizedState||(n.flags|=1024),l=!1)}return x=l,Zl(t,n),l=(n.flags&128)!==0,x||l?(x=n.stateNode,a=l&&typeof a.getDerivedStateFromError!="function"?null:x.render(),n.flags|=1,t!==null&&l?(n.child=pa(n,t.child,null,h),n.child=pa(n,null,a,h)):Nt(t,n,a,h),n.memoizedState=x.state,t=n.child):t=sr(t,n,h),t}function Ix(t,n,a,l){return As(),n.flags|=256,Nt(t,n,a,l),n.child}var vf={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function bf(t){return{baseLanes:t,cachePool:_g()}}function wf(t,n,a){return t=t!==null?t.childLanes&~a:0,n&&(t|=cn),t}function Bx(t,n,a){var l=n.pendingProps,h=!1,x=(n.flags&128)!==0,j;if((j=x)||(j=t!==null&&t.memoizedState===null?!1:(pt.current&2)!==0),j&&(h=!0,n.flags&=-129),j=(n.flags&32)!==0,n.flags&=-33,t===null){if(Ye){if(h?Rr(n):Dr(),Ye){var T=ot,V;if(V=T){e:{for(V=T,T=An;V.nodeType!==8;){if(!T){T=null;break e}if(V=yn(V.nextSibling),V===null){T=null;break e}}T=V}T!==null?(n.memoizedState={dehydrated:T,treeContext:go!==null?{id:er,overflow:tr}:null,retryLane:536870912,hydrationErrors:null},V=qt(18,null,null,0),V.stateNode=T,V.return=n,n.child=V,Rt=n,ot=null,V=!0):V=!1}V||vo(n)}if(T=n.memoizedState,T!==null&&(T=T.dehydrated,T!==null))return oh(T)?n.lanes=32:n.lanes=536870912,null;ar(n)}return T=l.children,l=l.fallback,h?(Dr(),h=n.mode,T=Kl({mode:"hidden",children:T},h),l=po(l,h,a,null),T.return=n,l.return=n,T.sibling=l,n.child=T,h=n.child,h.memoizedState=bf(a),h.childLanes=wf(t,j,a),n.memoizedState=vf,l):(Rr(n),Sf(n,T))}if(V=t.memoizedState,V!==null&&(T=V.dehydrated,T!==null)){if(x)n.flags&256?(Rr(n),n.flags&=-257,n=Nf(t,n,a)):n.memoizedState!==null?(Dr(),n.child=t.child,n.flags|=128,n=null):(Dr(),h=l.fallback,T=n.mode,l=Kl({mode:"visible",children:l.children},T),h=po(h,T,a,null),h.flags|=2,l.return=n,h.return=n,l.sibling=h,n.child=l,pa(n,t.child,null,a),l=n.child,l.memoizedState=bf(a),l.childLanes=wf(t,j,a),n.memoizedState=vf,n=h);else if(Rr(n),oh(T)){if(j=T.nextSibling&&T.nextSibling.dataset,j)var J=j.dgst;j=J,l=Error(s(419)),l.stack="",l.digest=j,Ms({value:l,source:null,stack:null}),n=Nf(t,n,a)}else if(vt||ks(t,n,a,!1),j=(a&t.childLanes)!==0,vt||j){if(j=Je,j!==null&&(l=a&-a,l=(l&42)!==0?1:ls(l),l=(l&(j.suspendedLanes|a))!==0?0:l,l!==0&&l!==V.retryLane))throw V.retryLane=l,oa(t,l),Kt(j,t,l),kx;T.data==="$?"||Pf(),n=Nf(t,n,a)}else T.data==="$?"?(n.flags|=192,n.child=t.child,n=null):(t=V.treeContext,ot=yn(T.nextSibling),Rt=n,Ye=!0,yo=null,An=!1,t!==null&&(an[sn++]=er,an[sn++]=tr,an[sn++]=go,er=t.id,tr=t.overflow,go=n),n=Sf(n,l.children),n.flags|=4096);return n}return h?(Dr(),h=l.fallback,T=n.mode,V=t.child,J=V.sibling,l=Jn(V,{mode:"hidden",children:l.children}),l.subtreeFlags=V.subtreeFlags&65011712,J!==null?h=Jn(J,h):(h=po(h,T,a,null),h.flags|=2),h.return=n,l.return=n,l.sibling=h,n.child=l,l=h,h=n.child,T=t.child.memoizedState,T===null?T=bf(a):(V=T.cachePool,V!==null?(J=mt._currentValue,V=V.parent!==J?{parent:J,pool:J}:V):V=_g(),T={baseLanes:T.baseLanes|a,cachePool:V}),h.memoizedState=T,h.childLanes=wf(t,j,a),n.memoizedState=vf,l):(Rr(n),a=t.child,t=a.sibling,a=Jn(a,{mode:"visible",children:l.children}),a.return=n,a.sibling=null,t!==null&&(j=n.deletions,j===null?(n.deletions=[t],n.flags|=16):j.push(t)),n.child=a,n.memoizedState=null,a)}function Sf(t,n){return n=Kl({mode:"visible",children:n},t.mode),n.return=t,t.child=n}function Kl(t,n){return t=qt(22,t,null,n),t.lanes=0,t.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null},t}function Nf(t,n,a){return pa(n,t.child,null,a),t=Sf(n,n.pendingProps.children),t.flags|=2,n.memoizedState=null,t}function Ux(t,n,a){t.lanes|=n;var l=t.alternate;l!==null&&(l.lanes|=n),Ud(t.return,n,a)}function Ef(t,n,a,l,h){var x=t.memoizedState;x===null?t.memoizedState={isBackwards:n,rendering:null,renderingStartTime:0,last:l,tail:a,tailMode:h}:(x.isBackwards=n,x.rendering=null,x.renderingStartTime=0,x.last=l,x.tail=a,x.tailMode=h)}function Px(t,n,a){var l=n.pendingProps,h=l.revealOrder,x=l.tail;if(Nt(t,n,l.children,a),l=pt.current,(l&2)!==0)l=l&1|2,n.flags|=128;else{if(t!==null&&(t.flags&128)!==0)e:for(t=n.child;t!==null;){if(t.tag===13)t.memoizedState!==null&&Ux(t,a,n);else if(t.tag===19)Ux(t,a,n);else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===n)break e;for(;t.sibling===null;){if(t.return===null||t.return===n)break e;t=t.return}t.sibling.return=t.return,t=t.sibling}l&=1}switch(Z(pt,l),h){case"forwards":for(a=n.child,h=null;a!==null;)t=a.alternate,t!==null&&Gl(t)===null&&(h=a),a=a.sibling;a=h,a===null?(h=n.child,n.child=null):(h=a.sibling,a.sibling=null),Ef(n,!1,h,a,x);break;case"backwards":for(a=null,h=n.child,n.child=null;h!==null;){if(t=h.alternate,t!==null&&Gl(t)===null){n.child=h;break}t=h.sibling,h.sibling=a,a=h,h=t}Ef(n,!0,a,null,x);break;case"together":Ef(n,!1,null,null,void 0);break;default:n.memoizedState=null}return n.child}function sr(t,n,a){if(t!==null&&(n.dependencies=t.dependencies),Ir|=n.lanes,(a&n.childLanes)===0)if(t!==null){if(ks(t,n,a,!1),(a&n.childLanes)===0)return null}else return null;if(t!==null&&n.child!==t.child)throw Error(s(153));if(n.child!==null){for(t=n.child,a=Jn(t,t.pendingProps),n.child=a,a.return=n;t.sibling!==null;)t=t.sibling,a=a.sibling=Jn(t,t.pendingProps),a.return=n;a.sibling=null}return n.child}function _f(t,n){return(t.lanes&n)!==0?!0:(t=t.dependencies,!!(t!==null&&kl(t)))}function vE(t,n,a){switch(n.tag){case 3:ae(n,n.stateNode.containerInfo),Cr(n,mt,t.memoizedState.cache),As();break;case 27:case 5:ge(n);break;case 4:ae(n,n.stateNode.containerInfo);break;case 10:Cr(n,n.type,n.memoizedProps.value);break;case 13:var l=n.memoizedState;if(l!==null)return l.dehydrated!==null?(Rr(n),n.flags|=128,null):(a&n.child.childLanes)!==0?Bx(t,n,a):(Rr(n),t=sr(t,n,a),t!==null?t.sibling:null);Rr(n);break;case 19:var h=(t.flags&128)!==0;if(l=(a&n.childLanes)!==0,l||(ks(t,n,a,!1),l=(a&n.childLanes)!==0),h){if(l)return Px(t,n,a);n.flags|=128}if(h=n.memoizedState,h!==null&&(h.rendering=null,h.tail=null,h.lastEffect=null),Z(pt,pt.current),l)break;return null;case 22:case 23:return n.lanes=0,Ox(t,n,a);case 24:Cr(n,mt,t.memoizedState.cache)}return sr(t,n,a)}function $x(t,n,a){if(t!==null)if(t.memoizedProps!==n.pendingProps)vt=!0;else{if(!_f(t,a)&&(n.flags&128)===0)return vt=!1,vE(t,n,a);vt=(t.flags&131072)!==0}else vt=!1,Ye&&(n.flags&1048576)!==0&&yg(n,Ml,n.index);switch(n.lanes=0,n.tag){case 16:e:{t=n.pendingProps;var l=n.elementType,h=l._init;if(l=h(l._payload),n.type=l,typeof l=="function")Dd(l)?(t=Eo(l,t),n.tag=1,n=Hx(null,n,l,t,a)):(n.tag=0,n=yf(null,n,l,t,a));else{if(l!=null){if(h=l.$$typeof,h===M){n.tag=11,n=Tx(null,n,l,t,a);break e}else if(h===I){n.tag=14,n=Rx(null,n,l,t,a);break e}}throw n=z(l)||l,Error(s(306,n,""))}}return n;case 0:return yf(t,n,n.type,n.pendingProps,a);case 1:return l=n.type,h=Eo(l,n.pendingProps),Hx(t,n,l,h,a);case 3:e:{if(ae(n,n.stateNode.containerInfo),t===null)throw Error(s(387));l=n.pendingProps;var x=n.memoizedState;h=x.element,Xd(t,n),Hs(n,l,null,a);var j=n.memoizedState;if(l=j.cache,Cr(n,mt,l),l!==x.cache&&Pd(n,[mt],a,!0),Ls(),l=j.element,x.isDehydrated)if(x={element:l,isDehydrated:!1,cache:j.cache},n.updateQueue.baseState=x,n.memoizedState=x,n.flags&256){n=Ix(t,n,l,a);break e}else if(l!==h){h=rn(Error(s(424)),n),Ms(h),n=Ix(t,n,l,a);break e}else{switch(t=n.stateNode.containerInfo,t.nodeType){case 9:t=t.body;break;default:t=t.nodeName==="HTML"?t.ownerDocument.body:t}for(ot=yn(t.firstChild),Rt=n,Ye=!0,yo=null,An=!0,a=bx(n,null,l,a),n.child=a;a;)a.flags=a.flags&-3|4096,a=a.sibling}else{if(As(),l===h){n=sr(t,n,a);break e}Nt(t,n,l,a)}n=n.child}return n;case 26:return Zl(t,n),t===null?(a=G0(n.type,null,n.pendingProps,null))?n.memoizedState=a:Ye||(a=n.type,t=n.pendingProps,l=uc(fe.current).createElement(a),l[yt]=n,l[At]=t,_t(l,a,t),ct(l),n.stateNode=l):n.memoizedState=G0(n.type,t.memoizedProps,n.pendingProps,t.memoizedState),null;case 27:return ge(n),t===null&&Ye&&(l=n.stateNode=V0(n.type,n.pendingProps,fe.current),Rt=n,An=!0,h=ot,$r(n.type)?(ah=h,ot=yn(l.firstChild)):ot=h),Nt(t,n,n.pendingProps.children,a),Zl(t,n),t===null&&(n.flags|=4194304),n.child;case 5:return t===null&&Ye&&((h=l=ot)&&(l=XE(l,n.type,n.pendingProps,An),l!==null?(n.stateNode=l,Rt=n,ot=yn(l.firstChild),An=!1,h=!0):h=!1),h||vo(n)),ge(n),h=n.type,x=n.pendingProps,j=t!==null?t.memoizedProps:null,l=x.children,th(h,x)?l=null:j!==null&&th(h,j)&&(n.flags|=32),n.memoizedState!==null&&(h=Jd(t,n,dE,null,null,a),ai._currentValue=h),Zl(t,n),Nt(t,n,l,a),n.child;case 6:return t===null&&Ye&&((t=a=ot)&&(a=FE(a,n.pendingProps,An),a!==null?(n.stateNode=a,Rt=n,ot=null,t=!0):t=!1),t||vo(n)),null;case 13:return Bx(t,n,a);case 4:return ae(n,n.stateNode.containerInfo),l=n.pendingProps,t===null?n.child=pa(n,null,l,a):Nt(t,n,l,a),n.child;case 11:return Tx(t,n,n.type,n.pendingProps,a);case 7:return Nt(t,n,n.pendingProps,a),n.child;case 8:return Nt(t,n,n.pendingProps.children,a),n.child;case 12:return Nt(t,n,n.pendingProps.children,a),n.child;case 10:return l=n.pendingProps,Cr(n,n.type,l.value),Nt(t,n,l.children,a),n.child;case 9:return h=n.type._context,l=n.pendingProps.children,wo(n),h=Mt(h),l=l(h),n.flags|=1,Nt(t,n,l,a),n.child;case 14:return Rx(t,n,n.type,n.pendingProps,a);case 15:return Dx(t,n,n.type,n.pendingProps,a);case 19:return Px(t,n,a);case 31:return l=n.pendingProps,a=n.mode,l={mode:l.mode,children:l.children},t===null?(a=Kl(l,a),a.ref=n.ref,n.child=a,a.return=n,n=a):(a=Jn(t.child,l),a.ref=n.ref,n.child=a,a.return=n,n=a),n;case 22:return Ox(t,n,a);case 24:return wo(n),l=Mt(mt),t===null?(h=Yd(),h===null&&(h=Je,x=$d(),h.pooledCache=x,x.refCount++,x!==null&&(h.pooledCacheLanes|=a),h=x),n.memoizedState={parent:l,cache:h},Gd(n),Cr(n,mt,h)):((t.lanes&a)!==0&&(Xd(t,n),Hs(n,null,null,a),Ls()),h=t.memoizedState,x=n.memoizedState,h.parent!==l?(h={parent:l,cache:l},n.memoizedState=h,n.lanes===0&&(n.memoizedState=n.updateQueue.baseState=h),Cr(n,mt,l)):(l=x.cache,Cr(n,mt,l),l!==h.cache&&Pd(n,[mt],a,!0))),Nt(t,n,n.pendingProps.children,a),n.child;case 29:throw n.pendingProps}throw Error(s(156,n.tag))}function ir(t){t.flags|=4}function Vx(t,n){if(n.type!=="stylesheet"||(n.state.loading&4)!==0)t.flags&=-16777217;else if(t.flags|=16777216,!W0(n)){if(n=ln.current,n!==null&&((Ue&4194048)===Ue?Mn!==null:(Ue&62914560)!==Ue&&(Ue&536870912)===0||n!==Mn))throw Os=qd,jg;t.flags|=8192}}function Wl(t,n){n!==null&&(t.flags|=4),t.flags&16384&&(n=t.tag!==22?sl():536870912,t.lanes|=n,va|=n)}function Ys(t,n){if(!Ye)switch(t.tailMode){case"hidden":n=t.tail;for(var a=null;n!==null;)n.alternate!==null&&(a=n),n=n.sibling;a===null?t.tail=null:a.sibling=null;break;case"collapsed":a=t.tail;for(var l=null;a!==null;)a.alternate!==null&&(l=a),a=a.sibling;l===null?n||t.tail===null?t.tail=null:t.tail.sibling=null:l.sibling=null}}function nt(t){var n=t.alternate!==null&&t.alternate.child===t.child,a=0,l=0;if(n)for(var h=t.child;h!==null;)a|=h.lanes|h.childLanes,l|=h.subtreeFlags&65011712,l|=h.flags&65011712,h.return=t,h=h.sibling;else for(h=t.child;h!==null;)a|=h.lanes|h.childLanes,l|=h.subtreeFlags,l|=h.flags,h.return=t,h=h.sibling;return t.subtreeFlags|=l,t.childLanes=a,n}function bE(t,n,a){var l=n.pendingProps;switch(Hd(n),n.tag){case 31:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return nt(n),null;case 1:return nt(n),null;case 3:return a=n.stateNode,l=null,t!==null&&(l=t.memoizedState.cache),n.memoizedState.cache!==l&&(n.flags|=2048),rr(mt),me(),a.pendingContext&&(a.context=a.pendingContext,a.pendingContext=null),(t===null||t.child===null)&&(Cs(n)?ir(n):t===null||t.memoizedState.isDehydrated&&(n.flags&256)===0||(n.flags|=1024,wg())),nt(n),null;case 26:return a=n.memoizedState,t===null?(ir(n),a!==null?(nt(n),Vx(n,a)):(nt(n),n.flags&=-16777217)):a?a!==t.memoizedState?(ir(n),nt(n),Vx(n,a)):(nt(n),n.flags&=-16777217):(t.memoizedProps!==l&&ir(n),nt(n),n.flags&=-16777217),null;case 27:se(n),a=fe.current;var h=n.type;if(t!==null&&n.stateNode!=null)t.memoizedProps!==l&&ir(n);else{if(!l){if(n.stateNode===null)throw Error(s(166));return nt(n),null}t=te.current,Cs(n)?vg(n):(t=V0(h,l,a),n.stateNode=t,ir(n))}return nt(n),null;case 5:if(se(n),a=n.type,t!==null&&n.stateNode!=null)t.memoizedProps!==l&&ir(n);else{if(!l){if(n.stateNode===null)throw Error(s(166));return nt(n),null}if(t=te.current,Cs(n))vg(n);else{switch(h=uc(fe.current),t){case 1:t=h.createElementNS("http://www.w3.org/2000/svg",a);break;case 2:t=h.createElementNS("http://www.w3.org/1998/Math/MathML",a);break;default:switch(a){case"svg":t=h.createElementNS("http://www.w3.org/2000/svg",a);break;case"math":t=h.createElementNS("http://www.w3.org/1998/Math/MathML",a);break;case"script":t=h.createElement("div"),t.innerHTML=" - + +
diff --git a/python/packages/devui/dev.md b/python/packages/devui/dev.md index 3d74f19970..a41b2d8aac 100644 --- a/python/packages/devui/dev.md +++ b/python/packages/devui/dev.md @@ -9,8 +9,6 @@ git clone https://github.com/microsoft/agent-framework.git cd agent-framework ``` -(or use the latest main branch if merged) - ## 2. Setup Environment Navigate to the Python directory and install dependencies: @@ -47,7 +45,7 @@ AZURE_OPENAI_CHAT_DEPLOYMENT_NAME="your-deployment-name" **Option A: In-Memory Mode (Recommended for quick testing)** ```bash -cd packages/devui/samples +cd samples/getting_started/devui python in_memory_mode.py ``` @@ -56,7 +54,7 @@ This runs a simple example with predefined agents and opens your browser automat **Option B: Directory-Based Discovery** ```bash -cd packages/devui/samples +cd samples/getting_started/devui devui ``` @@ -72,57 +70,91 @@ This launches the UI with all example agents/workflows at http://localhost:8080 You can also test via API calls: +### Single Request + ```bash curl -X POST http://localhost:8080/v1/responses \ -H "Content-Type: application/json" \ -d '{ - "model": "agent-framework", + "model": "weather_agent", + "input": "What is the weather in Seattle?" + }' +``` + +### Multi-turn Conversations + +```bash +# Create a conversation +curl -X POST http://localhost:8080/v1/conversations \ + -H "Content-Type: application/json" \ + -d '{"metadata": {"agent_id": "weather_agent"}}' + +# Returns: {"id": "conv_abc123", ...} + +# Use conversation ID in requests +curl -X POST http://localhost:8080/v1/responses \ + -H "Content-Type: application/json" \ + -d '{ + "model": "weather_agent", "input": "What is the weather in Seattle?", - "extra_body": {"entity_id": "weather_agent"} + "conversation": "conv_abc123" + }' + +# Continue the conversation +curl -X POST http://localhost:8080/v1/responses \ + -H "Content-Type: application/json" \ + -d '{ + "model": "weather_agent", + "input": "How about tomorrow?", + "conversation": "conv_abc123" }' ``` ## API Mapping -Messages and events from agents/workflows are mapped to OpenAI response types in `agent_framework_devui/_mapper.py`. See the mapping table below: +Agent Framework content types → OpenAI Responses API events (in `_mapper.py`): -| Agent Framework Content | OpenAI Event | Type | -| --------------------------------- | ----------------------------------------- | -------- | -| `TextContent` | `ResponseTextDeltaEvent` | Official | -| `TextReasoningContent` | `ResponseReasoningTextDeltaEvent` | Official | -| `FunctionCallContent` | `ResponseFunctionCallArgumentsDeltaEvent` | Official | -| `FunctionResultContent` | `ResponseFunctionResultComplete` | Custom | -| `ErrorContent` | `ResponseErrorEvent` | Official | -| `UsageContent` | `ResponseUsageEventComplete` | Custom | -| `DataContent` | `ResponseTraceEventComplete` | Custom | -| `UriContent` | `ResponseTraceEventComplete` | Custom | -| `HostedFileContent` | `ResponseTraceEventComplete` | Custom | -| `HostedVectorStoreContent` | `ResponseTraceEventComplete` | Custom | -| `FunctionApprovalRequestContent` | Custom event | Custom | -| `FunctionApprovalResponseContent` | Custom event | Custom | -| `WorkflowEvent` | `ResponseWorkflowEventComplete` | Custom | +| Agent Framework Content | OpenAI Event | Status | +| ------------------------------- | ---------------------------------------- | -------- | +| `TextContent` | `response.output_text.delta` | Standard | +| `TextReasoningContent` | `response.reasoning.delta` | Standard | +| `FunctionCallContent` (initial) | `response.output_item.added` | Standard | +| `FunctionCallContent` (args) | `response.function_call_arguments.delta` | Standard | +| `FunctionResultContent` | `response.function_result.complete` | Standard | +| `ErrorContent` | `response.error` | Standard | +| `UsageContent` | `response.usage.complete` | Extended | +| `WorkflowEvent` | `response.workflow.event` | DevUI | +| `DataContent`, `UriContent` | `response.trace.complete` | DevUI | + +- **Standard** = OpenAI spec, **Extended** = OpenAI + extra fields, **DevUI** = DevUI-specific ## Frontend Development -To build the frontend: - ```bash -cd frontend +cd python/packages/devui/frontend yarn install -# 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 +# Development (hot reload) yarn dev -# Build (copies to backend) +# Build (copies to backend ui/) yarn build ``` +## Running Tests + +```bash +cd python/packages/devui + +# All tests +pytest tests/ -v + +# Specific suites +pytest tests/test_conversations.py -v # Conversation store +pytest tests/test_server.py -v # API endpoints +pytest tests/test_mapper.py -v # Event mapping +``` + ## Troubleshooting - **Missing API key**: Make sure your `.env` file is in the `python/` directory with valid credentials. Or set environment variables directly in your shell before running DevUI. diff --git a/python/packages/devui/frontend/src/App.tsx b/python/packages/devui/frontend/src/App.tsx index e606b7335b..fdb6c0d52e 100644 --- a/python/packages/devui/frontend/src/App.tsx +++ b/python/packages/devui/frontend/src/App.tsx @@ -4,12 +4,10 @@ */ import { useState, useEffect, useCallback } from "react"; -import { AppHeader } from "@/components/shared/app-header"; -import { DebugPanel } from "@/components/shared/debug-panel"; -import { SettingsModal } from "@/components/shared/settings-modal"; -import { GalleryView } from "@/components/gallery"; -import { AgentView } from "@/components/agent/agent-view"; -import { WorkflowView } from "@/components/workflow/workflow-view"; +import { AppHeader, DebugPanel, SettingsModal } from "@/components/layout"; +import { GalleryView } from "@/components/features/gallery"; +import { AgentView } from "@/components/features/agent"; +import { WorkflowView } from "@/components/features/workflow"; import { LoadingState } from "@/components/ui/loading-state"; import { Toast } from "@/components/ui/toast"; import { apiClient } from "@/services/api"; diff --git a/python/packages/devui/frontend/src/components/shared/agent-details-modal.tsx b/python/packages/devui/frontend/src/components/features/agent/agent-details-modal.tsx similarity index 100% rename from python/packages/devui/frontend/src/components/shared/agent-details-modal.tsx rename to python/packages/devui/frontend/src/components/features/agent/agent-details-modal.tsx diff --git a/python/packages/devui/frontend/src/components/agent/agent-view.tsx b/python/packages/devui/frontend/src/components/features/agent/agent-view.tsx similarity index 52% rename from python/packages/devui/frontend/src/components/agent/agent-view.tsx rename to python/packages/devui/frontend/src/components/features/agent/agent-view.tsx index 7b0bcfb2bb..ff9a908895 100644 --- a/python/packages/devui/frontend/src/components/agent/agent-view.tsx +++ b/python/packages/devui/frontend/src/components/features/agent/agent-view.tsx @@ -1,6 +1,6 @@ /** * AgentView - Complete agent interaction interface - * Features: Chat interface, message streaming, thread management + * Features: Chat interface, message streaming, conversation management */ import { useState, useCallback, useRef, useEffect } from "react"; @@ -12,7 +12,7 @@ import { AttachmentGallery, type AttachmentItem, } from "@/components/ui/attachment-gallery"; -import { MessageRenderer } from "@/components/message_renderer"; +import { OpenAIMessageRenderer } from "./message-renderers/OpenAIMessageRenderer"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { Select, @@ -21,7 +21,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { AgentDetailsModal } from "@/components/shared/agent-details-modal"; +import { AgentDetailsModal } from "./agent-details-modal"; import { SendHorizontal, User, @@ -32,18 +32,20 @@ import { Info, Trash2, FileText, + Check, + X, } from "lucide-react"; import { apiClient } from "@/services/api"; import type { AgentInfo, - ChatMessage, RunAgentRequest, - ThreadInfo, + Conversation, ExtendedResponseStreamEvent, + PendingApproval, } from "@/types"; interface ChatState { - messages: ChatMessage[]; + items: import("@/types/openai").ConversationItem[]; // Pure OpenAI types - no legacy ChatMessage isStreaming: boolean; } @@ -54,103 +56,89 @@ interface AgentViewProps { onDebugEvent: DebugEventHandler; } -interface MessageBubbleProps { - message: ChatMessage; +interface ConversationItemBubbleProps { + item: import("@/types/openai").ConversationItem; } -function MessageBubble({ message }: MessageBubbleProps) { - const isUser = message.role === "user"; - const isError = message.error; - const Icon = isUser ? User : isError ? AlertCircle : Bot; +function ConversationItemBubble({ item }: ConversationItemBubbleProps) { + // Handle different item types + if (item.type === "message") { + const isUser = item.role === "user"; + const isError = item.status === "incomplete"; + const Icon = isUser ? User : isError ? AlertCircle : Bot; - return ( -
-
- -
- -
+ return ( +
- {isError && ( -
- - - Unable to process request - + +
+ +
+
+ {isError && ( +
+ + + Unable to process request + +
+ )} +
+
- )} -
- +
+ +
+ {new Date().toLocaleTimeString()} + {!isUser && item.usage && ( + <> + + + + ↓{item.usage.input_tokens} + + + ↑{item.usage.output_tokens} + + ({item.usage.total_tokens} tokens) + + + )}
- -
- {new Date(message.timestamp).toLocaleTimeString()} - {!isUser && message.usage && ( - <> - - - {message.usage.total_tokens >= 1000 - ? `${(message.usage.total_tokens / 1000).toFixed(2)}k` - : message.usage.total_tokens}{" "} - tokens - {message.usage.prompt_tokens > 0 && ( - - {" "} - ( - {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) - - )} - - - )} -
-
- ); -} + ); + } -function TypingIndicator() { + // Function calls and results - render with neutral styling return (
-
-
-
-
-
+
+
+
@@ -159,27 +147,32 @@ function TypingIndicator() { export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { const [chatState, setChatState] = useState({ - messages: [], + items: [], isStreaming: false, }); - const [currentThread, setCurrentThread] = useState( - undefined - ); - const [availableThreads, setAvailableThreads] = useState([]); + const [currentConversation, setCurrentConversation] = useState< + Conversation | undefined + >(undefined); + const [availableConversations, setAvailableConversations] = useState< + Conversation[] + >([]); const [inputValue, setInputValue] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [attachments, setAttachments] = useState([]); - const [loadingThreads, setLoadingThreads] = useState(false); + const [loadingConversations, setLoadingConversations] = useState(false); const [isDragOver, setIsDragOver] = useState(false); const [dragCounter, setDragCounter] = useState(0); const [pasteNotification, setPasteNotification] = useState( null ); const [detailsModalOpen, setDetailsModalOpen] = useState(false); - const [threadUsage, setThreadUsage] = useState<{ + const [conversationUsage, setConversationUsage] = useState<{ total_tokens: number; message_count: number; }>({ total_tokens: 0, message_count: 0 }); + const [pendingApprovals, setPendingApprovals] = useState( + [] + ); const scrollAreaRef = useRef(null); const messagesEndRef = useRef(null); @@ -187,64 +180,118 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { const textareaRef = useRef(null); const currentMessageUsage = useRef<{ total_tokens: number; - prompt_tokens: number; - completion_tokens: number; + input_tokens: number; + output_tokens: number; } | null>(null); - // Auto-scroll to bottom when new messages arrive + // Auto-scroll to bottom when new items arrive useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [chatState.messages, chatState.isStreaming]); + }, [chatState.items, chatState.isStreaming]); - // Load threads when agent changes + // Return focus to input after streaming completes useEffect(() => { - const loadThreads = async () => { + if (!chatState.isStreaming && !isSubmitting) { + textareaRef.current?.focus(); + } + }, [chatState.isStreaming, isSubmitting]); + + // Load conversations when agent changes + useEffect(() => { + const loadConversations = async () => { if (!selectedAgent) return; - setLoadingThreads(true); + setLoadingConversations(true); try { - const threads = await apiClient.getThreads(selectedAgent.id); - setAvailableThreads(threads); + // Step 1: Try to list conversations from backend (DevUI extension) + // This works with DevUI backend but fails with OpenAI/Azure (they don't have list endpoint) + try { + const { data: conversations } = await apiClient.listConversations( + selectedAgent.id + ); - // Auto-select the most recent thread if available - if (threads.length > 0) { - const mostRecentThread = threads[0]; // Assuming threads are sorted by creation date (newest first) - setCurrentThread(mostRecentThread); + if (conversations.length > 0) { + // Found conversations on backend - use most recent + const mostRecent = conversations[0]; + setAvailableConversations(conversations); + setCurrentConversation(mostRecent); - // Load messages for the selected thread - try { - const threadMessages = await apiClient.getThreadMessages( - mostRecentThread.id + // Load conversation items from backend + try { + const { data: items } = await apiClient.listConversationItems( + mostRecent.id + ); + + // Use OpenAI ConversationItems directly (no conversion!) + setChatState({ + items: items as import("@/types/openai").ConversationItem[], + isStreaming: false + }); + } catch { + setChatState({ items: [], isStreaming: false }); + } + + // Cache to localStorage for faster future loads + localStorage.setItem( + `devui_convs_${selectedAgent.id}`, + JSON.stringify(conversations) ); - setChatState({ - messages: threadMessages, - isStreaming: false, - }); - } catch (error) { - console.error("Failed to load thread messages:", error); - setChatState({ - messages: [], - isStreaming: false, - }); + return; + } + } catch { + // Backend doesn't support list endpoint (OpenAI, Azure, etc.) + // This is expected - fall through to localStorage + } + + // Step 2: Try localStorage (works with all backends) + const cachedKey = `devui_convs_${selectedAgent.id}`; + const cached = localStorage.getItem(cachedKey); + + if (cached) { + try { + const convs = JSON.parse(cached) as Conversation[]; + + if (convs.length > 0) { + // Use most recent cached conversation + setAvailableConversations(convs); + setCurrentConversation(convs[0]); + setChatState({ items: [], isStreaming: false }); + return; + } + } catch { + // Invalid cache - clear it + localStorage.removeItem(cachedKey); } } - } catch (error) { - console.error("Failed to load threads:", error); - setAvailableThreads([]); + + // Step 3: No conversations found - create new + const newConversation = await apiClient.createConversation({ + agent_id: selectedAgent.id, + }); + + setCurrentConversation(newConversation); + setAvailableConversations([newConversation]); + setChatState({ items: [], isStreaming: false }); + + // Save to localStorage + localStorage.setItem(cachedKey, JSON.stringify([newConversation])); + } catch { + setAvailableConversations([]); + setChatState({ items: [], isStreaming: false }); } finally { - setLoadingThreads(false); + setLoadingConversations(false); } }; // Clear chat when agent changes setChatState({ - messages: [], + items: [], isStreaming: false, }); - setCurrentThread(undefined); + setCurrentConversation(undefined); accumulatedText.current = ""; - loadThreads(); + loadConversations(); }, [selectedAgent]); // Handle file uploads @@ -369,12 +416,14 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { const start = textarea.selectionStart; const end = textarea.selectionEnd; const currentValue = textarea.value; - const newValue = currentValue.slice(0, start) + text + currentValue.slice(end); + const newValue = + currentValue.slice(0, start) + text + currentValue.slice(end); setInputValue(newValue); // Restore cursor position after the inserted text setTimeout(() => { - textarea.selectionStart = textarea.selectionEnd = start + text.length; + textarea.selectionStart = textarea.selectionEnd = + start + text.length; textarea.focus(); }, 0); } @@ -405,7 +454,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { // Detect file extension from content const detectFileExtension = (text: string): string => { const trimmed = text.trim(); - const lines = trimmed.split('\n'); + const lines = trimmed.split("\n"); // JSON detection if (/^{[\s\S]*}$|^\[[\s\S]*\]$/.test(trimmed)) return ".json"; @@ -421,18 +470,26 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { // CSV detection (more strict) - need multiple lines with consistent comma patterns if (lines.length > 2) { - const commaLines = lines.filter(line => line.includes(',')); - const semicolonLines = lines.filter(line => line.includes(';')); + const commaLines = lines.filter((line) => line.includes(",")); + const semicolonLines = lines.filter((line) => line.includes(";")); // If >50% of lines have commas and it looks tabular if (commaLines.length > lines.length * 0.5) { - const avgCommas = commaLines.reduce((sum, line) => sum + (line.match(/,/g) || []).length, 0) / commaLines.length; + const avgCommas = + commaLines.reduce( + (sum, line) => sum + (line.match(/,/g) || []).length, + 0 + ) / commaLines.length; if (avgCommas >= 2) return ".csv"; } // If >50% of lines have semicolons and it looks tabular if (semicolonLines.length > lines.length * 0.5) { - const avgSemicolons = semicolonLines.reduce((sum, line) => sum + (line.match(/;/g) || []).length, 0) / semicolonLines.length; + const avgSemicolons = + semicolonLines.reduce( + (sum, line) => sum + (line.match(/;/g) || []).length, + 0 + ) / semicolonLines.length; if (avgSemicolons >= 2) return ".csv"; } } @@ -457,28 +514,35 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { }); }; - // Handle new thread creation - const handleNewThread = useCallback(async () => { + // Handle new conversation creation + const handleNewConversation = useCallback(async () => { if (!selectedAgent) return; try { - const newThread = await apiClient.createThread(selectedAgent.id); - setCurrentThread(newThread); - setAvailableThreads((prev) => [newThread, ...prev]); + const newConversation = await apiClient.createConversation({ + agent_id: selectedAgent.id, + }); + setCurrentConversation(newConversation); + setAvailableConversations((prev) => [newConversation, ...prev]); setChatState({ - messages: [], + items: [], isStreaming: false, }); - setThreadUsage({ total_tokens: 0, message_count: 0 }); + setConversationUsage({ 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) => { + // Update localStorage cache with new conversation + const cachedKey = `devui_convs_${selectedAgent.id}`; + const updated = [newConversation, ...availableConversations]; + localStorage.setItem(cachedKey, JSON.stringify(updated)); + } catch { + // Failed to create conversation + } + }, [selectedAgent, availableConversations]); + + // Handle conversation deletion + const handleDeleteConversation = useCallback( + async (conversationId: string, e?: React.MouseEvent) => { // Prevent event from bubbling to SelectItem if (e) { e.preventDefault(); @@ -486,46 +550,46 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { } // Confirm deletion - if (!confirm("Delete this thread? This cannot be undone.")) { + if (!confirm("Delete this conversation? This cannot be undone.")) { return; } try { - const success = await apiClient.deleteThread(threadId); + const success = await apiClient.deleteConversation(conversationId); if (success) { - // Remove thread from available threads - const updatedThreads = availableThreads.filter((t) => t.id !== threadId); - setAvailableThreads(updatedThreads); + // Remove conversation from available conversations + const updatedConversations = availableConversations.filter( + (c) => c.id !== conversationId + ); + setAvailableConversations(updatedConversations); - // 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); + // Update localStorage cache + if (selectedAgent) { + const cachedKey = `devui_convs_${selectedAgent.id}`; + localStorage.setItem( + cachedKey, + JSON.stringify(updatedConversations) + ); + } - // 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); + // If deleted conversation was selected, switch to another conversation or clear chat + if (currentConversation?.id === conversationId) { + if (updatedConversations.length > 0) { + // Select the most recent remaining conversation + const nextConversation = updatedConversations[0]; + setCurrentConversation(nextConversation); setChatState({ - messages: [], + items: [], isStreaming: false, }); - setThreadUsage({ total_tokens: 0, message_count: 0 }); + } else { + // No conversations left, clear everything + setCurrentConversation(undefined); + setChatState({ + items: [], + isStreaming: false, + }); + setConversationUsage({ total_tokens: 0, message_count: 0 }); accumulatedText.current = ""; } } @@ -533,163 +597,176 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { // Clear debug panel onDebugEvent("clear"); } - } catch (error) { - console.error("Failed to delete thread:", error); - alert("Failed to delete thread. Please try again."); + } catch { + alert("Failed to delete conversation. Please try again."); } }, - [availableThreads, currentThread, onDebugEvent] + [availableConversations, currentConversation, selectedAgent, onDebugEvent] ); - // Handle thread selection - const handleThreadSelect = useCallback( - async (threadId: string) => { - const thread = availableThreads.find((t) => t.id === threadId); - if (!thread) return; + // Handle conversation selection + const handleConversationSelect = useCallback( + async (conversationId: string) => { + const conversation = availableConversations.find( + (c) => c.id === conversationId + ); + if (!conversation) return; - setCurrentThread(thread); + setCurrentConversation(conversation); - // Clear debug panel when switching threads + // Clear debug panel when switching conversations onDebugEvent("clear"); try { - // Load thread messages from backend - const threadMessages = await apiClient.getThreadMessages(threadId); + // Load conversation history from backend + const result = await apiClient.listConversationItems(conversationId); + + // Use OpenAI ConversationItems directly (no conversion!) + const items = result.data as import("@/types/openai").ConversationItem[]; setChatState({ - messages: threadMessages, + items, 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}` - ); - } catch (error) { - console.error("Failed to load thread messages:", error); - // Fallback to clearing messages + // Calculate usage from loaded items + setConversationUsage({ + total_tokens: 0, // We don't have usage info in stored items + message_count: items.length, + }); + } catch { + // Fallback to clearing items setChatState({ - messages: [], + items: [], isStreaming: false, }); + setConversationUsage({ total_tokens: 0, message_count: 0 }); } accumulatedText.current = ""; }, - [availableThreads] + [availableConversations, onDebugEvent] ); + // Handle function approval responses + const handleApproval = async (request_id: string, approved: boolean) => { + const approval = pendingApprovals.find((a) => a.request_id === request_id); + if (!approval) return; + + // Create approval response in OpenAI-compatible format + const approvalInput: import("@/types/agent-framework").ResponseInputParam = [ + { + type: "message", // CRITICAL: Must set type for backend to recognize it + role: "user", + content: [ + { + type: "function_approval_response", + request_id: request_id, + approved: approved, + function_call: approval.function_call, + } as import("@/types/openai").MessageFunctionApprovalResponseContent, + ], + }, + ]; + + // Send approval response through the conversation + // We'll call handleSendMessage directly when invoked (it's defined below) + const request: RunAgentRequest = { + input: approvalInput, + conversation_id: currentConversation?.id, + }; + + // Remove from pending immediately (will be confirmed by backend event) + setPendingApprovals((prev) => + prev.filter((a) => a.request_id !== request_id) + ); + + // Trigger send (we'll call this from the UI button handler) + return request; + }; + // Handle message sending const handleSendMessage = useCallback( async (request: RunAgentRequest) => { if (!selectedAgent) return; - // Extract text and attachments from OpenAI format for UI display - let displayText = ""; - const attachmentContents: import("@/types/agent-framework").Contents[] = - []; + // Extract content from OpenAI format to create ConversationMessage + const messageContent: import("@/types/openai").MessageContent[] = []; - // Parse OpenAI ResponseInputParam to extract display content + // Parse OpenAI ResponseInputParam to extract content for (const inputItem of request.input) { if (inputItem.type === "message" && Array.isArray(inputItem.content)) { for (const contentItem of inputItem.content) { if (contentItem.type === "input_text") { - displayText += contentItem.text + " "; + messageContent.push({ + type: "text", + text: contentItem.text, + }); } else if (contentItem.type === "input_image") { - attachmentContents.push({ - type: "data", - uri: contentItem.image_url || "", - media_type: "image/png", // Default, should extract from data URI - } as import("@/types/agent-framework").DataContent); + messageContent.push({ + type: "input_image", + image_url: contentItem.image_url || "", + detail: "auto", + }); } else if (contentItem.type === "input_file") { - const dataUri = `data:application/octet-stream;base64,${contentItem.file_data}`; - // Determine media type from filename - const filename = (contentItem as import("@/types/agent-framework").ResponseInputFileParam).filename || ""; - let mediaType = "application/octet-stream"; - - if (filename.endsWith(".pdf")) mediaType = "application/pdf"; - else if (filename.endsWith(".txt")) mediaType = "text/plain"; - else if (filename.endsWith(".json")) mediaType = "application/json"; - else if (filename.endsWith(".csv")) mediaType = "text/csv"; - else if (filename.endsWith(".html")) mediaType = "text/html"; - else if (filename.endsWith(".md")) mediaType = "text/markdown"; - - attachmentContents.push({ - type: "data", - uri: dataUri, - media_type: mediaType, - } as import("@/types/agent-framework").DataContent); + const fileItem = contentItem as import("@/types/agent-framework").ResponseInputFileParam; + messageContent.push({ + type: "input_file", + file_data: fileItem.file_data, + filename: fileItem.filename, + }); } } } } - const userMessageContents: import("@/types/agent-framework").Contents[] = - [ - ...(displayText.trim() - ? [ - { - type: "text", - text: displayText.trim(), - } as import("@/types/agent-framework").TextContent, - ] - : []), - ...attachmentContents, - ]; - - // Add user message to UI state - const userMessage: ChatMessage = { + // Add user message to UI state (OpenAI ConversationMessage) + const userMessage: import("@/types/openai").ConversationMessage = { id: `user-${Date.now()}`, + type: "message", role: "user", - contents: userMessageContents, - timestamp: new Date().toISOString(), + content: messageContent, + status: "completed", }; setChatState((prev) => ({ ...prev, - messages: [...prev.messages, userMessage], + items: [...prev.items, userMessage], isStreaming: true, })); // Create assistant message placeholder - const assistantMessage: ChatMessage = { + const assistantMessage: import("@/types/openai").ConversationMessage = { id: `assistant-${Date.now()}`, + type: "message", role: "assistant", - contents: [], - timestamp: new Date().toISOString(), - streaming: true, + content: [], // Will be filled during streaming + status: "in_progress", }; setChatState((prev) => ({ ...prev, - messages: [...prev.messages, assistantMessage], + items: [...prev.items, assistantMessage], })); try { - // If no thread selected, create one automatically - let threadToUse = currentThread; - if (!threadToUse) { + // If no conversation selected, create one automatically + let conversationToUse = currentConversation; + if (!conversationToUse) { try { - threadToUse = await apiClient.createThread(selectedAgent.id); - setCurrentThread(threadToUse); - setAvailableThreads((prev) => [threadToUse!, ...prev]); - } catch (error) { - console.error("Failed to create thread:", error); + conversationToUse = await apiClient.createConversation({ + agent_id: selectedAgent.id, + }); + setCurrentConversation(conversationToUse); + setAvailableConversations((prev) => [conversationToUse!, ...prev]); + } catch { + // Failed to create conversation } } const apiRequest = { input: request.input, - thread_id: threadToUse?.id, + conversation_id: conversationToUse?.id, }; // Clear text accumulator for new response @@ -708,18 +785,45 @@ 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) { + // Handle response.completed event (OpenAI standard) + if (openAIEvent.type === "response.completed") { + const completedEvent = openAIEvent as import("@/types/openai").ResponseCompletedEvent; + const usage = completedEvent.response?.usage; + + if (usage) { currentMessageUsage.current = { - total_tokens: usageEvent.data.total_tokens || 0, - prompt_tokens: usageEvent.data.prompt_tokens || 0, - completion_tokens: usageEvent.data.completion_tokens || 0, + input_tokens: usage.input_tokens, + output_tokens: usage.output_tokens, + total_tokens: usage.total_tokens, }; - console.log("📊 Set usage:", currentMessageUsage.current); } + continue; // Continue processing other events + } + + // Handle function approval request events + if (openAIEvent.type === "response.function_approval.requested") { + const approvalEvent = openAIEvent as import("@/types/openai").ResponseFunctionApprovalRequestedEvent; + + // Add to pending approvals + setPendingApprovals((prev) => [ + ...prev, + { + request_id: approvalEvent.request_id, + function_call: approvalEvent.function_call, + }, + ]); + continue; // Don't add approval requests to chat UI + } + + // Handle function approval response events + if (openAIEvent.type === "response.function_approval.responded") { + const responseEvent = openAIEvent as import("@/types/openai").ResponseFunctionApprovalRespondedEvent; + + // Remove from pending approvals + setPendingApprovals((prev) => + prev.filter((a) => a.request_id !== responseEvent.request_id) + ); + continue; } // Handle error events from the stream @@ -733,20 +837,19 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { setChatState((prev) => ({ ...prev, isStreaming: false, - messages: prev.messages.map((msg) => - msg.id === assistantMessage.id + items: prev.items.map((item) => + item.id === assistantMessage.id && item.type === "message" ? { - ...msg, - contents: [ + ...item, + content: [ { type: "text", text: errorMessage, - }, + } as import("@/types/openai").MessageTextContent, ], - streaming: false, - error: true, // Add error flag for styling + status: "incomplete" as const, } - : msg + : item ), })); return; // Exit stream processing early on error @@ -763,18 +866,19 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { // Update assistant message with accumulated content setChatState((prev) => ({ ...prev, - messages: prev.messages.map((msg) => - msg.id === assistantMessage.id + items: prev.items.map((item) => + item.id === assistantMessage.id && item.type === "message" ? { - ...msg, - contents: [ + ...item, + content: [ { type: "text", text: accumulatedText.current, - }, + } as import("@/types/openai").MessageTextContent, ], + status: "in_progress" as const, } - : msg + : item ), })); } @@ -783,45 +887,43 @@ 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 and attach usage + // Stream ended - mark as complete + // Usage is provided via response.completed event (OpenAI standard) 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 + items: prev.items.map((item) => + item.id === assistantMessage.id && item.type === "message" ? { - ...msg, - streaming: false, + ...item, + status: "completed" as const, usage: finalUsage || undefined, } - : msg + : item ), })); - // Update thread-level usage stats + // Update conversation-level usage stats if (finalUsage) { - setThreadUsage((prev) => ({ + setConversationUsage((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) => ({ ...prev, isStreaming: false, - messages: prev.messages.map((msg) => - msg.id === assistantMessage.id + items: prev.items.map((item) => + item.id === assistantMessage.id && item.type === "message" ? { - ...msg, - contents: [ + ...item, + content: [ { type: "text", text: `Error: ${ @@ -829,16 +931,16 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { ? error.message : "Failed to get response" }`, - }, + } as import("@/types/openai").MessageTextContent, ], - streaming: false, + status: "incomplete" as const, } - : msg + : item ), })); } }, - [selectedAgent, currentThread, onDebugEvent] + [selectedAgent, currentConversation, onDebugEvent] ); const handleSubmit = async (e: React.FormEvent) => { @@ -883,12 +985,12 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { } else if ( attachment.file.type === "text/plain" && (attachment.file.name.includes("pasted-text-") || - attachment.file.name.endsWith(".txt") || - attachment.file.name.endsWith(".csv") || - attachment.file.name.endsWith(".json") || - attachment.file.name.endsWith(".html") || - attachment.file.name.endsWith(".md") || - attachment.file.name.endsWith(".tsv")) + attachment.file.name.endsWith(".txt") || + attachment.file.name.endsWith(".csv") || + attachment.file.name.endsWith(".json") || + attachment.file.name.endsWith(".html") || + attachment.file.name.endsWith(".md") || + attachment.file.name.endsWith(".tsv")) ) { // Convert all text files (from pasted large text) back to input_text const text = await attachment.file.text(); @@ -920,7 +1022,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { // Use pure OpenAI format await handleSendMessage({ input: openaiInput, - thread_id: currentThread?.id, + conversation_id: currentConversation?.id, }); } else { // Simple text message using OpenAI format @@ -940,7 +1042,7 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) { await handleSendMessage({ input: openaiInput, - thread_id: currentThread?.id, + conversation_id: currentConversation?.id, }); } @@ -966,7 +1068,9 @@ export function AgentView({ selectedAgent, onDebugEvent }: AgentViewProps) {

- Chat with {selectedAgent.name || selectedAgent.id} + + Chat with {selectedAgent.name || selectedAgent.id} +

- {/* Thread Controls */} + {/* Conversation Controls */}