From 2f997760178415464f30dc6d20e90ec30166352d Mon Sep 17 00:00:00 2001 From: Edward Funnekotter Date: Fri, 23 Jan 2026 12:15:15 -0500 Subject: [PATCH] chore(DATAGO-122087): WF PR 23 - review cleanup for Workflows feature stack (#825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(workflows): Add foundation data models and constants Adds the foundational data models and utilities required by the Prescriptive Workflows feature: - StructuredInvocationRequest/Result data parts - WorkflowExecution* data parts for visualization - Extension URI constants - Agent card schema utilities * feat(workflows): Add workflow definition Pydantic models Adds the Pydantic models that define the YAML schema for workflow definitions with Argo Workflows-compatible syntax: - Node types: AgentNode, ConditionalNode, SwitchNode, LoopNode, MapNode - WorkflowDefinition with DAG validation - RetryStrategy and ExitHandler models * feat(workflows): Add structured invocation support for agents Enables agents to be invoked with schema-validated input/output: - StructuredInvocationHandler for schema validation and retry - Integration with SamAgentComponent - Result embed pattern for structured output - A2A event handler integration for structured invocation detection * feat(workflows): Add workflow tool for agent invocation Adds ADK Tool that allows agents to invoke workflows: - Dynamic tool generation from workflow schema - Dual-mode invocation (parameters or artifact) - Long-running execution with polling - LLM callback integration for workflow tool instructions * feat(workflows): Add workflow orchestrator component Adds the WorkflowExecutorComponent that coordinates execution: - Component lifecycle and message routing - Agent card generation with schemas - Event publishing for visualization - A2A protocol message handlers * feat(workflows): Add DAG executor core logic Adds the core DAG execution engine: - Dependency graph building and validation - Node execution dispatch - Template resolution for data flow - Conditional expression evaluation - Execution context management * feat(workflows): Add agent caller for A2A invocation Adds the AgentCaller for invoking agents via A2A: - Input template resolution - A2A message construction - Artifact creation for input data * feat(workflows): Add integration, examples, and tests Adds backend integration and comprehensive test coverage: - Gateway workflow event forwarding - Example workflows (all_node_types, jira_bug_triage) - Unit tests for pure functions (~1,770 lines) - Integration tests for error scenarios (~2,000 lines) - Declarative test workflows (8 YAML files) - Test fixtures for workflow apps * feat(workflows): Add frontend workflow visualization Adds all frontend changes for workflow visualization: - Layout engine for positioning nodes - FlowChart components (panel, renderer, edges) - Node components for all node types - NodeDetailsCard sidebar - Task visualizer processor - Provider updates - Agent utilities for workflow detection - Mermaid diagram modal * refactor(ui): Remove old FlowChart components Remove legacy FlowChart visualization files that were replaced by the new workflow activities visualization system. These files are no longer needed with the new Activities-based architecture. Files removed: - FlowChart/customEdges/GenericFlowEdge.tsx - FlowChart/customNodes/*.tsx (6 node types) - FlowChart/edgeAnimationService.ts - FlowChart/taskToFlowData.helpers.ts - FlowChart/taskToFlowData.ts - FlowChartPanel.tsx * chore(DATAGO-120348): Introduce a template to PRs (#690) * DATAGO-120348: Introduce a template to PRs so authors highlight what problem the PR is trying to solve, how it is solving the problem, and how it has been tested. * DATAGO-120348: enhance the template * fix(DATAGO-118177) - Manage memory usage for uploading large files (#683) * fix(DATAGO-118675): Start the user in AI mode when editing a prompt (#678) * Set default to ai mode when editing prompts and new unit test * added back fragment to make code change more readible * Missed file..before loading prompts for edit, set builder mode to ai-assisted * ci(ui): bump version to ui-v1.24.3 [skip ci] * fix: use BrokerOutput component for direct publisher initialization (#717) * fix: use BrokerOutput component for direct publisher initialization The direct publisher for deployer commands was failing because: 1. _init_direct_publisher() referenced self.broker_output which was never set 2. publish_a2a() used message_builder().build() which created empty messages Fixed by: - Finding BrokerOutput component via flow.component_groups (same pattern as App.send_message) - Storing _messaging_service reference for creating the publisher - Passing payload as bytearray directly to publish() instead of using message_builder - Adding lazy initialization in publish_a2a() as fallback * fix: Platform service agent registry cleanup and timer cancellation - Change agent_registry.cleanup() to agent_registry.clear() (correct method name) - Add cancel_timer() call before clearing registry during shutdown - Ensures health check timer is properly cancelled during cleanup * [ci skip] Bump version to 1.11.4 * fix: prevent double response in AuthMiddleware when token validation fails (#718) When _handle_authenticated_request sent an error response (401/500) and returned, the __call__ method would still call self.app(), causing a second response to be sent. This resulted in: "RuntimeError: Unexpected ASGI message 'http.response.start' sent, after response already completed." Fix: _handle_authenticated_request now returns a boolean indicating whether an error response was sent. The caller checks this and returns early to prevent calling self.app() after an error response. * ref: added image tools to the document (#696) * ref: update built-in tools document (#695) * ref: update python tools (#694) * ref: added custom artifact to documents (#693) * chore(DATAGO-117985): Refactoring chat provider for better encapsulation (#716) * chore: refactoring chat provider * chore: removing extraneous changes * chore: updating type name * chore: using extracted taskMigration * chore: using extracted artifact preview * chore: removing unused hooks * chore: using artifact operations * chore: tidying imports * chore: tidying imports * ci(ui): bump version to ui-v1.24.4 [skip ci] * chore: update storybook to 10.1.10 (#722) * ci(ui): bump version to ui-v1.24.5 [skip ci] * chore(DATAGO-120523): Update platform service API path from /enterprise to /platform (#721) * refactor: Update platform service API path from /enterprise to /platform Update Vite dev server proxy configuration and platform service architecture to use /api/v1/platform instead of /api/v1/enterprise. Changes: - Add PLATFORM_SERVICE_PREFIX constant in community main.py - Community now owns and applies the platform service prefix - Update router mounting logic to use community-defined prefix - Update vite.config.ts proxy for local development - Update documentation comments in examples and app.py Architecture: Community defines PLATFORM_SERVICE_PREFIX as single source of truth. Both community and enterprise platform routers use this prefix, ensuring consistent API structure under /api/v1/platform/*. * fix: Allow OPTIONS requests through OAuth2 middleware for CORS preflight * feat(DATAGO-120523): Add platform service health endpoint at /api/v1/platform/health - Create health_router.py with /health endpoint - Register health router in community routers - Remove old root-level /health endpoint from main.py - Update tests to use new /api/v1/platform/health path This aligns with the platform service API structure where all platform endpoints are under /api/v1/platform prefix. Related PRs: - Enterprise: https://github.com/SolaceDev/solace-agent-mesh-enterprise/pull/460 - K8s: https://github.com/SolaceDev/sam-kubernetes/pull/41 * fix(tests): Align test factory with production platform API prefix pattern Updated PlatformServiceFactory to use centralized PLATFORM_SERVICE_PREFIX instead of expecting prefix in router configs. This matches the production pattern where all platform service endpoints (community and enterprise) are consistently mounted under /api/v1/platform. Changes: - Test factory now uses hard-coded PLATFORM_SERVICE_PREFIX = "/api/v1/platform" - Removed fallback /health endpoint from test factory (now properly registered via community router) - Updated docstring to reflect that community routers are no longer empty * ci(ui): bump version to ui-v1.25.0 [skip ci] * [ci skip] Bump version to 1.11.5 * docs(DATAGO-118501): Document platform migration (#677) * docs: Platform service migration * docs: migration slight rewording * docs: move platform migration right under migrations/ and update enterprise version the migration applies to * docs: platform migration doc improvements * chore: trigger PR size validation workflow * fix: linking platform.yaml rather than copying in docs * fix: Update enterprise version now that enterprise PR has been merged * fix: link to platform.yaml in a way that works everywhere including CI/CD pipeline * feat(DATAGO-119380): Return the list of built-in tools using CLI command (#733) * feat: list tools using cli command * feat: add unit tests * fix: update a unit test to count cli commands * feat: document tools CLI command * chore: tidying ui (#737) * ci(ui): bump version to ui-v1.25.1 [skip ci] * refactor(DATAGO-119380): Update logging document (#691) * ref: update logging document * fix: cleaned log configuration document * ref(DATAGO-119380): Add vibe coding page to documents (#689) * ref: added vibe coding to documents * fix: update vibe coding page location * fix: moved vibe coding under Get Started menu * ref(DATAGO-119380): Improve context7 rules for vibe coding (#734) * fix: improve rules of context7 * ref: update context7 rules * ref: update context7 rules * fix: update context7 url * feat(DATAGO-118652): Add generic gateway auth framework + better agent registeration management (#610) * docs: add generic gateway adapter implementation plan Co-authored-by: aider (openai/gemini-2.5-pro) * docs: add implementation checklist for generic gateway adapter framework Co-authored-by: aider (openai/gemini-2.5-pro) * feat: add generic gateway adapter framework core types and interfaces Co-authored-by: aider (openai/gemini-2.5-pro) * feat: implement GenericGatewayApp with adapter loading schema Co-authored-by: aider (openai/gemini-2.5-pro) * feat: implement GenericGatewayComponent with adapter orchestration Co-authored-by: aider (openai/gemini-2.5-pro) * feat: add user feedback submission mechanism to gateway adapter framework Co-authored-by: aider (openai/gemini-2.5-pro) * feat: add Slack gateway adapter using generic framework Co-authored-by: aider (openai/gemini-2.5-pro) * fix: add missing SamTextPart import to slack adapter Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: move adapter config validation to pydantic models Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: remove try-except wrapper for slack_bolt imports Co-authored-by: aider (openai/gemini-2.5-pro) * chore: cleanup * refactor: remove abstract requirement for handle_text_chunk method Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: prioritize child class parameters in schema merging Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: move common gateway configs to base class Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: migrate slack gateway config to generic adapter framework Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: fix slack handler registration to use explicit signatures Co-authored-by: aider (openai/gemini-2.5-pro) * fix: align SlackAdapter method signatures with GatewayAdapter base class Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: align AuthClaims model with framework identity expectations Co-authored-by: aider (openai/gemini-2.5-pro) * fix: handle default values in cache service get methods Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: decouple Slack adapter from A2A protocol and filter data parts Co-authored-by: aider (openai/gemini-2.5-pro) * feat: filter duplicate text and file parts from final streaming responses Co-authored-by: aider (openai/gemini-2.5-pro) * fix: prevent duplicate content in slack final status messages Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: remove unused generate_a2a_session_id function * refactor: improve slack markdown formatting and feedback button placement Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: add underline and newline to Slack heading formatting Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: simplify Slack markdown heading format by removing separator * feat: implement multi-step feedback flow with text input in Slack adapter Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: separate feedback buttons into distinct message from status Co-authored-by: aider (openai/gemini-2.5-pro) * feat: add standardized feedback event publishing to gateway Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: remove redundant context from feedback events Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: remove platform_context and task_context from SamFeedback model Co-authored-by: aider (openai/gemini-2.5-pro) * chore: clean up the slack launch target * feat: enhance artifact creation progress display in Slack adapter Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: implement stateful artifact progress messages with sequential processing Co-authored-by: aider (openai/gemini-2.5-pro) * feat: add artifact content loading helper to gateway context Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: rename conversation_id to session_id and sanitize Slack IDs Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: use artifact helper for loading content in generic gateway Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: implement three-step external upload for Slack file ordering Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: associate file upload with descriptive message using initial_comment Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: format code with consistent line breaks and spacing * fix: add user_id_for_artifacts to external_request_context for artifact resolution Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: use individual a2a helper functions for FilePart processing Co-authored-by: aider (openai/gemini-2.5-pro) * chore: Fix issue with text/file ordering for slack results * feat: add /artifacts command to slack adapter with download support Co-authored-by: aider (openai/gemini-2.5-pro) * fix: handle missing thread_ts in artifacts command to prevent crash Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: implement command framework with !artifacts keyword trigger Co-authored-by: aider (openai/gemini-2.5-pro) * feat: add help command and fix artifacts to post in thread with truncated descriptions Co-authored-by: aider (openai/gemini-2.5-pro) * fix: handle user identity extraction from Slack action events and post errors to thread Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: remove ephemeral status message from download action Co-authored-by: aider (openai/gemini-2.5-pro) * refactor: improve artifact list UI and fix help command threading Co-authored-by: aider (openai/gemini-2.5-pro) * chore: cleanup docs * chore: cleanup docs * refactor: rename a2a_ prefixed constants to slack_ in Slack gateway utils Co-authored-by: aider (openai/gemini-2.5-pro) * chore: create CLI based gateway as an example * Add rich formatting * chore: Added a simple CLI gateway * chore: add example MCP gateway * chore: add some oauths support to MCP server * chore: add some more auth stuff * chore: debugging * chore: a few fixes for auth * fix: Require PKCE and remove any details from client auth failure responses * chore: remove cli and slack gateway examples * chore: cleanup * fix: update Slack adapter reference in configuration and clean up component initialization * fix: update logging config path in launch.json and adjust MCP client usage examples in mcp_gateway_example.yaml * fix: update launch configurations to use solace_agent_mesh.cli.main and adjust args for execution * fix: update MCP gateway example to listen on localhost and remove unused AuthClaims import in adapter * Remove utility functions for MCP Gateway Adapter from utils.py * add tool filtering configuration to MCP gateway example * fix: Improve logging for OAuth proxy route setup in gateway authentication * fix: Update .gitignore to include .sam and *.key files * feat: Implement user identity extraction method in GenericGatewayComponent * Add unit tests for authentication features and OAuth utilities - Implement unit tests for the AuthHandler interface in `test_auth_interface.py`, covering abstract methods, concrete implementations, and error handling. - Create tests for authentication setup and header injection in `test_component_auth.py`, ensuring proper behavior of the BaseGatewayComponent. - Develop tests for agent registry callbacks and agent listing functionality in `test_component_auth_and_callbacks.py`, validating interactions with the agent registry. - Introduce integration tests for OAuth utilities in `test_oauth_utils_integration.py`, verifying behavior with and without the enterprise package. * Add enterprise package checks and conditional OAuth tests * refactor: remove MCP gateway example configuration and related launch settings * refactor: streamline authentication setup and improve logging for agent registration * refactor: update OAuth proxy handling to use global variable and improve test coverage * refactor: update user info retrieval test to include query parameters * refactor: remove outdated comments from authentication endpoints * refactor: replace direct attribute access with setter methods for agent callbacks * refactor: remove unused global OAuth proxy functions and related tests * refactor: remove deprecated Gateway OAuth proxy endpoint from auth middleware * refactor: remove OAuth proxy functions and related middleware from main.py * refactor: remove unused OAuth-related tests and classes from integration tests * refactor: remove unused TestGetUserInfoWithEnterprise class from integration tests * refactor: update app module path and clean up unused tests in gateway components --------- Co-authored-by: Edward Funnekotter Co-authored-by: aider (openai/gemini-2.5-pro) * [ci skip] Bump version to 1.12.0 * fix(DATAGO-119380): Update Context7 URL (#743) * fix: update url * fix: update the unit test * ci(DATAGO-120987): fix release workflow with skipped security checks (#745) * feat(DATAGO-121014): Add cli command for adding proxy (#746) * feat(DATAGO-121014): Add cli command for adding proxy * minor changes * fix(DATAGO-120949): Move migrations to __init__ for sequential execution (#741) Both Platform and WebUI migrations now run in __init__() during component creation, ensuring sequential execution and preventing Alembic/SQLAlchemy race conditions. Changes: - Platform: Migrations in __init__() via _run_database_migrations() - WebUI: Migrations in __init__() via _run_database_migrations() - Removed unused component parameter from _setup_database() - Simplified comments and docstrings - Added Alembic log prefixing for better debugging * [ci skip] Bump version to 1.12.1 * feat(DATAGO-119367): Add wide logs for auditing MCP server usage (#742) Adds structured logging for MCP tool calls to enable tracking usage per user/agent. The log includes user_id, agent_id, tool_name, and session_id both in the message and as structured extra fields for easy filtering and auditing. * feat(DATAGO-120289): Add stepper ui component (#744) * Add stepper ui component * Add storybook test * ci(ui): bump version to ui-v1.26.0 [skip ci] * feat(DATAGO-118768): Support remote MCP server configuration in sam add agent --gui (#735) * feat: new ui * fix: config file formatting bugs * fix: remove oauth2 configuration, alter headers * fix: remove duplicate env variables input * fix: remove blocking deletion, description * fix: copilot requests * fix: remove redundant deletion logic * chore: log message * Cleanup of orginal change. * Updated help text * revert help text back to original --------- Co-authored-by: Robert Zuchniak * Testing fix for env (#751) * fix: update module path in launch configurations to use relative import (#750) * chore(DATAGO-120962): deleting unused projects code (#747) * chore: deleting unused code * chore: removing more unused code * ci(ui): bump version to ui-v1.26.1 [skip ci] * fix: Updating expected test result based on PEP8 standard (#752) * [ci skip] Bump version to 1.12.2 * feat(DATAGO-121143): Exclude Platform Service health endpoint from authentication (#755) - Add /api/v1/platform/health to skip_paths in OAuth middleware - Allows load balancers and monitoring systems to check Platform Service health without authentication - Gateway health endpoints (/api/v1/platform/gateways/health) still require authentication 🤖 Generated with [Claude Code](https://claude.com/claude-code) * chore: clearning tokens and tidying (#758) * ci(ui): bump version to ui-v1.26.2 [skip ci] * chore(DATAGO-120962): replacing custom components with standard ones in project (and prompt) UI (#740) * chore: replacing custom components with standard ones * chore: tidying * chore: tidying * chore: tidying * chore: responding to UX feedback * chore: switching to standard dialog to support linear progress display * chore: tidying * chore: making titles uniform * chore: tidying delete dialog * chore: tidying * chore: making dialog more consistent with prompt import * chore: improving badge for ux feedback * chore: tweaking for dark mode * ci(ui): bump version to ui-v1.26.3 [skip ci] * feat(DATAGO-116403): add sam_access_token validation to middleware (#738) * feat: add sam_access_token validation to middleware - Validate sam_access_token before falling back to IdP token - Extract roles from token, resolve scopes via authorization_service - Feature flag controlled via trust_manager.config.access_token_enabled - Fully backwards compatible with existing IdP token flow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * fix trust manager property * logging update * update logs --------- Co-authored-by: Claude * feat: add cors_allowed_origin_regex config for Platform Service (#760) Add support for regex-based CORS origin matching in Platform Service. This enables local development scenarios where ports are dynamically assigned (e.g., minikube service), by allowing a regex pattern like 'https?://(localhost|127\.0\.0\.1):\d+' to match any localhost port. Changes: - Add cors_allowed_origin_regex config option to Platform Service schema - Pass allow_origin_regex to CORSMiddleware when configured - Add unit tests for regex functionality Usage in values.yaml: cors_allowed_origin_regex: "https?://(localhost|127\\.0\\.0\\.1):\\d+" * feat(DATAGO-117225): Web Search and Deep Research (#574) * wip(deep-research): initial commit * fix(deep-research): fix search metadata pass-through from frontend to tool * fix(deep-research): UI styling updates * chore: update RAG sources icon * feat(deep-research): render citations as clickable links in the markdown output * chore: restrict source types to web and kb * feat: web search UI and agent * fix(web search): ui fixes * feat(web-search): tools ui * chore: remove logging_config.ini from version control * fix(deep research): remove UI toggle * fix(deep research): UI/UX updates * fix(deep-research): backend tool cleanup * UI updates to Sources Moved the location and changed styling of the sources button under chat responses * Added completed research progress state Combined research timeline with completed research progress state. * Updated behaviour for active progress state * add closing animation to accordion * Fixed visual bug on closing animation * fix: example agents * fix(deep-research): sidepanel sources were cumulative * fix: should not render sources for user messages * fix: updated agent in examples * fix: web sources UI * fix: change frontend to use camelCase vars * fix: modify agent prompt to not show sources separately * fix: camelCase for source citation IDs * fix(web-search): image source cards styling * fix: remove duplicate image links * fix: simplify sources component * fix: UI styling updates * fix: show confirmation dialog when navigating away from active deep research session * fix: web search citation markers in peer agent responses should be kept * fix: return deep research report result as an artifact from the tool * fix: remove deep research warning when navigating away, will be handled by background tasks * feat: integration test suite, support for Brave and Exa web search APIs * fix: show image sources in side panel * fix: add pytest markers * fix: move rag dto to common * fix: Replace ADKToolWrapper with direct artifact service call in deep_research_tools, refactor filename sanitization * fix: simplify the deep research agent configuration * fix: remove hardcoded LLM config from deep research agent configuration * fix: add max response size limit for web request * fix: minor ephemeral source ordering issue * fix: removed Tavily, Exa, Brave web search integrations * fix: remove Tavily fallback from deep research tool * fix: report rendering updates for better UX * fix: group deep research web sources (full vs. partial) * fix: remove redundant description * refactor: extract publish_data_signal_from_thread to component.py * chore: Rename _web_search_google to web_search_google * feat: update docs for web search and deep research * fix: add TTS and copy support for deep research reports * fix: clear sources when switching chat sessions * fix: deep research links in preview panel were not clickable * fix: address minor issue for rendering of deep research artifacts during background tasks * fix: citation styling * fix: allow ask followup on deep research reports * chore: theme colors for web links * fix: update tests * fix: update deep research invoke criteria in prompt * fix: the trio selectordid not look right at the minimum width of the panel * fix: conditionally render sources tab * feat: new research agent example * fix: deep research report artifact reload form URI * fix: error handling for artifact failing to be saved * fix: ariakit refactor, other minor fixes * fix: LLM-geenrated sources tab title * fix: auto title generation for Sources panel, minor agent updates * fix: support comma-separated citation markers like [[cite:search1, search2]] * fix: deep research progress shows query-URL pairs correctly during research * fix: filter intermediate web_content_ artifacts from workflow visualization * fix: resolve report duplication due to partial response appending in deep research tool, also filter out intermediate web artifacts * fix: increase max_output_tokens for Gemini 2.5 Pro reasoning tokens * fix: keep artifact info in peer deep research response for delegating agent to use artifact_return * chore: remove unused tool_config_status and frontend settings components for web_search and deep_research * fix: fixed JSON parsing errors in deep research tool for Gemini 2.5 Pro compatibility. * fix: refactor duplicated frontend code * fix: re-enable background tasks * fix: reset peer timeout on status signal forwarding and add task flags * fix: handle multi citation formats * fix: replaced regex for cleaning up of LLM response * chore: unit tests * chore: increase test coverage * chore: further increase test coverage * chore: additional unit test for deep research --------- Co-authored-by: jessieli-ux <104792472+jessieli-ux@users.noreply.github.com> * ci(ui): bump version to ui-v1.27.0 [skip ci] * [ci skip] Bump version to 1.13.0 * fix(DATAGO-120298): Investigate and fix garbled conversation displayed in AI Assistant (#724) * fix(STT): add STT to AI prompt builder, bugfix for STT language settings * fix: snake_case to camelCase for prompt text * fix: AI builder returns camelCase now * fix: production_prompt to camelCase in frontend * fix: more casing fixes * fix: is_pinned casing * fix(DATAGO-118327): pasted text artifact fix * fix: updated text selection menu options and flow * fix: add mimeType to PastedArtifactItem model * fix: reorder highlighted context and prompt * fix: editable badges for large pasted text input * fix: long import fields breaking the UI * fix: limit number of large text paste badges to 5 * fix: hide non-configured STT/TTS providers * chore: update Start New Chat on prompt detail page to "Use in Chat" * fix: experimental badge, prompts usage from prompts page starts new chat * fix: update prompt icon to notepad-text * revert: remove limit on number of pasted text badges * fix: add scrolling for pasted text badges * fix: should not allow to save empty artifact * fix: Ask Followup popup should always stay above the highlighted textand be left aligned with the start of the highlight * fix: import validation on field max lengths * feat: added syntax-highlighting to the prompt content field * fix: increase font size of experimental badge * fix: remove toast notification when pinning or on un-pinning prompts * fix: change artifact to file in paste action dialog * fix: version metadata fields of prompt templates as well * fix: address update problem when using prompt in new chat * fix: auto detect pasted text artifact MIME type * fix: confirmation dialog for artifact overwrite * fix: text selection bug, paste action auto detection removed * fix: experimental badge colors * refactor: refactor the PromptImportDialog to use the zod schema with react-hook-form * fix: auto upload pasted text badge as artifact * fix: remove redundant artifact save toast * fix: nullish values for prompt import * fix: change category to tag in import dialog * fix: increase toast duration, unify toast messages * fix: inform user about truncation of prompt in import dialog * fix: switch to warning variant for prompt truncation on import * fix: improve artifacts upload UX for large pasted text * fix: warn on duplicate filenames across pending pasted text items * fix: tooltip placement * fix: reset form state on new creation * fix: prompt import UX improvements * fix: update error message colours * fix: minor styling * fix: conflicting fields should remain editable * fix: remove asterisk on required fields on import if no conflicts * fix: minor import ui fixes * fix(DATAGO-120298): Investigate and fix garbled conversation displayed in AI Assistant * ci(ui): bump version to ui-v1.28.0 [skip ci] * fix(DATAGO-120962): tidying format and improving ui when added files to projects (#765) * chore: fixing dialog closure and improving formatting * chore: removing inaccessible fileupload section from UI * chore: more formatting * ci(ui): bump version to ui-v1.28.1 [skip ci] * feat(DATAGO-118201): Add React Query (#736) * feat: migrate from Context-based data fetching to React Query * refactor: remove obsolete migration guide and example usage files; update query hooks for improved handling * feat: integrate React Query for data fetching and add query client configuration * refactor: remove unnecessary comments from project hooks for clarity * refactor: simplify query client configuration by removing retry logic and unnecessary comments * fix: update React Query and ESLint plugin versions to remove caret (^) for consistency * feat: implement QueryProvider for React Query integration and remove obsolete queryClient file * fix: add documentation comments for shared QueryClient instance in QueryProvider * refactor: rename useProjectArtifacts to useProjectArtifactsNew for clarity * feat: update hooks documentation to clarify development status and usage warnings * feat: add QueryClient and refactor QueryProvider to utilize shared instance * ci(ui): bump version to ui-v1.29.0 [skip ci] * chore(DATAGO-108909): Add adaptive stream rendering of chat result text to smooth it out (#759) ### What is the purpose of this change? Smooth out the rendering of streaming chunks of text from the backend. This is done to improve the user experience in cases where the chunk size is large and the rendering becomes very stuttery. With this change, we are now able to greatly increase the batching size in the backend to reduce the large number of status update messages we are getting today, which are only there to improve the smooth rendering. * ci(ui): bump version to ui-v1.29.1 [skip ci] * fix(DATAGO-121140): Fix loss of embedded templates in sub-agent responses (#756) Fixes an issue where `template_liquid` blocks in LLM responses were being stripped when responses were sent back to calling agents in peer-to-peer communication. This caused confusion in the calling agent as they couldn't see or forward the template blocks to the gateway for resolution. **Root Cause:** When an LLM generates a response containing `template_liquid` blocks, the stream parser extracts the template content and emits a `TemplateBlockData` signal for the gateway to resolve. However, for RUN_BASED sessions (peer-to-peer agent requests), the calling agent only receives the aggregated response text—which had the template blocks stripped out. **Fix:** For RUN_BASED sessions, when a `TemplateBlockCompletedEvent` is processed, we now preserve the original template block text in the response at its original position. This allows calling agents to forward templates to the gateway for resolution. * fix: Change MCP results to 'all or none' and allow full configuration of schema discovery depth * Revert "fix: Change MCP results to 'all or none' and allow full configuration of schema discovery depth" This reverts commit 1aa2af0423a60eadadf8c369eb031c33ccc8937b. * fix(DATAGO-121333): Change MCP results to 'all or none' and allow full configuration of schema discovery depth (#767) - **All-or-nothing approach for large MCP responses**: When MCP tool responses exceed the LLM return size limit, the data is now completely withheld rather than being truncated. This prevents LLM hallucination caused by partial data - LLMs tend to confidently fill in gaps when given truncated content. - **Configurable schema inference depth**: Added `schema_inference_depth` configuration (default: 4, range: 1-10) that can be set at the agent level or per-tool level via `tool_config`. This controls how deeply nested structures are inspected when generating artifact metadata. - **Improved LLM guidance**: When data is withheld, the LLM receives explicit instructions to use `template_liquid` (for displaying to users) or `load_artifact` (for processing) to access the full data from the saved artifact. * chore: remove conditional node type from worklfows * chore: Remove mermaid diagram from workflow agent-card * test: Add unit tests for workflow agent card JSON config Add tests to verify that workflow configurations are correctly serialized to JSON for inclusion in agent card extensions. Tests cover: - Simple agent workflows - Workflows with input/output schemas - Switch nodes with cases and defaults - Map nodes with items - Loop nodes with conditions and max_iterations - Agent nodes with explicit input mappings - JSON serialization round-trip * feat(ui): Add Workflows tab to Agent Mesh page - Rename sidebar navigation from "Agents" to "Agent Mesh" - Add tabbed navigation with "Agents" and "Workflows" tabs - Create WorkflowList component with table view, filtering, and pagination - Create WorkflowDetailPanel with markdown description and JSON schema viewer - Create WorkflowOnboardingBanner with dismissible intro and docs link - Filter agents by type using isWorkflowAgent() helper - Remove stale MermaidDiagramModal export * chore: some consistency fixes * fix(DATAGO-118655) - Auth Enabled MCP gateway (part 2) (#772) * feat: Add unit testing configuration and dependencies (#778) - Updated vitest.config.ts to include a new project configuration for unit tests. - Modified package.json to add necessary dependencies for testing, including @testing-library/react and jsdom. - Enhanced vitest.setup.ts with a workaround for window.matchMedia. - Updated package-lock.json to reflect the new dependencies and their versions. * ci(ui): bump version to ui-v1.30.0 [skip ci] * feat(workflows): Add instruction field to workflow agent nodes Add optional instruction field to AgentNode that allows workflow creators to provide additional context/guidance to target agents. The instruction supports template expressions (e.g., {{workflow.input.context}}) that are resolved at runtime. Changes: - Add instruction field to AgentNode model in app.py - Add _resolve_string_with_templates() to handle embedded templates - Update workflow_schema.json with instruction property - Add unit tests for instruction field - Add integration test validating instruction appears in LLM requests - Add example usage in all_node_types_workflow.yaml - Fix deprecated conditional node type in test fixtures DATAGO-121520 * chore: added a version config to the workflow config schema * fix(DATAGO-121435) Web search citations not properly mapped to sources with parallel tool calls (#769) * fix: parallel tool call citation handling * fix: improve web search citation accuracy with unique turn-based IDs * fix: more backend tests * ci(ui): bump version to ui-v1.30.1 [skip ci] * chore(DATAGO-117142): improving logout clearing (#782) * chore: improving logout * chore: rename test for conflict * ci(ui): bump version to ui-v1.30.2 [skip ci] * fix(DATAGO-118139): fix vulnerable regex found in Sonarqube hotspot (#660) * fix: regex * rename a unit test file name to avoid the conflict with integration test --------- Co-authored-by: ZiyangWang * chore: first cut at workflow diagram * fix(security): update dependencies to address vulnerabilities (#784) - Update @remix-run/node, @remix-run/react, @remix-run/dev to 2.17.4 (fixes CVEs in react-router, cookie, mdast-util-to-hast) - Update react-router-dom to 7.12.0 (fixes 4 security issues) Note: urllib3 vulnerability cannot be fixed yet as solace-ai-connector==3.2.0 pins urllib3==2.6.0. Will require upstream update. * ci(ui): bump version to ui-v1.30.3 [skip ci] * chore: fix a few workflow rendering issues * Fix minor issues with the proxy template (#781) * chore(DATAGO-121164): Upversion aiohttp and pdfminer.six (#770) * DATAGO-121164 fix vulnerabilities CVE-2025-69227, CVE-2025-69223 (aiohttp) * DATAGO-121164 upversion connector * chore: Add always-run status check for UI CI workflow (#779) * chore: Add always-run status check for UI CI workflow The UI CI workflow uses path filters and only runs when frontend code changes. This causes issues with required status checks in GitHub - when only backend code is modified, the check doesn't run and isn't marked as passed, blocking PRs unnecessarily. Changes: - Add check-paths job to detect UI file changes using dorny/paths-filter - Make ui-build-and-test conditional based on path filter results - Add ui-ci-status job that always runs and reports final status: - Passes when UI files unchanged (allows backend-only PRs) - Passes when UI tests run and succeed - Fails when UI tests run and fail - Update bump-version to only run when UI files changed To use: Set "UI CI Status" as the required status check in GitHub branch protection rules instead of "Build and Test UI". Co-Authored-By: Claude * Remove path filters from workflow triggers Since we're now using the dorny/paths-filter action within the workflow to detect UI changes, we no longer need the path filters on the workflow triggers. The workflow will now always run, but the check-paths job determines whether UI tests execute. Co-Authored-By: Claude * chore: Pin GitHub Actions to specific commit hashes Pin all GitHub Actions to specific commit hashes for security using pinact. This follows security best practices by ensuring actions are immutable. Actions pinned: - actions/checkout@v4 -> v4.3.1 - dorny/paths-filter@v3 -> v3.0.2 - amannn/action-semantic-pull-request@v5 - actions/setup-node@v4 -> v4.4.0 - actions/cache@v4 -> v4.3.0 - webfactory/ssh-agent@v0.9.1 Note: Some actions (@main, @master) cannot be pinned and remain as-is. Co-Authored-By: Claude * Rollback some version changes * . * Adjust messages --------- Co-authored-by: Claude * chore: Many changes done to the workflow visualization * ci(DATAGO-121651): fix idendical image digests (#789) * chore: more updates to the workflow diagrams * Adjust the workflow list for the onboarding and table size * chore(DATAGO-121602): swapping out dialogs for standard in order to show linear progress (#792) * chore: swapping out dialogs to show linear progress * chore: updating storybook * ci(ui): bump version to ui-v1.30.4 [skip ci] * chore: fix agent status * chore: Some workflow diagram fixes to align better with the HLM * feat(DATAGO-118825): Add success/failure logging with duration for MCP tool calls (#766) * feat: Add success logging with duration for MCP tool calls Adds structured logging when MCP tools complete successfully to enable complete tool lifecycle auditing (call + success). The success log includes user_id, agent_id, tool_name, session_id, and execution duration in milliseconds. This complements the existing call logging (from commit 1715f911a) by tracking when tools finish executing and how long they took, enabling performance monitoring and usage auditing per user/agent. * feat: Add failure logging for MCP tool execution errors Adds error logging when MCP tools fail execution, complementing the existing success logging. The failure log includes execution duration and error details to enable performance monitoring and error tracking. Changes: - Added _log_mcp_tool_failure function for error logging - Wrapped tool execution in try/catch to capture and log failures - Exception re-raised after logging to maintain existing behavior - Added comprehensive tests verifying exception propagation The failure log includes user_id, agent_id, tool_name, session_id, duration_ms, and error message in both the log message and structured extra dict. * undo uv lock * refactor: Extract MCP tool audit logging into dedicated method Refactored logging logic into _execute_tool_with_audit_logs method to reduce duplication and improve maintainability. The new method handles start/success/failure logging with duration tracking in a single location. - Extract audit logging into _execute_tool_with_audit_logs method - Add type annotation for _original_mcp_tool field - Use lambda to simplify tool execution call - Import log helper functions in test file * test: Add tests for _execute_tool_with_audit_logs method Replace error handling tests with focused audit logging tests: - test_logs_success_with_duration: Verifies success logging with duration - test_logs_failure_with_duration: Verifies failure logging with duration - test_run_async_impl_calls_audit_logs: Verifies integration with _run_async_impl * fix: enable task_logging in webui config template (#797) * ci: add ability to release exact version (#796) * ci: add ability to release exact version * update version * [ci skip] Bump version to 1.13.2 * chore: workflow node added * docs(DATAGO-118857): Adding documentation covering connectors (#687) * docs(DATAGO-118857): Adding documentation covering connectors * docs: Fixing typo * docs: Provide PR suggestions (#723) * Update docs * docs: remove trailing periods from connector documentation lists * docs: Add coming soon banner to openapi connectors --------- Co-authored-by: Jack Clarke <48411623+m9p909@users.noreply.github.com> * chore: remove the node_id tooltip for nodes in the workflow diagram * fix(DATAGO-121281): Fix Pydantic Schema Validation for Enum values (#800) * fix: Adding new function to normalize schema dict * fix: Adding test coverage * [ci skip] Bump version to 1.13.3 * feat(DATAGO-118823): Add audit logging for open api tool calls. (#768) * feat: Implement OpenAPI audit logging callbacks and integrate with tool invocation lifecycle * refactor: Enhance OpenAPI audit logging with error details and consolidate into callbacks.py - Add endpoint path and error type to failed OpenAPI tool call logs - Extract full URI (base URL + path template) for better debugging - Classify error types (auth_error, not_found, timeout, network_error) - Detect and log pending auth responses as WARNING (not success) - Consolidate OpenAPI audit code from separate file into callbacks.py - Extract _extract_openapi_metadata() helper to reduce code duplication - Update tests to match current implementation (7/7 passing) - Security: Log only path templates, never resolved values with sensitive params Log format improvements: - Success: [openapi-tool] [corr:xxx] POST: operation completed - Latency: Xms - Error: [openapi-tool] [corr:xxx] POST: operation failed - Path: /posts, Error Type: auth_error, Latency: Xms - Pending: [openapi-tool] [corr:xxx] POST: operation pending auth - Latency: Xms 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * chore: Reset uv.lock to main branch version 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * refactor: Simplify OpenAPI metadata extraction by removing unused arguments * refactor: Update OpenAPI tool origin check for audit logging * refactor: Simplify OpenAPI tool origin check in callbacks.py --------- Co-authored-by: Claude * feat(DATAGO-120931): Gateway Auto-Registration via A2A Discovery (#719) * [ci skip] Bump version to 1.13.4 * refactor(DATAGO-119380): Improve vibe coding doc (#791) * fix: update examples * fix: add vibe coding to the readme * fix: add vibe coding to the readme * fix: add vibe coding to the readme * fix: add vibe coding to the readme * fix: update vibe coding doc * fix: update context7 rules and vide coding doc * fix: update vibe coding doc * fix: update vibe coding document * fix: update vibe coding document * fix: move vibe coding menu * Update README.md Improve writing Signed-off-by: Ali Parvizi <91437594+alimosaed@users.noreply.github.com> --------- Signed-off-by: Ali Parvizi <91437594+alimosaed@users.noreply.github.com> * Add note about the new LLM Cache env var for agent builder. (#795) * fix: skip confirmation dialog when clicking Discard Changes button during prompt edits (#731) * ci(ui): bump version to ui-v1.30.5 [skip ci] * chore: some refactoring with workflow calls * fix(DATAGO-121519): Develop a preprocessor to clean input data before TTS submission (#777) * feat(tts): add markdown preprocessing and fix streaming playback * fix: remove redundant lines to meet PR size check requirement * fix: add max input length limit * fix: meet PR size check * fix: markdown parsing via marked library * fix: add new endpoint for markdown stripping * fix: remove redundant comment * refactor: use markdown-it-py and BeautifulSoup for TTS preprocessing, add /preprocess endpoint * chore: remove redundant fallbacks * fix: use commonmark preset * fix: ordered list parsing * fix: code block placeholoder for TTS * fix: parallelize TTS chunk generation to reduce playback gaps * ci(ui): bump version to ui-v1.31.0 [skip ci] * feat(DATAGO-121021): Add OAuth 2.0 Authentication Support to MCP Configuration Portal UI (#793) * feat(DATAGO-121021): Add OAuth 2.0 support to MCP config UI initial commit * Updated example to be more generic * fixed up duplicate code * chore: first version of feedback fixes * chore: more updates after testing * chore: project file version fixes (#798) * ci(ui): bump version to ui-v1.31.1 [skip ci] * fix: remove tooltip from cancel button in ChatInputArea component (#805) Co-authored-by: Claude * ci(ui): bump version to ui-v1.31.2 [skip ci] * chore: fixed some sub-node things * chore: fix some info in the runtime graph * chore: Fixed issue with breadcrumbs with nested workflows * chore: Change Workflow to Activity for viewing the task's activity * chore: fix the table styling for workflows * chore(DATAGO-121940): Address PR review comments for Workflows feature stack Fixes from PR review comments: - Use ArtifactRef in StructuredInvocationResult instead of separate artifact_name and artifact_version fields - Improve loop delay description with format examples - Add on_cancel field to ExitHandler for cancellation handling - Update _execute_exit_handlers to handle cancelled outcome - Validate result embed has artifact OR explicit error status - Add header comment to new_node_types_test.yaml Co-Authored-By: Claude Opus 4.5 * chore: some tests were out of date with recent code changes * chore: a couple more adjustmenst post merge * chore: Made a few style adjustments * chore: replaced simpleeval with a custom built evaluator * docs(DATAGO-121452): Add Workflows documentation Add documentation for the Workflows feature: - Add components/workflows.md reference documentation covering node types, template expressions, error handling, and configuration options - Add developing/creating-workflows.md tutorial guide with step-by-step examples for building workflows - Update components/components.md and developing/developing.md index pages with links to new documentation Also: - Move workflow_schema.json to common/schemas/ for better discoverability - Add missing onCancel/on_cancel fields to exitHandler in schema * chore: reordered components in the overview * chore: some more doc corrections * chore(DATAGO-122096): Clean up remaining conditional node type references Remove references to the deprecated "conditional" node type which was replaced by the "switch" node type. Changes: - Remove "conditional" pytest marker from pyproject.toml - Change nodeType check from "conditional" to "switch" in VisualizerStepCard - Update node type description in data_parts.py to list current types - Update condition field description from "conditional/loop" to "switch/loop" - Remove dead conditional node validation block in app.py - Update comment in all_node_types_workflow.yaml - Rename test ID from "conditional_wf" to "guarded_wf" * chore(DATAGO-122087): Remove authenticatedFetch export, use api.webui.get Refactor to use the existing api.webui.get pattern with fullResponse: true instead of exporting authenticatedFetch directly. Changes: - Remove export from authenticatedFetch in client.ts - Remove re-export from api/index.ts - Update NodeDetailsCard.tsx to use api.webui.get with fullResponse: true * refactor: use named React imports instead of namespace imports Replace `import React from 'react'` with named imports like `import { type FC, useState, ... } from 'react'` across workflow visualization and activity components. Also replaces `React.FC` with `FC`, `React.useState` with `useState`, etc. Part of DATAGO-122087 * chore: fixed an unintentional absolute file path --------- Signed-off-by: Ali Parvizi <91437594+alimosaed@users.noreply.github.com> Co-authored-by: moodiRealist <40175386+moodiRealist@users.noreply.github.com> Co-authored-by: enavitan Co-authored-by: Paul Jones Co-authored-by: Automated Version Bump Co-authored-by: Mohamed Radwan <104789800+mo-radwan1@users.noreply.github.com> Co-authored-by: GitHub Action Co-authored-by: Ali Parvizi <91437594+alimosaed@users.noreply.github.com> Co-authored-by: Linda Hillis <101145690+lgh-solace@users.noreply.github.com> Co-authored-by: Carol Morneau <5495913+carolmorneau@users.noreply.github.com> Co-authored-by: Cyrus Mobini <68962752+cyrus2281@users.noreply.github.com> Co-authored-by: aider (openai/gemini-2.5-pro) Co-authored-by: Art Morozov Co-authored-by: Tamimi Ahmad Co-authored-by: Jack Clarke <48411623+m9p909@users.noreply.github.com> Co-authored-by: Ziyang Wang Co-authored-by: Robert Zuchniak Co-authored-by: Hugo Paré Co-authored-by: Mackenzie Stewart Co-authored-by: Claude Co-authored-by: Amir Ghasemi Co-authored-by: jessieli-ux <104792472+jessieli-ux@users.noreply.github.com> Co-authored-by: Jamie Karam Co-authored-by: Raman Gupta Co-authored-by: Rudraneel Chakraborty <97415665+rudraneel-chakraborty@users.noreply.github.com> Co-authored-by: Michael Du Plessis Co-authored-by: evenjessie Co-authored-by: Greg Meldrum --- .github/pull_request_template.md | 22 + .github/workflows/ci.yaml | 9 + .github/workflows/release.yml | 16 +- .github/workflows/ui-ci.yml | 52 +- .gitignore | 4 +- .vscode/launch.json | 94 +- README.md | 3 + cli/__init__.py | 2 +- cli/commands/add_cmd/__init__.py | 4 +- cli/commands/add_cmd/agent_cmd.py | 7 +- cli/commands/add_cmd/proxy_cmd.py | 100 + cli/commands/tools_cmd.py | 315 ++ cli/main.py | 2 + .../webui/frontend/.storybook/vitest.setup.ts | 16 + client/webui/frontend/package-lock.json | 2837 ++++++++++++----- client/webui/frontend/package.json | 27 +- client/webui/frontend/src/App.tsx | 38 +- client/webui/frontend/src/lib/api/index.ts | 1 + .../frontend/src/lib/api/projects/hooks.ts | 134 + .../frontend/src/lib/api/projects/index.ts | 1 + .../frontend/src/lib/api/projects/keys.ts | 13 + .../frontend/src/lib/api/projects/service.ts | 81 + .../activities/FlowChart/EdgeLayer.tsx | 172 + .../activities/FlowChart/FlowChartPanel.tsx | 300 ++ .../activities/FlowChart/NodeDetailsCard.tsx | 1214 +++++++ .../activities/FlowChart/PanZoomCanvas.tsx | 489 +++ .../activities/FlowChart/WorkflowRenderer.tsx | 358 +++ .../FlowChart/customEdges/GenericFlowEdge.tsx | 109 - .../customNodes/GenericAgentNode.tsx | 34 - .../customNodes/GenericArtifactNode.tsx | 26 - .../FlowChart/customNodes/GenericToolNode.tsx | 44 - .../FlowChart/customNodes/LLMNode.tsx | 41 - .../customNodes/OrchestratorAgentNode.tsx | 35 - .../FlowChart/customNodes/UserNode.tsx | 45 - .../FlowChart/edgeAnimationService.ts | 122 - .../components/activities/FlowChart/index.ts | 5 + .../activities/FlowChart/nodes/AgentNode.tsx | 292 ++ .../activities/FlowChart/nodes/LLMNode.tsx | 42 + .../activities/FlowChart/nodes/LoopNode.tsx | 159 + .../activities/FlowChart/nodes/MapNode.tsx | 181 ++ .../activities/FlowChart/nodes/SwitchNode.tsx | 72 + .../activities/FlowChart/nodes/ToolNode.tsx | 53 + .../activities/FlowChart/nodes/UserNode.tsx | 34 + .../FlowChart/nodes/WorkflowGroup.tsx | 417 +++ .../FlowChart/taskToFlowData.helpers.ts | 821 ----- .../activities/FlowChart/taskToFlowData.ts | 869 ----- .../FlowChart/utils/layoutEngine.ts | 1545 +++++++++ .../FlowChart/utils/nodeDetailsHelper.ts | 406 +++ .../activities/FlowChart/utils/types.ts | 132 + .../components/activities/FlowChartPanel.tsx | 401 --- .../activities/VisualizerStepCard.tsx | 218 +- .../src/lib/components/activities/index.ts | 2 +- .../activities/taskVisualizerProcessor.ts | 476 ++- .../components/agents/AgentDisplayCard.tsx | 20 +- .../src/lib/components/chat/ChatInputArea.tsx | 12 +- .../src/lib/components/chat/ChatMessage.tsx | 519 ++- .../src/lib/components/chat/ChatSidePanel.tsx | 81 +- .../src/lib/components/chat/Citation.tsx | 552 ++++ .../chat/ConnectionRequiredModal.tsx | 57 + .../components/chat/MessageHoverButtons.tsx | 26 +- .../lib/components/chat/PromptsCommand.tsx | 4 +- .../src/lib/components/chat/SessionList.tsx | 48 +- .../src/lib/components/chat/SessionSearch.tsx | 16 +- .../components/chat/artifact/ArtifactBar.tsx | 38 +- .../chat/artifact/ArtifactPreviewContent.tsx | 29 +- .../components/chat/file/ArtifactMessage.tsx | 67 +- .../lib/components/chat/file/FileDetails.tsx | 39 + .../lib/components/chat/file/FileLabel.tsx | 16 + .../lib/components/chat/file/ProjectBadge.tsx | 14 + .../src/lib/components/chat/file/index.ts | 2 + .../frontend/src/lib/components/chat/index.ts | 3 + .../chat/preview/ContentRenderer.tsx | 9 +- .../preview/Renderers/MarkdownRenderer.tsx | 33 +- .../chat/preview/Renderers/TextRenderer.tsx | 14 +- .../chat/preview/Renderers/index.ts | 1 + .../components/chat/preview/previewUtils.ts | 1 - .../lib/components/chat/rag/RAGInfoPanel.tsx | 541 ++++ .../selection/SelectableMessageContent.tsx | 2 +- .../chat/selection/TextSelectionContext.tsx | 4 + .../chat/selection/TextSelectionProvider.tsx | 13 +- .../lib/components/chat/selection/index.ts | 6 +- .../chat/selection/useTextSelection.tsx | 10 + .../components/common/ConfirmationDialog.tsx | 7 +- .../src/lib/components/common/ErrorLabel.tsx | 3 + .../src/lib/components/common/FileUpload.tsx | 183 ++ .../lib/components/common/MarkdownWrapper.tsx | 24 + .../components/common/StreamingMarkdown.tsx | 17 + .../src/lib/components/common/index.ts | 4 + .../frontend/src/lib/components/index.ts | 25 +- .../lib/components/jsonViewer/JSONViewer.tsx | 6 +- .../navigation/NavigationButton.tsx | 2 +- .../components/navigation/NavigationList.tsx | 12 +- .../lib/components/navigation/navigation.ts | 4 +- .../lib/components/pages/AgentMeshPage.tsx | 55 +- .../src/lib/components/pages/ChatPage.tsx | 28 +- .../src/lib/components/pages/PromptsPage.tsx | 2 +- .../projects/AddProjectFilesDialog.tsx | 83 +- .../projects/DeleteProjectFileDialog.tsx | 32 + .../components/projects/DocumentListItem.tsx | 98 - .../projects/EditFileDescriptionDialog.tsx | 63 +- .../components/projects/FileDetailsDialog.tsx | 32 +- .../components/projects/KnowledgeSection.tsx | 189 +- .../projects/ProjectDescription.tsx | 87 - .../projects/ProjectDetailPanel.tsx | 129 - .../components/projects/ProjectDetailView.tsx | 3 +- .../projects/ProjectFilesManager.tsx | 151 - .../lib/components/projects/ProjectHeader.tsx | 94 - .../projects/ProjectImportDialog.tsx | 286 +- .../lib/components/projects/ProjectList.tsx | 36 - .../components/projects/ProjectListItem.tsx | 64 - .../projects/ProjectListSidebar.tsx | 103 - .../projects/ProjectMetadataSidebar.tsx | 63 - .../projects/SystemPromptSection.tsx | 2 +- .../src/lib/components/projects/index.ts | 5 - .../components/prompts/PromptImportDialog.tsx | 363 +-- .../prompts/PromptTemplateBuilder.tsx | 4 +- .../prompts/TemplatePreviewPanel.tsx | 27 +- .../research/DeepResearchReportContent.tsx | 125 + .../components/research/ImageSearchGrid.tsx | 151 + .../research/InlineResearchProgress.tsx | 611 ++++ .../components/research/ResearchProgress.tsx | 170 + .../src/lib/components/research/index.ts | 2 + .../lib/components/ui/ViewWorkflowButton.tsx | 4 +- .../frontend/src/lib/components/ui/badge.tsx | 4 +- .../src/lib/components/ui/checkbox.tsx | 37 + .../src/lib/components/ui/dropdown-menu.tsx | 2 +- .../frontend/src/lib/components/ui/form.tsx | 221 +- .../frontend/src/lib/components/ui/index.ts | 3 + .../src/lib/components/ui/progress.tsx | 21 + .../src/lib/components/ui/stepper.tsx | 377 +++ .../src/lib/components/web/Citation.tsx | 218 ++ .../src/lib/components/web/Sources.tsx | 99 + .../lib/components/web/StackedFavicons.tsx | 80 + .../frontend/src/lib/components/web/index.ts | 3 + .../workflowVisualization/CanvasControls.tsx | 83 + .../IMPLEMENTATION_STATUS.md | 131 + .../InputMappingViewer.tsx | 208 ++ .../WorkflowDetailsSidePanel.tsx | 47 + .../workflowVisualization/WorkflowDiagram.tsx | 295 ++ .../WorkflowNodeDetailPanel.tsx | 593 ++++ .../WorkflowNodeRenderer.tsx | 157 + .../WorkflowVisualizationPage.tsx | 407 +++ .../workflowVisualization/edges/EdgeLayer.tsx | 87 + .../components/workflowVisualization/index.ts | 25 + .../workflowVisualization/nodes/AgentNode.tsx | 37 + .../nodes/ConditionPillNode.tsx | 62 + .../workflowVisualization/nodes/EndNode.tsx | 29 + .../workflowVisualization/nodes/LoopNode.tsx | 145 + .../workflowVisualization/nodes/MapNode.tsx | 118 + .../workflowVisualization/nodes/StartNode.tsx | 30 + .../nodes/SwitchNode.tsx | 76 + .../nodes/WorkflowRefNode.tsx | 70 + .../utils/expressionParser.ts | 100 + .../utils/layoutEngine.ts | 1100 +++++++ .../workflowVisualization/utils/types.ts | 173 + .../workflows/WorkflowDetailPanel.tsx | 287 ++ .../lib/components/workflows/WorkflowList.tsx | 283 ++ .../workflows/WorkflowOnboardingBanner.tsx | 43 + .../src/lib/components/workflows/index.ts | 3 + .../frontend/src/lib/constants/streaming.ts | 26 + .../frontend/src/lib/contexts/ChatContext.ts | 20 +- .../frontend/src/lib/contexts/TaskContext.ts | 1 + client/webui/frontend/src/lib/hooks/index.ts | 5 + .../src/lib/hooks/useArtifactOperations.ts | 293 ++ .../src/lib/hooks/useArtifactPreview.ts | 242 ++ .../src/lib/hooks/useArtifactRendering.ts | 12 +- .../frontend/src/lib/hooks/useArtifacts.ts | 17 +- .../src/lib/hooks/useProjectArtifacts.ts | 17 +- .../src/lib/hooks/useStreamingAnimation.ts | 58 + .../src/lib/hooks/useStreamingSpeed.ts | 72 + .../frontend/src/lib/hooks/useTextToSpeech.ts | 64 +- client/webui/frontend/src/lib/index.css | 83 +- .../src/lib/providers/AuthProvider.tsx | 24 +- .../src/lib/providers/ChatProvider.tsx | 934 +++--- .../src/lib/providers/ConfigProvider.tsx | 20 +- .../frontend/src/lib/providers/QueryClient.ts | 13 + .../src/lib/providers/QueryProvider.tsx | 11 + .../src/lib/providers/TaskProvider.tsx | 37 +- .../webui/frontend/src/lib/providers/index.ts | 2 + .../frontend/src/lib/types/activities.ts | 107 +- client/webui/frontend/src/lib/types/be.ts | 2 + client/webui/frontend/src/lib/types/fe.ts | 52 +- .../webui/frontend/src/lib/types/storage.ts | 12 +- .../frontend/src/lib/utils/agentUtils.ts | 107 + .../webui/frontend/src/lib/utils/citations.ts | 315 ++ .../src/lib/utils/deepResearchUtils.ts | 62 + .../webui/frontend/src/lib/utils/download.ts | 32 +- client/webui/frontend/src/lib/utils/file.ts | 90 + client/webui/frontend/src/lib/utils/guard.ts | 30 + client/webui/frontend/src/lib/utils/index.ts | 6 + .../src/lib/utils/sourceUrlHelpers.ts | 128 + .../frontend/src/lib/utils/taskMigration.ts | 99 + client/webui/frontend/src/lib/utils/url.ts | 33 + client/webui/frontend/src/router.tsx | 12 +- .../Prompts/PromptImportDialog.stories.tsx | 2 +- .../Prompts/PromptTemplateBuilder.stories.tsx | 43 + .../src/stories/SettingsDialog.stories.tsx | 4 +- .../frontend/src/stories/Stepper.stories.tsx | 174 + .../src/stories/mocks/MockChatProvider.tsx | 8 +- .../src/stories/mocks/MockTaskProvider.tsx | 1 + .../mocks/MockTextSelectionProvider.tsx | 2 +- client/webui/frontend/vite.config.ts | 4 +- client/webui/frontend/vite.lib.config.ts | 1 + client/webui/frontend/vitest.config.ts | 14 +- config_portal/backend/common.py | 10 + .../frontend/app/components/AddAgentFlow.tsx | 20 +- .../components/steps/agent/AgentToolsStep.tsx | 719 ++++- .../app/components/ui/KeyValueInput.tsx | 148 + .../frontend/app/components/ui/ListInput.tsx | 163 + config_portal/frontend/package-lock.json | 394 +-- config_portal/frontend/package.json | 6 +- context7.json | 42 +- .../components/builtin-tools/builtin-tools.md | 61 +- .../components/builtin-tools/image-tools.md | 151 + .../builtin-tools/research-tools.md | 268 ++ docs/docs/documentation/components/cli.md | 58 +- .../documentation/components/components.md | 4 + .../documentation/components/workflows.md | 406 +++ docs/docs/documentation/deploying/logging.md | 16 +- .../developing/creating-python-tools.md | 80 +- .../developing/creating-workflows.md | 950 ++++++ .../documentation/developing/developing.md | 4 + .../documentation/enterprise/agent-builder.md | 4 + .../enterprise/connectors/connectors.md | 89 +- .../enterprise/connectors/mcp-connectors.md | 236 ++ .../connectors/openapi-connectors.md | 177 + .../enterprise/connectors/sql-connectors.md | 181 ++ .../getting-started/getting-started.md | 4 + .../artifact-storage.md | 54 + .../migrations/platform-service-split.md | 120 + docs/docs/documentation/vibe_coding.md | 116 + examples/agents/all_node_types_workflow.yaml | 1256 ++++++++ .../agents/complex_branching_workflow.yaml | 521 +++ examples/agents/deep_research_agent.yaml | 274 ++ examples/agents/jira_bug_triage_workflow.yaml | 569 ++++ examples/agents/new_node_types_test.yaml | 373 +++ examples/agents/orchestrator_example.yaml | 2 +- examples/agents/research_agent.yaml | 231 ++ examples/agents/simple_nested_test.yaml | 158 + examples/agents/test_agent_example.yaml | 100 + examples/agents/web_search_agent.yaml | 150 + .../agents/workflow_to_workflow_example.yaml | 767 +++++ examples/gateways/webui_gateway_example.yaml | 40 +- pyproject.toml | 9 +- src/__init__.py | 1 + src/solace_agent_mesh/agent/adk/callbacks.py | 493 ++- .../agent/adk/embed_resolving_mcp_toolset.py | 103 +- .../agent/adk/intelligent_mcp_callbacks.py | 57 +- .../agent/adk/models/lite_llm.py | 109 +- src/solace_agent_mesh/agent/adk/runner.py | 43 +- src/solace_agent_mesh/agent/adk/setup.py | 104 +- .../agent/protocol/event_handlers.py | 210 +- .../agent/proxies/base/component.py | 4 +- src/solace_agent_mesh/agent/sac/app.py | 61 +- src/solace_agent_mesh/agent/sac/component.py | 215 +- .../sac/structured_invocation/__init__.py | 0 .../sac/structured_invocation/handler.py | 1169 +++++++ .../sac/structured_invocation/validator.py | 29 + .../agent/sac/task_execution_context.py | 31 + src/solace_agent_mesh/agent/tools/__init__.py | 2 + .../agent/tools/deep_research_tools.py | 2161 +++++++++++++ .../agent/tools/web_search_tools.py | 279 ++ .../agent/tools/web_tools.py | 142 +- .../agent/tools/workflow_tool.py | 426 +++ .../agent/utils/artifact_helpers.py | 95 +- src/solace_agent_mesh/common/__init__.py | 10 + src/solace_agent_mesh/common/a2a/__init__.py | 21 +- src/solace_agent_mesh/common/a2a/protocol.py | 17 +- src/solace_agent_mesh/common/a2a/types.py | 9 + src/solace_agent_mesh/common/a2a/utils.py | 89 + .../common/agent_card_utils.py | 35 + .../common/agent_registry.py | 123 +- src/solace_agent_mesh/common/base_registry.py | 246 ++ src/solace_agent_mesh/common/constants.py | 6 +- src/solace_agent_mesh/common/data_parts.py | 321 +- .../common/gateway_registry.py | 164 + src/solace_agent_mesh/common/rag_dto.py | 156 + .../common/sac/sam_component_base.py | 13 +- .../common/schemas/workflow_schema.json | 562 ++++ .../common/utils/embeds/resolver.py | 21 + .../common/utils/markdown_to_speech.py | 357 +++ .../utils/templates/template_resolver.py | 6 +- src/solace_agent_mesh/gateway/adapter/base.py | 29 +- .../gateway/adapter/types.py | 9 + src/solace_agent_mesh/gateway/base/app.py | 47 +- .../gateway/base/auth_interface.py | 103 + .../gateway/base/component.py | 415 ++- .../gateway/generic/component.py | 266 +- src/solace_agent_mesh/gateway/http_sse/app.py | 2 +- .../gateway/http_sse/component.py | 41 +- .../gateway/http_sse/main.py | 82 +- .../gateway/http_sse/routers/artifacts.py | 30 +- .../gateway/http_sse/routers/auth.py | 172 +- .../gateway/http_sse/routers/config.py | 2 +- .../gateway/http_sse/routers/speech.py | 92 +- .../gateway/http_sse/routers/sse.py | 32 + .../gateway/http_sse/routers/tasks.py | 33 +- .../http_sse/services/audio_service.py | 126 +- .../http_sse/services/session_service.py | 10 +- .../http_sse/services/task_logger_service.py | 139 +- .../http_sse/utils/artifact_copy_utils.py | 80 - .../services/platform/api/dependencies.py | 15 + .../services/platform/api/main.py | 107 +- .../services/platform/api/routers/__init__.py | 15 +- .../platform/api/routers/health_router.py | 31 + .../services/platform/app.py | 13 +- .../services/platform/component.py | 331 +- .../shared/auth/middleware.py | 83 +- .../tools/web_search/__init__.py | 18 + .../tools/web_search/base.py | 84 + .../tools/web_search/google_search.py | 247 ++ .../tools/web_search/models.py | 99 + src/solace_agent_mesh/workflow/__init__.py | 3 + .../workflow/agent_caller.py | 518 +++ src/solace_agent_mesh/workflow/app.py | 671 ++++ src/solace_agent_mesh/workflow/component.py | 957 ++++++ .../workflow/dag_executor.py | 1304 ++++++++ .../workflow/flow_control/__init__.py | 0 .../workflow/flow_control/conditional.py | 220 ++ .../workflow/protocol/__init__.py | 0 .../workflow/protocol/event_handlers.py | 430 +++ src/solace_agent_mesh/workflow/utils.py | 55 + .../workflow/workflow_execution_context.py | 120 + templates/proxy_template.yaml | 62 + templates/webui.yaml | 9 +- .../apis/persistence/test_tasks_api.py | 71 + .../apis/platform/test_health_endpoint.py | 6 +- tests/integration/conftest.py | 656 ++++ .../test_mcp_save_forced_by_truncation.yaml | 26 +- .../mcp/test_mcp_schema_inference_depth.yaml | 91 + ...st_mcp_schema_inference_depth_shallow.yaml | 80 + ...test_mcp_trigger_above_all_thresholds.yaml | 29 +- .../workflows/test_loop_workflow.yaml | 58 + .../workflows/test_map_workflow.yaml | 57 + .../test_simple_two_node_workflow.yaml | 74 + .../test_switch_workflow_create_case.yaml | 43 + .../test_switch_workflow_default_case.yaml | 43 + .../test_workflow_with_structured_input.yaml | 76 + .../agent/test_deep_research_tool.py | 642 ++++ .../agent/test_web_search_tools.py | 491 +++ .../gateway/test_gateway_card_publishing.py | 309 ++ .../gateway/test_gateway_discovery.py | 377 +++ .../test_workflow_errors.py | 2008 ++++++++++++ .../test_workflow_invoke_workflow.py | 370 +++ .../platform_service_factory.py | 10 +- .../test_lite_llm_schema_serialization.py | 39 + .../adk/models/test_oauth2_token_manager.py | 2 +- .../adk/test_execute_tool_with_audit_logs.py | 136 + .../adk/test_intelligent_mcp_callbacks.py | 159 + .../agent/adk/test_openapi_audit_callback.py | 279 ++ .../agent/tools/test_deep_research_tools.py | 2142 +++++++++++++ .../unit/agent/utils/test_artifact_helpers.py | 114 + .../cli/commands/add_cmd/test_proxy_cmd.py | 212 ++ tests/unit/cli/commands/test_tools_cmd.py | 521 +++ tests/unit/cli/test_main.py | 15 +- tests/unit/common/a2a/test_a2a_utils.py | 258 ++ tests/unit/common/test_agent_registry.py | 224 +- tests/unit/common/test_base_registry.py | 439 +++ tests/unit/common/test_gateway_registry.py | 706 ++++ .../common/utils/test_markdown_to_speech.py | 575 ++++ .../adapter/test_agent_registry_handlers.py | 393 +++ .../adapter/test_types_session_behavior.py | 286 ++ .../unit/gateway/base/test_auth_interface.py | 272 ++ .../unit/gateway/base/test_component_auth.py | 294 ++ .../base/test_gateway_card_building.py | 331 ++ .../test_component_auth_and_callbacks.py | 233 ++ .../unit/gateway/http_sse/routers/__init__.py | 0 .../http_sse/routers/test_artifacts.py | 66 +- .../gateway/http_sse/routers/test_auth.py | 74 +- .../unit/gateway/http_sse/routers/test_sse.py | 300 ++ .../services/test_task_logger_service.py | 921 ++++++ .../gateway/http_sse/test_http_sse_main.py | 191 ++ .../http_sse/test_oauth_utils_integration.py | 152 + .../utils/test_artifact_copy_utils.py | 455 +++ tests/unit/services/platform/__init__.py | 0 .../services/platform/routers/__init__.py | 0 .../platform/test_cors_auto_construction.py | 50 +- .../tools/web_search/test_google_search.py | 354 ++ .../web_search/test_web_search_tool_unit.py | 384 +++ .../web_search/test_web_search_tools_unit.py | 384 +++ tests/unit/workflow/__init__.py | 1 + tests/unit/workflow/test_agent_caller.py | 311 ++ .../workflow/test_conditional_evaluation.py | 366 +++ tests/unit/workflow/test_dag_logic.py | 517 +++ .../workflow/test_safe_eval_expression.py | 705 ++++ .../unit/workflow/test_template_resolution.py | 338 ++ tests/unit/workflow/test_utils.py | 130 + .../unit/workflow/test_workflow_agent_card.py | 467 +++ tests/unit/workflow/test_workflow_models.py | 449 +++ uv.lock | 205 +- 390 files changed, 63835 insertions(+), 7418 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 cli/commands/add_cmd/proxy_cmd.py create mode 100644 cli/commands/tools_cmd.py create mode 100644 client/webui/frontend/src/lib/api/projects/hooks.ts create mode 100644 client/webui/frontend/src/lib/api/projects/index.ts create mode 100644 client/webui/frontend/src/lib/api/projects/keys.ts create mode 100644 client/webui/frontend/src/lib/api/projects/service.ts create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/EdgeLayer.tsx create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/FlowChartPanel.tsx create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/NodeDetailsCard.tsx create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/PanZoomCanvas.tsx create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/WorkflowRenderer.tsx delete mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/customEdges/GenericFlowEdge.tsx delete mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/GenericAgentNode.tsx delete mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/GenericArtifactNode.tsx delete mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/GenericToolNode.tsx delete mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/LLMNode.tsx delete mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/OrchestratorAgentNode.tsx delete mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/UserNode.tsx delete mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/edgeAnimationService.ts create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/index.ts create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/nodes/AgentNode.tsx create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/nodes/LLMNode.tsx create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/nodes/LoopNode.tsx create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/nodes/MapNode.tsx create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/nodes/SwitchNode.tsx create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/nodes/ToolNode.tsx create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/nodes/UserNode.tsx create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/nodes/WorkflowGroup.tsx delete mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/taskToFlowData.helpers.ts delete mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/taskToFlowData.ts create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/utils/layoutEngine.ts create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/utils/nodeDetailsHelper.ts create mode 100644 client/webui/frontend/src/lib/components/activities/FlowChart/utils/types.ts delete mode 100644 client/webui/frontend/src/lib/components/activities/FlowChartPanel.tsx create mode 100644 client/webui/frontend/src/lib/components/chat/Citation.tsx create mode 100644 client/webui/frontend/src/lib/components/chat/ConnectionRequiredModal.tsx create mode 100644 client/webui/frontend/src/lib/components/chat/file/FileDetails.tsx create mode 100644 client/webui/frontend/src/lib/components/chat/file/FileLabel.tsx create mode 100644 client/webui/frontend/src/lib/components/chat/file/ProjectBadge.tsx create mode 100644 client/webui/frontend/src/lib/components/chat/rag/RAGInfoPanel.tsx create mode 100644 client/webui/frontend/src/lib/components/chat/selection/TextSelectionContext.tsx create mode 100644 client/webui/frontend/src/lib/components/chat/selection/useTextSelection.tsx create mode 100644 client/webui/frontend/src/lib/components/common/ErrorLabel.tsx create mode 100644 client/webui/frontend/src/lib/components/common/FileUpload.tsx create mode 100644 client/webui/frontend/src/lib/components/common/MarkdownWrapper.tsx create mode 100644 client/webui/frontend/src/lib/components/common/StreamingMarkdown.tsx create mode 100644 client/webui/frontend/src/lib/components/projects/DeleteProjectFileDialog.tsx delete mode 100644 client/webui/frontend/src/lib/components/projects/DocumentListItem.tsx delete mode 100644 client/webui/frontend/src/lib/components/projects/ProjectDescription.tsx delete mode 100644 client/webui/frontend/src/lib/components/projects/ProjectDetailPanel.tsx delete mode 100644 client/webui/frontend/src/lib/components/projects/ProjectFilesManager.tsx delete mode 100644 client/webui/frontend/src/lib/components/projects/ProjectHeader.tsx delete mode 100644 client/webui/frontend/src/lib/components/projects/ProjectList.tsx delete mode 100644 client/webui/frontend/src/lib/components/projects/ProjectListItem.tsx delete mode 100644 client/webui/frontend/src/lib/components/projects/ProjectListSidebar.tsx delete mode 100644 client/webui/frontend/src/lib/components/projects/ProjectMetadataSidebar.tsx create mode 100644 client/webui/frontend/src/lib/components/research/DeepResearchReportContent.tsx create mode 100644 client/webui/frontend/src/lib/components/research/ImageSearchGrid.tsx create mode 100644 client/webui/frontend/src/lib/components/research/InlineResearchProgress.tsx create mode 100644 client/webui/frontend/src/lib/components/research/ResearchProgress.tsx create mode 100644 client/webui/frontend/src/lib/components/research/index.ts create mode 100644 client/webui/frontend/src/lib/components/ui/checkbox.tsx create mode 100644 client/webui/frontend/src/lib/components/ui/progress.tsx create mode 100644 client/webui/frontend/src/lib/components/ui/stepper.tsx create mode 100644 client/webui/frontend/src/lib/components/web/Citation.tsx create mode 100644 client/webui/frontend/src/lib/components/web/Sources.tsx create mode 100644 client/webui/frontend/src/lib/components/web/StackedFavicons.tsx create mode 100644 client/webui/frontend/src/lib/components/web/index.ts create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/CanvasControls.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/IMPLEMENTATION_STATUS.md create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/InputMappingViewer.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/WorkflowDetailsSidePanel.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/WorkflowDiagram.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/WorkflowNodeDetailPanel.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/WorkflowNodeRenderer.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/WorkflowVisualizationPage.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/edges/EdgeLayer.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/index.ts create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/nodes/AgentNode.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/nodes/ConditionPillNode.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/nodes/EndNode.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/nodes/LoopNode.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/nodes/MapNode.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/nodes/StartNode.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/nodes/SwitchNode.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/nodes/WorkflowRefNode.tsx create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/utils/expressionParser.ts create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/utils/layoutEngine.ts create mode 100644 client/webui/frontend/src/lib/components/workflowVisualization/utils/types.ts create mode 100644 client/webui/frontend/src/lib/components/workflows/WorkflowDetailPanel.tsx create mode 100644 client/webui/frontend/src/lib/components/workflows/WorkflowList.tsx create mode 100644 client/webui/frontend/src/lib/components/workflows/WorkflowOnboardingBanner.tsx create mode 100644 client/webui/frontend/src/lib/components/workflows/index.ts create mode 100644 client/webui/frontend/src/lib/constants/streaming.ts create mode 100644 client/webui/frontend/src/lib/hooks/useArtifactOperations.ts create mode 100644 client/webui/frontend/src/lib/hooks/useArtifactPreview.ts create mode 100644 client/webui/frontend/src/lib/hooks/useStreamingAnimation.ts create mode 100644 client/webui/frontend/src/lib/hooks/useStreamingSpeed.ts create mode 100644 client/webui/frontend/src/lib/providers/QueryClient.ts create mode 100644 client/webui/frontend/src/lib/providers/QueryProvider.tsx create mode 100644 client/webui/frontend/src/lib/utils/agentUtils.ts create mode 100644 client/webui/frontend/src/lib/utils/citations.ts create mode 100644 client/webui/frontend/src/lib/utils/deepResearchUtils.ts create mode 100644 client/webui/frontend/src/lib/utils/file.ts create mode 100644 client/webui/frontend/src/lib/utils/sourceUrlHelpers.ts create mode 100644 client/webui/frontend/src/lib/utils/taskMigration.ts create mode 100644 client/webui/frontend/src/lib/utils/url.ts create mode 100644 client/webui/frontend/src/stories/Stepper.stories.tsx create mode 100644 config_portal/frontend/app/components/ui/KeyValueInput.tsx create mode 100644 config_portal/frontend/app/components/ui/ListInput.tsx create mode 100644 docs/docs/documentation/components/builtin-tools/image-tools.md create mode 100644 docs/docs/documentation/components/builtin-tools/research-tools.md create mode 100644 docs/docs/documentation/components/workflows.md create mode 100644 docs/docs/documentation/developing/creating-workflows.md create mode 100644 docs/docs/documentation/enterprise/connectors/mcp-connectors.md create mode 100644 docs/docs/documentation/enterprise/connectors/openapi-connectors.md create mode 100644 docs/docs/documentation/enterprise/connectors/sql-connectors.md create mode 100644 docs/docs/documentation/migrations/platform-service-split.md create mode 100644 docs/docs/documentation/vibe_coding.md create mode 100644 examples/agents/all_node_types_workflow.yaml create mode 100644 examples/agents/complex_branching_workflow.yaml create mode 100644 examples/agents/deep_research_agent.yaml create mode 100644 examples/agents/jira_bug_triage_workflow.yaml create mode 100644 examples/agents/new_node_types_test.yaml create mode 100644 examples/agents/research_agent.yaml create mode 100644 examples/agents/simple_nested_test.yaml create mode 100644 examples/agents/web_search_agent.yaml create mode 100644 examples/agents/workflow_to_workflow_example.yaml create mode 100644 src/solace_agent_mesh/agent/sac/structured_invocation/__init__.py create mode 100644 src/solace_agent_mesh/agent/sac/structured_invocation/handler.py create mode 100644 src/solace_agent_mesh/agent/sac/structured_invocation/validator.py create mode 100644 src/solace_agent_mesh/agent/tools/deep_research_tools.py create mode 100644 src/solace_agent_mesh/agent/tools/web_search_tools.py create mode 100644 src/solace_agent_mesh/agent/tools/workflow_tool.py create mode 100644 src/solace_agent_mesh/common/a2a/utils.py create mode 100644 src/solace_agent_mesh/common/agent_card_utils.py create mode 100644 src/solace_agent_mesh/common/base_registry.py create mode 100644 src/solace_agent_mesh/common/gateway_registry.py create mode 100644 src/solace_agent_mesh/common/rag_dto.py create mode 100644 src/solace_agent_mesh/common/schemas/workflow_schema.json create mode 100644 src/solace_agent_mesh/common/utils/markdown_to_speech.py create mode 100644 src/solace_agent_mesh/gateway/base/auth_interface.py create mode 100644 src/solace_agent_mesh/services/platform/api/routers/health_router.py create mode 100644 src/solace_agent_mesh/tools/web_search/__init__.py create mode 100644 src/solace_agent_mesh/tools/web_search/base.py create mode 100644 src/solace_agent_mesh/tools/web_search/google_search.py create mode 100644 src/solace_agent_mesh/tools/web_search/models.py create mode 100644 src/solace_agent_mesh/workflow/__init__.py create mode 100644 src/solace_agent_mesh/workflow/agent_caller.py create mode 100644 src/solace_agent_mesh/workflow/app.py create mode 100644 src/solace_agent_mesh/workflow/component.py create mode 100644 src/solace_agent_mesh/workflow/dag_executor.py create mode 100644 src/solace_agent_mesh/workflow/flow_control/__init__.py create mode 100644 src/solace_agent_mesh/workflow/flow_control/conditional.py create mode 100644 src/solace_agent_mesh/workflow/protocol/__init__.py create mode 100644 src/solace_agent_mesh/workflow/protocol/event_handlers.py create mode 100644 src/solace_agent_mesh/workflow/utils.py create mode 100644 src/solace_agent_mesh/workflow/workflow_execution_context.py create mode 100644 templates/proxy_template.yaml create mode 100644 tests/integration/scenarios_declarative/test_data/mcp/test_mcp_schema_inference_depth.yaml create mode 100644 tests/integration/scenarios_declarative/test_data/mcp/test_mcp_schema_inference_depth_shallow.yaml create mode 100644 tests/integration/scenarios_declarative/test_data/workflows/test_loop_workflow.yaml create mode 100644 tests/integration/scenarios_declarative/test_data/workflows/test_map_workflow.yaml create mode 100644 tests/integration/scenarios_declarative/test_data/workflows/test_simple_two_node_workflow.yaml create mode 100644 tests/integration/scenarios_declarative/test_data/workflows/test_switch_workflow_create_case.yaml create mode 100644 tests/integration/scenarios_declarative/test_data/workflows/test_switch_workflow_default_case.yaml create mode 100644 tests/integration/scenarios_declarative/test_data/workflows/test_workflow_with_structured_input.yaml create mode 100644 tests/integration/scenarios_programmatic/agent/test_deep_research_tool.py create mode 100644 tests/integration/scenarios_programmatic/agent/test_web_search_tools.py create mode 100644 tests/integration/scenarios_programmatic/gateway/test_gateway_card_publishing.py create mode 100644 tests/integration/scenarios_programmatic/gateway/test_gateway_discovery.py create mode 100644 tests/integration/scenarios_programmatic/test_workflow_errors.py create mode 100644 tests/integration/scenarios_programmatic/test_workflow_invoke_workflow.py create mode 100644 tests/unit/agent/adk/test_execute_tool_with_audit_logs.py create mode 100644 tests/unit/agent/adk/test_intelligent_mcp_callbacks.py create mode 100644 tests/unit/agent/adk/test_openapi_audit_callback.py create mode 100644 tests/unit/agent/tools/test_deep_research_tools.py create mode 100644 tests/unit/cli/commands/add_cmd/test_proxy_cmd.py create mode 100644 tests/unit/cli/commands/test_tools_cmd.py create mode 100644 tests/unit/common/a2a/test_a2a_utils.py create mode 100644 tests/unit/common/test_base_registry.py create mode 100644 tests/unit/common/test_gateway_registry.py create mode 100644 tests/unit/common/utils/test_markdown_to_speech.py create mode 100644 tests/unit/gateway/adapter/test_agent_registry_handlers.py create mode 100644 tests/unit/gateway/adapter/test_types_session_behavior.py create mode 100644 tests/unit/gateway/base/test_auth_interface.py create mode 100644 tests/unit/gateway/base/test_component_auth.py create mode 100644 tests/unit/gateway/base/test_gateway_card_building.py create mode 100644 tests/unit/gateway/generic/test_component_auth_and_callbacks.py create mode 100644 tests/unit/gateway/http_sse/routers/__init__.py create mode 100644 tests/unit/gateway/http_sse/routers/test_sse.py create mode 100644 tests/unit/gateway/http_sse/services/test_task_logger_service.py create mode 100644 tests/unit/gateway/http_sse/test_oauth_utils_integration.py create mode 100644 tests/unit/gateway/http_sse/utils/test_artifact_copy_utils.py create mode 100644 tests/unit/services/platform/__init__.py create mode 100644 tests/unit/services/platform/routers/__init__.py create mode 100644 tests/unit/tools/web_search/test_google_search.py create mode 100644 tests/unit/tools/web_search/test_web_search_tool_unit.py create mode 100644 tests/unit/tools/web_search/test_web_search_tools_unit.py create mode 100644 tests/unit/workflow/__init__.py create mode 100644 tests/unit/workflow/test_agent_caller.py create mode 100644 tests/unit/workflow/test_conditional_evaluation.py create mode 100644 tests/unit/workflow/test_dag_logic.py create mode 100644 tests/unit/workflow/test_safe_eval_expression.py create mode 100644 tests/unit/workflow/test_template_resolution.py create mode 100644 tests/unit/workflow/test_utils.py create mode 100644 tests/unit/workflow/test_workflow_agent_card.py create mode 100644 tests/unit/workflow/test_workflow_models.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..471f9d5d2 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,22 @@ +### What is the purpose of this change? + + Brief summary - what problem does this solve? + +### How was this change implemented? + + High-level approach - what files/components changed and why? + +### Key Design Decisions _(optional - delete if not applicable)_ + + Why did you choose this approach over alternatives? + +### How was this change tested? + +- [ ] Manual testing: [describe scenarios] +- [ ] Unit tests: [new/modified tests] +- [ ] Integration tests: [if applicable] +- [ ] Known limitations: [what wasn't tested] + +### Is there anything the reviewers should focus on/be aware of? + + Special attention areas, potential risks, or open questions diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1914b9ee9..66372eb9e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -247,6 +247,12 @@ jobs: echo "AMD64 Tag: ${AMD_TAG}" echo "ARM64 Tag: ${ARM_TAG}" + # OCI annotations to ensure unique manifest digest per commit + # This allows Prisma Cloud and other tools to index each tag uniquely + # while still benefiting from layer caching + COMMIT_SHA="${{ needs.prepare-metadata.outputs.commit_hash }}" + BUILD_TIME="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" + # Convert comma-separated tags string to array and create manifest for each IFS=',' read -ra TAGS <<< "${{ steps.image_tags.outputs.tags }}" for TAG in "${TAGS[@]}"; do @@ -255,6 +261,9 @@ jobs: TAG=$(echo "$TAG" | xargs) echo "Creating manifest for tag: $TAG" docker buildx imagetools create \ + --annotation "index:org.opencontainers.image.revision=${COMMIT_SHA}" \ + --annotation "index:org.opencontainers.image.created=${BUILD_TIME}" \ + --annotation "index:org.opencontainers.image.source=https://github.com/${{ github.repository }}" \ --tag "$TAG" \ "$AMD_TAG" \ "$ARM_TAG" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6142d751a..888ffb7b5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,11 @@ name: Release (PyPI & Docker) on: workflow_dispatch: inputs: + ref: + type: string + required: true + description: "Git ref to release from" + default: "main" version: type: choice required: true @@ -10,6 +15,12 @@ on: - patch - minor - major + default: patch + exact_version: + type: string + required: false + description: "Exact version to release (e.g., 1.13.2). Overrides 'version' input if provided." + default: "" skip_security_checks: type: boolean required: false @@ -39,6 +50,7 @@ jobs: uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 # Need enough history to find last non-skip-ci commit + ref: ${{ github.event.inputs.ref }} - name: Find Last RC-Tested Commit id: find-commit @@ -153,6 +165,7 @@ jobs: with: fetch-depth: 0 ssh-key: ${{ secrets.COMMIT_KEY }} + ref: ${{ github.event.inputs.ref }} - name: Setup Node.js uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 @@ -171,7 +184,7 @@ jobs: id: prep uses: SolaceDev/solace-public-workflows/.github/actions/hatch-release-prep@main with: - version: ${{ github.event.inputs.version }} + version: ${{ github.event.inputs.exact_version || github.event.inputs.version }} # Publish using Trusted Publishing - must be directly in workflow, not in composite action # See: https://docs.pypi.org/trusted-publishers/using-a-publisher/ @@ -191,6 +204,7 @@ jobs: build_and_push_docker: name: Build and Push to DockerHub needs: release + if: always() && (needs.release.result == 'success') uses: SolaceLabs/solace-agent-mesh/.github/workflows/build-push-dockerhub.yml@main with: version: ${{ needs.release.outputs.new_version }} diff --git a/.github/workflows/ui-ci.yml b/.github/workflows/ui-ci.yml index 6d3498641..9a0a1e0c2 100644 --- a/.github/workflows/ui-ci.yml +++ b/.github/workflows/ui-ci.yml @@ -5,14 +5,10 @@ on: push: branches: - "main" - paths: - - "client/webui/frontend/**" pull_request: types: [opened, synchronize] branches: - "main" - paths: - - "client/webui/frontend/**" permissions: contents: write @@ -25,13 +21,30 @@ permissions: repository-projects: read jobs: + check-paths: + name: "Check if UI files changed" + runs-on: ubuntu-latest + outputs: + should-run: ${{ steps.filter.outputs.ui }} + steps: + - name: Checkout code + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + + - name: Check for UI changes + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: filter + with: + filters: | + ui: + - 'client/webui/frontend/**' + validate-conventional-commit: name: "Validate Conventional Commit" runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 @@ -63,13 +76,15 @@ jobs: ui-build-and-test: name: "Build and Test UI" + needs: check-paths + if: needs.check-paths.outputs.should-run == 'true' runs-on: ubuntu-latest defaults: run: working-directory: client/webui/frontend steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: fetch-depth: 0 @@ -218,11 +233,30 @@ jobs: python whitesource_vulnerability_checker.py " + ui-ci-status: + name: "UI CI Status" + runs-on: ubuntu-latest + needs: [check-paths, ui-build-and-test] + if: always() + steps: + - name: Check build status + run: | + if [[ "${{ needs.check-paths.outputs.should-run }}" == "false" ]]; then + echo "UI files not changed, skipping UI build and tests" + exit 0 + elif [[ "${{ needs.ui-build-and-test.result }}" == "success" ]]; then + echo "UI build/tests passed" + exit 0 + else + echo "UI build/tests failed" + exit 1 + fi + bump-version: - needs: ui-build-and-test + needs: [check-paths, ui-build-and-test] name: "Bump UI Version" runs-on: ubuntu-latest - if: github.event_name == 'push' && github.ref_name == github.event.repository.default_branch + if: github.event_name == 'push' && github.ref_name == github.event.repository.default_branch && needs.check-paths.outputs.should-run == 'true' outputs: new-tag: ${{ steps.bump.outputs.newTag }} defaults: @@ -230,7 +264,7 @@ jobs: working-directory: client/webui/frontend steps: - name: "Checkout source code" - uses: "actions/checkout@v4" + uses: "actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd" # v5.0.1 with: ref: ${{ github.ref }} token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 156d0c454..c24d92d44 100644 --- a/.gitignore +++ b/.gitignore @@ -146,7 +146,8 @@ cython_debug/ # PyPI configuration file .pypirc tmp -.sam +.sam +*.key playground.py # VS Code @@ -168,3 +169,4 @@ client/webui/frontend/static/ui-version.json # workaround requirements.txt not working in ci requirements.txt data/artifacts/ +CLAUDE.md \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 0a65a8163..7c5e0adf0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,10 +8,10 @@ "name": "SAM", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "python": "${workspaceFolder}/.venv/bin/python", "console": "integratedTerminal", - "args": "--envfile .env examples/agents/orchestrator_example.yaml examples/agents/a2a_agents_example.yaml examples/gateways/webui_gateway_example.yaml examples/services/platform_service_example.yaml", + "args": "run examples/agents/orchestrator_example.yaml examples/agents/a2a_agents_example.yaml examples/gateways/webui_gateway_example.yaml examples/services/platform_service_example.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -21,9 +21,9 @@ "name": "SAM (Agents Only)", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/agents/orchestrator_example.yaml examples/agents/a2a_agents_example.yaml examples/agents/a2a_multimodal_example.yaml", + "args": "run examples/agents/orchestrator_example.yaml examples/agents/a2a_agents_example.yaml examples/agents/a2a_multimodal_example.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -33,21 +33,45 @@ "name": "SAM (test)", "type": "debugpy", "request": "launch", + "module": "cli.main", + "console": "integratedTerminal", + "args": "run examples/agents/orchestrator_example.yaml examples/agents/a2a_agents_example.yaml examples/gateways/webui_gateway_example.yaml examples/agents/test_agent_example.yaml examples/agents/a2a_multimodal_example.yaml", + "justMyCode": false, + "env": { + "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" + } + }, + { + "name": "SAM (workflow)", + "type": "debugpy", + "request": "launch", "module": "solace_ai_connector.main", "console": "integratedTerminal", - "args": "--envfile .env examples/agents/orchestrator_example.yaml examples/agents/a2a_agents_example.yaml examples/gateways/webui_gateway_example.yaml examples/agents/test_agent_example.yaml examples/agents/a2a_multimodal_example.yaml", + "args": "--envfile .env examples/agents/orchestrator_example.yaml examples/agents/test_agent_example.yaml examples/agents/jira_bug_triage_workflow.yaml examples/gateways/webui_gateway_example.yaml examples/agents/advanced_workflow_test.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" } }, { - "name": "SAM (preset)", + "name": "SAM (new node types)", "type": "debugpy", "request": "launch", "module": "solace_ai_connector.main", "console": "integratedTerminal", - "args": "--envfile .env preset/agents/basic/main_orchestrator.yaml preset/agents/basic/webui.yaml preset/agents/markitdown_agents.yaml preset/agents/mermaid_agents.yaml preset/agents/web_agents.yaml", + "args": "--envfile .env examples/agents/orchestrator_example.yaml examples/gateways/webui_gateway_example.yaml examples/agents/all_node_types_workflow.yaml examples/agents/complex_branching_workflow.yaml examples/agents/workflow_to_workflow_example.yaml examples/agents/simple_nested_test.yaml", + "justMyCode": false, + "env": { + "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" + } + }, + { + "name": "SAM (preset)", + "type": "debugpy", + "request": "launch", + "module": "cli.main", + "console": "integratedTerminal", + "args": "run preset/agents/basic/main_orchestrator.yaml preset/agents/basic/webui.yaml preset/agents/markitdown_agents.yaml preset/agents/mermaid_agents.yaml preset/agents/web_agents.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -57,9 +81,9 @@ "name": "SAM (a2a-proxy)", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/agents/orchestrator_example.yaml examples/gateways/webui_gateway_example.yaml examples/agents/test_agent_example.yaml examples/a2a_proxy_example.yaml", + "args": "run examples/agents/orchestrator_example.yaml examples/gateways/webui_gateway_example.yaml examples/agents/test_agent_example.yaml examples/a2a_proxy_example.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -69,18 +93,18 @@ "name": "SAM (test with Slack)", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/gateways/slack_gateway_example.yaml examples/agents/orchestrator_example.yaml examples/agents/a2a_agents_example.yaml examples/gateways/webui_gateway_example.yaml examples/agents/test_agent_example.yaml examples/agents/a2a_multimodal_example.yaml", + "args": "run examples/gateways/slack_gateway_example.yaml examples/agents/orchestrator_example.yaml examples/agents/a2a_agents_example.yaml examples/gateways/webui_gateway_example.yaml examples/agents/test_agent_example.yaml examples/agents/a2a_multimodal_example.yaml", "justMyCode": false, }, { "name": "SAM (simple)", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/agents/orchestrator_example.yaml examples/gateways/webui_gateway_example.yaml examples/agents/test_agent_example.yaml examples/services/platform_service_example.yaml", + "args": "run examples/agents/orchestrator_example.yaml examples/gateways/webui_gateway_example.yaml examples/agents/test_agent_example.yaml examples/services/platform_service_example.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -90,9 +114,9 @@ "name": "SAM (with REST)", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/agents/orchestrator_example.yaml examples/agents/a2a_agents_example.yaml examples/gateways/webui_gateway_example.yaml examples/gateways/rest_gateway_example.yaml examples/agents/test_agent_example.yaml examples/agents/a2a_multimodal_example.yaml", + "args": "run examples/agents/orchestrator_example.yaml examples/agents/a2a_agents_example.yaml examples/gateways/webui_gateway_example.yaml examples/gateways/rest_gateway_example.yaml examples/agents/test_agent_example.yaml examples/agents/a2a_multimodal_example.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -102,9 +126,9 @@ "name": "Multimodal Agent (With UI)", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/agents/a2a_multimodal_example.yaml examples/gateways/webui_gateway_example.yaml ", + "args": "run examples/agents/a2a_multimodal_example.yaml examples/gateways/webui_gateway_example.yaml ", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -114,9 +138,9 @@ "name": "Orchestrator", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/agents/orchestrator_example.yaml", + "args": "run examples/agents/orchestrator_example.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -126,9 +150,9 @@ "name": "Agents", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/agents/a2a_agents_example.yaml", + "args": "run examples/agents/a2a_agents_example.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -138,9 +162,9 @@ "name": "MCP examples", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/agents/a2a_mcp_example.yaml", + "args": "run examples/agents/a2a_mcp_example.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -150,9 +174,9 @@ "name": "Slack", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/gateways/slack_gateway_example.yaml", + "args": "run examples/gateways/slack_gateway_example.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -162,9 +186,9 @@ "name": "Webhook Gateway", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/gateways/webhook_gateway_example.yaml", + "args": "run examples/gateways/webhook_gateway_example.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -174,9 +198,9 @@ "name": "WebUI", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/gateways/webui_gateway_example.yaml", + "args": "run examples/gateways/webui_gateway_example.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -186,9 +210,9 @@ "name": "Minimal", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/agents/orchestrator_example.yaml examples/gateways/webui_gateway_example.yaml", + "args": "run examples/agents/orchestrator_example.yaml examples/gateways/webui_gateway_example.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -198,9 +222,9 @@ "name": "Playwrite", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/agents/a2a_mcp_example.yaml examples/gateways/webui_gateway_example.yaml", + "args": "run examples/agents/a2a_mcp_example.yaml examples/gateways/webui_gateway_example.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" @@ -210,9 +234,9 @@ "name": "EM Gateway", "type": "debugpy", "request": "launch", - "module": "solace_ai_connector.main", + "module": "cli.main", "console": "integratedTerminal", - "args": "--envfile .env examples/gateways/event_mesh_gateway_example.yaml examples/agents/a2a_multimodal_example.yaml examples/agents/orchestrator_example.yaml examples/gateways/webui_gateway_example.yaml", + "args": "run examples/gateways/event_mesh_gateway_example.yaml examples/agents/a2a_multimodal_example.yaml examples/agents/orchestrator_example.yaml examples/gateways/webui_gateway_example.yaml", "justMyCode": false, "env": { "LOGGING_CONFIG_PATH": "${workspaceFolder}/preset/logging_config.yaml" diff --git a/README.md b/README.md index 3f3ca669b..15196bf07 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,9 @@ To run Solace Agent Mesh locally, you'll need: - **OS**: MacOS, Linux, or Windows (with [WSL](https://learn.microsoft.com/en-us/windows/wsl/)) - **LLM API key** (any major provider or custom endpoint) +### 🎸 Vibe Coding +To quickly setup and customize your Agent Mesh, check out the [Vibe Coding Quickstart Guide](docs/docs/documentation/getting-started/vibe_coding.md). This guide walks you through the essential steps to get Solace Agent Mesh up and running with minimal effort. + ### 💻 Setup Steps #### 1. Create a directory for a new project diff --git a/cli/__init__.py b/cli/__init__.py index b8045f577..f7d8b107f 100644 --- a/cli/__init__.py +++ b/cli/__init__.py @@ -1 +1 @@ -__version__ = "1.11.3" +__version__ = "1.13.4" diff --git a/cli/commands/add_cmd/__init__.py b/cli/commands/add_cmd/__init__.py index 0ee41a325..e86c28286 100644 --- a/cli/commands/add_cmd/__init__.py +++ b/cli/commands/add_cmd/__init__.py @@ -1,15 +1,17 @@ import click from .agent_cmd import add_agent from .gateway_cmd import add_gateway +from .proxy_cmd import add_proxy @click.group(name="add") def add(): """ - Creates templates for agents or gateways. + Creates templates for agents, gateways, or proxies. """ pass add.add_command(add_agent, name="agent") add.add_command(add_gateway, name="gateway") +add.add_command(add_proxy, name="proxy") diff --git a/cli/commands/add_cmd/agent_cmd.py b/cli/commands/add_cmd/agent_cmd.py index 2a2ce4ff4..e27a75a81 100644 --- a/cli/commands/add_cmd/agent_cmd.py +++ b/cli/commands/add_cmd/agent_cmd.py @@ -5,7 +5,11 @@ import click import yaml -from config_portal.backend.common import AGENT_DEFAULTS, USE_DEFAULT_SHARED_ARTIFACT, USE_DEFAULT_SHARED_SESSION +from config_portal.backend.common import ( + AGENT_DEFAULTS, + USE_DEFAULT_SHARED_ARTIFACT, + USE_DEFAULT_SHARED_SESSION, +) from ...utils import ( ask_if_not_provided, @@ -143,6 +147,7 @@ def _write_agent_yaml_from_data( Dumper=yaml.SafeDumper, default_flow_style=False, indent=2, + sort_keys=False, ).strip() if "\n" in tools_replacement_value: diff --git a/cli/commands/add_cmd/proxy_cmd.py b/cli/commands/add_cmd/proxy_cmd.py new file mode 100644 index 000000000..effe67527 --- /dev/null +++ b/cli/commands/add_cmd/proxy_cmd.py @@ -0,0 +1,100 @@ +import sys +from pathlib import Path + +import click + +from ...utils import ( + get_formatted_names, + load_template, +) + + +def _write_proxy_yaml(proxy_name_input: str, project_root: Path) -> tuple[bool, str, str]: + """ + Writes the proxy YAML file based on proxy_template.yaml. + + Args: + proxy_name_input: Name provided by user + project_root: Project root directory + + Returns: + Tuple of (success, message, relative_file_path) + """ + agents_config_dir = project_root / "configs" / "agents" + agents_config_dir.mkdir(parents=True, exist_ok=True) + + formatted_names = get_formatted_names(proxy_name_input) + proxy_name_pascal = formatted_names["PASCAL_CASE_NAME"] + file_name_snake = formatted_names["SNAKE_CASE_NAME"] + + proxy_config_file_path = agents_config_dir / f"{file_name_snake}_proxy.yaml" + + try: + # Load template + template_content = load_template("proxy_template.yaml") + + # Replace placeholder + modified_content = template_content.replace("__PROXY_NAME__", proxy_name_pascal) + + # Write file + with open(proxy_config_file_path, "w", encoding="utf-8") as f: + f.write(modified_content) + + relative_file_path = str(proxy_config_file_path.relative_to(project_root)) + return ( + True, + f"Proxy configuration created: {relative_file_path}", + relative_file_path, + ) + except FileNotFoundError as e: + return ( + False, + f"Error: Template file 'proxy_template.yaml' not found: {e}", + "", + ) + except Exception as e: + import traceback + click.echo( + f"DEBUG: Error in _write_proxy_yaml: {e}\n{traceback.format_exc()}", + err=True, + ) + return ( + False, + f"Error creating proxy configuration file {proxy_config_file_path}: {e}", + "", + ) + + +@click.command(name="proxy") +@click.argument("name", required=False) +@click.option( + "--skip", + is_flag=True, + help="Skip interactive prompts (creates proxy with default template).", +) +def add_proxy(name: str, skip: bool = False): + """ + Creates a new A2A proxy configuration. + + NAME: Name of the proxy component to create (e.g., my-proxy). + """ + if not name: + click.echo( + click.style( + "Error: You must provide a proxy name.", + fg="red", + ), + err=True, + ) + return + + click.echo(f"Creating proxy configuration for '{name}'...") + + project_root = Path.cwd() + success, message, _ = _write_proxy_yaml(name, project_root) + + if success: + click.echo(click.style(message, fg="green")) + else: + click.echo(click.style(message, fg="red"), err=True) + sys.exit(1) diff --git a/cli/commands/tools_cmd.py b/cli/commands/tools_cmd.py new file mode 100644 index 000000000..2f699109d --- /dev/null +++ b/cli/commands/tools_cmd.py @@ -0,0 +1,315 @@ +import click +import json +from typing import Optional, List, Dict, Any +from collections import defaultdict + +# Import to trigger tool registration +import solace_agent_mesh.agent.tools # noqa: F401 +from solace_agent_mesh.agent.tools.registry import tool_registry +from cli.utils import error_exit + + +def format_parameter_schema(schema) -> str: + """ + Format the parameter schema into a readable string. + + Args: + schema: A google.genai.types.Schema object + + Returns: + Formatted string representation of parameters + """ + if not schema or not hasattr(schema, 'properties') or not schema.properties: + return " No parameters" + + lines = [] + required = schema.required if hasattr(schema, 'required') else [] + + for prop_name, prop_schema in schema.properties.items(): + is_required = prop_name in required + req_str = "required" if is_required else "optional" + type_str = getattr(prop_schema, 'type', 'unknown') + desc = getattr(prop_schema, 'description', '') + lines.append(f" - {prop_name} ({type_str}, {req_str}): {desc}") + + return "\n".join(lines) + + +def format_tool_table_brief(tools: List) -> None: + """ + Format tools as a brief list and echo to console. + + Groups tools by category and displays only names and descriptions. + + Args: + tools: List of BuiltinTool objects + """ + if not tools: + click.echo("No tools found.") + return + + # Group tools by category + tools_by_category = defaultdict(list) + for tool in tools: + tools_by_category[tool.category].append(tool) + + # Sort categories alphabetically + sorted_categories = sorted(tools_by_category.keys()) + + total_tools = len(tools) + + for category in sorted_categories: + category_tools = sorted(tools_by_category[category], key=lambda t: t.name) + + # Get category metadata from first tool in category + first_tool = category_tools[0] + category_name = first_tool.category_name or category + + # Display category header + click.echo() + click.echo(click.style(f"═══ {category_name} ═══", bold=True, fg='cyan')) + click.echo() + + # Display tools in category (brief format) + for tool in category_tools: + click.echo(f" • {click.style(tool.name, bold=True, fg='green')}") + # Wrap description at 70 characters + desc_words = tool.description.split() + lines = [] + current_line = " " + for word in desc_words: + if len(current_line) + len(word) + 1 <= 74: + current_line += (" " if len(current_line) > 4 else "") + word + else: + lines.append(current_line) + current_line = " " + word + if current_line.strip(): + lines.append(current_line) + for line in lines: + click.echo(line) + click.echo() + + # Display summary + click.echo(click.style(f"Total: {total_tools} tool{'s' if total_tools != 1 else ''}", bold=True, fg='blue')) + + +def format_tool_table(tools: List) -> None: + """ + Format tools as a detailed table and echo to console. + + Groups tools by category and displays detailed information for each tool. + + Args: + tools: List of BuiltinTool objects + """ + if not tools: + click.echo("No tools found.") + return + + # Group tools by category + tools_by_category = defaultdict(list) + for tool in tools: + tools_by_category[tool.category].append(tool) + + # Sort categories alphabetically + sorted_categories = sorted(tools_by_category.keys()) + + total_tools = len(tools) + + for category in sorted_categories: + category_tools = sorted(tools_by_category[category], key=lambda t: t.name) + + # Get category metadata from first tool in category + first_tool = category_tools[0] + category_name = first_tool.category_name or category + category_desc = first_tool.category_description or "" + + # Display category header + header_width = 60 + click.echo() + click.echo("╭" + "─" * (header_width - 2) + "╮") + click.echo("│ " + click.style(category_name, bold=True, fg='cyan') + + " " * (header_width - len(category_name) - 3) + "│") + if category_desc: + # Wrap description if needed + desc_lines = [] + current_line = "" + for word in category_desc.split(): + if len(current_line) + len(word) + 1 <= header_width - 4: + current_line += (" " if current_line else "") + word + else: + desc_lines.append(current_line) + current_line = word + if current_line: + desc_lines.append(current_line) + + for desc_line in desc_lines: + click.echo("│ " + desc_line + " " * (header_width - len(desc_line) - 3) + "│") + click.echo("╰" + "─" * (header_width - 2) + "╯") + click.echo() + + # Display tools in category + for tool in category_tools: + click.echo(click.style(f"Tool: {tool.name}", bold=True, fg='green')) + click.echo(f"Description: {tool.description}") + + # Format and display parameters + click.echo("Parameters:") + params_str = format_parameter_schema(tool.parameters) + click.echo(params_str) + + # Display required scopes + if tool.required_scopes: + scopes_str = ", ".join(tool.required_scopes) + click.echo(f"Required Scopes: {click.style(scopes_str, fg='yellow')}") + else: + click.echo("Required Scopes: None") + + click.echo() # Blank line between tools + + # Display summary + click.echo(click.style(f"Total: {total_tools} tool{'s' if total_tools != 1 else ''}", + bold=True, fg='blue')) + + +def tools_to_json(tools: List, detailed: bool = False) -> str: + """ + Convert tools list to JSON format. + + Args: + tools: List of BuiltinTool objects + detailed: If True, include parameters and all metadata. If False, only name and description. + + Returns: + JSON string representation of tools + """ + result = [] + + for tool in tools: + if detailed: + # Convert Schema to dict if possible + try: + if hasattr(tool.parameters, 'model_dump'): + params_dict = tool.parameters.model_dump() + elif hasattr(tool.parameters, 'to_dict'): + params_dict = tool.parameters.to_dict() + else: + # Fallback: manually construct dict from Schema + params_dict = { + "type": getattr(tool.parameters, 'type', None), + "properties": {}, + "required": getattr(tool.parameters, 'required', []) + } + if hasattr(tool.parameters, 'properties') and tool.parameters.properties: + for prop_name, prop_schema in tool.parameters.properties.items(): + params_dict["properties"][prop_name] = { + "type": getattr(prop_schema, 'type', None), + "description": getattr(prop_schema, 'description', '') + } + except Exception: + params_dict = {"error": "Could not serialize parameters"} + + tool_dict = { + "name": tool.name, + "description": tool.description, + "category": tool.category, + "category_name": tool.category_name, + "category_description": tool.category_description, + "required_scopes": tool.required_scopes, + "parameters": params_dict, + "examples": tool.examples, + "raw_string_args": tool.raw_string_args + } + else: + # Brief format: only name and description + tool_dict = { + "name": tool.name, + "description": tool.description, + "category": tool.category, + "category_name": tool.category_name + } + + result.append(tool_dict) + + return json.dumps(result, indent=2) + + +@click.group("tools") +def tools(): + """Manage and explore SAM built-in tools.""" + pass + + +@tools.command("list") +@click.option( + "--category", "-c", + type=str, + default=None, + help="Filter tools by category (e.g., 'artifact_management', 'data_analysis')" +) +@click.option( + "--detailed", "-d", + is_flag=True, + help="Show detailed information including parameters and required scopes" +) +@click.option( + "--json", "output_json", + is_flag=True, + help="Output in JSON format instead of pretty table" +) +def list_tools(category: Optional[str], detailed: bool, output_json: bool): + """ + List all built-in tools available in Solace Agent Mesh. + + By default, shows brief information with tool names and descriptions. + Use --detailed flag to see parameters and required scopes. + + Examples: + + # List all tools (brief) + sam tools list + + # List with full details + sam tools list --detailed + + # Filter by category + sam tools list --category artifact_management + + # Detailed view with category filter + sam tools list -c web --detailed + + # Output as JSON + sam tools list --json + + # Filter and output as JSON + sam tools list -c web --json + """ + # Fetch tools from registry + if category: + tools_list = tool_registry.get_tools_by_category(category) + if not tools_list: + # Get all categories to show valid options + all_tools = tool_registry.get_all_tools() + if not all_tools: + error_exit("No tools are registered in the tool registry.") + + categories = sorted(set(t.category for t in all_tools)) + error_exit( + f"No tools found for category '{category}'.\n" + f"Valid categories: {', '.join(categories)}" + ) + else: + tools_list = tool_registry.get_all_tools() + if not tools_list: + error_exit("No tools are registered in the tool registry.") + + # Output based on format preference + if output_json: + json_output = tools_to_json(tools_list, detailed=detailed) + click.echo(json_output) + else: + # Use detailed format only if --detailed flag is provided + if detailed: + format_tool_table(tools_list) + else: + format_tool_table_brief(tools_list) diff --git a/cli/main.py b/cli/main.py index dd3e06499..1f7bdc631 100644 --- a/cli/main.py +++ b/cli/main.py @@ -18,6 +18,7 @@ from cli.commands.plugin_cmd import plugin from cli.commands.eval_cmd import eval_cmd from cli.commands.docs_cmd import docs +from cli.commands.tools_cmd import tools @click.group(context_settings=dict(help_option_names=['-h', '--help'])) @@ -42,6 +43,7 @@ def cli(): cli.add_command(plugin) cli.add_command(eval_cmd) cli.add_command(docs) +cli.add_command(tools) def main(): diff --git a/client/webui/frontend/.storybook/vitest.setup.ts b/client/webui/frontend/.storybook/vitest.setup.ts index e775d5395..3feee4deb 100644 --- a/client/webui/frontend/.storybook/vitest.setup.ts +++ b/client/webui/frontend/.storybook/vitest.setup.ts @@ -1,6 +1,22 @@ import { setProjectAnnotations } from "@storybook/react-vite"; import * as projectAnnotations from "./preview"; +// Official workaround for "TypeError: window.matchMedia is not a function" +// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom +Object.defineProperty(window, "matchMedia", { + writable: true, + value: (query: any) => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, // deprecated + removeListener: () => {}, // deprecated + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => {}, + }), +}); + // This is an important step to apply the right configuration when testing your stories. // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations setProjectAnnotations([projectAnnotations]); diff --git a/client/webui/frontend/package-lock.json b/client/webui/frontend/package-lock.json index a747f068e..fb9925705 100644 --- a/client/webui/frontend/package-lock.json +++ b/client/webui/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@SolaceLabs/solace-agent-mesh-ui", - "version": "1.24.2", + "version": "1.31.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@SolaceLabs/solace-agent-mesh-ui", - "version": "1.24.2", + "version": "1.31.2", "license": "Apache-2.0", "dependencies": { "@hookform/resolvers": "^5.2.2", @@ -22,9 +22,11 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.2.7", + "@stepperize/react": "^5.1.9", "@tailwindcss/vite": "^4.1.10", + "@tanstack/react-query": "5.90.16", "@tanstack/react-table": "^8.21.3", - "@xyflow/react": "^12.6.4", + "@use-gesture/react": "^10.3.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dompurify": "^3.2.6", @@ -34,13 +36,14 @@ "jszip": "^3.10.1", "lucide-react": "^0.511.0", "marked": "^15.0.12", + "mermaid": "^11.12.2", "react": "19.0.0", "react-dom": "19.0.0", "react-hook-form": "^7.65.0", "react-intersection-observer": "^9.16.0", "react-json-view-lite": "^2.4.1", "react-resizable-panels": "^3.0.3", - "react-router-dom": "7.9.3", + "react-router-dom": "7.12.0", "tailwind-merge": "^3.3.0", "tailwind-scrollbar-hide": "^4.0.0", "tailwindcss": "^4.1.10", @@ -50,10 +53,14 @@ "devDependencies": { "@a2a-js/sdk": "^0.3.2", "@eslint/js": "^9.25.0", - "@storybook/addon-vitest": "^10.0.7", - "@storybook/react-vite": "^10.0.7", + "@storybook/addon-vitest": "^10.1.10", + "@storybook/react": "^10.1.8", + "@storybook/react-vite": "^10.1.10", "@tailwindcss/typography": "^0.5.16", + "@tanstack/eslint-plugin-query": "5.91.2", "@testing-library/cypress": "^10.0.3", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", "@types/js-yaml": "^4.0.9", "@types/node": "^22.15.29", "@types/react": "19.0.0", @@ -65,8 +72,9 @@ "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", - "eslint-plugin-storybook": "^10.0.7", + "eslint-plugin-storybook": "^10.1.10", "globals": "^16.0.0", + "jsdom": "^27.0.1", "lint-staged": "^16.2.3", "mocha-junit-reporter": "^2.2.1", "msw": "^2.12.3", @@ -74,7 +82,7 @@ "playwright": "^1.56.1", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.6.13", - "storybook": "^10.0.7", + "storybook": "^10.1.10", "tw-animate-css": "^1.3.3", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", @@ -103,7 +111,7 @@ "@types/react-dom": "19.0.0", "react": "19.0.0", "react-dom": "19.0.0", - "react-router-dom": "7.9.3" + "react-router-dom": "7.12.0" } }, "node_modules/@a2a-js/sdk": { @@ -152,6 +160,79 @@ "node": ">=6.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/install-pkg/node_modules/tinyexec": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", + "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz", + "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "dev": true, @@ -286,8 +367,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -316,8 +395,6 @@ }, "node_modules/@babel/parser": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -398,8 +475,6 @@ }, "node_modules/@babel/types": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { @@ -412,14 +487,188 @@ }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.1", + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": { + "version": "4.17.21", + "license": "MIT" + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast/node_modules/lodash-es": { + "version": "4.17.21", + "license": "MIT" + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "license": "Apache-2.0" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.25", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.25.tgz", + "integrity": "sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@cypress/request": { "version": "3.0.9", "dev": true, @@ -536,7 +785,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -657,8 +908,6 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -671,8 +920,6 @@ }, "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -778,10 +1025,21 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.0" + } + }, "node_modules/@inquirer/ansi": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", - "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", "dev": true, "license": "MIT", "engines": { @@ -790,8 +1048,6 @@ }, "node_modules/@inquirer/confirm": { "version": "5.1.21", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", - "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", "dev": true, "license": "MIT", "dependencies": { @@ -812,8 +1068,6 @@ }, "node_modules/@inquirer/core": { "version": "10.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", - "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", "dev": true, "license": "MIT", "dependencies": { @@ -840,8 +1094,6 @@ }, "node_modules/@inquirer/core/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -850,15 +1102,11 @@ }, "node_modules/@inquirer/core/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/@inquirer/core/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -867,8 +1115,6 @@ }, "node_modules/@inquirer/core/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -882,8 +1128,6 @@ }, "node_modules/@inquirer/core/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -895,8 +1139,6 @@ }, "node_modules/@inquirer/core/node_modules/wrap-ansi": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "license": "MIT", "dependencies": { @@ -910,8 +1152,6 @@ }, "node_modules/@inquirer/figures": { "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", - "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", "dev": true, "license": "MIT", "engines": { @@ -920,8 +1160,6 @@ }, "node_modules/@inquirer/type": { "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", - "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", "dev": true, "license": "MIT", "engines": { @@ -936,8 +1174,33 @@ } } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, "license": "ISC", "dependencies": { @@ -954,6 +1217,8 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -965,11 +1230,15 @@ }, "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { @@ -986,6 +1255,8 @@ }, "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1011,12 +1282,13 @@ } }, "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { - "version": "0.6.1", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.6.3.tgz", + "integrity": "sha512-9TGZuAX+liGkNKkwuo3FYJu7gHWT0vkBcf7GkOe7s7fmC19XwH/4u5u7sDIFrMooe558ORcmuBvBz7Ur5PlbHw==", "dev": true, "license": "MIT", "dependencies": { - "glob": "^10.0.0", - "magic-string": "^0.30.0", + "glob": "^11.1.0", "react-docgen-typescript": "^2.2.2" }, "peerDependencies": { @@ -1057,24 +1329,25 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mermaid-js/parser": { + "version": "0.6.3", + "license": "MIT", + "dependencies": { + "langium": "3.3.1" + } + }, "node_modules/@mswjs/interceptors": { "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz", - "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1123,15 +1396,11 @@ }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", "dev": true, "license": "MIT" }, "node_modules/@open-draft/logger": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1141,24 +1410,11 @@ }, "node_modules/@open-draft/until": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "dev": true, "license": "MIT" }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@polka/url": { "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "dev": true, "license": "MIT" }, @@ -2293,6 +2549,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2306,6 +2563,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2317,6 +2575,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2330,6 +2589,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2343,6 +2603,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2356,6 +2617,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2369,6 +2631,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2382,6 +2645,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2408,6 +2672,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2421,6 +2686,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2434,6 +2700,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2447,6 +2714,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2460,6 +2728,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2473,6 +2742,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2499,6 +2769,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2512,6 +2783,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2525,6 +2797,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2538,6 +2811,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2546,8 +2820,6 @@ }, "node_modules/@standard-schema/spec": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "dev": true, "license": "MIT" }, @@ -2555,17 +2827,36 @@ "version": "0.3.0", "license": "MIT" }, + "node_modules/@stepperize/core": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@stepperize/core/-/core-1.2.7.tgz", + "integrity": "sha512-XiUwLZ0XRAfaDK6AzWVgqvI/BcrylyplhUXKO8vzgRw0FTmyMKHAAbQLDvU//ZJAqnmG2cSLZDSkcwLxU5zSYA==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5.0.2" + } + }, + "node_modules/@stepperize/react": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@stepperize/react/-/react-5.1.9.tgz", + "integrity": "sha512-yBgw1I5Tx6/qZB4xTdVBaPGfTqH5aYS1WFB5vtR8+fwPeqd3YNuOnQ1pJM6w/xV/gvryuy31hbFw080lZc+/hw==", + "license": "MIT", + "dependencies": { + "@stepperize/core": "1.2.7" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@storybook/addon-vitest": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.0.7.tgz", - "integrity": "sha512-i6v/mAl+elrUxb+1f4NdnM17t/fg+KGJWL1U9quflXTd3KiLY0xJB4LwNP6yYo7Imc5NIO2fRkJbGvNqLBRe2Q==", + "version": "10.1.11", + "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.1.11.tgz", + "integrity": "sha512-YbZzeKO3v+Xr97/malT4DZIATkVZT5EHNYx3xzEfPVuk19dDETAVYXO+tzcqCQHsgdKQHkmd56vv8nN3J3/kvw==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.6.0", - "prompts": "^2.4.0", - "ts-dedent": "^2.2.0" + "@storybook/icons": "^2.0.0" }, "funding": { "type": "opencollective", @@ -2575,7 +2866,7 @@ "@vitest/browser": "^3.0.0 || ^4.0.0", "@vitest/browser-playwright": "^4.0.0", "@vitest/runner": "^3.0.0 || ^4.0.0", - "storybook": "^10.0.7", + "storybook": "^10.1.11", "vitest": "^3.0.0 || ^4.0.0" }, "peerDependenciesMeta": { @@ -2594,13 +2885,14 @@ } }, "node_modules/@storybook/builder-vite": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.0.7.tgz", - "integrity": "sha512-wk2TAoUY5+9t78GWVBndu9rEo9lo6Ec3SRrLT4VpIlcS2GPK+5f26UC2uvIBwOF/N7JrUUKq/zWDZ3m+do9QDg==", + "version": "10.1.11", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.1.11.tgz", + "integrity": "sha512-MMD09Ap7FyzDfWG961pkIMv/w684XXe1bBEi+wCEpHxvrgAd3j3A9w/Rqp9Am2uRDPCEdi1QgSzS3SGW3aGThQ==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "10.0.7", + "@storybook/csf-plugin": "10.1.11", + "@vitest/mocker": "3.2.4", "ts-dedent": "^2.0.0" }, "funding": { @@ -2608,14 +2900,14 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.0.7", + "storybook": "^10.1.11", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/@storybook/csf-plugin": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.0.7.tgz", - "integrity": "sha512-YaYYlCyJBwxaMk7yREOdz+9MDSgxIYGdeJ9EIq/bUndmkoj9SRo1P9/0lC5dseWQoiGy4T3PbZiWruD8uM5m3g==", + "version": "10.1.11", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.1.11.tgz", + "integrity": "sha512-Ant0NhgqHKzQsseeVTSetZCuDHHs0W2HRkHt51Kg/sUl0T/sDtfVA+fWZT8nGzGZqYSFkxqYPWjauPmIhPtaRw==", "dev": true, "license": "MIT", "dependencies": { @@ -2628,7 +2920,7 @@ "peerDependencies": { "esbuild": "*", "rollup": "*", - "storybook": "^10.0.7", + "storybook": "^10.1.11", "vite": "*", "webpack": "*" }, @@ -2653,28 +2945,26 @@ "license": "MIT" }, "node_modules/@storybook/icons": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.6.0.tgz", - "integrity": "sha512-hcFZIjW8yQz8O8//2WTIXylm5Xsgc+lW9ISLgUk1xGmptIJQRdlhVIXCpSyLrQaaRiyhQRaVg7l3BD9S216BHw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-2.0.1.tgz", + "integrity": "sha512-/smVjw88yK3CKsiuR71vNgWQ9+NuY2L+e8X7IMrFjexjm6ZR8ULrV2DRkTA61aV6ryefslzHEGDInGpnNeIocg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=14.0.0" - }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@storybook/react": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.0.7.tgz", - "integrity": "sha512-1GSDIMo2GkdG55DhpIIFaAJv+QzmsRb36qWsKqfbtFjEhnqu5/3zqyys2dCIiHOG1Czba4SGsTS4cay3KDQJgA==", + "version": "10.1.11", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.1.11.tgz", + "integrity": "sha512-rmMGmEwBaM2YpB8oDk2moM0MNjNMqtwyoPPZxjyruY9WVhYca8EDPGKEdRzUlb4qZJsTgLi7VU4eqg6LD/mL3Q==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "10.0.7" + "@storybook/react-dom-shim": "10.1.11", + "react-docgen": "^8.0.2" }, "funding": { "type": "opencollective", @@ -2683,7 +2973,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.0.7", + "storybook": "^10.1.11", "typescript": ">= 4.9.x" }, "peerDependenciesMeta": { @@ -2693,9 +2983,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.0.7.tgz", - "integrity": "sha512-bp4OnMtZGwPJQDqNRi4K5iibLbZ2TZZMkWW7oSw5jjPFpGSreSjCe8LH9yj/lDnK8Ox9bGMCBFE5RV5XuML29w==", + "version": "10.1.11", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.1.11.tgz", + "integrity": "sha512-o8WPhRlZbORUWG9lAgDgJP0pi905VHJUFJr1Kp8980gHqtlemtnzjPxKy5vFwj6glNhAlK8SS8OOYzWP7hloTQ==", "dev": true, "license": "MIT", "funding": { @@ -2705,20 +2995,20 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.0.7" + "storybook": "^10.1.11" } }, "node_modules/@storybook/react-vite": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.0.7.tgz", - "integrity": "sha512-EAv2cwYkRctQNcPC1jLsZPm+C6RVk6t6axKrkc/+cFe/t5MnKG7oRf0c/6apWYi/cQv6kzNsFxMV2jj8r/VoBg==", + "version": "10.1.11", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.1.11.tgz", + "integrity": "sha512-qh1BCD25nIoiDfqwha+qBkl7pcG4WuzM+c8tsE63YEm8AFIbNKg5K8lVUoclF+4CpFz7IwBpWe61YUTDfp+91w==", "dev": true, "license": "MIT", "dependencies": { - "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", + "@joshwooding/vite-plugin-react-docgen-typescript": "^0.6.3", "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "10.0.7", - "@storybook/react": "10.0.7", + "@storybook/builder-vite": "10.1.11", + "@storybook/react": "10.1.11", "empathic": "^2.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.0", @@ -2732,7 +3022,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.0.7", + "storybook": "^10.1.11", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, @@ -2863,35 +3153,258 @@ "vite": "^5.2.0 || ^6 || ^7" } }, - "node_modules/@tanstack/react-table": { - "version": "8.21.3", + "node_modules/@tanstack/eslint-plugin-query": { + "version": "5.91.2", + "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.91.2.tgz", + "integrity": "sha512-UPeWKl/Acu1IuuHJlsN+eITUHqAaa9/04geHHPedY8siVarSaWprY0SVMKrkpKfk5ehRT7+/MZ5QwWuEtkWrFw==", + "dev": true, "license": "MIT", "dependencies": { - "@tanstack/table-core": "8.21.3" - }, - "engines": { - "node": ">=12" + "@typescript-eslint/utils": "^8.44.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" + "eslint": "^8.57.0 || ^9.0.0" } }, - "node_modules/@tanstack/table-core": { - "version": "8.21.3", + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/project-service": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz", + "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==", + "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.53.0", + "@typescript-eslint/types": "^8.53.0", + "debug": "^4.4.3" + }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz", + "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz", + "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz", + "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz", + "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.53.0", + "@typescript-eslint/tsconfig-utils": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz", + "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", + "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.16", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.16.tgz", + "integrity": "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.16", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.16.tgz", + "integrity": "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/cypress": { "version": "10.1.0", "dev": true, @@ -2949,6 +3462,34 @@ "dev": true, "license": "MIT" }, + "node_modules/@testing-library/react": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.1.tgz", + "integrity": "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@testing-library/user-event": { "version": "14.6.1", "dev": true, @@ -3012,10 +3553,84 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "license": "MIT" + }, "node_modules/@types/d3-color": { "version": "3.1.3", "license": "MIT" }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "license": "MIT" + }, "node_modules/@types/d3-drag": { "version": "3.0.7", "license": "MIT", @@ -3023,6 +3638,40 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "license": "MIT" + }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "license": "MIT", @@ -3030,10 +3679,56 @@ "@types/d3-color": "*" } }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "license": "MIT" + }, "node_modules/@types/d3-selection": { "version": "3.0.11", "license": "MIT" }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "license": "MIT" + }, "node_modules/@types/d3-transition": { "version": "3.0.9", "license": "MIT", @@ -3061,6 +3756,11 @@ }, "node_modules/@types/estree": { "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", "license": "MIT" }, "node_modules/@types/js-yaml": { @@ -3075,7 +3775,7 @@ }, "node_modules/@types/node": { "version": "22.16.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3083,9 +3783,7 @@ }, "node_modules/@types/react": { "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.0.tgz", - "integrity": "sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -3093,9 +3791,7 @@ }, "node_modules/@types/react-dom": { "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-1KfiQKsH1o00p9m5ag12axHQSb3FOU9H20UTrujVSkNhuCrRHiQWFqgEnTNK5ZNfnzZv8UWrnXVqCmCF9fgY3w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/react": "*" @@ -3118,8 +3814,6 @@ }, "node_modules/@types/statuses": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", - "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", "dev": true, "license": "MIT" }, @@ -3379,6 +4073,20 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.6.0", "dev": true, @@ -3400,8 +4108,6 @@ }, "node_modules/@vitest/browser": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.8.tgz", - "integrity": "sha512-oG6QJAR0d7S5SDnIYZwjxCj/a5fhbp9ZE7GtMgZn+yCUf4CxtqbBV6aXyg0qmn8nbUWT+rGuXL2ZB6qDBUjv/A==", "dev": true, "license": "MIT", "dependencies": { @@ -3423,8 +4129,6 @@ }, "node_modules/@vitest/browser-playwright": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.8.tgz", - "integrity": "sha512-MUi0msIAPXcA2YAuVMcssrSYP/yylxLt347xyTC6+ODl0c4XQFs0d2AN3Pc3iTa0pxIGmogflUV6eogXpPbJeA==", "dev": true, "license": "MIT", "dependencies": { @@ -3447,8 +4151,6 @@ }, "node_modules/@vitest/browser-playwright/node_modules/@vitest/mocker": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.8.tgz", - "integrity": "sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==", "dev": true, "license": "MIT", "dependencies": { @@ -3474,8 +4176,6 @@ }, "node_modules/@vitest/browser-playwright/node_modules/@vitest/spy": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.8.tgz", - "integrity": "sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==", "dev": true, "license": "MIT", "funding": { @@ -3484,8 +4184,6 @@ }, "node_modules/@vitest/browser-playwright/node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -3494,8 +4192,6 @@ }, "node_modules/@vitest/browser-playwright/node_modules/tinyrainbow": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, "license": "MIT", "engines": { @@ -3504,8 +4200,6 @@ }, "node_modules/@vitest/browser/node_modules/@vitest/mocker": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.8.tgz", - "integrity": "sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==", "dev": true, "license": "MIT", "dependencies": { @@ -3531,8 +4225,6 @@ }, "node_modules/@vitest/browser/node_modules/@vitest/pretty-format": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.8.tgz", - "integrity": "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==", "dev": true, "license": "MIT", "dependencies": { @@ -3544,8 +4236,6 @@ }, "node_modules/@vitest/browser/node_modules/@vitest/spy": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.8.tgz", - "integrity": "sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==", "dev": true, "license": "MIT", "funding": { @@ -3554,8 +4244,6 @@ }, "node_modules/@vitest/browser/node_modules/@vitest/utils": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.8.tgz", - "integrity": "sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==", "dev": true, "license": "MIT", "dependencies": { @@ -3568,8 +4256,6 @@ }, "node_modules/@vitest/browser/node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -3578,8 +4264,6 @@ }, "node_modules/@vitest/browser/node_modules/tinyrainbow": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, "license": "MIT", "engines": { @@ -3588,8 +4272,6 @@ }, "node_modules/@vitest/coverage-v8": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.8.tgz", - "integrity": "sha512-wQgmtW6FtPNn4lWUXi8ZSYLpOIb92j3QCujxX3sQ81NTfQ/ORnE0HtK7Kqf2+7J9jeveMGyGyc4NWc5qy3rC4A==", "dev": true, "license": "MIT", "dependencies": { @@ -3620,8 +4302,6 @@ }, "node_modules/@vitest/coverage-v8/node_modules/@vitest/pretty-format": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.8.tgz", - "integrity": "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==", "dev": true, "license": "MIT", "dependencies": { @@ -3633,8 +4313,6 @@ }, "node_modules/@vitest/coverage-v8/node_modules/@vitest/utils": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.8.tgz", - "integrity": "sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==", "dev": true, "license": "MIT", "dependencies": { @@ -3647,8 +4325,6 @@ }, "node_modules/@vitest/coverage-v8/node_modules/tinyrainbow": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, "license": "MIT", "engines": { @@ -3672,6 +4348,8 @@ }, "node_modules/@vitest/mocker": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3697,6 +4375,8 @@ }, "node_modules/@vitest/mocker/node_modules/estree-walker": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -3716,8 +4396,6 @@ }, "node_modules/@vitest/runner": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.8.tgz", - "integrity": "sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3730,8 +4408,6 @@ }, "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.8.tgz", - "integrity": "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==", "dev": true, "license": "MIT", "dependencies": { @@ -3743,8 +4419,6 @@ }, "node_modules/@vitest/runner/node_modules/@vitest/utils": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.8.tgz", - "integrity": "sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==", "dev": true, "license": "MIT", "dependencies": { @@ -3757,8 +4431,6 @@ }, "node_modules/@vitest/runner/node_modules/tinyrainbow": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, "license": "MIT", "engines": { @@ -3767,8 +4439,6 @@ }, "node_modules/@vitest/snapshot": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.8.tgz", - "integrity": "sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==", "dev": true, "license": "MIT", "dependencies": { @@ -3782,8 +4452,6 @@ }, "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.8.tgz", - "integrity": "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==", "dev": true, "license": "MIT", "dependencies": { @@ -3795,8 +4463,6 @@ }, "node_modules/@vitest/snapshot/node_modules/tinyrainbow": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, "license": "MIT", "engines": { @@ -3827,37 +4493,8 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@xyflow/react": { - "version": "12.8.1", - "license": "MIT", - "dependencies": { - "@xyflow/system": "0.0.65", - "classcat": "^5.0.3", - "zustand": "^4.4.0" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@xyflow/system": { - "version": "0.0.65", - "license": "MIT", - "dependencies": { - "@types/d3-drag": "^3.0.7", - "@types/d3-interpolate": "^3.0.4", - "@types/d3-selection": "^3.0.10", - "@types/d3-transition": "^3.0.8", - "@types/d3-zoom": "^3.0.8", - "d3-drag": "^3.0.0", - "d3-interpolate": "^3.0.1", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0" - } - }, "node_modules/acorn": { "version": "8.15.0", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3874,6 +4511,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "dev": true, @@ -4026,8 +4673,6 @@ }, "node_modules/ast-v8-to-istanbul": { "version": "0.3.8", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", - "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4038,8 +4683,6 @@ }, "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -4048,8 +4691,6 @@ }, "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "dev": true, "license": "MIT" }, @@ -4124,6 +4765,16 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/blob-util": { "version": "2.0.2", "dev": true, @@ -4154,12 +4805,6 @@ "node": ">=8" } }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "dev": true, - "license": "ISC", - "peer": true - }, "node_modules/browserslist": { "version": "4.25.1", "dev": true, @@ -4214,12 +4859,28 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", + "node_modules/buffer-crc32": { + "version": "0.2.13", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, "engines": { - "node": "*" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cachedir": { @@ -4265,18 +4926,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001726", "dev": true, @@ -4355,21 +5004,32 @@ "node": ">= 0.8.0" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "dev": true, + "node_modules/chevrotain": { + "version": "11.0.3", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", "license": "MIT", - "peer": true, "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" + "lodash-es": "^4.17.21" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "peerDependencies": { + "chevrotain": "^11.0.0" } }, + "node_modules/chevrotain/node_modules/lodash-es": { + "version": "4.17.21", + "license": "MIT" + }, "node_modules/chownr": { "version": "3.0.0", "license": "BlueOak-1.0.0", @@ -4401,10 +5061,6 @@ "url": "https://polar.sh/cva" } }, - "node_modules/classcat": { - "version": "5.0.5", - "license": "MIT" - }, "node_modules/clean-stack": { "version": "2.2.0", "dev": true, @@ -4503,8 +5159,6 @@ }, "node_modules/cli-width": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, "license": "ISC", "engines": { @@ -4654,6 +5308,10 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.1.8", + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "dev": true, @@ -4661,8 +5319,6 @@ }, "node_modules/cookie": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "license": "MIT", "engines": { "node": ">=18" @@ -4672,6 +5328,13 @@ "version": "1.0.2", "license": "MIT" }, + "node_modules/cose-base": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "dev": true, @@ -4693,6 +5356,20 @@ "node": "*" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css.escape": { "version": "1.5.1", "dev": true, @@ -4709,9 +5386,35 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/csstype": { "version": "3.1.3", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/cypress": { @@ -4973,103 +5676,400 @@ "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/cypress/node_modules/type-fest": { + "version": "0.21.3", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cypress/node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cytoscape": { + "version": "3.33.1", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/cypress/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", + "node_modules/d3-force": { + "version": "3.0.0", + "license": "ISC", "dependencies": { - "ansi-regex": "^5.0.1" + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/cypress/node_modules/supports-color": { - "version": "8.1.1", - "dev": true, - "license": "MIT", + "node_modules/d3-format": { + "version": "3.1.0", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "license": "ISC", "dependencies": { - "has-flag": "^4.0.0" + "d3-array": "2.5.0 - 3" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">=12" } }, - "node_modules/cypress/node_modules/type-fest": { - "version": "0.21.3", - "dev": true, - "license": "(MIT OR CC0-1.0)", + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "license": "ISC", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/cypress/node_modules/wrap-ansi": { - "version": "7.0.0", - "dev": true, - "license": "MIT", + "node_modules/d3-interpolate": { + "version": "3.0.1", + "license": "ISC", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "d3-color": "1 - 3" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=12" } }, - "node_modules/d3-color": { + "node_modules/d3-path": { "version": "3.1.0", "license": "ISC", "engines": { "node": ">=12" } }, - "node_modules/d3-dispatch": { + "node_modules/d3-polygon": { "version": "3.0.1", "license": "ISC", "engines": { "node": ">=12" } }, - "node_modules/d3-drag": { - "version": "3.0.0", + "node_modules/d3-quadtree": { + "version": "3.0.1", "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, "engines": { "node": ">=12" } }, - "node_modules/d3-ease": { + "node_modules/d3-random": { "version": "3.0.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, "engines": { "node": ">=12" } }, - "node_modules/d3-interpolate": { - "version": "3.0.1", + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", "license": "ISC", "dependencies": { - "d3-color": "1 - 3" + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" }, "engines": { "node": ">=12" @@ -5082,6 +6082,36 @@ "node": ">=12" } }, + "node_modules/d3-shape": { + "version": "3.2.0", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-timer": { "version": "3.0.1", "license": "ISC", @@ -5120,6 +6150,14 @@ "node": ">=12" } }, + "node_modules/dagre-d3-es": { + "version": "7.0.13", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, "node_modules/dashdash": { "version": "1.14.1", "dev": true, @@ -5131,15 +6169,26 @@ "node": ">=0.10" } }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/dayjs": { "version": "1.11.18", - "dev": true, "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -5154,17 +6203,12 @@ } } }, - "node_modules/decamelize": { - "version": "4.0.0", + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, "node_modules/deep-eql": { "version": "5.0.2", @@ -5179,6 +6223,56 @@ "dev": true, "license": "MIT" }, + "node_modules/default-browser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", + "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "dev": true, @@ -5206,15 +6300,6 @@ "version": "1.1.0", "license": "MIT" }, - "node_modules/diff": { - "version": "7.0.0", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/doctrine": { "version": "3.0.0", "dev": true, @@ -5310,6 +6395,8 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, "license": "MIT" }, @@ -5334,8 +6421,6 @@ }, "node_modules/empathic": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", - "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", "dev": true, "license": "MIT", "engines": { @@ -5431,8 +6516,6 @@ }, "node_modules/es-module-lexer": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, @@ -5463,6 +6546,7 @@ }, "node_modules/esbuild": { "version": "0.25.5", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -5597,9 +6681,9 @@ } }, "node_modules/eslint-plugin-storybook": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.0.7.tgz", - "integrity": "sha512-qOQq9KdT1jsBgT3qsxUH2n67aj1WR8D1XCoER8Q6yuVlS5TimNwk1mZeWkXVf/o4RQQT6flT2y5cG2gPLZPvJA==", + "version": "10.1.11", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.1.11.tgz", + "integrity": "sha512-mbq2r2kK5+AcLl0XDJ3to91JOgzCbHOqj+J3n+FRw6drk+M1boRqMShSoMMm0HdzXPLmlr7iur+qJ5ZuhH6ayQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5607,7 +6691,7 @@ }, "peerDependencies": { "eslint": ">=8", - "storybook": "^10.0.7" + "storybook": "^10.1.11" } }, "node_modules/eslint-scope": { @@ -5766,8 +6850,6 @@ }, "node_modules/expect-type": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5922,15 +7004,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "bin": { - "flat": "cli.js" - } - }, "node_modules/flat-cache": { "version": "4.0.1", "dev": true, @@ -5950,6 +7023,8 @@ }, "node_modules/foreground-child": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, "license": "ISC", "dependencies": { @@ -6002,6 +7077,7 @@ }, "node_modules/fsevents": { "version": "2.3.3", + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6119,22 +7195,25 @@ } }, "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" }, + "engines": { + "node": "20 || >=22" + }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -6150,23 +7229,17 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -6219,14 +7292,16 @@ }, "node_modules/graphql": { "version": "16.12.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", - "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "dev": true, "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "license": "MIT" + }, "node_modules/has-flag": { "version": "4.0.0", "dev": true, @@ -6286,19 +7361,8 @@ "node": ">= 0.4" } }, - "node_modules/he": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "he": "bin/he" - } - }, "node_modules/headers-polyfill": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", - "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", "dev": true, "license": "MIT" }, @@ -6310,10 +7374,21 @@ "htmlparser2": "10.0.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-escaper": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, "license": "MIT" }, @@ -6353,6 +7428,20 @@ "entities": "^6.0.0" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/http-signature": { "version": "1.4.0", "dev": true, @@ -6366,12 +7455,36 @@ "node": ">=0.10" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "1.1.1", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=8.12.0" + "node": ">=8.12.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/ieee754": { @@ -6403,8 +7516,6 @@ }, "node_modules/immediate": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, "node_modules/import-fresh": { @@ -6440,8 +7551,6 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { @@ -6456,6 +7565,13 @@ "version": "0.2.4", "license": "MIT" }, + "node_modules/internmap": { + "version": "2.0.3", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-buffer": { "version": "1.1.6", "dev": true, @@ -6475,6 +7591,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "dev": true, @@ -6508,6 +7640,25 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-installed-globally": { "version": "0.4.0", "dev": true, @@ -6525,8 +7676,6 @@ }, "node_modules/is-node-process": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", "dev": true, "license": "MIT" }, @@ -6546,14 +7695,12 @@ "node": ">=8" } }, - "node_modules/is-plain-obj": { - "version": "2.1.0", + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } + "license": "MIT" }, "node_modules/is-stream": { "version": "2.0.1", @@ -6582,10 +7729,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, "node_modules/isexe": { @@ -6600,8 +7761,6 @@ }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -6610,8 +7769,6 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6625,8 +7782,6 @@ }, "node_modules/istanbul-lib-source-maps": { "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6640,8 +7795,6 @@ }, "node_modules/istanbul-reports": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6653,17 +7806,19 @@ } }, "node_modules/jackspeak": { - "version": "3.4.3", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, + "engines": { + "node": "20 || >=22" + }, "funding": { "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/jiti": { @@ -6680,8 +7835,6 @@ }, "node_modules/js-yaml": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6695,6 +7848,79 @@ "dev": true, "license": "MIT" }, + "node_modules/jsdom": { + "version": "27.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.1.tgz", + "integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/dom-selector": "^6.7.2", + "cssstyle": "^5.3.1", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tldts": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", + "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/jsdom/node_modules/tldts-core": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", + "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom/node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/jsesc": { "version": "3.1.0", "dev": true, @@ -6780,8 +8006,6 @@ }, "node_modules/jszip": { "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { "lie": "~3.3.0", @@ -6790,6 +8014,27 @@ "setimmediate": "^1.0.5" } }, + "node_modules/katex": { + "version": "0.16.27", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/keyv": { "version": "4.5.4", "dev": true, @@ -6798,16 +8043,27 @@ "json-buffer": "3.0.1" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, + "node_modules/khroma": { + "version": "2.1.0" + }, + "node_modules/langium": { + "version": "3.3.1", "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, "engines": { - "node": ">=6" + "node": ">=16.0.0" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "license": "MIT" + }, "node_modules/lazy-ass": { "version": "1.6.0", "dev": true, @@ -6830,8 +8086,6 @@ }, "node_modules/lie": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "license": "MIT", "dependencies": { "immediate": "~3.0.5" @@ -6984,6 +8238,10 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.17.22", + "license": "MIT" + }, "node_modules/lodash.castarray": { "version": "4.4.0", "dev": true, @@ -7067,8 +8325,6 @@ }, "node_modules/magic-string": { "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -7076,8 +8332,6 @@ }, "node_modules/magicast": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", - "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", "dev": true, "license": "MIT", "dependencies": { @@ -7088,8 +8342,6 @@ }, "node_modules/make-dir": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { @@ -7104,8 +8356,6 @@ }, "node_modules/make-dir/node_modules/semver": { "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -7143,6 +8393,13 @@ "is-buffer": "~1.1.6" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge-stream": { "version": "2.0.0", "dev": true, @@ -7156,6 +8413,53 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.12.2", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.1", + "@mermaid-js/parser": "^0.6.3", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.13", + "dayjs": "^1.11.18", + "dompurify": "^3.2.5", + "katex": "^0.16.22", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^16.2.1", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mermaid/node_modules/marked": { + "version": "16.4.2", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "11.1.0", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/micromatch": { "version": "4.0.8", "dev": true, @@ -7263,40 +8567,14 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha": { - "version": "11.7.4", - "dev": true, + "node_modules/mlly": { + "version": "1.8.0", "license": "MIT", - "peer": true, "dependencies": { - "browser-stdout": "^1.3.1", - "chokidar": "^4.0.1", - "debug": "^4.3.5", - "diff": "^7.0.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^9.0.5", - "ms": "^2.1.3", - "picocolors": "^1.1.1", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^9.2.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" } }, "node_modules/mocha-junit-reporter": { @@ -7333,49 +8611,8 @@ "node": ">=8" } }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "9.0.5", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/mrmime": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, "license": "MIT", "engines": { @@ -7389,8 +8626,6 @@ }, "node_modules/msw": { "version": "2.12.3", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.3.tgz", - "integrity": "sha512-/5rpGC0eK8LlFqsHaBmL19/PVKxu/CCt8pO1vzp9X6SDLsRDh/Ccudkf3Ur5lyaKxJz9ndAx+LaThdv0ySqB6A==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7434,8 +8669,6 @@ }, "node_modules/msw-storybook-addon": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-2.0.6.tgz", - "integrity": "sha512-ExCwDbcJoM2V3iQU+fZNp+axVfNc7DWMRh4lyTXebDO8IbpUNYKGFUrA8UqaeWiRGKVuS7+fU+KXEa9b0OP6uA==", "dev": true, "license": "MIT", "dependencies": { @@ -7447,8 +8680,6 @@ }, "node_modules/msw/node_modules/tldts": { "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", - "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", "dev": true, "license": "MIT", "dependencies": { @@ -7460,15 +8691,11 @@ }, "node_modules/msw/node_modules/tldts-core": { "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", - "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", "dev": true, "license": "MIT" }, "node_modules/msw/node_modules/tough-cookie": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7480,8 +8707,6 @@ }, "node_modules/msw/node_modules/type-fest": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.2.0.tgz", - "integrity": "sha512-xxCJm+Bckc6kQBknN7i9fnP/xobQRsRQxR01CztFkp/h++yfVxUUcmMgfR2HttJx/dpWjS9ubVuyspJv24Q9DA==", "dev": true, "license": "(MIT OR CC0-1.0)", "dependencies": { @@ -7496,8 +8721,6 @@ }, "node_modules/mute-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "dev": true, "license": "ISC", "engines": { @@ -7517,6 +8740,7 @@ }, "node_modules/nanoid": { "version": "3.3.11", + "dev": true, "funding": [ { "type": "github", @@ -7593,6 +8817,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "dev": true, @@ -7616,8 +8859,6 @@ }, "node_modules/outvariant": { "version": "1.4.3", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", - "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", "dev": true, "license": "MIT" }, @@ -7665,13 +8906,17 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "license": "MIT" + }, "node_modules/pako": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, "node_modules/parent-module": { @@ -7685,6 +8930,23 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "dev": true, @@ -7707,37 +8969,39 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.11.1", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/path-to-regexp": { "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "dev": true, "license": "MIT" }, "node_modules/pathe": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, "license": "MIT" }, "node_modules/pathval": { @@ -7760,6 +9024,7 @@ }, "node_modules/picocolors": { "version": "1.1.1", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -7794,8 +9059,6 @@ }, "node_modules/pixelmatch": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz", - "integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==", "dev": true, "license": "ISC", "dependencies": { @@ -7805,10 +9068,17 @@ "pixelmatch": "bin/pixelmatch" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/playwright": { "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", - "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -7826,8 +9096,6 @@ }, "node_modules/playwright-core": { "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", - "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -7839,10 +9107,7 @@ }, "node_modules/playwright/node_modules/fsevents": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -7854,16 +9119,27 @@ }, "node_modules/pngjs": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", - "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", "dev": true, "license": "MIT", - "engines": { - "node": ">=14.19.0" + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" } }, "node_modules/postcss": { "version": "8.5.6", + "dev": true, "funding": [ { "type": "opencollective", @@ -8057,24 +9333,8 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/proxy-from-env": { "version": "1.0.0", "dev": true, @@ -8130,15 +9390,6 @@ ], "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/react": { "version": "19.0.0", "license": "MIT", @@ -8168,6 +9419,8 @@ }, "node_modules/react-docgen-typescript": { "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", + "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -8285,9 +9538,9 @@ } }, "node_modules/react-router": { - "version": "7.9.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz", - "integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -8307,12 +9560,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.9.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.3.tgz", - "integrity": "sha512-1QSbA0TGGFKTAc/aWjpfW/zoEukYfU4dc1dLkT/vvf54JoGMkW+fNA+3oyo2gWVW1GM7BxjJVHz5GnPJv40rvg==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz", + "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==", "license": "MIT", "dependencies": { - "react-router": "7.9.3" + "react-router": "7.12.0" }, "engines": { "node": ">=20.0.0" @@ -8344,8 +9597,6 @@ }, "node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -8359,23 +9610,8 @@ }, "node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/readdirp": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/recast": { "version": "0.23.11", "dev": true, @@ -8430,6 +9666,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "dev": true, @@ -8474,8 +9720,6 @@ }, "node_modules/rettime": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz", - "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", "dev": true, "license": "MIT" }, @@ -8493,8 +9737,13 @@ "dev": true, "license": "MIT" }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.44.1", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -8537,6 +9786,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8550,12 +9800,43 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/roughjs": { + "version": "4.6.6", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "dev": true, @@ -8578,6 +9859,10 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "license": "BSD-3-Clause" + }, "node_modules/rxjs": { "version": "7.8.2", "dev": true, @@ -8607,9 +9892,21 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "dev": true, "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.25.0", "license": "MIT" @@ -8622,15 +9919,6 @@ "semver": "bin/semver.js" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -8639,8 +9927,6 @@ }, "node_modules/setimmediate": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, "node_modules/shebang-command": { @@ -8732,8 +10018,6 @@ }, "node_modules/siginfo": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, @@ -8750,8 +10034,6 @@ }, "node_modules/sirv": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, "license": "MIT", "dependencies": { @@ -8763,13 +10045,6 @@ "node": ">=18" } }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, "node_modules/slice-ansi": { "version": "7.1.2", "dev": true, @@ -8837,15 +10112,11 @@ }, "node_modules/stackback": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, "node_modules/statuses": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { @@ -8854,28 +10125,27 @@ }, "node_modules/std-env": { "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, "node_modules/storybook": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.0.7.tgz", - "integrity": "sha512-7smAu0o+kdm378Q2uIddk32pn0UdIbrtTVU+rXRVtTVTCrK/P2cCui2y4JH+Bl3NgEq1bbBQpCAF/HKrDjk2Qw==", + "version": "10.1.11", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.1.11.tgz", + "integrity": "sha512-pKP5jXJYM4OjvNklGuHKO53wOCAwfx79KvZyOWHoi9zXUH5WVMFUe/ZfWyxXG/GTcj0maRgHGUjq/0I43r0dDQ==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.6.0", + "@storybook/icons": "^2.0.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", "@vitest/spy": "3.2.4", - "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", + "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", + "open": "^10.2.0", "recast": "^0.23.5", "semver": "^7.6.2", + "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "bin": { @@ -8907,15 +10177,11 @@ }, "node_modules/strict-event-emitter": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", "dev": true, "license": "MIT" }, "node_modules/string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -8923,8 +10189,6 @@ }, "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, "node_modules/string-argv": { @@ -8953,6 +10217,8 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -8966,6 +10232,8 @@ }, "node_modules/string-width-cjs/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -8974,11 +10242,15 @@ }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -8987,6 +10259,8 @@ }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -9013,6 +10287,8 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -9024,6 +10300,8 @@ }, "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -9082,6 +10360,10 @@ "inline-style-parser": "0.2.4" } }, + "node_modules/stylis": { + "version": "4.3.6", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "dev": true, @@ -9104,10 +10386,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tagged-tag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", - "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", "dev": true, "license": "MIT", "engines": { @@ -9185,22 +10472,17 @@ }, "node_modules/tinybench": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -9215,8 +10497,7 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -9232,8 +10513,7 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -9295,8 +10575,6 @@ }, "node_modules/totalist": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, "license": "MIT", "engines": { @@ -9314,6 +10592,19 @@ "node": ">=16" } }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "dev": true, @@ -9323,7 +10614,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -9335,9 +10628,6 @@ }, "node_modules/ts-dedent": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.10" @@ -9436,9 +10726,13 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/ufo": { + "version": "1.6.1", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/universalify": { @@ -9450,9 +10744,9 @@ } }, "node_modules/unplugin": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.10.tgz", - "integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==", + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", "dev": true, "license": "MIT", "dependencies": { @@ -9480,8 +10774,6 @@ }, "node_modules/until-async": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", - "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", "dev": true, "license": "MIT", "funding": { @@ -9609,8 +10901,7 @@ }, "node_modules/vite": { "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -9683,6 +10974,7 @@ }, "node_modules/vite/node_modules/fdir": { "version": "6.4.6", + "dev": true, "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -9695,6 +10987,7 @@ }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.2", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -9705,8 +10998,6 @@ }, "node_modules/vitest": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.8.tgz", - "integrity": "sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==", "dev": true, "license": "MIT", "dependencies": { @@ -9783,8 +11074,6 @@ }, "node_modules/vitest/node_modules/@vitest/expect": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.8.tgz", - "integrity": "sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==", "dev": true, "license": "MIT", "dependencies": { @@ -9801,8 +11090,6 @@ }, "node_modules/vitest/node_modules/@vitest/mocker": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.8.tgz", - "integrity": "sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==", "dev": true, "license": "MIT", "dependencies": { @@ -9828,8 +11115,6 @@ }, "node_modules/vitest/node_modules/@vitest/pretty-format": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.8.tgz", - "integrity": "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==", "dev": true, "license": "MIT", "dependencies": { @@ -9841,8 +11126,6 @@ }, "node_modules/vitest/node_modules/@vitest/spy": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.8.tgz", - "integrity": "sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==", "dev": true, "license": "MIT", "funding": { @@ -9851,8 +11134,6 @@ }, "node_modules/vitest/node_modules/@vitest/utils": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.8.tgz", - "integrity": "sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==", "dev": true, "license": "MIT", "dependencies": { @@ -9865,8 +11146,6 @@ }, "node_modules/vitest/node_modules/chai": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", - "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", "dev": true, "license": "MIT", "engines": { @@ -9875,8 +11154,6 @@ }, "node_modules/vitest/node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -9885,8 +11162,6 @@ }, "node_modules/vitest/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -9898,14 +11173,72 @@ }, "node_modules/vitest/node_modules/tinyrainbow": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -9913,6 +11246,44 @@ "dev": true, "license": "MIT" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/which": { "version": "2.0.2", "dev": true, @@ -9929,8 +11300,6 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -9952,12 +11321,6 @@ "node": ">=0.10.0" } }, - "node_modules/workerpool": { - "version": "9.3.4", - "dev": true, - "license": "Apache-2.0", - "peer": true - }, "node_modules/wrap-ansi": { "version": "9.0.2", "dev": true, @@ -9977,6 +11340,8 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -9993,6 +11358,8 @@ }, "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -10001,11 +11368,15 @@ }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -10014,6 +11385,8 @@ }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -10027,6 +11400,8 @@ }, "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -10088,11 +11463,44 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xml": { "version": "1.0.1", "dev": true, "license": "MIT" }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "dev": true, @@ -10108,7 +11516,7 @@ }, "node_modules/yaml": { "version": "2.8.1", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -10142,21 +11550,6 @@ "node": ">=12" } }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/yargs/node_modules/ansi-regex": { "version": "5.0.1", "dev": true, @@ -10224,8 +11617,6 @@ }, "node_modules/yoctocolors-cjs": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", - "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", "dev": true, "license": "MIT", "engines": { @@ -10241,32 +11632,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zustand": { - "version": "4.5.7", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } } } } diff --git a/client/webui/frontend/package.json b/client/webui/frontend/package.json index e578f7ce2..d60395079 100644 --- a/client/webui/frontend/package.json +++ b/client/webui/frontend/package.json @@ -1,6 +1,6 @@ { "name": "@SolaceLabs/solace-agent-mesh-ui", - "version": "1.24.2", + "version": "1.31.2", "description": "Solace Agent Mesh UI components - React library for building agent communication interfaces", "author": "SolaceLabs ", "license": "Apache-2.0", @@ -53,7 +53,8 @@ "preview": "vite preview", "storybook": "storybook dev -p 6006", "test:storybook": "vitest --project=storybook", - "ci:storybook": "CI=true npm run test:storybook" + "test:unit": "vitest --project=unit", + "ci:storybook": "CI=true vitest --project=unit --project=storybook" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ @@ -78,9 +79,11 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.2.7", + "@stepperize/react": "^5.1.9", "@tailwindcss/vite": "^4.1.10", + "@tanstack/react-query": "5.90.16", "@tanstack/react-table": "^8.21.3", - "@xyflow/react": "^12.6.4", + "@use-gesture/react": "^10.3.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dompurify": "^3.2.6", @@ -90,13 +93,14 @@ "jszip": "^3.10.1", "lucide-react": "^0.511.0", "marked": "^15.0.12", + "mermaid": "^11.12.2", "react": "19.0.0", "react-dom": "19.0.0", "react-hook-form": "^7.65.0", "react-intersection-observer": "^9.16.0", "react-json-view-lite": "^2.4.1", "react-resizable-panels": "^3.0.3", - "react-router-dom": "7.9.3", + "react-router-dom": "7.12.0", "tailwind-merge": "^3.3.0", "tailwind-scrollbar-hide": "^4.0.0", "tailwindcss": "^4.1.10", @@ -108,15 +112,19 @@ "@types/react-dom": "19.0.0", "react": "19.0.0", "react-dom": "19.0.0", - "react-router-dom": "7.9.3" + "react-router-dom": "7.12.0" }, "devDependencies": { "@a2a-js/sdk": "^0.3.2", "@eslint/js": "^9.25.0", - "@storybook/addon-vitest": "^10.0.7", - "@storybook/react-vite": "^10.0.7", + "@storybook/addon-vitest": "^10.1.10", + "@storybook/react": "^10.1.8", + "@storybook/react-vite": "^10.1.10", "@tailwindcss/typography": "^0.5.16", + "@tanstack/eslint-plugin-query": "5.91.2", "@testing-library/cypress": "^10.0.3", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", "@types/js-yaml": "^4.0.9", "@types/node": "^22.15.29", "@types/react": "19.0.0", @@ -128,8 +136,9 @@ "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", - "eslint-plugin-storybook": "^10.0.7", + "eslint-plugin-storybook": "^10.1.10", "globals": "^16.0.0", + "jsdom": "^27.0.1", "lint-staged": "^16.2.3", "mocha-junit-reporter": "^2.2.1", "msw": "^2.12.3", @@ -137,7 +146,7 @@ "playwright": "^1.56.1", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.6.13", - "storybook": "^10.0.7", + "storybook": "^10.1.10", "tw-animate-css": "^1.3.3", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", diff --git a/client/webui/frontend/src/App.tsx b/client/webui/frontend/src/App.tsx index e957fbceb..43d5a3303 100644 --- a/client/webui/frontend/src/App.tsx +++ b/client/webui/frontend/src/App.tsx @@ -1,7 +1,7 @@ import { RouterProvider } from "react-router-dom"; import { TextSelectionProvider } from "@/lib/components/chat/selection"; -import { AuthProvider, ConfigProvider, CsrfProvider, ProjectProvider, TaskProvider, ThemeProvider, AudioSettingsProvider } from "@/lib/providers"; +import { AuthProvider, ConfigProvider, CsrfProvider, ProjectProvider, TaskProvider, ThemeProvider, AudioSettingsProvider, QueryProvider } from "@/lib/providers"; import { createRouter } from "./router"; @@ -11,23 +11,25 @@ function AppContent() { function App() { return ( - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + ); } diff --git a/client/webui/frontend/src/lib/api/index.ts b/client/webui/frontend/src/lib/api/index.ts index f23e1b1be..26007e32b 100644 --- a/client/webui/frontend/src/lib/api/index.ts +++ b/client/webui/frontend/src/lib/api/index.ts @@ -1 +1,2 @@ export { api } from "./client"; +export * from "./projects"; diff --git a/client/webui/frontend/src/lib/api/projects/hooks.ts b/client/webui/frontend/src/lib/api/projects/hooks.ts new file mode 100644 index 000000000..bb5ff54ef --- /dev/null +++ b/client/webui/frontend/src/lib/api/projects/hooks.ts @@ -0,0 +1,134 @@ +/** + * ⚠️ WARNING: THESE HOOKS ARE NOT YET READY FOR USE ⚠️ + * + * This file contains React Query hooks that are still under development and testing. + * DO NOT import or use these hooks in your components yet. + * + * Current Status: + * - ❌ Not fully tested + * - ❌ May have breaking API changes + * - ❌ Not documented for public use + * - ❌ Currently being refactored and tested in enterprise + * + * When ready for use, this warning will be removed and proper documentation will be added. + * + * @internal - These exports are marked as internal and should not be used outside this package + */ + +import { skipToken, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { CreateProjectRequest, Project, UpdateProjectData } from "@/lib/types/projects"; +import { projectKeys } from "./keys"; +import * as projectService from "./service"; + +/** + * @internal - DO NOT USE: Still under development + */ +export function useProjects() { + return useQuery({ + queryKey: projectKeys.lists(), + queryFn: projectService.getProjects, + }); +} + +/** @internal - DO NOT USE: Still under development */ +export function useProjectArtifactsNew(projectId: string | null) { + return useQuery({ + queryKey: projectId ? projectKeys.artifacts(projectId) : ["projects", "artifacts", "empty"], + queryFn: projectId ? () => projectService.getProjectArtifacts(projectId) : skipToken, + }); +} + +/** @internal - DO NOT USE: Still under development */ +export function useCreateProject() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: CreateProjectRequest) => projectService.createProject(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: projectKeys.lists() }); + }, + }); +} + +/** @internal - DO NOT USE: Still under development */ +export function useUpdateProject() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ projectId, data }: { projectId: string; data: UpdateProjectData }) => projectService.updateProject(projectId, data), + onSuccess: (updatedProject: Project) => { + queryClient.invalidateQueries({ queryKey: projectKeys.lists() }); + queryClient.invalidateQueries({ queryKey: projectKeys.detail(updatedProject.id) }); + }, + }); +} + +/** @internal - DO NOT USE: Still under development */ +export function useDeleteProject() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (projectId: string) => projectService.deleteProject(projectId), + onSuccess: (_, projectId) => { + queryClient.invalidateQueries({ queryKey: projectKeys.lists() }); + queryClient.removeQueries({ queryKey: projectKeys.detail(projectId) }); + }, + }); +} + +/** @internal - DO NOT USE: Still under development */ +export function useAddFilesToProject() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ projectId, files, fileMetadata }: { projectId: string; files: File[]; fileMetadata?: Record }) => projectService.addFilesToProject(projectId, files, fileMetadata), + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ queryKey: projectKeys.lists() }); + queryClient.invalidateQueries({ queryKey: projectKeys.artifacts(projectId) }); + }, + }); +} + +/** @internal - DO NOT USE: Still under development */ +export function useRemoveFileFromProject() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ projectId, filename }: { projectId: string; filename: string }) => projectService.removeFileFromProject(projectId, filename), + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ queryKey: projectKeys.lists() }); + queryClient.invalidateQueries({ queryKey: projectKeys.artifacts(projectId) }); + }, + }); +} + +/** @internal - DO NOT USE: Still under development */ +export function useUpdateFileMetadata() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ projectId, filename, description }: { projectId: string; filename: string; description: string }) => projectService.updateFileMetadata(projectId, filename, description), + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ queryKey: projectKeys.artifacts(projectId) }); + }, + }); +} + +/** @internal - DO NOT USE: Still under development */ +export function useExportProject() { + return useMutation({ + mutationFn: (projectId: string) => projectService.exportProject(projectId), + }); +} + +/** @internal - DO NOT USE: Still under development */ +export function useImportProject() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ file, options }: { file: File; options: { preserveName: boolean; customName?: string } }) => projectService.importProject(file, options), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: projectKeys.lists() }); + }, + }); +} diff --git a/client/webui/frontend/src/lib/api/projects/index.ts b/client/webui/frontend/src/lib/api/projects/index.ts new file mode 100644 index 000000000..007f69d09 --- /dev/null +++ b/client/webui/frontend/src/lib/api/projects/index.ts @@ -0,0 +1 @@ +export * from "./hooks"; diff --git a/client/webui/frontend/src/lib/api/projects/keys.ts b/client/webui/frontend/src/lib/api/projects/keys.ts new file mode 100644 index 000000000..cceaf43b8 --- /dev/null +++ b/client/webui/frontend/src/lib/api/projects/keys.ts @@ -0,0 +1,13 @@ +/** + * Query keys for React Query caching and invalidation + * Following the pattern: ['entity', ...filters/ids] + */ +export const projectKeys = { + all: ["projects"] as const, + lists: () => [...projectKeys.all, "list"] as const, + list: (filters?: Record) => [...projectKeys.lists(), { filters }] as const, + details: () => [...projectKeys.all, "detail"] as const, + detail: (id: string) => [...projectKeys.details(), id] as const, + artifacts: (id: string) => [...projectKeys.detail(id), "artifacts"] as const, + sessions: (id: string) => [...projectKeys.detail(id), "sessions"] as const, +}; diff --git a/client/webui/frontend/src/lib/api/projects/service.ts b/client/webui/frontend/src/lib/api/projects/service.ts new file mode 100644 index 000000000..79720873d --- /dev/null +++ b/client/webui/frontend/src/lib/api/projects/service.ts @@ -0,0 +1,81 @@ +import { api } from "@/lib/api"; +import type { ArtifactInfo, CreateProjectRequest, Project, UpdateProjectData } from "@/lib"; +import type { PaginatedSessionsResponse } from "@/lib/components/chat/SessionList"; + +export const getProjects = async () => { + const response = await api.webui.get<{ projects: Project[]; total: number }>("/api/v1/projects?include_artifact_count=true"); + return response; +}; + +export const createProject = async (data: CreateProjectRequest) => { + const formData = new FormData(); + formData.append("name", data.name); + + if (data.description) { + formData.append("description", data.description); + } + + const response = await api.webui.post("/api/v1/projects", formData); + return response; +}; + +export const addFilesToProject = async (projectId: string, files: File[], fileMetadata?: Record) => { + const formData = new FormData(); + + files.forEach(file => { + formData.append("files", file); + }); + + if (fileMetadata && Object.keys(fileMetadata).length > 0) { + formData.append("fileMetadata", JSON.stringify(fileMetadata)); + } + + const response = await api.webui.post(`/api/v1/projects/${projectId}/artifacts`, formData); + return response; +}; + +export const removeFileFromProject = async (projectId: string, filename: string) => { + const response = await api.webui.delete(`/api/v1/projects/${projectId}/artifacts/${encodeURIComponent(filename)}`); + return response; +}; + +export const updateFileMetadata = async (projectId: string, filename: string, description: string) => { + const formData = new FormData(); + formData.append("description", description); + + const response = await api.webui.patch(`/api/v1/projects/${projectId}/artifacts/${encodeURIComponent(filename)}`, formData); + return response; +}; + +export const updateProject = async (projectId: string, data: UpdateProjectData) => { + const response = await api.webui.put(`/api/v1/projects/${projectId}`, data); + return response; +}; + +export const deleteProject = async (projectId: string) => { + await api.webui.delete(`/api/v1/projects/${projectId}`); +}; + +export const getProjectArtifacts = async (projectId: string) => { + const response = await api.webui.get(`/api/v1/projects/${projectId}/artifacts`); + return response; +}; + +export const getProjectSessions = async (projectId: string) => { + const response = await api.webui.get(`/api/v1/sessions?project_id=${projectId}&pageNumber=1&pageSize=100`); + return response.data; +}; + +export const exportProject = async (projectId: string) => { + const response = await api.webui.get(`/api/v1/projects/${projectId}/export`, { fullResponse: true }); + return await response.blob(); +}; + +export const importProject = async (file: File, options: { preserveName: boolean; customName?: string }) => { + const formData = new FormData(); + formData.append("file", file); + formData.append("options", JSON.stringify(options)); + + const result = await api.webui.post("/api/v1/projects/import", formData); + return result; +}; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/EdgeLayer.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/EdgeLayer.tsx new file mode 100644 index 000000000..98794c1cd --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/EdgeLayer.tsx @@ -0,0 +1,172 @@ +import React, { useState } from "react"; +import type { Edge } from "./utils/types"; + +interface EdgeLayerProps { + edges: Edge[]; + selectedEdgeId?: string | null; + onEdgeClick?: (edge: Edge) => void; +} + +const EdgeLayer: React.FC = ({ edges, selectedEdgeId, onEdgeClick }) => { + const [hoveredEdgeId, setHoveredEdgeId] = useState(null); + + // Calculate bezier curve path + const getBezierPath = (edge: Edge): string => { + const { sourceX, sourceY, targetX, targetY } = edge; + + // Calculate control points for bezier curve + const deltaY = targetY - sourceY; + const controlOffset = Math.min(Math.abs(deltaY) * 0.5, 100); + + const control1X = sourceX; + const control1Y = sourceY + controlOffset; + const control2X = targetX; + const control2Y = targetY - controlOffset; + + return `M ${sourceX} ${sourceY} C ${control1X} ${control1Y}, ${control2X} ${control2Y}, ${targetX} ${targetY}`; + }; + + // Get edge style + const getEdgeStyle = (edge: Edge, isHovered: boolean) => { + const isSelected = edge.id === selectedEdgeId; + + // Priority: Error > Selected > Hover > Default + if (edge.isError) { + return { + stroke: isHovered ? "#dc2626" : "#ef4444", + strokeWidth: isHovered ? 3 : 2, + }; + } + + if (isSelected) { + return { + stroke: "#3b82f6", + strokeWidth: 3, + }; + } + + if (isHovered) { + return { + stroke: "#6b7280", + strokeWidth: 3, + }; + } + + return { + stroke: "#9ca3af", + strokeWidth: 2, + }; + }; + + return ( + + + + + + + + + + + + + + {edges.map(edge => { + const isHovered = edge.id === hoveredEdgeId; + const isSelected = edge.id === selectedEdgeId; + const path = getBezierPath(edge); + const style = getEdgeStyle(edge, isHovered); + + // Determine marker + let markerEnd = "url(#arrowhead)"; + if (edge.isError) { + markerEnd = "url(#arrowhead-error)"; + } else if (isSelected) { + markerEnd = "url(#arrowhead-selected)"; + } + + return ( + + {/* Invisible wider path for easier clicking */} + setHoveredEdgeId(edge.id)} + onMouseLeave={() => setHoveredEdgeId(null)} + onClick={() => onEdgeClick?.(edge)} + /> + + {/* Visible edge path */} + + + {/* Label */} + {edge.label && isHovered && ( + + {edge.label} + + )} + + ); + })} + + ); +}; + +export default EdgeLayer; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/FlowChartPanel.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/FlowChartPanel.tsx new file mode 100644 index 000000000..035a497d7 --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/FlowChartPanel.tsx @@ -0,0 +1,300 @@ +import React, { useCallback, useState, useRef, useEffect } from "react"; +import { Home } from "lucide-react"; +import type { VisualizerStep } from "@/lib/types"; +import { Dialog, DialogContent, DialogFooter, VisuallyHidden, DialogTitle, DialogDescription, Button, Tooltip, TooltipTrigger, TooltipContent } from "@/lib/components/ui"; +import { useTaskContext } from "@/lib/hooks"; +import { useAgentCards } from "@/lib/hooks"; +import WorkflowRenderer from "./WorkflowRenderer"; +import type { LayoutNode, Edge } from "./utils/types"; +import { findNodeDetails, type NodeDetails } from "./utils/nodeDetailsHelper"; +import NodeDetailsCard from "./NodeDetailsCard"; +import PanZoomCanvas, { type PanZoomCanvasRef } from "./PanZoomCanvas"; + +// Approximate width of the right side panel when visible +const RIGHT_PANEL_WIDTH = 400; + +interface FlowChartPanelProps { + processedSteps: VisualizerStep[]; + isRightPanelVisible?: boolean; + isSidePanelTransitioning?: boolean; +} + +const FlowChartPanel: React.FC = ({ + processedSteps, + isRightPanelVisible = false +}) => { + const { highlightedStepId, setHighlightedStepId } = useTaskContext(); + const { agentNameMap } = useAgentCards(); + + // Dialog state + const [selectedNodeDetails, setSelectedNodeDetails] = useState(null); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [isDialogExpanded, setIsDialogExpanded] = useState(false); + + // Show detail toggle - controls whether to show nested agent internals + const [showDetail, setShowDetail] = useState(true); + + // Pan/zoom canvas ref + const canvasRef = useRef(null); + + // Ref to measure actual rendered content dimensions + const contentRef = useRef(null); + + // Track if user has manually interacted with pan/zoom + const hasUserInteracted = useRef(false); + const prevStepCount = useRef(processedSteps.length); + + // Track content dimensions (measured from actual DOM, adjusted for current scale) + // Using a ref so effects don't re-run when it changes + const contentWidthRef = useRef(800); + + // Use ResizeObserver to automatically detect content size changes + // This handles node expansions, collapses, and any other layout changes + useEffect(() => { + const element = contentRef.current; + if (!element) return; + + const measureContent = () => { + if (contentRef.current && canvasRef.current) { + const rect = contentRef.current.getBoundingClientRect(); + // getBoundingClientRect returns scaled dimensions, so divide by current scale + // to get the "natural" width at scale 1.0 + const currentScale = canvasRef.current.getTransform().scale; + const naturalWidth = rect.width / currentScale; + contentWidthRef.current = naturalWidth; + } + }; + + // Initial measurement + measureContent(); + + // Watch for size changes + const resizeObserver = new ResizeObserver(() => { + measureContent(); + }); + resizeObserver.observe(element); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + // Calculate side panel width for auto-fit calculations + const sidePanelWidth = isRightPanelVisible ? RIGHT_PANEL_WIDTH : 0; + + // Reset interaction flag when a new task starts (step count goes back to near zero) + useEffect(() => { + if (processedSteps.length <= 1) { + hasUserInteracted.current = false; + prevStepCount.current = 0; + } + }, [processedSteps.length]); + + // Auto-fit when new steps are added - only if user hasn't interacted + useEffect(() => { + const currentCount = processedSteps.length; + if (currentCount > prevStepCount.current && !hasUserInteracted.current) { + // New steps added and user hasn't interacted - fit to content with animation + setTimeout(() => { + canvasRef.current?.fitToContent(contentWidthRef.current, { animated: true }); + }, 150); // Longer delay to let content measurement update + } + prevStepCount.current = currentCount; + }, [processedSteps.length]); + + // Re-fit when showDetail changes - only if user hasn't manually adjusted the view + useEffect(() => { + if (!hasUserInteracted.current) { + setTimeout(() => { + canvasRef.current?.fitToContent(contentWidthRef.current, { animated: true, maxFitScale: 2.5 }); + }, 150); // Longer delay to let content measurement update + } + }, [showDetail]); + + // Re-fit when side panel visibility changes (if user hasn't interacted) + useEffect(() => { + if (!hasUserInteracted.current) { + setTimeout(() => { + canvasRef.current?.fitToContent(contentWidthRef.current, { animated: true }); + }, 150); + } + }, [isRightPanelVisible]); + + // Handler to mark user interaction + const handleUserInteraction = useCallback(() => { + hasUserInteracted.current = true; + }, []); + + // Handle node click + const handleNodeClick = useCallback( + (node: LayoutNode) => { + // Mark user interaction to stop auto-fit + hasUserInteracted.current = true; + + const stepId = node.data.visualizerStepId; + + // Find detailed information about this node + const nodeDetails = findNodeDetails(node, processedSteps); + + // Set highlighted step for synchronization with other views + if (stepId) { + setHighlightedStepId(stepId); + } + + if (isRightPanelVisible) { + // Right panel is open, just highlight + } else { + // Show dialog with node details + setSelectedNodeDetails(nodeDetails); + setIsDialogOpen(true); + } + }, + [processedSteps, isRightPanelVisible, setHighlightedStepId] + ); + + // Handle edge click + const handleEdgeClick = useCallback( + (edge: Edge) => { + const stepId = edge.visualizerStepId; + if (!stepId) return; + + // For edges, just highlight the step + setHighlightedStepId(stepId); + + // Note: Edges don't have request/result pairs like nodes do, + // so we don't show a popover for them + }, + [setHighlightedStepId] + ); + + // Handle dialog close + const handleDialogClose = useCallback(() => { + setIsDialogOpen(false); + setSelectedNodeDetails(null); + setIsDialogExpanded(false); + }, []); + + // Handle dialog width change from NodeDetailsCard (NP-3) + const handleDialogWidthChange = useCallback((isExpanded: boolean) => { + setIsDialogExpanded(isExpanded); + }, []); + + // Handle pane click (clear selection) + const handlePaneClick = useCallback( + (event: React.MouseEvent) => { + // Only clear if clicking on the wrapper itself, not on nodes + if (event.target === event.currentTarget) { + setHighlightedStepId(null); + } + }, + [setHighlightedStepId] + ); + + // Handle re-center button click - allow zooming in up to 2.5x + const handleRecenter = useCallback(() => { + canvasRef.current?.fitToContent(contentWidthRef.current, { animated: true, maxFitScale: 2.5 }); + hasUserInteracted.current = false; + }, []); + + return ( +
+ {/* Controls bar - Show Detail toggle and Re-center button */} +
+ {/* Re-center button (D-6) */} + + + + + Re-center diagram + + +
+ + + Show Detail + + + + + + {showDetail ? "Hide nested agent details" : "Show nested agent details"} + +
+ + +
+
+ +
+
+
+ + {/* Node Details Dialog */} + + e.preventDefault()} + onInteractOutside={(e) => e.preventDefault()} + > + + Node Details + Details for the selected node + + {selectedNodeDetails && ( +
+ +
+ )} + + + +
+
+
+ ); +}; + +export default FlowChartPanel; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/NodeDetailsCard.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/NodeDetailsCard.tsx new file mode 100644 index 000000000..85fb98b82 --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/NodeDetailsCard.tsx @@ -0,0 +1,1214 @@ +import React, { useState, useEffect } from "react"; +import { ArrowRight, Bot, CheckCircle, Eye, FileText, GitBranch, Loader2, RefreshCw, Terminal, User, Workflow, Wrench, X, Zap } from "lucide-react"; +import type { NodeDetails } from "./utils/nodeDetailsHelper"; +import { JSONViewer, MarkdownHTMLConverter } from "@/lib/components"; +import type { VisualizerStep, ToolDecision } from "@/lib/types"; +import { useChatContext } from "@/lib/hooks"; +import { parseArtifactUri } from "@/lib/utils/download"; +import { api } from "@/lib/api"; + +const MAX_ARTIFACT_DISPLAY_LENGTH = 5000; + +interface ArtifactContentViewerProps { + uri?: string; + name: string; + version?: number; + mimeType?: string; +} + +/** + * Component to fetch and display artifact content inline + */ +const ArtifactContentViewer: React.FC = ({ uri, name, version, mimeType }) => { + const { sessionId } = useChatContext(); + const [content, setContent] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [isTruncated, setIsTruncated] = useState(false); + + useEffect(() => { + const fetchContent = async () => { + if (!uri && !name) return; + + setIsLoading(true); + setError(null); + + try { + let filename = name; + let artifactVersion = version?.toString() || "latest"; + + // Try to parse URI if available + if (uri) { + const parsed = parseArtifactUri(uri); + if (parsed) { + filename = parsed.filename; + if (parsed.version) { + artifactVersion = parsed.version; + } + } + } + + // Construct API endpoint + const endpoint = `/api/v1/artifacts/${encodeURIComponent(sessionId || "null")}/${encodeURIComponent(filename)}/versions/${artifactVersion}`; + + const response = await api.webui.get(endpoint, { fullResponse: true, credentials: "include" }); + if (!response.ok) { + throw new Error(`Failed to fetch artifact: ${response.statusText}`); + } + + const blob = await response.blob(); + const text = await blob.text(); + + // Truncate if too long + if (text.length > MAX_ARTIFACT_DISPLAY_LENGTH) { + setContent(text.substring(0, MAX_ARTIFACT_DISPLAY_LENGTH)); + setIsTruncated(true); + } else { + setContent(text); + setIsTruncated(false); + } + } catch (err) { + console.error("Error fetching artifact:", err); + setError(err instanceof Error ? err.message : "Failed to load artifact"); + } finally { + setIsLoading(false); + } + }; + + fetchContent(); + }, [uri, name, version, sessionId]); + + const renderContent = () => { + if (!content) return null; + + const effectiveMimeType = mimeType || (name.endsWith(".json") ? "application/json" : + name.endsWith(".yaml") || name.endsWith(".yml") ? "text/yaml" : + name.endsWith(".csv") ? "text/csv" : "text/plain"); + + // Try to parse and format JSON + if (effectiveMimeType === "application/json" || name.endsWith(".json")) { + try { + const parsed = JSON.parse(content); + return ( +
+ +
+ ); + } catch { + // Fall through to plain text + } + } + + // For YAML, CSV, and other text formats, show as preformatted text + return ( +
+                {content}
+            
+ ); + }; + + if (isLoading) { + return ( +
+ + Loading artifact content... +
+ ); + } + + if (error) { + return ( +
+ {error} +
+ ); + } + + if (!content) { + return ( +
+ No content available +
+ ); + } + + return ( +
+ {renderContent()} + {isTruncated && ( +
+ Content truncated (showing first {MAX_ARTIFACT_DISPLAY_LENGTH.toLocaleString()} characters) +
+ )} +
+ ); +}; + +interface NodeDetailsCardProps { + nodeDetails: NodeDetails; + onClose?: () => void; + onWidthChange?: (isExpanded: boolean) => void; +} + +/** + * Component to display detailed request and result information for a clicked node + */ +const NodeDetailsCard: React.FC = ({ nodeDetails, onClose, onWidthChange }) => { + const { artifacts, setPreviewArtifact: setSidePanelPreviewArtifact, setActiveSidePanelTab, setIsSidePanelCollapsed, navigateArtifactVersion } = useChatContext(); + + // Local state for inline artifact preview (NP-3) + const [inlinePreviewArtifact, setInlinePreviewArtifact] = useState<{ name: string; version?: number; mimeType?: string } | null>(null); + + // Notify parent when expansion state changes + useEffect(() => { + onWidthChange?.(inlinePreviewArtifact !== null); + }, [inlinePreviewArtifact, onWidthChange]); + + const getNodeIcon = () => { + switch (nodeDetails.nodeType) { + case 'user': + return ; + case 'agent': + return ; + case 'llm': + return ; + case 'tool': + return ; + case 'switch': + return ; + case 'loop': + return ; + case 'group': + return ; + default: + return ; + } + }; + + const renderStepContent = (step: VisualizerStep | undefined, isRequest: boolean) => { + if (!step) { + return ( +
+ {isRequest ? "No request data available" : "No result data available"} +
+ ); + } + + // Format timestamp with milliseconds + const date = new Date(step.timestamp); + const timeString = date.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, // Use 24-hour format + }); + const milliseconds = String(date.getMilliseconds()).padStart(3, "0"); + const formattedTimestamp = `${timeString}.${milliseconds}`; + + return ( +
+ {/* Timestamp */} +
+ {formattedTimestamp} +
+ + {/* Step-specific content */} + {renderStepTypeContent(step)} +
+ ); + }; + + const renderStepTypeContent = (step: VisualizerStep) => { + switch (step.type) { + case 'USER_REQUEST': + return renderUserRequest(step); + case 'WORKFLOW_AGENT_REQUEST': + return renderWorkflowAgentRequest(step); + case 'AGENT_RESPONSE_TEXT': + return renderAgentResponse(step); + case 'AGENT_LLM_CALL': + return renderLLMCall(step); + case 'AGENT_LLM_RESPONSE_TO_AGENT': + return renderLLMResponse(step); + case 'AGENT_LLM_RESPONSE_TOOL_DECISION': + return renderLLMToolDecision(step); + case 'AGENT_TOOL_INVOCATION_START': + return renderToolInvocation(step); + case 'AGENT_TOOL_EXECUTION_RESULT': + return renderToolResult(step); + case 'WORKFLOW_NODE_EXECUTION_START': + // For agent nodes in workflows, show as agent invocation + if (step.data.workflowNodeExecutionStart?.nodeType === 'agent') { + return renderWorkflowAgentInvocation(step); + } + return renderWorkflowNodeStart(step); + case 'WORKFLOW_NODE_EXECUTION_RESULT': + return renderWorkflowNodeResult(step); + case 'WORKFLOW_EXECUTION_START': + return renderWorkflowStart(step); + case 'WORKFLOW_EXECUTION_RESULT': + return renderWorkflowResult(step); + default: + return ( +
+ {step.title} +
+ ); + } + }; + + const renderUserRequest = (step: VisualizerStep) => ( +
+

User Input

+ {step.data.text && ( +
+ {step.data.text} +
+ )} +
+ ); + + const renderWorkflowAgentRequest = (step: VisualizerStep) => { + const data = step.data.workflowAgentRequest; + if (!data) return null; + + return ( +
+

+ Workflow Agent Request +

+
+ {data.nodeId && ( +
+ Node Id:{' '} + {data.nodeId} +
+ )} + + {/* Instruction from workflow node */} + {data.instruction && ( +
+
Instruction:
+
+ {data.instruction} +
+
+ )} + + {/* Input as artifact reference */} + {data.inputArtifactRef && ( +
+
+ Input: + {data.inputArtifactRef.name} + {data.inputArtifactRef.version !== undefined && ( + v{data.inputArtifactRef.version} + )} +
+
+ +
+
+ )} + + {/* Input as text (for simple text schemas) */} + {data.inputText && !data.inputArtifactRef && ( +
+
Input:
+
+ {data.inputText} +
+
+ )} + + {/* Input Schema */} + {data.inputSchema && ( +
+
Input Schema:
+
+ +
+
+ )} + + {/* Output Schema */} + {data.outputSchema && ( +
+
Output Schema:
+
+ +
+
+ )} + + {/* No input data available */} + {!data.inputText && !data.inputArtifactRef && !data.instruction && ( +
+ No input data available +
+ )} +
+
+ ); + }; + + const renderAgentResponse = (step: VisualizerStep) => ( +
+

Agent Response

+ {step.data.text && ( +
+ {step.data.text} +
+ )} +
+ ); + + const renderLLMCall = (step: VisualizerStep) => { + const data = step.data.llmCall; + if (!data) return null; + + return ( +
+

LLM Request

+
+
+ Model: {data.modelName} +
+
+
Prompt:
+
+                            {data.promptPreview}
+                        
+
+
+
+ ); + }; + + const renderLLMResponse = (step: VisualizerStep) => { + const data = step.data.llmResponseToAgent; + if (!data) return null; + + return ( +
+

LLM Response

+
+ {data.modelName && ( +
+ Model: {data.modelName} +
+ )} +
+
+                            {data.response || data.responsePreview}
+                        
+
+ {data.isFinalResponse !== undefined && ( +
+ Final Response: {data.isFinalResponse ? "Yes" : "No"} +
+ )} +
+
+ ); + }; + + const renderLLMToolDecision = (step: VisualizerStep) => { + const data = step.data.toolDecision; + if (!data) return null; + + return ( +
+

+ LLM Tool Decision{data.isParallel ? " (Parallel)" : ""} +

+
+ {data.decisions && data.decisions.length > 0 && ( +
+
Tools to invoke:
+
+ {data.decisions.map((decision: ToolDecision, index: number) => ( +
+
+ {decision.toolName} + {decision.isPeerDelegation && ( + + {decision.toolName.startsWith('workflow_') ? 'Workflow' : 'Peer Agent'} + + )} +
+ {decision.toolArguments && Object.keys(decision.toolArguments).length > 0 && ( +
+ {renderFormattedArguments(decision.toolArguments)} +
+ )} +
+ ))} +
+
+ )} +
+
+ ); + }; + + const renderToolInvocation = (step: VisualizerStep) => { + const data = step.data.toolInvocationStart; + if (!data) return null; + + return ( +
+

+ {data.isPeerInvocation ? "Peer Agent Call" : "Tool Invocation"} +

+
+
+ Tool: {data.toolName} +
+
+
Arguments:
+ {renderFormattedArguments(data.toolArguments)} +
+
+
+ ); + }; + + const renderFormattedArguments = (args: Record) => { + const entries = Object.entries(args); + + if (entries.length === 0) { + return ( +
+ No arguments +
+ ); + } + + return ( +
+ {entries.map(([key, value]) => ( +
+
+ {key} +
+
+ {renderArgumentValue(value)} +
+
+ ))} +
+ ); + }; + + const renderArgumentValue = (value: any): React.ReactNode => { + // Handle null/undefined + if (value === null) { + return null; + } + if (value === undefined) { + return undefined; + } + + // Handle primitives + if (typeof value === 'string') { + return {value}; + } + if (typeof value === 'number') { + return {value}; + } + if (typeof value === 'boolean') { + return {value.toString()}; + } + + // Handle arrays + if (Array.isArray(value)) { + if (value.length === 0) { + return []; + } + // For simple arrays of primitives, show inline + if (value.every(item => typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean')) { + return ( +
+ {value.map((item, idx) => ( +
+ {renderArgumentValue(item)} +
+ ))} +
+ ); + } + // For complex arrays, use JSONViewer + return ( +
+ +
+ ); + } + + // Handle objects + if (typeof value === 'object') { + const entries = Object.entries(value); + + // For small objects with simple values, render inline + if (entries.length <= 5 && entries.every(([_, v]) => + typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null + )) { + return ( +
+ {entries.map(([k, v]) => ( +
+ {k}: + {renderArgumentValue(v)} +
+ ))} +
+ ); + } + + // For complex objects, use JSONViewer + return ( +
+ +
+ ); + } + + // Fallback + return {String(value)}; + }; + + const renderToolResult = (step: VisualizerStep) => { + const data = step.data.toolResult; + if (!data) return null; + + return ( +
+

+ {data.isPeerResponse ? "Peer Agent Result" : "Tool Result"} +

+
+
+ Tool: {data.toolName} +
+
+
Result:
+ {typeof data.resultData === "object" && data.resultData !== null ? ( + renderFormattedArguments(data.resultData) + ) : ( +
+
+ {renderArgumentValue(data.resultData)} +
+
+ )} +
+
+
+ ); + }; + + const renderWorkflowAgentInvocation = (step: VisualizerStep) => { + const data = step.data.workflowNodeExecutionStart; + if (!data) return null; + + return ( +
+

+ Workflow Agent Invocation +

+
+
+ Agent: {data.agentName || data.nodeId} +
+
+ Workflow Node: {data.nodeId} +
+ {(data.iterationIndex !== undefined && data.iterationIndex !== null && typeof data.iterationIndex === 'number') && ( +
+ Iteration #{data.iterationIndex} +
+ )} + {data.inputArtifactRef && ( +
+
Input:
+
+
+
+ Artifact Reference +
+
+
+ name: + {data.inputArtifactRef.name} +
+ {data.inputArtifactRef.version !== undefined && ( +
+ version: + {data.inputArtifactRef.version} +
+ )} +
+
+
+
+ )} +
+ This agent was invoked by the workflow with the input specified above. +
+
+
+ ); + }; + + const renderWorkflowNodeStart = (step: VisualizerStep) => { + const data = step.data.workflowNodeExecutionStart; + if (!data) return null; + + // Switch node specific rendering + if (data.nodeType === 'switch') { + return ( +
+

+ Switch Node +

+
+
+ Node ID: {data.nodeId} +
+ + {/* Cases */} + {data.cases && data.cases.length > 0 && ( +
+
Cases:
+
+ {data.cases.map((caseItem, index) => ( +
+
+ + Case {index + 1} + + + + {caseItem.node} + +
+ + {caseItem.condition} + +
+ ))} +
+
+ )} + + {/* Default branch */} + {data.defaultBranch && ( +
+
+ + Default + + + + {data.defaultBranch} + +
+
+ )} +
+
+ ); + } + + // Loop node specific rendering + if (data.nodeType === 'loop') { + return ( +
+

+ Loop Node +

+
+
+ Node ID: {data.nodeId} +
+ {data.condition && ( +
+
Condition:
+ + {data.condition} + +
+ )} + {data.maxIterations !== undefined && ( +
+ Max Iterations: {data.maxIterations} +
+ )} + {data.loopDelay && ( +
+ Delay: {data.loopDelay} +
+ )} +
+
+ ); + } + + // Default rendering for other node types + return ( +
+

+ Workflow Node Start +

+
+
+ Node ID: {data.nodeId} +
+
+ Type: {data.nodeType} +
+ {data.agentName && ( +
+ Agent: {data.agentName} +
+ )} + {data.condition && ( +
+
Condition:
+ + {data.condition} + +
+ )} + {(data.iterationIndex !== undefined && data.iterationIndex !== null && typeof data.iterationIndex === 'number') && ( +
+ Iteration #{data.iterationIndex} +
+ )} +
+
+ ); + }; + + const renderWorkflowNodeResult = (step: VisualizerStep) => { + const data = step.data.workflowNodeExecutionResult; + if (!data) return null; + + // Extract switch-specific data from metadata + const selectedBranch = data.metadata?.selected_branch; + const selectedCaseIndex = data.metadata?.selected_case_index; + const isSwitch = selectedBranch !== undefined || selectedCaseIndex !== undefined; + + return ( +
+

+ {isSwitch ? "Switch Result" : "Workflow Node Result"} +

+
+
+ Status:{" "} + + {data.status} + +
+ + {/* Switch node result - selected branch */} + {selectedBranch !== undefined && ( +
+
+ + + Selected Branch: + + + {selectedBranch} + +
+ {selectedCaseIndex !== undefined && selectedCaseIndex !== null && ( +
+ Matched Case #{selectedCaseIndex + 1} +
+ )} + {selectedCaseIndex === null && ( +
+ (Default branch - no case matched) +
+ )} +
+ )} + + {data.conditionResult !== undefined && ( +
+ Condition Result:{" "} + + {data.conditionResult ? "True" : "False"} + +
+ )} + {data.metadata?.condition && ( +
+
Condition:
+ + {data.metadata.condition} + +
+ )} + {data.errorMessage && ( +
+ Error: {data.errorMessage} +
+ )} +
+
+ ); + }; + + const renderWorkflowStart = (step: VisualizerStep) => { + const data = step.data.workflowExecutionStart; + if (!data) return null; + + return ( +
+

+ Workflow Start +

+
+
+ Workflow: {data.workflowName} +
+ {data.workflowInput && ( +
+
Input:
+ {renderFormattedArguments(data.workflowInput)} +
+ )} +
+
+ ); + }; + + const renderWorkflowResult = (step: VisualizerStep) => { + const data = step.data.workflowExecutionResult; + if (!data) return null; + + return ( +
+

+ Workflow Result +

+
+
+ Status:{" "} + + {data.status} + +
+ {data.workflowOutput && ( +
+
Output:
+ {renderFormattedArguments(data.workflowOutput)} +
+ )} + {data.errorMessage && ( +
+ Error: {data.errorMessage} +
+ )} +
+
+ ); + }; + + const hasRequestAndResult = nodeDetails.requestStep && nodeDetails.resultStep; + const hasCreatedArtifacts = nodeDetails.createdArtifacts && nodeDetails.createdArtifacts.length > 0; + + // Helper to render output artifact if available + const renderOutputArtifact = () => { + const outputArtifactRef = nodeDetails.outputArtifactStep?.data?.workflowNodeExecutionResult?.outputArtifactRef; + if (!outputArtifactRef) return null; + + return ( +
+
+ Output Artifact: + {outputArtifactRef.name} + {outputArtifactRef.version !== undefined && ( + v{outputArtifactRef.version} + )} +
+
+ +
+
+ ); + }; + + // Helper to render created artifacts for tool nodes + // When asColumn is true, renders without the top border (for 3-column layout) + const renderCreatedArtifacts = (asColumn: boolean = false) => { + if (!nodeDetails.createdArtifacts || nodeDetails.createdArtifacts.length === 0) return null; + + const handleArtifactClick = (filename: string, version?: number) => { + // Find the artifact by filename + const artifact = artifacts.find(a => a.filename === filename); + + if (artifact) { + // Switch to Files tab + setActiveSidePanelTab("files"); + + // Expand side panel if collapsed + setIsSidePanelCollapsed(false); + + // Set preview artifact to open the file + setSidePanelPreviewArtifact(artifact); + + // If a specific version is indicated, navigate to it + if (version !== undefined && version !== artifact.version) { + // Wait a bit for the file to load, then navigate to the specific version + setTimeout(() => { + navigateArtifactVersion(filename, version); + }, 100); + } + + // Close the popover + onClose?.(); + } + }; + + return ( +
+
+
+ +

+ {asColumn ? "CREATED ARTIFACTS" : `Created Artifacts (${nodeDetails.createdArtifacts.length})`} +

+
+
+ {nodeDetails.createdArtifacts.map((artifact, index) => ( +
+
+ +
+ {/* Inline preview button (NP-3) */} + + {artifact.version !== undefined && ( + + v{artifact.version} + + )} +
+
+ {artifact.description && ( +

+ {artifact.description} +

+ )} + {artifact.mimeType && ( +
+ Type: {artifact.mimeType} +
+ )} +
+ +
+
+ ))} +
+
+ ); + }; + + // Render the main node details content + const renderMainContent = () => ( +
+ {/* Header */} +
+ {getNodeIcon()} +
+

+ {nodeDetails.label} +

+ {nodeDetails.description ? ( +

+ {nodeDetails.description} +

+ ) : ( +

+ {nodeDetails.nodeType} Node +

+ )} +
+
+ + {/* Content */} +
+ {hasRequestAndResult ? ( + /* Split view for request and result (and optionally created artifacts) */ +
+ {/* Request Column */} +
+
+
+

+ REQUEST +

+
+ {renderStepContent(nodeDetails.requestStep, true)} +
+ + {/* Result Column */} +
+
+
+

+ RESULT +

+
+ {renderStepContent(nodeDetails.resultStep, false)} + {renderOutputArtifact()} +
+ + {/* Created Artifacts Column (when present) */} + {hasCreatedArtifacts && ( +
+ {renderCreatedArtifacts(true)} +
+ )} +
+ ) : ( + /* Single view when only request or result is available */ +
+ {nodeDetails.requestStep && ( +
+
+
+

+ REQUEST +

+
+ {renderStepContent(nodeDetails.requestStep, true)} +
+ )} + {nodeDetails.resultStep && ( +
+
+
+

+ RESULT +

+
+ {renderStepContent(nodeDetails.resultStep, false)} + {renderOutputArtifact()} + {renderCreatedArtifacts()} +
+ )} + {!nodeDetails.requestStep && !nodeDetails.resultStep && ( +
+ No detailed information available for this node +
+ )} +
+ )} +
+
+ ); + + // Render the inline artifact preview panel (NP-3) + const renderArtifactPreviewPanel = () => { + if (!inlinePreviewArtifact) return null; + + return ( +
+ {/* Preview Header */} +
+
+ +
+

+ {inlinePreviewArtifact.name} +

+ {inlinePreviewArtifact.version !== undefined && ( +

+ Version {inlinePreviewArtifact.version} +

+ )} +
+
+ +
+ + {/* Preview Content */} +
+ +
+
+ ); + }; + + return ( +
+ {/* Main content */} +
+ {renderMainContent()} +
+ + {/* Artifact preview panel (NP-3) */} + {renderArtifactPreviewPanel()} +
+ ); +}; + +export default NodeDetailsCard; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/PanZoomCanvas.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/PanZoomCanvas.tsx new file mode 100644 index 000000000..18dd96bf7 --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/PanZoomCanvas.tsx @@ -0,0 +1,489 @@ +import React, { useRef, useState, useCallback, useEffect } from "react"; + +interface PanZoomCanvasProps { + children: React.ReactNode; + initialScale?: number; + minScale?: number; + maxScale?: number; + onTransformChange?: (transform: { scale: number; x: number; y: number }) => void; + onUserInteraction?: () => void; + /** Width of any side panel that reduces available viewport width */ + sidePanelWidth?: number; +} + +export interface PanZoomCanvasRef { + resetTransform: () => void; + getTransform: () => { scale: number; x: number; y: number }; + /** Fit content to viewport, showing full width and top-aligned */ + fitToContent: (contentWidth: number, options?: { animated?: boolean; maxFitScale?: number }) => void; + /** Zoom in by 10% (rounded to nearest 10%), centered on viewport */ + zoomIn: (options?: { animated?: boolean }) => void; + /** Zoom out by 10% (rounded to nearest 10%), centered on viewport */ + zoomOut: (options?: { animated?: boolean }) => void; + /** Zoom to a specific scale, centered on viewport or specified point */ + zoomTo: (scale: number, options?: { animated?: boolean; centerX?: number; centerY?: number }) => void; + /** Pan to center a point (in content coordinates) in the viewport */ + panToPoint: (contentX: number, contentY: number, options?: { animated?: boolean }) => void; +} + +interface PointerState { + x: number; + y: number; +} + +interface GestureState { + centerX: number; + centerY: number; + distance: number; +} + +const PanZoomCanvas = React.forwardRef( + ( + { + children, + initialScale = 1, + minScale = 0.1, + maxScale = 4, + onTransformChange, + onUserInteraction, + sidePanelWidth = 0, + }, + ref + ) => { + const containerRef = useRef(null); + const [transform, setTransform] = useState({ + scale: initialScale, + x: 0, + y: 0, + }); + const [isAnimating, setIsAnimating] = useState(false); + + // Track active pointers for multi-touch + const pointersRef = useRef>(new Map()); + const lastGestureRef = useRef(null); + const isDraggingRef = useRef(false); + const lastDragPosRef = useRef<{ x: number; y: number } | null>(null); + + // Clamp scale within bounds (defined early for use in ref methods) + const clampScale = useCallback( + (scale: number) => Math.min(Math.max(scale, minScale), maxScale), + [minScale, maxScale] + ); + + // Expose methods via ref + React.useImperativeHandle(ref, () => ({ + resetTransform: () => { + setTransform({ scale: initialScale, x: 0, y: 0 }); + }, + getTransform: () => transform, + fitToContent: (contentWidth: number, options?: { animated?: boolean; maxFitScale?: number }) => { + const container = containerRef.current; + if (!container) return; + + const rect = container.getBoundingClientRect(); + // Account for side panel width + const availableWidth = rect.width - sidePanelWidth; + + // Padding around the content + const padding = 80; // 40px on each side + const topPadding = 60; // Extra space at top for controls + + // Calculate scale to fit width + // Default max is 1.0 (don't zoom in), but can be overridden + const fitMaxScale = options?.maxFitScale ?? 1.0; + const scaleToFitWidth = (availableWidth - padding) / contentWidth; + const newScale = Math.min(Math.max(scaleToFitWidth, minScale), fitMaxScale); + + // Center horizontally, align to top + const scaledContentWidth = contentWidth * newScale; + const newX = (availableWidth - scaledContentWidth) / 2; + const newY = topPadding; + + if (options?.animated) { + setIsAnimating(true); + // Disable animation after transition completes + setTimeout(() => setIsAnimating(false), 300); + } + + setTransform({ scale: newScale, x: newX, y: newY }); + }, + zoomIn: (options?: { animated?: boolean }) => { + const container = containerRef.current; + if (!container) return; + + const rect = container.getBoundingClientRect(); + // Zoom toward center of viewport (accounting for side panel) + const centerX = (rect.width - sidePanelWidth) / 2; + const centerY = rect.height / 2; + + setTransform(prev => { + // Round to nearest 10% and add 10% + const currentPercent = Math.round(prev.scale * 100); + const roundedPercent = Math.round(currentPercent / 10) * 10; + const targetPercent = Math.min(roundedPercent + 10, maxScale * 100); + const newScale = clampScale(targetPercent / 100); + const scaleRatio = newScale / prev.scale; + + // Zoom toward center + const newX = centerX - (centerX - prev.x) * scaleRatio; + const newY = centerY - (centerY - prev.y) * scaleRatio; + + return { scale: newScale, x: newX, y: newY }; + }); + + if (options?.animated) { + setIsAnimating(true); + setTimeout(() => setIsAnimating(false), 300); + } + }, + zoomOut: (options?: { animated?: boolean }) => { + const container = containerRef.current; + if (!container) return; + + const rect = container.getBoundingClientRect(); + // Zoom toward center of viewport (accounting for side panel) + const centerX = (rect.width - sidePanelWidth) / 2; + const centerY = rect.height / 2; + + setTransform(prev => { + // Round to nearest 10% and subtract 10% + const currentPercent = Math.round(prev.scale * 100); + const roundedPercent = Math.round(currentPercent / 10) * 10; + const targetPercent = Math.max(roundedPercent - 10, minScale * 100); + const newScale = clampScale(targetPercent / 100); + const scaleRatio = newScale / prev.scale; + + // Zoom toward center + const newX = centerX - (centerX - prev.x) * scaleRatio; + const newY = centerY - (centerY - prev.y) * scaleRatio; + + return { scale: newScale, x: newX, y: newY }; + }); + + if (options?.animated) { + setIsAnimating(true); + setTimeout(() => setIsAnimating(false), 300); + } + }, + zoomTo: (targetScale: number, options?: { animated?: boolean; centerX?: number; centerY?: number }) => { + const container = containerRef.current; + if (!container) return; + + const rect = container.getBoundingClientRect(); + // Use provided center or viewport center + const centerX = options?.centerX ?? (rect.width - sidePanelWidth) / 2; + const centerY = options?.centerY ?? rect.height / 2; + + const newScale = clampScale(targetScale); + + setTransform(prev => { + const scaleRatio = newScale / prev.scale; + const newX = centerX - (centerX - prev.x) * scaleRatio; + const newY = centerY - (centerY - prev.y) * scaleRatio; + return { scale: newScale, x: newX, y: newY }; + }); + + if (options?.animated) { + setIsAnimating(true); + setTimeout(() => setIsAnimating(false), 300); + } + }, + panToPoint: (contentX: number, contentY: number, options?: { animated?: boolean }) => { + const container = containerRef.current; + if (!container) return; + + const rect = container.getBoundingClientRect(); + // Calculate viewport center (accounting for side panel) + const viewportCenterX = (rect.width - sidePanelWidth) / 2; + const viewportCenterY = rect.height / 2; + + setTransform(prev => { + // Convert content coordinates to screen coordinates at current scale + // Then calculate the offset needed to center that point + const newX = viewportCenterX - contentX * prev.scale; + const newY = viewportCenterY - contentY * prev.scale; + return { ...prev, x: newX, y: newY }; + }); + + if (options?.animated) { + setIsAnimating(true); + setTimeout(() => setIsAnimating(false), 300); + } + }, + })); + + // Notify parent of transform changes + useEffect(() => { + onTransformChange?.(transform); + }, [transform, onTransformChange]); + + // Calculate gesture state from two pointers + const calculateGestureState = useCallback((pointers: Map): GestureState | null => { + const points = Array.from(pointers.values()); + if (points.length < 2) return null; + + const [p1, p2] = points; + const centerX = (p1.x + p2.x) / 2; + const centerY = (p1.y + p2.y) / 2; + const distance = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); + + return { centerX, centerY, distance }; + }, []); + + // Handle pointer down + const handlePointerDown = useCallback((e: React.PointerEvent) => { + // Capture the pointer for tracking + (e.target as HTMLElement).setPointerCapture(e.pointerId); + + pointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY }); + + if (pointersRef.current.size === 1) { + // Single pointer - start drag + isDraggingRef.current = true; + lastDragPosRef.current = { x: e.clientX, y: e.clientY }; + } else if (pointersRef.current.size === 2) { + // Two pointers - start pinch gesture + isDraggingRef.current = false; + lastDragPosRef.current = null; + lastGestureRef.current = calculateGestureState(pointersRef.current); + } + }, [calculateGestureState]); + + // Handle pointer move + const handlePointerMove = useCallback((e: React.PointerEvent) => { + if (!pointersRef.current.has(e.pointerId)) return; + + // Update pointer position + pointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY }); + + const rect = containerRef.current?.getBoundingClientRect(); + if (!rect) return; + + if (pointersRef.current.size >= 2) { + // Multi-touch: calculate pan AND zoom simultaneously + const currentGesture = calculateGestureState(pointersRef.current); + const lastGesture = lastGestureRef.current; + + if (currentGesture && lastGesture) { + // Pan: movement of the center point (average finger displacement) + const panDeltaX = currentGesture.centerX - lastGesture.centerX; + const panDeltaY = currentGesture.centerY - lastGesture.centerY; + + // Zoom: change in distance between fingers + const zoomFactor = currentGesture.distance / lastGesture.distance; + + // Pinch center relative to container + const cursorX = currentGesture.centerX - rect.left; + const cursorY = currentGesture.centerY - rect.top; + + setTransform((prev) => { + const newScale = clampScale(prev.scale * zoomFactor); + const scaleRatio = newScale / prev.scale; + + // Apply zoom toward pinch center + let newX = cursorX - (cursorX - prev.x) * scaleRatio; + let newY = cursorY - (cursorY - prev.y) * scaleRatio; + + // Apply pan from finger movement (simultaneously!) + newX += panDeltaX; + newY += panDeltaY; + + return { scale: newScale, x: newX, y: newY }; + }); + + onUserInteraction?.(); + } + + lastGestureRef.current = currentGesture; + } else if (isDraggingRef.current && lastDragPosRef.current) { + // Single pointer drag - pan only + const dx = e.clientX - lastDragPosRef.current.x; + const dy = e.clientY - lastDragPosRef.current.y; + + setTransform((prev) => ({ + ...prev, + x: prev.x + dx, + y: prev.y + dy, + })); + + lastDragPosRef.current = { x: e.clientX, y: e.clientY }; + onUserInteraction?.(); + } + }, [calculateGestureState, clampScale, onUserInteraction]); + + // Handle pointer up/cancel + const handlePointerUp = useCallback((e: React.PointerEvent) => { + (e.target as HTMLElement).releasePointerCapture(e.pointerId); + pointersRef.current.delete(e.pointerId); + + if (pointersRef.current.size < 2) { + lastGestureRef.current = null; + } + + if (pointersRef.current.size === 1) { + // Went from 2 to 1 pointer - switch to drag mode + const remaining = Array.from(pointersRef.current.values())[0]; + isDraggingRef.current = true; + lastDragPosRef.current = { x: remaining.x, y: remaining.y }; + } else if (pointersRef.current.size === 0) { + isDraggingRef.current = false; + lastDragPosRef.current = null; + } + }, []); + + // Handle wheel events (mouse wheel zoom + trackpad gestures) + // Must be added manually with { passive: false } to allow preventDefault() + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const handleWheel = (e: WheelEvent) => { + e.preventDefault(); + + const rect = container.getBoundingClientRect(); + + const dx = e.deltaX; + const dy = e.deltaY; + const ctrlKey = e.ctrlKey || e.metaKey; + const shiftKey = e.shiftKey; + + // Check if this looks like a trackpad 2-finger swipe (has horizontal component) + const isTrackpadSwipe = Math.abs(dx) > 1 && !ctrlKey; + + if (shiftKey) { + // Shift+scroll -> pan horizontally + setTransform((prev) => ({ + ...prev, + x: prev.x - dy, + })); + onUserInteraction?.(); + } else if (ctrlKey) { + // Trackpad pinch -> zoom + pan + const zoomFactor = 1 - dy * 0.01; + const cursorX = e.clientX - rect.left; + const cursorY = e.clientY - rect.top; + + setTransform((prev) => { + const newScale = clampScale(prev.scale * zoomFactor); + const scaleRatio = newScale / prev.scale; + + let newX = cursorX - (cursorX - prev.x) * scaleRatio; + let newY = cursorY - (cursorY - prev.y) * scaleRatio; + newX -= dx; + + return { scale: newScale, x: newX, y: newY }; + }); + onUserInteraction?.(); + } else if (isTrackpadSwipe) { + // Trackpad 2-finger swipe -> pan + setTransform((prev) => ({ + ...prev, + x: prev.x - dx, + y: prev.y - dy, + })); + onUserInteraction?.(); + } else { + // Mouse wheel -> zoom toward cursor + const zoomFactor = 1 - dy * 0.005; + const cursorX = e.clientX - rect.left; + const cursorY = e.clientY - rect.top; + + setTransform((prev) => { + const newScale = clampScale(prev.scale * zoomFactor); + const scaleRatio = newScale / prev.scale; + + const newX = cursorX - (cursorX - prev.x) * scaleRatio; + const newY = cursorY - (cursorY - prev.y) * scaleRatio; + + return { scale: newScale, x: newX, y: newY }; + }); + onUserInteraction?.(); + } + }; + + // Add with passive: false to allow preventDefault() + container.addEventListener("wheel", handleWheel, { passive: false }); + + return () => { + container.removeEventListener("wheel", handleWheel); + }; + }, [clampScale, onUserInteraction]); + + // Handle double-click to zoom in at click location + const handleDoubleClick = useCallback((e: React.MouseEvent) => { + // Prevent text selection on double-click + e.preventDefault(); + + // Only handle if clicking on the container background (not on interactive nodes) + // Check if the click target is a button, link, or has data-no-zoom attribute + const target = e.target as HTMLElement; + if ( + target.closest("button") || + target.closest("a") || + target.closest("[data-no-zoom]") || + target.closest("[role='button']") + ) { + return; + } + + const rect = containerRef.current?.getBoundingClientRect(); + if (!rect) return; + + const cursorX = e.clientX - rect.left; + const cursorY = e.clientY - rect.top; + + // Zoom in by 10% toward click location + setTransform(prev => { + const currentPercent = Math.round(prev.scale * 100); + const roundedPercent = Math.round(currentPercent / 10) * 10; + const targetPercent = Math.min(roundedPercent + 10, maxScale * 100); + const newScale = clampScale(targetPercent / 100); + const scaleRatio = newScale / prev.scale; + + const newX = cursorX - (cursorX - prev.x) * scaleRatio; + const newY = cursorY - (cursorY - prev.y) * scaleRatio; + + return { scale: newScale, x: newX, y: newY }; + }); + + setIsAnimating(true); + setTimeout(() => setIsAnimating(false), 300); + onUserInteraction?.(); + }, [clampScale, maxScale, onUserInteraction]); + + return ( +
+
+ {children} +
+
+ ); + } +); + +PanZoomCanvas.displayName = "PanZoomCanvas"; + +export default PanZoomCanvas; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/WorkflowRenderer.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/WorkflowRenderer.tsx new file mode 100644 index 000000000..87ebdaff3 --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/WorkflowRenderer.tsx @@ -0,0 +1,358 @@ +import React, { useMemo, useState } from "react"; +import type { VisualizerStep } from "@/lib/types"; +import { processSteps } from "./utils/layoutEngine"; +import type { LayoutNode, Edge } from "./utils/types"; +import AgentNode from "./nodes/AgentNode"; +import UserNode from "./nodes/UserNode"; +import WorkflowGroup from "./nodes/WorkflowGroup"; +// import EdgeLayer from "./EdgeLayer"; + +/** + * Check if a node or any of its descendants has status 'in-progress' + */ +function hasProcessingDescendant(node: LayoutNode): boolean { + if (node.data.status === 'in-progress') { + return true; + } + for (const child of node.children) { + if (hasProcessingDescendant(child)) { + return true; + } + } + if (node.parallelBranches) { + for (const branch of node.parallelBranches) { + for (const branchNode of branch) { + if (hasProcessingDescendant(branchNode)) { + return true; + } + } + } + } + return false; +} + +/** + * Recursively collapse nested agents (level > 0) and recalculate their dimensions + */ +function collapseNestedAgents(node: LayoutNode, nestingLevel: number, expandedNodeIds: Set = new Set()): LayoutNode { + // Check if this node is manually expanded + const isManuallyExpanded = expandedNodeIds.has(node.id); + + // Special handling for Map/Fork nodes (pill variant with parallel branches) + // Don't collapse these - instead, flatten their parallel branches + if (node.type === 'agent' && node.data.variant === 'pill' && node.parallelBranches && node.parallelBranches.length > 0) { + // Flatten all branches into a single array of children + const flattenedChildren: LayoutNode[] = []; + for (const branch of node.parallelBranches) { + for (const child of branch) { + flattenedChildren.push(collapseNestedAgents(child, nestingLevel + 1, expandedNodeIds)); + } + } + + // Recalculate height based on flattened children + const padding = 16; + const gap = 16; + + const childrenHeight = flattenedChildren.reduce((sum, child, idx) => { + return sum + child.height + (idx < flattenedChildren.length - 1 ? gap : 0); + }, 0); + + // Height includes the pill itself (40px) + padding + children + const newHeight = 40 + padding * 2 + childrenHeight; + + return { + ...node, + children: flattenedChildren, + parallelBranches: undefined, // Clear parallel branches + height: newHeight, + }; + } + + // For regular agents at level > 0, collapse them (unless manually expanded) + if (node.type === 'agent' && nestingLevel > 0) { + if (isManuallyExpanded) { + // Node is manually expanded - process children but mark as expanded + const expandedChildren = node.children.map(child => collapseNestedAgents(child, nestingLevel + 1, expandedNodeIds)); + + // Recalculate height + const headerHeight = 50; + const padding = 16; + const gap = 16; + const childrenHeight = expandedChildren.reduce((sum, child, idx) => { + return sum + child.height + (idx < expandedChildren.length - 1 ? gap : 0); + }, 0); + const newHeight = headerHeight + padding * 2 + childrenHeight; + + return { + ...node, + children: expandedChildren, + height: newHeight, + data: { + ...node.data, + isExpanded: true, // Mark as expanded so collapse icon shows + }, + }; + } + + // Check if any children are processing before we collapse them + const childrenProcessing = hasProcessingDescendant(node); + + // Collapsed agent: just header + padding, no children + const headerHeight = 50; + const padding = 16; + const collapsedHeight = headerHeight + padding; + + return { + ...node, + children: [], + parallelBranches: undefined, + height: collapsedHeight, + data: { + ...node.data, + isCollapsed: true, + // If children were processing, mark the collapsed node as processing + hasProcessingChildren: childrenProcessing, + }, + }; + } + + // For workflow groups, collapse them entirely (unless manually expanded) + if (node.type === 'group') { + if (isManuallyExpanded) { + // Node is manually expanded - process children but mark as expanded + const expandedChildren = node.children.map(child => collapseNestedAgents(child, nestingLevel + 1, expandedNodeIds)); + + // Recalculate height (group uses 24px padding) + const padding = 24; + const gap = 16; + const childrenHeight = expandedChildren.reduce((sum, child, idx) => { + return sum + child.height + (idx < expandedChildren.length - 1 ? gap : 0); + }, 0); + const newHeight = padding * 2 + childrenHeight; + + return { + ...node, + children: expandedChildren, + height: newHeight, + data: { + ...node.data, + isExpanded: true, // Mark as expanded so collapse icon shows + }, + }; + } + + // Check if any children are processing before we collapse them + const childrenProcessing = hasProcessingDescendant(node); + + // Collapsed workflow: just header + padding, no children + const headerHeight = 50; + const padding = 16; + const collapsedHeight = headerHeight + padding; + + return { + ...node, + children: [], + parallelBranches: undefined, + height: collapsedHeight, + data: { + ...node.data, + isCollapsed: true, + // If children were processing, mark the collapsed node as processing + hasProcessingChildren: childrenProcessing, + }, + }; + } + + // For top-level nodes or non-agent nodes, process children recursively + if (node.children.length > 0) { + const collapsedChildren = node.children.map(child => collapseNestedAgents(child, nestingLevel + 1, expandedNodeIds)); + + // Recalculate height + const headerHeight = node.type === 'agent' ? 50 : 0; + const padding = node.type === 'agent' ? 16 : ((node.type as string) === 'group' ? 24 : 0); + const gap = 16; + + const childrenHeight = collapsedChildren.reduce((sum, child, idx) => { + return sum + child.height + (idx < collapsedChildren.length - 1 ? gap : 0); + }, 0); + + const newHeight = headerHeight + padding * 2 + childrenHeight; + + return { + ...node, + children: collapsedChildren, + height: newHeight, + }; + } + + // Handle parallel branches - flatten them into sequential children when collapsed + if (node.parallelBranches && node.parallelBranches.length > 0) { + // Flatten all branches into a single array of children + const flattenedChildren: LayoutNode[] = []; + for (const branch of node.parallelBranches) { + for (const child of branch) { + flattenedChildren.push(collapseNestedAgents(child, nestingLevel + 1, expandedNodeIds)); + } + } + + // Recalculate height based on flattened children + const headerHeight = node.type === 'agent' ? 50 : 0; + const padding = node.type === 'agent' ? 16 : ((node.type as string) === 'group' ? 24 : 0); + const gap = 16; + + const childrenHeight = flattenedChildren.reduce((sum, child, idx) => { + return sum + child.height + (idx < flattenedChildren.length - 1 ? gap : 0); + }, 0); + + const newHeight = headerHeight + padding * 2 + childrenHeight; + + return { + ...node, + children: flattenedChildren, + parallelBranches: undefined, // Clear parallel branches + height: newHeight, + }; + } + + return node; +} + +interface WorkflowRendererProps { + processedSteps: VisualizerStep[]; + agentNameMap: Record; + selectedStepId?: string | null; + onNodeClick?: (node: LayoutNode) => void; + onEdgeClick?: (edge: Edge) => void; + showDetail?: boolean; +} + +const WorkflowRenderer: React.FC = ({ + processedSteps, + agentNameMap, + selectedStepId, + onNodeClick, + onEdgeClick, + showDetail = true, +}) => { + const [_selectedEdgeId, setSelectedEdgeId] = useState(null); + const [expandedNodeIds, setExpandedNodeIds] = useState>(new Set()); + + // Handle expand toggle for a node + const handleExpandNode = (nodeId: string) => { + setExpandedNodeIds(prev => { + const newSet = new Set(prev); + if (newSet.has(nodeId)) { + newSet.delete(nodeId); + } else { + newSet.add(nodeId); + } + return newSet; + }); + }; + + // Process steps into layout + const baseLayoutResult = useMemo(() => { + if (!processedSteps || processedSteps.length === 0) { + return { nodes: [], edges: [], totalWidth: 800, totalHeight: 600 }; + } + + try { + return processSteps(processedSteps, agentNameMap); + } catch (error) { + console.error("[WorkflowRenderer] Error processing steps:", error); + return { nodes: [], edges: [], totalWidth: 800, totalHeight: 600 }; + } + }, [processedSteps, agentNameMap]); + + // Collapse nested agents when showDetail is false + const layoutResult = useMemo(() => { + if (showDetail) { + return baseLayoutResult; + } + + // Deep clone and collapse nodes (respecting manually expanded nodes) + const collapsedNodes = baseLayoutResult.nodes.map(node => collapseNestedAgents(node, 0, expandedNodeIds)); + return { + ...baseLayoutResult, + nodes: collapsedNodes, + }; + }, [baseLayoutResult, showDetail, expandedNodeIds]); + + const { nodes, edges: _edges, totalWidth: _totalWidth, totalHeight: _totalHeight } = layoutResult; + + // Handle node click + const handleNodeClick = (node: LayoutNode) => { + onNodeClick?.(node); + }; + + // Handle edge click - currently unused but kept for future use + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const handleEdgeClick = (edge: Edge) => { + setSelectedEdgeId(edge.id); + onEdgeClick?.(edge); + }; + void handleEdgeClick; // Suppress unused variable warning + + // Render a top-level node + const renderNode = (node: LayoutNode, index: number) => { + const isSelected = node.data.visualizerStepId === selectedStepId; + + const nodeProps = { + node, + isSelected, + onClick: handleNodeClick, + onChildClick: handleNodeClick, // For nested clicks + onExpand: handleExpandNode, + onCollapse: handleExpandNode, // Same handler - toggles expanded state + }; + + let component: React.ReactNode; + + switch (node.type) { + case 'agent': + component = ; + break; + case 'user': + component = ; + break; + case 'group': + component = ; + break; + default: + return null; + } + + return ( + + {component} + {/* Add connector line between nodes */} + {index < nodes.length - 1 && ( +
+ )} + + ); + }; + + if (nodes.length === 0) { + return ( +
+ {processedSteps.length > 0 ? "Processing flow data..." : "No steps to display in flow chart."} +
+ ); + } + + return ( +
+ {/* Nodes in vertical flow */} + {nodes.map((node, index) => renderNode(node, index))} +
+ ); +}; + +export default WorkflowRenderer; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/customEdges/GenericFlowEdge.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/customEdges/GenericFlowEdge.tsx deleted file mode 100644 index 69da5cf0a..000000000 --- a/client/webui/frontend/src/lib/components/activities/FlowChart/customEdges/GenericFlowEdge.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useState } from "react"; - -import { type EdgeProps, getBezierPath } from "@xyflow/react"; - -export interface AnimatedEdgeData { - visualizerStepId: string; - isAnimated?: boolean; - animationType?: "request" | "response" | "static"; - isSelected?: boolean; - isError?: boolean; - errorMessage?: string; -} - -const GenericFlowEdge: React.FC = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style = {}, markerEnd, data }) => { - const [isHovered, setIsHovered] = useState(false); - - const [edgePath] = getBezierPath({ - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition, - }); - - const getEdgeStyle = () => { - const baseStyle = { - strokeWidth: isHovered ? 3 : 2, - stroke: "var(--color-muted-foreground)", - ...style, - }; - - const edgeData = data as unknown as AnimatedEdgeData; - - // Priority: Error > Selected > Animated > Hover > Default - if (edgeData?.isError) { - return { - ...baseStyle, - stroke: isHovered ? "var(--color-error-wMain)" : "var(--color-error-w70)", - strokeWidth: isHovered ? 3 : 2, - }; - } - - if (edgeData?.isSelected) { - return { - ...baseStyle, - stroke: "#3b82f6", // same as VisualizerStepCard - strokeWidth: 3, - }; - } - - // Enhanced logic: handle both animation and hover states - if (edgeData?.isAnimated) { - return { - ...baseStyle, - stroke: isHovered ? "#1d4ed8" : "#3b82f6", - strokeWidth: isHovered ? 4 : 3, - }; - } - - // For non-animated edges, change color on hover - if (isHovered) { - return { - ...baseStyle, - stroke: "var(--edge-hover-color)", - }; - } - - return baseStyle; - }; - - const handleMouseEnter = () => setIsHovered(true); - const handleMouseLeave = () => setIsHovered(false); - - return ( - <> - {/* Invisible wider path for easier clicking */} - - {/* Visible edge path */} - - - ); -}; - -export default GenericFlowEdge; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/GenericAgentNode.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/GenericAgentNode.tsx deleted file mode 100644 index ab00382f5..000000000 --- a/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/GenericAgentNode.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react"; - -import { Handle, Position, type NodeProps, type Node } from "@xyflow/react"; - -export interface GenericNodeData extends Record { - label: string; - description?: string; - icon?: string; - subflow?: boolean; - isInitial?: boolean; - isFinal?: boolean; -} - -const GenericAgentNode: React.FC>> = ({ data }) => { - return ( -
- - - - - -
-
- {data.label} -
-
-
- ); -}; - -export default GenericAgentNode; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/GenericArtifactNode.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/GenericArtifactNode.tsx deleted file mode 100644 index dc69856d7..000000000 --- a/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/GenericArtifactNode.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from "react"; - -import { Handle, Position, type NodeProps, type Node } from "@xyflow/react"; - -import type { GenericNodeData } from "./GenericAgentNode"; - -export type ArtifactNodeType = Node; - -const ArtifactNode: React.FC> = ({ data, id }) => { - return ( -
- -
-
-
-
{data.label}
-
-
-
- ); -}; - -export default ArtifactNode; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/GenericToolNode.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/GenericToolNode.tsx deleted file mode 100644 index 2f32311ab..000000000 --- a/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/GenericToolNode.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from "react"; - -import { Handle, Position, type NodeProps, type Node } from "@xyflow/react"; - -import type { GenericNodeData } from "./GenericAgentNode"; - -export type GenericToolNodeType = Node; - -const GenericToolNode: React.FC> = ({ data, id }) => { - const getStatusColor = () => { - switch (data.status) { - case "completed": - return "bg-green-500"; - case "in-progress": - return "bg-blue-500"; - case "error": - return "bg-red-500"; - case "started": - return "bg-yellow-400"; - case "idle": - default: - return "bg-cyan-500"; - } - }; - - return ( -
- -
-
-
- {data.label} -
-
- - -
- ); -}; - -export default GenericToolNode; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/LLMNode.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/LLMNode.tsx deleted file mode 100644 index 0a492abdb..000000000 --- a/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/LLMNode.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from "react"; - -import { Handle, Position, type NodeProps, type Node } from "@xyflow/react"; - -import type { GenericNodeData } from "./GenericAgentNode"; - -export type LLMNodeType = Node; - -const LLMNode: React.FC> = ({ data }) => { - const getStatusColor = () => { - switch (data.status) { - case "completed": - return "bg-green-500"; - case "in-progress": - return "bg-blue-500"; - case "error": - return "bg-red-500"; - case "started": - return "bg-yellow-400"; - case "idle": - default: - return "bg-teal-500"; - } - }; - - return ( -
- -
-
-
{data.label}
-
- -
- ); -}; - -export default LLMNode; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/OrchestratorAgentNode.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/OrchestratorAgentNode.tsx deleted file mode 100644 index f2519a0dd..000000000 --- a/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/OrchestratorAgentNode.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from "react"; - -import { Handle, Position, type NodeProps, type Node } from "@xyflow/react"; - -import type { GenericNodeData } from "./GenericAgentNode"; - -export type OrchestratorAgentNodeType = Node; - -const OrchestratorAgentNode: React.FC> = ({ data }) => { - return ( -
- - - - - -
-
- {data.label} -
-
-
- ); -}; - -export default OrchestratorAgentNode; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/UserNode.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/UserNode.tsx deleted file mode 100644 index 800918b48..000000000 --- a/client/webui/frontend/src/lib/components/activities/FlowChart/customNodes/UserNode.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from "react"; -import { Handle, Position, type NodeProps, type Node } from "@xyflow/react"; -import type { GenericNodeData } from "./GenericAgentNode"; - -export interface UserNodeData extends GenericNodeData { - isTopNode?: boolean; // true if created by handleUserRequest - isBottomNode?: boolean; // true if created by createNewUserNodeAtBottom -} - -export type UserNodeType = Node; - -const UserNode: React.FC> = ({ data }) => { - const getStatusColor = () => { - switch (data.status) { - case "completed": - return "bg-green-500"; - case "in-progress": - return "bg-blue-500"; - case "error": - return "bg-red-500"; - case "started": - return "bg-yellow-400"; - case "idle": - default: - return "bg-purple-500"; - } - }; - - return ( -
- {data.isTopNode && } - {data.isBottomNode && } - {!data.isTopNode && !data.isBottomNode && } -
-
-
{data.label}
-
-
- ); -}; - -export default UserNode; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/edgeAnimationService.ts b/client/webui/frontend/src/lib/components/activities/FlowChart/edgeAnimationService.ts deleted file mode 100644 index d310aeb01..000000000 --- a/client/webui/frontend/src/lib/components/activities/FlowChart/edgeAnimationService.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { VisualizerStep } from "@/lib/types"; - -export interface EdgeAnimationState { - isAnimated: boolean; - animationType: "request" | "response" | "static"; -} - -export class EdgeAnimationService { - /** - * Simplified animation logic: Only animate agent-to-tool request edges - * until their corresponding response is received. - */ - public getEdgeAnimationState(edgeStepId: string, upToStep: number, allSteps: VisualizerStep[]): EdgeAnimationState { - const currentStep = allSteps.find(step => step.id === edgeStepId); - if (!currentStep) { - return { isAnimated: false, animationType: "static" }; - } - - // Only animate agent-to-tool interactions - if (!this.isAgentToToolRequest(currentStep)) { - return { isAnimated: false, animationType: "static" }; - } - - // Check if this request has been completed by looking at steps up to current point - const stepsUpToPoint = allSteps.slice(0, upToStep + 1); - const isCompleted = this.hasMatchingResponse(currentStep, stepsUpToPoint); - - if (isCompleted) { - return { isAnimated: false, animationType: "static" }; - } - - return { - isAnimated: true, - animationType: "request", - }; - } - - /** - * Check if a step represents an agent-to-tool request that should be animated - */ - private isAgentToToolRequest(step: VisualizerStep): boolean { - switch (step.type) { - case "AGENT_LLM_CALL": - // Agent calling LLM (lane 2 to lane 3) - return true; - - case "AGENT_TOOL_INVOCATION_START": { - // Only animate if it's a tool call, not a peer delegation - const isPeerDelegation = step.data.toolDecision?.isPeerDelegation || step.data.toolInvocationStart?.isPeerInvocation || (step.target && step.target.startsWith("peer_")); - return !isPeerDelegation; - } - - default: - return false; - } - } - - /** - * Check if there's a matching response for the given request step - */ - private hasMatchingResponse(requestStep: VisualizerStep, stepsToCheck: VisualizerStep[]): boolean { - switch (requestStep.type) { - case "AGENT_LLM_CALL": - return this.hasLLMResponse(requestStep, stepsToCheck); - - case "AGENT_TOOL_INVOCATION_START": - return this.hasToolResponse(requestStep, stepsToCheck); - - default: - return false; - } - } - - /** - * Check if there's an LLM response for the given LLM call - */ - private hasLLMResponse(llmCallStep: VisualizerStep, stepsToCheck: VisualizerStep[]): boolean { - const callTimestamp = new Date(llmCallStep.timestamp).getTime(); - const callingAgent = llmCallStep.source; - - // Look for any step that comes after this LLM call from the same agent - // This indicates the LLM call has completed and the agent is proceeding - return stepsToCheck.some(step => { - const stepTimestamp = new Date(step.timestamp).getTime(); - - if (stepTimestamp < callTimestamp) return false; - - // Check for direct LLM responses to the agent - const isDirectLLMResponse = (step.type === "AGENT_LLM_RESPONSE_TOOL_DECISION" || step.type === "AGENT_LLM_RESPONSE_TO_AGENT") && step.target === callingAgent; - - // Check for any subsequent action by the same agent (indicates LLM call completed) - const isSubsequentAgentAction = step.source === callingAgent && (step.type === "AGENT_TOOL_INVOCATION_START" || step.type === "TASK_COMPLETED"); - const isPeerResponse = step.type === "AGENT_TOOL_EXECUTION_RESULT" && step.data.toolResult?.isPeerResponse; - - return isDirectLLMResponse || isSubsequentAgentAction || isPeerResponse; - }); - } - - /** - * Check if there's a tool response for the given tool invocation - */ - private hasToolResponse(toolCallStep: VisualizerStep, stepsToCheck: VisualizerStep[]): boolean { - const callTimestamp = new Date(toolCallStep.timestamp).getTime(); - const toolName = toolCallStep.target; - const callingAgent = toolCallStep.source; - - return stepsToCheck.some(step => { - const stepTimestamp = new Date(step.timestamp).getTime(); - if (stepTimestamp < callTimestamp) return false; - - return step.type === "AGENT_TOOL_EXECUTION_RESULT" && step.source === toolName && step.target === callingAgent; - }); - } - - public isRequestStep(step: VisualizerStep): boolean { - return this.isAgentToToolRequest(step); - } - - public isResponseStep(step: VisualizerStep): boolean { - return ["AGENT_TOOL_EXECUTION_RESULT", "AGENT_LLM_RESPONSE_TOOL_DECISION", "AGENT_LLM_RESPONSE_TO_AGENT"].includes(step.type); - } -} diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/index.ts b/client/webui/frontend/src/lib/components/activities/FlowChart/index.ts new file mode 100644 index 000000000..41db74874 --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/index.ts @@ -0,0 +1,5 @@ +export { default as FlowChartPanel } from "./FlowChartPanel"; +export { default as NodeDetailsCard } from "./NodeDetailsCard"; +export type { LayoutNode, Edge } from "./utils/types"; +export type { NodeDetails } from "./utils/nodeDetailsHelper"; +export { findNodeDetails } from "./utils/nodeDetailsHelper"; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/AgentNode.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/AgentNode.tsx new file mode 100644 index 000000000..5fdc6658a --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/AgentNode.tsx @@ -0,0 +1,292 @@ +import { Fragment, type FC } from "react"; +import { Bot, Maximize2, Minimize2 } from "lucide-react"; +import type { LayoutNode } from "../utils/types"; +import LLMNode from "./LLMNode"; +import ToolNode from "./ToolNode"; +import SwitchNode from "./SwitchNode"; +import LoopNode from "./LoopNode"; +import WorkflowGroup from "./WorkflowGroup"; + + +interface AgentNodeProps { + node: LayoutNode; + isSelected?: boolean; + onClick?: (node: LayoutNode) => void; + onChildClick?: (child: LayoutNode) => void; + onExpand?: (nodeId: string) => void; + onCollapse?: (nodeId: string) => void; +} + +const AgentNode: FC = ({ node, isSelected, onClick, onChildClick, onExpand, onCollapse }) => { + // Render a child node recursively + const renderChild = (child: LayoutNode) => { + const childProps = { + node: child, + onClick: onChildClick, + onExpand, + onCollapse, + }; + + switch (child.type) { + case 'agent': + // Recursive! + return ; + case 'llm': + return ; + case 'tool': + return ; + case 'switch': + return ; + case 'loop': + return ; + case 'group': + return ; + case 'parallelBlock': + // Render parallel block - children displayed side-by-side with bounding box + return ( +
+ {child.children.map((parallelChild) => renderChild(parallelChild))} +
+ ); + default: + return null; + } + }; + + // Pill variant for Start/Finish/Join/Map/Fork nodes + if (node.data.variant === 'pill') { + const opacityClass = node.data.isSkipped ? "opacity-50" : ""; + const borderStyleClass = node.data.isSkipped ? "border-dashed" : "border-solid"; + const hasParallelBranches = node.parallelBranches && node.parallelBranches.length > 0; + const hasChildren = node.children && node.children.length > 0; + const isError = node.data.status === 'error'; + + // Color classes based on error status + const pillColorClasses = isError + ? "border-red-500 bg-red-50 text-red-900 dark:border-red-400 dark:bg-red-900/50 dark:text-red-100" + : "border-indigo-500 bg-indigo-50 text-indigo-900 dark:border-indigo-400 dark:bg-indigo-900/50 dark:text-indigo-100"; + + // If it's a simple pill (no parallel branches and no children), render compact version + if (!hasParallelBranches && !hasChildren) { + return ( +
{ + e.stopPropagation(); + onClick?.(node); + }} + title={node.data.description} + > +
+
{node.data.label}
+
+
+ ); + } + + // Map/Fork pill with sequential children (flattened from parallel branches when detail is off) + if (hasChildren && !hasParallelBranches) { + return ( +
+ {/* Pill label */} +
{ + e.stopPropagation(); + onClick?.(node); + }} + title={node.data.description} + > +
+
{node.data.label}
+
+
+ + {/* Connector line to children */} +
+ + {/* Sequential children below */} + {node.children.map((child, index) => ( + + {renderChild(child)} + {/* Connector line to next child */} + {index < node.children.length - 1 && ( +
+ )} + + ))} +
+ ); + } + + // Map/Fork pill with parallel branches + return ( +
+ {/* Pill label */} +
{ + e.stopPropagation(); + onClick?.(node); + }} + title={node.data.description} + > +
+
{node.data.label}
+
+
+ + {/* Connector line to branches */} +
+ + {/* Parallel branches below */} +
+
+ {node.parallelBranches!.map((branch, branchIndex) => ( +
+ {branch.map((child, index) => ( + + {renderChild(child)} + {/* Connector line to next child in branch */} + {index < branch.length - 1 && ( +
+ )} + + ))} +
+ ))} +
+
+
+ ); + } + + // Regular agent node with children + const opacityClass = node.data.isSkipped ? "opacity-50" : ""; + const borderStyleClass = node.data.isSkipped ? "border-dashed" : "border-solid"; + // Show effect if this node is processing OR if children are hidden but processing + const isProcessing = node.data.status === "in-progress" || node.data.hasProcessingChildren; + + const haloClass = isProcessing ? 'processing-halo' : ''; + + const isCollapsed = node.data.isCollapsed; + + // Check if this is an expanded node (manually expanded from collapsed state) + const isExpanded = node.data.isExpanded; + + return ( +
+ {/* Collapse icon - top right, only show on hover when expanded */} + {isExpanded && onCollapse && ( + + { + e.stopPropagation(); + onCollapse(node.id); + }} + /> + + )} + {/* Expand icon - top right, only show on hover when collapsed */} + {isCollapsed && onExpand && ( + + { + e.stopPropagation(); + onExpand(node.id); + }} + /> + + )} + {/* Header */} +
{ + e.stopPropagation(); + onClick?.(node); + }} + title={node.data.description} + > +
+ +
+ {node.data.label} +
+
+
+ + {/* Content - Children with inline connectors */} + {node.children.length > 0 && ( +
+ {node.children.map((child, index) => ( + + {renderChild(child)} + {/* Connector line to next child */} + {index < node.children.length - 1 && ( +
+ )} + + ))} +
+ )} + + {/* Parallel Branches */} + {node.parallelBranches && node.parallelBranches.length > 0 && ( +
+
+ {node.parallelBranches.map((branch, branchIndex) => ( +
+ {branch.map((child, index) => ( + + {renderChild(child)} + {/* Connector line to next child in branch */} + {index < branch.length - 1 && ( +
+ )} + + ))} +
+ ))} +
+
+ )} +
+ ); +}; + +export default AgentNode; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/LLMNode.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/LLMNode.tsx new file mode 100644 index 000000000..444e73e23 --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/LLMNode.tsx @@ -0,0 +1,42 @@ +import type { FC } from "react"; +import { Tooltip, TooltipTrigger, TooltipContent } from "@/lib/components/ui"; +import type { LayoutNode } from "../utils/types"; + +interface LLMNodeProps { + node: LayoutNode; + isSelected?: boolean; + onClick?: (node: LayoutNode) => void; +} + +const LLMNode: FC = ({ node, isSelected, onClick }) => { + const isProcessing = node.data.status === "in-progress"; + const haloClass = isProcessing ? 'processing-halo' : ''; + + return ( + + +
{ + e.stopPropagation(); + onClick?.(node); + }} + > +
+ {node.data.label} +
+
+
+ {node.data.description && ( + {node.data.description} + )} +
+ ); +}; + +export default LLMNode; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/LoopNode.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/LoopNode.tsx new file mode 100644 index 000000000..a1492d48f --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/LoopNode.tsx @@ -0,0 +1,159 @@ +import { Fragment, type FC } from "react"; +import type { LayoutNode } from "../utils/types"; +import AgentNode from "./AgentNode"; + +interface LoopNodeProps { + node: LayoutNode; + isSelected?: boolean; + onClick?: (node: LayoutNode) => void; + onChildClick?: (child: LayoutNode) => void; + onExpand?: (nodeId: string) => void; + onCollapse?: (nodeId: string) => void; +} + +const LoopNode: FC = ({ node, isSelected, onClick, onChildClick, onExpand, onCollapse }) => { + const getStatusColor = () => { + switch (node.data.status) { + case "completed": + return "bg-teal-100 border-teal-500 dark:bg-teal-900/30 dark:border-teal-500"; + case "in-progress": + return "bg-blue-100 border-blue-500 dark:bg-blue-900/30 dark:border-blue-500"; + case "error": + return "bg-red-100 border-red-500 dark:bg-red-900/30 dark:border-red-500"; + default: + return "bg-gray-100 border-gray-400 dark:bg-gray-800 dark:border-gray-600"; + } + }; + + const currentIteration = node.data.currentIteration ?? 0; + const maxIterations = node.data.maxIterations ?? 100; + const hasChildren = node.children && node.children.length > 0; + + // Render a child node (loop iterations are agent nodes) + const renderChild = (child: LayoutNode) => { + const childProps = { + node: child, + onClick: onChildClick, + onChildClick: onChildClick, + onExpand, + onCollapse, + }; + + switch (child.type) { + case 'agent': + return ; + default: + // Loop children are typically agents, but handle other types if needed + return null; + } + }; + + // If the loop has children (iterations), render as a container + if (hasChildren) { + return ( +
+ {/* Loop Label with icon - clickable */} +
{ + e.stopPropagation(); + onClick?.(node); + }} + title={`Loop: ${node.data.condition || 'while condition'} (max ${maxIterations})`} + > + {/* Loop Arrow Icon */} + + + + {node.data.label} +
+ + {/* Children (loop iterations) with inline connectors */} +
+ {node.children.map((child, index) => ( + + {/* Iteration label */} +
+ Iteration {index + 1} +
+ {renderChild(child)} + {/* Connector line to next child */} + {index < node.children.length - 1 && ( +
+ )} + + ))} +
+
+ ); + } + + // No children yet - render as compact badge + return ( +
{ + e.stopPropagation(); + onClick?.(node); + }} + title={node.data.description || `Loop: ${node.data.condition || 'while condition'} (max ${maxIterations})`} + > + {/* Stadium/Pill shape with loop indicator */} +
+ {/* Loop Arrow Icon */} + + + + + {/* Content */} +
+
+ {node.data.label} +
+
+
+ + {/* Iteration Counter (if in progress) */} + {node.data.status === 'in-progress' && currentIteration > 0 && ( +
+ Iteration {currentIteration} +
+ )} +
+ ); +}; + +export default LoopNode; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/MapNode.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/MapNode.tsx new file mode 100644 index 000000000..8b1f0a9d0 --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/MapNode.tsx @@ -0,0 +1,181 @@ +import { Fragment, useMemo, type FC } from "react"; +import { Tooltip, TooltipTrigger, TooltipContent } from "@/lib/components/ui"; +import type { LayoutNode } from "../utils/types"; +import AgentNode from "./AgentNode"; + +interface MapNodeProps { + node: LayoutNode; + isSelected?: boolean; + onClick?: (node: LayoutNode) => void; + onChildClick?: (child: LayoutNode) => void; + onExpand?: (nodeId: string) => void; + onCollapse?: (nodeId: string) => void; +} + +const MapNode: FC = ({ node, isSelected, onClick, onChildClick, onExpand, onCollapse }) => { + const getStatusColor = () => { + switch (node.data.status) { + case "completed": + return "bg-indigo-100 border-indigo-500 dark:bg-indigo-900/30 dark:border-indigo-500"; + case "in-progress": + return "bg-blue-100 border-blue-500 dark:bg-blue-900/30 dark:border-blue-500"; + case "error": + return "bg-red-100 border-red-500 dark:bg-red-900/30 dark:border-red-500"; + default: + return "bg-gray-100 border-gray-400 dark:bg-gray-800 dark:border-gray-600"; + } + }; + + // Group children by iterationIndex to create branches + const branches = useMemo(() => { + const branchMap = new Map(); + for (const child of node.children) { + const iterationIndex = child.data.iterationIndex ?? 0; + if (!branchMap.has(iterationIndex)) { + branchMap.set(iterationIndex, []); + } + branchMap.get(iterationIndex)!.push(child); + } + // Sort by iteration index and return as array of arrays + return Array.from(branchMap.entries()) + .sort((a, b) => a[0] - b[0]) + .map(([, children]) => children); + }, [node]); + + const hasChildren = branches.length > 0; + const label = 'Map'; + const colorClass = "border-indigo-400 bg-indigo-50/30 dark:border-indigo-600 dark:bg-indigo-900/20"; + const labelColorClass = "text-indigo-600 dark:text-indigo-400 border-indigo-300 dark:border-indigo-700 hover:bg-indigo-50 dark:hover:bg-indigo-900/50"; + const connectorColor = "bg-indigo-400 dark:bg-indigo-600"; + + // Render a child node (iterations are agent nodes) + const renderChild = (child: LayoutNode) => { + const childProps = { + node: child, + onClick: onChildClick, + onChildClick: onChildClick, + onExpand, + onCollapse, + }; + + switch (child.type) { + case 'agent': + return ; + default: + return null; + } + }; + + // If the node has children, render as a container with parallel branches + if (hasChildren) { + return ( +
+ {/* Label with icon - clickable */} + + +
{ + e.stopPropagation(); + onClick?.(node); + }} + > + {/* Parallel/Branch Icon */} + + + + {node.data.label || label} +
+
+ {`${label}: ${branches.length} parallel branches`} +
+ + {/* Parallel branches displayed side-by-side */} +
+ {branches.map((branch, branchIndex) => ( +
+ {/* Branch children */} + {branch.map((child, childIndex) => ( + + {renderChild(child)} + {/* Connector line to next child in same branch */} + {childIndex < branch.length - 1 && ( +
+ )} + + ))} +
+ ))} +
+
+ ); + } + + // No parallel branches yet - render as compact badge + const badgeTooltip = node.data.description || `Map: Waiting for items...`; + + return ( + + +
{ + e.stopPropagation(); + onClick?.(node); + }} + > + {/* Stadium/Pill shape */} +
+ {/* Parallel Icon */} + + + + + {/* Content */} +
+
+ {node.data.label || label} +
+
+
+
+
+ {badgeTooltip} +
+ ); +}; + +export default MapNode; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/SwitchNode.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/SwitchNode.tsx new file mode 100644 index 000000000..5ad41bc95 --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/SwitchNode.tsx @@ -0,0 +1,72 @@ +import type { FC } from "react"; +import { Tooltip, TooltipTrigger, TooltipContent } from "@/lib/components/ui"; +import type { LayoutNode } from "../utils/types"; + +interface SwitchNodeProps { + node: LayoutNode; + isSelected?: boolean; + onClick?: (node: LayoutNode) => void; +} + +const SwitchNode: FC = ({ node, isSelected, onClick }) => { + const getStatusColor = () => { + switch (node.data.status) { + case "completed": + return "bg-purple-100 border-purple-500 dark:bg-purple-900/30 dark:border-purple-500"; + case "in-progress": + return "bg-blue-100 border-blue-500 dark:bg-blue-900/30 dark:border-blue-500"; + case "error": + return "bg-red-100 border-red-500 dark:bg-red-900/30 dark:border-red-500"; + default: + return "bg-gray-100 border-gray-400 dark:bg-gray-800 dark:border-gray-600"; + } + }; + + const casesCount = node.data.cases?.length || 0; + const hasDefault = !!node.data.defaultBranch; + + // Build tooltip with selected branch info + const baseTooltip = node.data.description || `Switch with ${casesCount} case${casesCount !== 1 ? 's' : ''}${hasDefault ? ' + default' : ''}`; + const tooltip = node.data.selectedBranch + ? `${baseTooltip}\nSelected: ${node.data.selectedBranch}` + : baseTooltip; + + return ( + + +
{ + e.stopPropagation(); + onClick?.(node); + }} + > + {/* Diamond Shape using rotation - same as conditional */} +
+ + {/* Content (unrotated) */} +
+
+ {/* Show selected branch when completed, otherwise show label */} + {node.data.selectedBranch || node.data.label} +
+ {/* Show case count only when not yet completed */} + {!node.data.selectedBranch && ( +
+ {casesCount} case{casesCount !== 1 ? 's' : ''} +
+ )} +
+
+ + {tooltip} + + ); +}; + +export default SwitchNode; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/ToolNode.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/ToolNode.tsx new file mode 100644 index 000000000..7225d1fa9 --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/ToolNode.tsx @@ -0,0 +1,53 @@ +import type { FC } from "react"; +import { FileText, Wrench } from "lucide-react"; +import { Tooltip, TooltipTrigger, TooltipContent } from "@/lib/components/ui"; +import type { LayoutNode } from "../utils/types"; + +interface ToolNodeProps { + node: LayoutNode; + isSelected?: boolean; + onClick?: (node: LayoutNode) => void; +} + +const ToolNode: FC = ({ node, isSelected, onClick }) => { + const isProcessing = node.data.status === "in-progress"; + const haloClass = isProcessing ? 'processing-halo' : ''; + const artifactCount = node.data.createdArtifacts?.length || 0; + + return ( + + +
{ + e.stopPropagation(); + onClick?.(node); + }} + > +
+ +
{node.data.label}
+ {artifactCount > 0 && ( + + + + + {artifactCount} + + + {`${artifactCount} ${artifactCount === 1 ? 'artifact' : 'artifacts'} created`} + + )} +
+
+
+ {node.data.description && ( + {node.data.description} + )} +
+ ); +}; + +export default ToolNode; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/UserNode.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/UserNode.tsx new file mode 100644 index 000000000..941dd77ba --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/UserNode.tsx @@ -0,0 +1,34 @@ +import type { FC } from "react"; +import { User } from "lucide-react"; +import type { LayoutNode } from "../utils/types"; + +interface UserNodeProps { + node: LayoutNode; + isSelected?: boolean; + onClick?: (node: LayoutNode) => void; +} + +const UserNode: FC = ({ node, isSelected, onClick }) => { + return ( +
{ + e.stopPropagation(); + onClick?.(node); + }} + > +
+ +
{node.data.label}
+
+
+ ); +}; + +export default UserNode; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/WorkflowGroup.tsx b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/WorkflowGroup.tsx new file mode 100644 index 000000000..511c79106 --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/nodes/WorkflowGroup.tsx @@ -0,0 +1,417 @@ +import React, { useRef, useState, useLayoutEffect, useEffect, useCallback } from "react"; +import { Workflow, Maximize2, Minimize2 } from "lucide-react"; +import type { LayoutNode } from "../utils/types"; +import AgentNode from "./AgentNode"; +import SwitchNode from "./SwitchNode"; +import LoopNode from "./LoopNode"; +import MapNode from "./MapNode"; + +interface WorkflowGroupProps { + node: LayoutNode; + isSelected?: boolean; + onClick?: (node: LayoutNode) => void; + onChildClick?: (child: LayoutNode) => void; + onExpand?: (nodeId: string) => void; + onCollapse?: (nodeId: string) => void; +} + +interface BezierPath { + id: string; + d: string; +} + +/** + * Generate a cubic bezier path from source bottom-center to target top-center + * The curve uses a constant control offset at the source for a consistent departure curve, + * and a scaled control offset at the target to create long vertical sections for longer distances. + * + * @param scale - The current zoom scale factor (to convert screen coordinates to SVG coordinates) + */ +function generateBezierPath( + sourceRect: DOMRect, + targetRect: DOMRect, + containerRect: DOMRect, + scale: number = 1 +): string { + // Source: bottom center of the source element + // Divide by scale to convert from screen coordinates (affected by zoom) to SVG coordinates + const x1 = (sourceRect.left + sourceRect.width / 2 - containerRect.left) / scale; + const y1 = (sourceRect.bottom - containerRect.top) / scale; + + // Target: top center of the target element + const x2 = (targetRect.left + targetRect.width / 2 - containerRect.left) / scale; + const y2 = (targetRect.top - containerRect.top) / scale; + + // Control points for a curve with vertical start and end + const verticalDistance = Math.abs(y2 - y1); + + // Target control point: constant offset for consistent curve at arrival + const targetControlOffset = 40; + + // Source control point: extends far down to create a very long vertical section + // Using 90% of the distance creates an almost straight drop from the source + const sourceControlOffset = Math.max(verticalDistance * 1.0, 40); + + // Control point 1: directly below source (same x) for vertical start + const cx1 = x1; + const cy1 = y1 + sourceControlOffset; + + // Control point 2: directly above target (same x) for vertical end + const cx2 = x2; + const cy2 = y2 - targetControlOffset; + + return `M ${x1},${y1} C ${cx1},${cy1} ${cx2},${cy2} ${x2},${y2}`; +} + +const WorkflowGroup: React.FC = ({ node, isSelected, onClick, onChildClick, onExpand, onCollapse }) => { + const containerRef = useRef(null); + const [bezierPaths, setBezierPaths] = useState([]); + const [resizeCounter, setResizeCounter] = useState(0); + + const isCollapsed = node.data.isCollapsed; + const isExpanded = node.data.isExpanded; + const isProcessing = node.data.hasProcessingChildren; + const haloClass = isProcessing ? 'processing-halo' : ''; + + // Function to calculate bezier paths + const calculateBezierPaths = useCallback(() => { + if (!containerRef.current || isCollapsed) { + setBezierPaths([]); + return; + } + + const container = containerRef.current; + const containerRect = container.getBoundingClientRect(); + + // Calculate the current zoom scale by comparing the visual size (getBoundingClientRect) + // with the actual size (offsetWidth). When zoomed, the visual size changes but offsetWidth stays the same. + const scale = containerRect.width / container.offsetWidth; + + const paths: BezierPath[] = []; + + // Find all parallel blocks and their preceding/following nodes + const parallelBlocks = container.querySelectorAll('[data-parallel-block]'); + + parallelBlocks.forEach((blockEl) => { + const blockId = blockEl.getAttribute('data-parallel-block'); + const precedingNodeId = blockEl.getAttribute('data-preceding-node'); + const followingNodeId = blockEl.getAttribute('data-following-node'); + + // Draw lines FROM preceding node TO branch starts + if (precedingNodeId) { + const precedingWrapper = container.querySelector(`[data-node-id="${precedingNodeId}"]`); + if (precedingWrapper) { + const precedingEl = precedingWrapper.firstElementChild || precedingWrapper; + const precedingRect = precedingEl.getBoundingClientRect(); + + const branchStartNodes = blockEl.querySelectorAll('[data-branch-start="true"]'); + branchStartNodes.forEach((branchStartEl, index) => { + const targetEl = branchStartEl.firstElementChild || branchStartEl; + const targetRect = targetEl.getBoundingClientRect(); + const pathD = generateBezierPath(precedingRect, targetRect, containerRect, scale); + + paths.push({ + id: `${blockId}-start-${index}`, + d: pathD, + }); + }); + } + } + + // Draw lines FROM branch ends TO following node + if (followingNodeId) { + const followingWrapper = container.querySelector(`[data-node-id="${followingNodeId}"]`); + if (followingWrapper) { + const followingEl = followingWrapper.firstElementChild || followingWrapper; + const followingRect = followingEl.getBoundingClientRect(); + + const branchEndNodes = blockEl.querySelectorAll('[data-branch-end="true"]'); + branchEndNodes.forEach((branchEndEl, index) => { + const sourceEl = branchEndEl.firstElementChild || branchEndEl; + const sourceRect = sourceEl.getBoundingClientRect(); + const pathD = generateBezierPath(sourceRect, followingRect, containerRect, scale); + + paths.push({ + id: `${blockId}-end-${index}`, + d: pathD, + }); + }); + } + } + }); + + setBezierPaths(paths); + }, [isCollapsed]); + + // Calculate bezier paths after render + useLayoutEffect(() => { + calculateBezierPaths(); + }, [node.children, isCollapsed, resizeCounter, calculateBezierPaths]); + + // Use ResizeObserver to detect when children expand/collapse (changes their size) + useEffect(() => { + if (!containerRef.current || isCollapsed) return; + + const resizeObserver = new ResizeObserver(() => { + // Trigger recalculation by incrementing counter + setResizeCounter(c => c + 1); + }); + + // Observe the container and all nodes within it + resizeObserver.observe(containerRef.current); + const nodes = containerRef.current.querySelectorAll('[data-node-id]'); + nodes.forEach(node => resizeObserver.observe(node)); + + return () => resizeObserver.disconnect(); + }, [node.children, isCollapsed]); + + // Use MutationObserver to detect zoom/pan changes from react-zoom-pan-pinch + // The transform is applied to ancestor elements, so we watch for style changes + useEffect(() => { + if (!containerRef.current || isCollapsed) return; + + // Find the TransformComponent wrapper by looking for an ancestor with transform style + let transformedParent: Element | null = containerRef.current.parentElement; + while (transformedParent && !transformedParent.hasAttribute('style')) { + transformedParent = transformedParent.parentElement; + } + + if (!transformedParent) return; + + const mutationObserver = new MutationObserver((mutations) => { + // Check if any mutation is a style change (which includes transform changes) + const hasStyleChange = mutations.some(m => m.attributeName === 'style'); + if (hasStyleChange) { + setResizeCounter(c => c + 1); + } + }); + + // Observe style attribute changes on the transformed parent and its ancestors + // (react-zoom-pan-pinch may apply transforms at different levels) + let current: Element | null = transformedParent; + while (current && current !== document.body) { + mutationObserver.observe(current, { attributes: true, attributeFilter: ['style'] }); + current = current.parentElement; + } + + return () => mutationObserver.disconnect(); + }, [isCollapsed]); + + // Render a child node with data attributes for connector calculation + const renderChild = (child: LayoutNode, precedingNodeId?: string, followingNodeId?: string): React.ReactNode => { + const childProps = { + node: child, + onClick: onChildClick, + onExpand, + onCollapse, + }; + + switch (child.type) { + case 'agent': + return ( +
+ +
+ ); + case 'switch': + return ( +
+ +
+ ); + case 'loop': + return ( +
+ +
+ ); + case 'map': + return ( +
+ +
+ ); + case 'group': + // Nested workflow group - render recursively + return ( +
+ +
+ ); + case 'parallelBlock': { + // Group children by iterationIndex (branch index) for proper chain visualization + const branches = new Map(); + for (const parallelChild of child.children) { + const branchIdx = parallelChild.data.iterationIndex ?? 0; + if (!branches.has(branchIdx)) { + branches.set(branchIdx, []); + } + branches.get(branchIdx)!.push(parallelChild); + } + + // Sort branches by index + const sortedBranches = Array.from(branches.entries()).sort((a, b) => a[0] - b[0]); + + // Render parallel block - branches side-by-side, nodes within each branch stacked vertically + // Container is invisible - connectors are drawn via SVG bezier paths + return ( +
+ {sortedBranches.map(([branchIdx, branchChildren]) => ( +
+ {branchChildren.map((branchChild, nodeIdx) => ( + +
+ {renderChild(branchChild)} +
+ {/* Connector line to next node in branch */} + {nodeIdx < branchChildren.length - 1 && ( +
+ )} + + ))} +
+ ))} +
+ ); + } + default: + return null; + } + }; + + // Collapsed view - similar to collapsed agent but with workflow styling + if (isCollapsed) { + return ( +
+ {/* Expand icon - top right, only show on hover */} + {onExpand && ( + + { + e.stopPropagation(); + onExpand(node.id); + }} + /> + + )} + {/* Header */} +
{ + e.stopPropagation(); + onClick?.(node); + }} + title={node.data.description || "Click to view workflow details"} + > +
+ +
+ {node.data.label} +
+
+
+
+ ); + } + + // Full expanded view + return ( +
+ {/* SVG overlay for bezier connectors */} + {bezierPaths.length > 0 && ( + + {bezierPaths.map((path) => ( + + ))} + + )} + + {/* Collapse icon - top right, only show on hover when expanded */} + {isExpanded && onCollapse && ( + + { + e.stopPropagation(); + onCollapse(node.id); + }} + /> + + )} + {/* Label - clickable */} + {node.data.label && ( +
{ + e.stopPropagation(); + onClick?.(node); + }} + title="Click to view workflow details" + > + + {node.data.label} +
+ )} + + {/* Children with inline connectors */} +
+ {node.children.map((child, index) => { + // Track the preceding and following nodes for parallel blocks + const precedingNode = index > 0 ? node.children[index - 1] : null; + const precedingNodeId = precedingNode?.id; + const followingNode = index < node.children.length - 1 ? node.children[index + 1] : null; + const followingNodeId = followingNode?.id; + + return ( + + {renderChild(child, precedingNodeId, followingNodeId)} + {/* Connector line to next child (only if current is not parallelBlock and next is not parallelBlock) */} + {index < node.children.length - 1 && + child.type !== 'parallelBlock' && + node.children[index + 1].type !== 'parallelBlock' && ( +
+ )} + + ); + })} +
+
+ ); +}; + +export default WorkflowGroup; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/taskToFlowData.helpers.ts b/client/webui/frontend/src/lib/components/activities/FlowChart/taskToFlowData.helpers.ts deleted file mode 100644 index 60b54428b..000000000 --- a/client/webui/frontend/src/lib/components/activities/FlowChart/taskToFlowData.helpers.ts +++ /dev/null @@ -1,821 +0,0 @@ -import type { Node, Edge } from "@xyflow/react"; -import type { VisualizerStep } from "@/lib/types"; -import { EdgeAnimationService } from "./edgeAnimationService"; - -// Helper function to resolve agent name to display name -export function resolveAgentDisplayName(agentName: string, agentNameMap?: Record): string { - return agentNameMap?.[agentName] || agentName; -} - -export interface NodeUpdateData { - label?: string; - status?: string; - isInitial?: boolean; - isFinal?: boolean; - description?: string; -} - -export interface LayoutContext { - currentY: number; - mainX: number; - subflowXOffset: number; - yIncrement: number; - subflowDepth: number; - agentYPositions: Map; // Store Y position of agent nodes for alignment - orchestratorCopyCount: number; // For unique IDs of duplicated orchestrator nodes - userGatewayCopyCount: number; // For unique IDs of duplicated user/gateway nodes - llmNodeXOffset: number; // Horizontal offset for LLM nodes from their calling agent - agentHorizontalSpacing: number; // Horizontal spacing between sibling agent nodes - agentsByLevel: Map; // Store agents at each Y level for horizontal alignment -} - -// Layout Management Interfaces -export interface NodeInstance { - id: string; - xPosition?: number; - yPosition: number; - height: number; - width: number; - functionCallId?: string; // The functionCallId that initiated this node -} - -export interface PhaseContext { - id: string; - orchestratorAgent: NodeInstance; - userNodes: NodeInstance[]; - subflows: SubflowContext[]; - // Stores all tool instances for tools called directly by this phase's orchestrator - toolInstances: NodeInstance[]; - currentToolYOffset: number; // Tracks Y offset for next tool called by orchestrator in this phase - maxY: number; // Max Y reached by elements directly in this phase (orchestrator, its tools) -} - -export interface SubflowContext { - id: string; // Corresponds to subTaskId - functionCallId: string; // The functionCallId that initiated this subflow - isParallel: boolean; // True if this subflow is part of a parallel execution - peerAgent: NodeInstance; // Stores absolute position of the peer agent - groupNode: NodeInstance; // Stores absolute position of the group - // Stores all tool instances for tools called by this subflow's peer agent - toolInstances: NodeInstance[]; - currentToolYOffset: number; // Tracks Y offset for next tool called by peer agent in this subflow - maxY: number; // Max Y (absolute) reached by elements within this subflow group - maxContentXRelative: number; // Max X reached by content relative to group's left edge - callingPhaseId: string; - // Parent context tracking for nested parallel flows - parentSubflowId?: string; // ID of the parent subflow (if nested) - inheritedXOffset?: number; // X offset inherited from parent parallel flow - lastSubflow?: SubflowContext; // Last subflow context for this subflow for nested flows -} - -export interface ParallelFlowContext { - subflowFunctionCallIds: string[]; - completedSubflows: Set; - startX: number; - startY: number; - currentXOffset: number; - maxHeight: number; -} - -export interface AgentNodeInfo { - id: string; - name: string; - type: "orchestrator" | "peer"; - phaseId?: string; - subflowId?: string; - context: "main" | "subflow"; - nodeInstance: NodeInstance; -} - -export interface AgentRegistry { - agents: Map; - findAgentByName(name: string): AgentNodeInfo | null; - findAgentById(id: string): AgentNodeInfo | null; - registerAgent(info: AgentNodeInfo): void; -} - -export interface TimelineLayoutManager { - phases: PhaseContext[]; - currentPhaseIndex: number; - currentSubflowIndex: number; // -1 if not in a subflow - parallelFlows: Map; // Key: an ID for the parallel block - - nextAvailableGlobalY: number; // Tracks the Y for the next major top-level element - - nodeIdCounter: number; // For generating unique IDs - allCreatedNodeIds: Set; // For the addNode helper - nodePositions: Map; // For quick lookup if needed by createEdge - - // Global UserNode tracking for new logic - allUserNodes: NodeInstance[]; // Global tracking of all user nodes - userNodeCounter: number; // For unique user node IDs - - // Agent registry for peer-to-peer delegation - agentRegistry: AgentRegistry; - - // Indentation tracking for agent delegation visualization - indentationLevel: number; // Current indentation level - indentationStep: number; // Pixels to indent per level - - // Agent name to display name mapping - agentNameMap?: Record; -} - -// Layout Constants -export const LANE_X_POSITIONS = { - USER: 50, - MAIN_FLOW: 300, // Orchestrator, PeerAgent - TOOLS: 600, -}; - -export const Y_START = 50; -export const NODE_HEIGHT = 50; // Approximate height for calculations -export const NODE_WIDTH = 330; // Approximate width for nodes (increased from 250) -export const VERTICAL_SPACING = 50; // Space between distinct phases or elements -export const GROUP_PADDING_Y = 20; // Vertical padding inside a group box -export const GROUP_PADDING_X = 10; // Horizontal padding inside a group box -export const TOOL_STACKING_OFFSET = NODE_HEIGHT + 20; // Vertical offset for stacked tools under the same agent -export const USER_NODE_Y_OFFSET = 90; // Offset to position UserNode slightly lower - -// Helper to add a node and corresponding action -export function addNode(nodes: Node[], createdNodeIds: Set, nodePayload: Node): Node { - nodes.push(nodePayload); - createdNodeIds.add(nodePayload.id); - return nodePayload; -} - -// Helper to add an edge and corresponding action -export function addEdgeAction(edges: Edge[], edgePayload: Edge): Edge { - edges.push(edgePayload); - return edgePayload; -} - -// Utility Functions -export function generateNodeId(context: TimelineLayoutManager, prefix: string): string { - context.nodeIdCounter++; - return `${prefix.replace(/[^a-zA-Z0-9_]/g, "_")}_${context.nodeIdCounter}`; -} - -export function getCurrentPhase(context: TimelineLayoutManager): PhaseContext | null { - if (context.currentPhaseIndex === -1 || context.currentPhaseIndex >= context.phases.length) { - return null; - } - return context.phases[context.currentPhaseIndex]; -} - -export function getCurrentSubflow(context: TimelineLayoutManager): SubflowContext | null { - const phase = getCurrentPhase(context); - if (!phase || context.currentSubflowIndex === -1 || context.currentSubflowIndex >= phase.subflows.length) { - return null; - } - return phase.subflows[context.currentSubflowIndex]; -} - -// Helper function to find tool instance by name in array -export function findToolInstanceByName(toolInstances: NodeInstance[], toolName: string, nodes: Node[]): NodeInstance | null { - // Find the most recent tool instance with matching name - for (let i = toolInstances.length - 1; i >= 0; i--) { - const toolInstance = toolInstances[i]; - const toolNode = nodes.find(n => n.id === toolInstance.id); - if (toolNode?.data?.toolName === toolName) { - return toolInstance; - } - } - return null; -} - -export function findSubflowByFunctionCallId(context: TimelineLayoutManager, functionCallId: string | undefined): SubflowContext | null { - if (!functionCallId) return null; - const phase = getCurrentPhase(context); - if (!phase) return null; - - return phase.subflows.findLast(sf => sf.functionCallId === functionCallId) || null; -} - -export function findSubflowBySubTaskId(context: TimelineLayoutManager, subTaskId: string | undefined): SubflowContext | null { - if (!subTaskId) return null; - const phase = getCurrentPhase(context); - if (!phase) return null; - return phase.subflows.findLast(sf => sf.id === subTaskId) || null; -} - -// Enhanced context resolution with multiple fallback strategies -export function resolveSubflowContext(manager: TimelineLayoutManager, step: VisualizerStep): SubflowContext | null { - const currentPhase = getCurrentPhase(manager); - if (!currentPhase) return null; - - // 1. The most reliable match is the functionCallId that initiated the subflow. - if (step.functionCallId) { - const directMatch = findSubflowByFunctionCallId(manager, step.functionCallId); - if (directMatch) { - return directMatch; - } - } - - // 2. The next best match is the sub-task's own task ID. - if (step.owningTaskId && step.isSubTaskStep) { - const taskMatch = findSubflowBySubTaskId(manager, step.owningTaskId); - if (taskMatch) { - // This check is a safeguard against race conditions where two subflows might get the same ID, which shouldn't happen. - const subflows = currentPhase.subflows || []; - const subflowIdHasDuplicate = new Set(subflows.map(sf => sf.id)).size !== subflows.length; - if (!subflowIdHasDuplicate) { - return taskMatch; - } - } - } - - // 3. As a fallback, check if the event source matches the agent of the "current" subflow. - // This is less reliable in parallel scenarios but can help with event ordering issues. - const currentSubflow = getCurrentSubflow(manager); - if (currentSubflow && step.source) { - const normalizedStepSource = step.source.replace(/[^a-zA-Z0-9_]/g, "_"); - const normalizedStepTarget = step.target?.replace(/[^a-zA-Z0-9_]/g, "_"); - const peerAgentId = currentSubflow.peerAgent.id; - if (peerAgentId.includes(normalizedStepSource) || (normalizedStepTarget && peerAgentId.includes(normalizedStepTarget))) { - return currentSubflow; - } - } - - // 4. Final fallback to the current subflow context. - if (currentSubflow) { - return currentSubflow; - } - - return null; -} - -// Enhanced subflow finder by sub-task ID with better matching -export function findSubflowBySubTaskIdEnhanced(manager: TimelineLayoutManager, subTaskId: string): SubflowContext | null { - const currentPhase = getCurrentPhase(manager); - if (!currentPhase) return null; - - // Find all matching subflows, using direct or partial match - const matchingSubflows = currentPhase.subflows.filter(sf => sf.id === subTaskId || sf.id.includes(subTaskId) || subTaskId.includes(sf.id)); - - if (matchingSubflows.length === 0) { - return null; - } - - // Return the last one in the array, as it's the most recent instance. - return matchingSubflows[matchingSubflows.length - 1]; -} - -// Determine if this is truly a parallel flow -export function isParallelFlow(step: VisualizerStep, manager: TimelineLayoutManager): boolean { - // Case 1: The decision step itself. This is where a parallel flow is defined. - if (step.data.toolDecision?.isParallel === true) { - return true; - } - - // Case 2: The invocation step. This is where a branch of a parallel flow is executed. - // We must check the specific functionCallId of the invocation, not the parent task's ID, - // which is what `step.functionCallId` often contains in nested scenarios. - const invocationFunctionCallId = step.data?.toolInvocationStart?.functionCallId; - - if (invocationFunctionCallId) { - // Check if this specific invocation is part of any registered parallel flow. - return Array.from(manager.parallelFlows.values()).some(p => p.subflowFunctionCallIds.includes(invocationFunctionCallId)); - } - - // If the step is not a parallel decision or a tool invocation with a specific ID, - // it's not considered part of a parallel flow by this logic. - return false; -} - -export function findToolInstanceByNameEnhanced(toolInstances: NodeInstance[], toolName: string, nodes: Node[], functionCallId?: string): NodeInstance | null { - // First try to match by function call ID if provided - if (functionCallId) { - for (let i = toolInstances.length - 1; i >= 0; i--) { - const toolInstance = toolInstances[i]; - if (toolInstance.functionCallId === functionCallId) { - const toolNode = nodes.find(n => n.id === toolInstance.id); - if (toolNode?.data?.toolName === toolName || toolName === "LLM") { - return toolInstance; - } - } - } - } - - return findToolInstanceByName(toolInstances, toolName, nodes); -} - -// Helper function to find parent parallel subflow context (recursive) -export function findParentParallelSubflow(manager: TimelineLayoutManager, sourceAgentName: string, targetAgentName: string): SubflowContext | null { - const currentPhase = getCurrentPhase(manager); - if (!currentPhase) return null; - - const normalizedSourceAgentName = sourceAgentName.replace(/[^a-zA-Z0-9_]/g, "_"); - const normalizedTargeAgentName = targetAgentName?.replace(/[^a-zA-Z0-9_]/g, "_"); - - let matchedSubflow: SubflowContext | null = null; - for (const subflow of currentPhase.subflows) { - if (subflow.peerAgent.id.includes(normalizedSourceAgentName) || (normalizedTargeAgentName && subflow.peerAgent.id.includes(normalizedTargeAgentName))) { - matchedSubflow = subflow; - break; - } - } - - if (!matchedSubflow) return null; - - // If the source subflow is parallel, return it - if (matchedSubflow.isParallel) { - return matchedSubflow; - } - - // If the source subflow is not parallel but has a parent, recursively look up - if (matchedSubflow.parentSubflowId) { - const parentSubflow = currentPhase.subflows.find(sf => sf.id === matchedSubflow.parentSubflowId); - if (parentSubflow && parentSubflow.isParallel) { - return parentSubflow; - } - // Recursively check parent's parent (for deeper nesting) - if (parentSubflow) { - return findParentParallelSubflowRecursive(currentPhase, parentSubflow); - } - } - - return null; -} - -// Helper function for recursive parent lookup -function findParentParallelSubflowRecursive(currentPhase: PhaseContext, subflow: SubflowContext): SubflowContext | null { - if (subflow.isParallel) { - return subflow; - } - - if (subflow.parentSubflowId) { - const parentSubflow = currentPhase.subflows.find(sf => sf.id === subflow.parentSubflowId); - if (parentSubflow) { - return findParentParallelSubflowRecursive(currentPhase, parentSubflow); - } - } - - return null; -} - -export function createNewMainPhase(manager: TimelineLayoutManager, agentName: string, step: VisualizerStep, nodes: Node[]): PhaseContext { - const phaseId = `phase_${manager.phases.length}`; - const orchestratorNodeId = generateNodeId(manager, `${agentName}_${phaseId}`); - const yPos = manager.nextAvailableGlobalY; - - // Use display name for the node label, fall back to agentName if not found - const displayName = resolveAgentDisplayName(agentName, manager.agentNameMap); - - const orchestratorNode: Node = { - id: orchestratorNodeId, - type: "orchestratorNode", - position: { x: LANE_X_POSITIONS.MAIN_FLOW, y: yPos }, - data: { label: displayName, visualizerStepId: step.id }, - }; - addNode(nodes, manager.allCreatedNodeIds, orchestratorNode); - manager.nodePositions.set(orchestratorNodeId, orchestratorNode.position); - - const orchestratorInstance: NodeInstance = { id: orchestratorNodeId, yPosition: yPos, height: NODE_HEIGHT, width: NODE_WIDTH }; - - // Register the orchestrator agent in the registry - const agentInfo: AgentNodeInfo = { - id: orchestratorNodeId, - name: agentName, - type: "orchestrator", - phaseId: phaseId, - context: "main", - nodeInstance: orchestratorInstance, - }; - manager.agentRegistry.registerAgent(agentInfo); - - const newPhase: PhaseContext = { - id: phaseId, - orchestratorAgent: orchestratorInstance, - userNodes: [], - subflows: [], - toolInstances: [], - currentToolYOffset: 0, - maxY: yPos + NODE_HEIGHT, - }; - manager.phases.push(newPhase); - manager.currentPhaseIndex = manager.phases.length - 1; - manager.currentSubflowIndex = -1; // Ensure we are not in a subflow context - manager.nextAvailableGlobalY = newPhase.maxY + VERTICAL_SPACING; // Prepare Y for next element - - return newPhase; -} - -export function startNewSubflow(manager: TimelineLayoutManager, peerAgentName: string, step: VisualizerStep, nodes: Node[], isParallel: boolean): SubflowContext | null { - const currentPhase = getCurrentPhase(manager); - if (!currentPhase) return null; - - const isPeerReturn = step.type === "AGENT_TOOL_EXECUTION_RESULT" && step.data.toolResult?.isPeerResponse === true; - - const sourceAgentName = step.source || ""; - const targetAgentName = step.target || ""; - const isFromOrchestrator = isOrchestratorAgent(sourceAgentName); - - if (!isPeerReturn && !isFromOrchestrator && !isParallel) { - manager.indentationLevel++; - } - - const subflowId = step.delegationInfo?.[0]?.subTaskId || step.owningTaskId; - const peerAgentNodeId = generateNodeId(manager, `${peerAgentName}_${subflowId}`); - const groupNodeId = generateNodeId(manager, `group_${peerAgentName}_${subflowId}`); - - const invocationFunctionCallId = step.data?.toolInvocationStart?.functionCallId || step.functionCallId || ""; - - let groupNodeX: number; - let groupNodeY: number; - let peerAgentY: number; - - const parallelFlow = Array.from(manager.parallelFlows.values()).find(p => p.subflowFunctionCallIds.includes(invocationFunctionCallId)); - - // Find the current subflow context (the immediate parent of this new subflow) - const currentSubflow = getCurrentSubflow(manager); - - // Check for parent parallel context - const parentParallelSubflow = findParentParallelSubflow(manager, sourceAgentName, targetAgentName); - - if (isParallel && parallelFlow) { - // Standard parallel flow positioning - groupNodeX = parallelFlow.startX + parallelFlow.currentXOffset; - groupNodeY = parallelFlow.startY; - peerAgentY = groupNodeY + GROUP_PADDING_Y; - parallelFlow.currentXOffset += (NODE_WIDTH + GROUP_PADDING_X) * 2.2; - } else if (parentParallelSubflow) { - // Nested flow within parallel context - inherit parent's X offset - peerAgentY = manager.nextAvailableGlobalY; - const baseX = parentParallelSubflow.groupNode.xPosition || LANE_X_POSITIONS.MAIN_FLOW - 50; - groupNodeX = baseX + manager.indentationLevel * manager.indentationStep; - groupNodeY = peerAgentY - GROUP_PADDING_Y; - } else { - // Standard sequential flow positioning - peerAgentY = manager.nextAvailableGlobalY; - const baseX = LANE_X_POSITIONS.MAIN_FLOW - 50; - groupNodeX = baseX + manager.indentationLevel * manager.indentationStep; - groupNodeY = peerAgentY - GROUP_PADDING_Y; - } - - // Use display name for the peer agent node label, fall back to peerAgentName if not found - const displayName = resolveAgentDisplayName(peerAgentName, manager.agentNameMap); - - const peerAgentNode: Node = { - id: peerAgentNodeId, - type: "genericAgentNode", - position: { - x: 50, - y: GROUP_PADDING_Y, - }, - data: { label: displayName, visualizerStepId: step.id }, - parentId: groupNodeId, - }; - - const groupNode: Node = { - id: groupNodeId, - type: "group", - position: { x: groupNodeX, y: groupNodeY }, - data: { label: `${displayName} Sub-flow` }, - style: { - backgroundColor: "rgba(220, 220, 255, 0.1)", - border: "1px solid #aac", - borderRadius: "8px", - minHeight: `${NODE_HEIGHT + 2 * GROUP_PADDING_Y}px`, - }, - }; - addNode(nodes, manager.allCreatedNodeIds, groupNode); - addNode(nodes, manager.allCreatedNodeIds, peerAgentNode); - manager.nodePositions.set(peerAgentNodeId, peerAgentNode.position); - manager.nodePositions.set(groupNodeId, groupNode.position); - - const peerAgentInstance: NodeInstance = { id: peerAgentNodeId, xPosition: LANE_X_POSITIONS.MAIN_FLOW, yPosition: peerAgentY, height: NODE_HEIGHT, width: NODE_WIDTH }; - - const agentInfo: AgentNodeInfo = { - id: peerAgentNodeId, - name: peerAgentName, - type: "peer", - phaseId: currentPhase.id, - subflowId: subflowId, - context: "subflow", - nodeInstance: peerAgentInstance, - }; - manager.agentRegistry.registerAgent(agentInfo); - - const newSubflow: SubflowContext = { - id: subflowId, - functionCallId: invocationFunctionCallId, - isParallel: isParallel, - peerAgent: peerAgentInstance, - groupNode: { id: groupNodeId, xPosition: groupNodeX, yPosition: groupNodeY, height: NODE_HEIGHT + 2 * GROUP_PADDING_Y, width: 0 }, - toolInstances: [], - currentToolYOffset: 0, - maxY: peerAgentY + NODE_HEIGHT, - maxContentXRelative: peerAgentNode.position.x + NODE_WIDTH, - callingPhaseId: currentPhase.id, - // Add parent context tracking - parentSubflowId: currentSubflow?.id, - inheritedXOffset: parentParallelSubflow?.groupNode.xPosition, - }; - currentPhase.subflows.push(newSubflow); - if (parentParallelSubflow) { - parentParallelSubflow.lastSubflow = newSubflow; // Track last subflow for nested flows - } - manager.currentSubflowIndex = currentPhase.subflows.length - 1; - - if (isParallel && parallelFlow) { - parallelFlow.maxHeight = Math.max(parallelFlow.maxHeight, newSubflow.groupNode.height); - manager.nextAvailableGlobalY = parallelFlow.startY + parallelFlow.maxHeight + VERTICAL_SPACING; - } else { - manager.nextAvailableGlobalY = newSubflow.groupNode.yPosition + newSubflow.groupNode.height + VERTICAL_SPACING; - } - return newSubflow; -} - -export function createNewToolNodeInContext( - manager: TimelineLayoutManager, - toolName: string, - toolType: string, // e.g., 'llmNode', 'genericAgentNode' for tools - step: VisualizerStep, - nodes: Node[], - subflow: SubflowContext | null, - isLLM: boolean = false -): NodeInstance | null { - const currentPhase = getCurrentPhase(manager); - if (!currentPhase) return null; - - const contextToolArray = subflow ? subflow.toolInstances : currentPhase.toolInstances; - const baseLabel = isLLM ? "LLM" : `Tool: ${toolName}`; - const baseNodeIdPrefix = isLLM ? "LLM" : toolName; - - // Always create new tool instance instead of reusing - const parentGroupId = subflow ? subflow.groupNode.id : undefined; - - // Calculate tool's absolute Y position - let toolY_absolute: number; - let toolX_absolute: number; - - // Position for the node to be created (can be relative or absolute) - let nodePositionX: number; - let nodePositionY: number; - - if (subflow) { - // Tool's Y is relative to the peer agent's absolute Y, plus current offset within the subflow - toolY_absolute = subflow.peerAgent.yPosition + subflow.currentToolYOffset; - subflow.currentToolYOffset += TOOL_STACKING_OFFSET; - - // Position tools with a consistent offset from the peer agent node - // This ensures tools are properly positioned regardless of group indentation - const peerAgentRelativeX = 50; // The peer agent's x position relative to group - const toolOffsetFromPeer = 300; // Desired x-distance from peer agent to tool - - // Position the tool relative to the peer agent - nodePositionX = peerAgentRelativeX + toolOffsetFromPeer; - - // For nodes inside a group, position must be relative to the group's origin - // groupNode.xPosition and yPosition are absolute - if (subflow.groupNode.xPosition === undefined || subflow.groupNode.yPosition === undefined) { - return null; - } - - // Set absolute position for tracking - toolX_absolute = subflow.groupNode.xPosition + nodePositionX; - - // Y position relative to group - nodePositionY = toolY_absolute - subflow.groupNode.yPosition; - } else { - // For tools in the main flow (not in a subflow) - toolX_absolute = LANE_X_POSITIONS.TOOLS; // Default absolute X for main flow tools - - // Tool's Y is relative to the orchestrator agent's absolute Y, plus current offset - toolY_absolute = currentPhase.orchestratorAgent.yPosition + currentPhase.currentToolYOffset; - currentPhase.currentToolYOffset += TOOL_STACKING_OFFSET; - nodePositionX = toolX_absolute; - nodePositionY = toolY_absolute; - } - - // Generate unique ID for each tool call using step ID - const toolNodeId = generateNodeId(manager, `${baseNodeIdPrefix}_${step.id}`); - const toolNode: Node = { - id: toolNodeId, - type: toolType, - position: { x: nodePositionX, y: nodePositionY }, - data: { - label: baseLabel, - visualizerStepId: step.id, - toolName: toolName, - }, - parentId: parentGroupId, - }; - addNode(nodes, manager.allCreatedNodeIds, toolNode); - manager.nodePositions.set(toolNodeId, { x: nodePositionX, y: nodePositionY }); - - // The toolInstance should store the *absolute* position for logical tracking - const toolInstance: NodeInstance = { - id: toolNodeId, - xPosition: toolX_absolute, - yPosition: toolY_absolute, - height: NODE_HEIGHT, - width: NODE_WIDTH, - functionCallId: step.functionCallId, - }; - - // Add to array instead of map - contextToolArray.push(toolInstance); - - // Update maxY for the current context (phase or subflow) using absolute Y - const newMaxYInContext = toolY_absolute + NODE_HEIGHT; // Use absolute Y for maxY tracking - if (subflow) { - subflow.maxY = Math.max(subflow.maxY, newMaxYInContext); // subflow.maxY is absolute - - // Update maxContentXRelative to ensure it accounts for the tool node width - subflow.maxContentXRelative = Math.max(subflow.maxContentXRelative, nodePositionX + NODE_WIDTH); - - // Update group height and nextAvailableGlobalY - // groupNode.yPosition is absolute. maxY is absolute. - const requiredGroupHeight = subflow.maxY - subflow.groupNode.yPosition + GROUP_PADDING_Y; - subflow.groupNode.height = Math.max(subflow.groupNode.height, requiredGroupHeight); - - // Update group width to accommodate the tool nodes - const requiredGroupWidth = subflow.maxContentXRelative + GROUP_PADDING_X; - subflow.groupNode.width = Math.max(subflow.groupNode.width || 0, requiredGroupWidth); - - const groupNodeData = nodes.find(n => n.id === subflow.groupNode.id); - if (groupNodeData) { - groupNodeData.style = { - ...groupNodeData.style, - height: `${subflow.groupNode.height}px`, - width: `${subflow.groupNode.width}px`, - }; - } - - manager.nextAvailableGlobalY = Math.max(manager.nextAvailableGlobalY, subflow.groupNode.yPosition + subflow.groupNode.height + VERTICAL_SPACING); - } else { - currentPhase.maxY = Math.max(currentPhase.maxY, newMaxYInContext); - manager.nextAvailableGlobalY = Math.max(manager.nextAvailableGlobalY, currentPhase.maxY + VERTICAL_SPACING); - } - - return toolInstance; -} - -export function createNewUserNodeAtBottom(manager: TimelineLayoutManager, currentPhase: PhaseContext, step: VisualizerStep, nodes: Node[]): NodeInstance { - manager.userNodeCounter++; - const userNodeId = generateNodeId(manager, `User_response_${manager.userNodeCounter}`); - - // Position at the bottom of the chart - const userNodeY = manager.nextAvailableGlobalY + 20; - - const userNode: Node = { - id: userNodeId, - type: "userNode", - position: { x: LANE_X_POSITIONS.USER, y: userNodeY }, - data: { label: "User", visualizerStepId: step.id, isBottomNode: true }, - }; - - addNode(nodes, manager.allCreatedNodeIds, userNode); - manager.nodePositions.set(userNodeId, userNode.position); - - const userNodeInstance: NodeInstance = { - id: userNodeId, - yPosition: userNodeY, - height: NODE_HEIGHT, - width: NODE_WIDTH, - }; - - // Add to both phase and global tracking - currentPhase.userNodes.push(userNodeInstance); - manager.allUserNodes.push(userNodeInstance); - - // Update layout tracking - const newMaxY = userNodeY + NODE_HEIGHT; - currentPhase.maxY = Math.max(currentPhase.maxY, newMaxY); - - return userNodeInstance; -} - -export function createTimelineEdge( - sourceNodeId: string, - targetNodeId: string, - step: VisualizerStep, - edges: Edge[], - manager: TimelineLayoutManager, - edgeAnimationService: EdgeAnimationService, - _processedSteps: VisualizerStep[], - sourceHandleId?: string, - targetHandleId?: string -): void { - if (!sourceNodeId || !targetNodeId || sourceNodeId === targetNodeId) { - return; - } - - // Validate that source and target nodes exist - const sourceExists = manager.allCreatedNodeIds.has(sourceNodeId); - const targetExists = manager.allCreatedNodeIds.has(targetNodeId); - - if (!sourceExists) { - return; - } - - if (!targetExists) { - return; - } - - const edgeId = `edge-${sourceNodeId}${sourceHandleId || ""}-to-${targetNodeId}${targetHandleId || ""}-${step.id}`; - - const edgeExists = edges.some(e => e.id === edgeId); - - if (!edgeExists) { - const label = step.title && step.title.length > 30 ? step.type.replace(/_/g, " ").toLowerCase() : step.title || ""; - - // For initial edge creation, assume all agent-to-tool requests start animated - const isAgentToToolRequest = edgeAnimationService.isRequestStep(step); - - const newEdge: Edge = { - id: edgeId, - source: sourceNodeId, - target: targetNodeId, - label: label, - type: "defaultFlowEdge", // Ensure this custom edge type is registered - data: { - visualizerStepId: step.id, - isAnimated: isAgentToToolRequest, // Start animated if it's an agent-to-tool request - animationType: isAgentToToolRequest ? "request" : "static", - duration: 1.0, - } as unknown as Record, - }; - - // Only add handles if they are provided and valid - if (sourceHandleId) { - newEdge.sourceHandle = sourceHandleId; - } - if (targetHandleId) { - newEdge.targetHandle = targetHandleId; - } - - addEdgeAction(edges, newEdge); - } -} - -// Agent Registry Implementation -export function createAgentRegistry(): AgentRegistry { - const agents = new Map(); - - return { - agents, - findAgentByName(name: string): AgentNodeInfo | null { - // Normalize the name to handle variations like "peer_hirerarchy2" vs "hirerarchy2" - const normalizedName = name.startsWith("peer_") ? name.substring(5) : name; - - // Find all agents with matching name and return the most recent one - const matchingAgents: AgentNodeInfo[] = []; - for (const [, agentInfo] of agents) { - if (agentInfo.name === normalizedName || agentInfo.name === name) { - matchingAgents.push(agentInfo); - } - } - - if (matchingAgents.length === 0) { - return null; - } - - // Return the most recently created agent (highest node ID counter) - // Node IDs are generated with incrementing counter, so higher ID = more recent - return matchingAgents.reduce((latest, current) => { - const latestIdNum = parseInt(latest.id.split("_").pop() || "0"); - const currentIdNum = parseInt(current.id.split("_").pop() || "0"); - return currentIdNum > latestIdNum ? current : latest; - }); - }, - - findAgentById(id: string): AgentNodeInfo | null { - for (const [, agentInfo] of agents) { - if (agentInfo.id === id) { - return agentInfo; - } - } - return null; - }, - - registerAgent(info: AgentNodeInfo): void { - agents.set(info.id, info); - }, - }; -} - -// Helper function to get correct handle IDs based on agent type -export function getAgentHandle(agentType: "orchestrator" | "peer", direction: "input" | "output", position: "top" | "bottom" | "right"): string { - if (agentType === "orchestrator") { - if (direction === "output") { - return position === "bottom" ? "orch-bottom-output" : "orch-right-output-tools"; - } else { - return position === "top" ? "orch-top-input" : "orch-right-input-tools"; - } - } else { - // peer - if (direction === "output") { - return position === "bottom" ? "peer-bottom-output" : "peer-right-output-tools"; - } else { - return position === "top" ? "peer-top-input" : "peer-right-input-tools"; - } - } -} - -// Helper function to determine if an agent name represents an orchestrator -export function isOrchestratorAgent(agentName: string): boolean { - return agentName === "OrchestratorAgent" || agentName.toLowerCase().includes("orchestrator"); -} diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/taskToFlowData.ts b/client/webui/frontend/src/lib/components/activities/FlowChart/taskToFlowData.ts deleted file mode 100644 index d106f3fd0..000000000 --- a/client/webui/frontend/src/lib/components/activities/FlowChart/taskToFlowData.ts +++ /dev/null @@ -1,869 +0,0 @@ -import type { Node, Edge } from "@xyflow/react"; - -import type { VisualizerStep } from "@/lib/types"; - -import { - addNode, - type TimelineLayoutManager, - type NodeInstance, - LANE_X_POSITIONS, - Y_START, - NODE_HEIGHT, - NODE_WIDTH, - VERTICAL_SPACING, - GROUP_PADDING_Y, - GROUP_PADDING_X, - USER_NODE_Y_OFFSET, - generateNodeId, - getCurrentPhase, - getCurrentSubflow, - resolveSubflowContext, - isParallelFlow, - findToolInstanceByNameEnhanced, - createNewMainPhase, - startNewSubflow, - createNewToolNodeInContext, - createTimelineEdge, - createNewUserNodeAtBottom, - createAgentRegistry, - getAgentHandle, - isOrchestratorAgent, -} from "./taskToFlowData.helpers"; -import { EdgeAnimationService } from "./edgeAnimationService"; - -// Relevant step types that should be processed in the flow chart -const RELEVANT_STEP_TYPES = [ - "USER_REQUEST", - "AGENT_LLM_CALL", - "AGENT_LLM_RESPONSE_TO_AGENT", - "AGENT_LLM_RESPONSE_TOOL_DECISION", - "AGENT_TOOL_INVOCATION_START", - "AGENT_TOOL_EXECUTION_RESULT", - "AGENT_RESPONSE_TEXT", - "AGENT_ARTIFACT_NOTIFICATION", - "TASK_COMPLETED", - "TASK_FAILED", -]; - -interface FlowData { - nodes: Node[]; - edges: Edge[]; -} - -export interface AnimatedEdgeData { - visualizerStepId: string; - isAnimated?: boolean; - animationType?: "request" | "response" | "static"; -} - -function handleUserRequest(step: VisualizerStep, manager: TimelineLayoutManager, nodes: Node[], edges: Edge[], edgeAnimationService: EdgeAnimationService, processedSteps: VisualizerStep[]): void { - const targetAgentName = step.target as string; - const sanitizedTargetAgentName = targetAgentName.replace(/[^a-zA-Z0-9_]/g, "_"); - - const currentPhase = getCurrentPhase(manager); - const currentSubflow = getCurrentSubflow(manager); - - let lastAgentNode: NodeInstance | undefined; - let connectToLastAgent = false; - - if (currentSubflow) { - lastAgentNode = currentSubflow.peerAgent; - if (lastAgentNode.id.startsWith(sanitizedTargetAgentName + "_")) { - connectToLastAgent = true; - } - } else if (currentPhase) { - lastAgentNode = currentPhase.orchestratorAgent; - if (lastAgentNode.id.startsWith(sanitizedTargetAgentName + "_")) { - connectToLastAgent = true; - } - } - - if (connectToLastAgent && lastAgentNode && currentPhase) { - // Continued conversation: Create a "middle" user node and connect it to the last agent. - manager.userNodeCounter++; - const userNodeId = generateNodeId(manager, `User_continue_${manager.userNodeCounter}`); - - // Position the new user node at the current bottom of the flow. - const userNodeY = manager.nextAvailableGlobalY; - - const userNode: Node = { - id: userNodeId, - type: "userNode", - position: { x: LANE_X_POSITIONS.USER, y: userNodeY }, - // No isTopNode or isBottomNode, so it will be a "middle" node with a right handle. - data: { label: "User", visualizerStepId: step.id }, - }; - - addNode(nodes, manager.allCreatedNodeIds, userNode); - manager.nodePositions.set(userNodeId, userNode.position); - - const userNodeInstance: NodeInstance = { - id: userNodeId, - yPosition: userNodeY, - height: NODE_HEIGHT, - width: NODE_WIDTH, - }; - - // Add to tracking - currentPhase.userNodes.push(userNodeInstance); - manager.allUserNodes.push(userNodeInstance); - - // Update layout tracking to position subsequent nodes correctly. - const newMaxY = userNodeY + NODE_HEIGHT; - // An agent will be created at the same Y level, so we take the max. - lastAgentNode.yPosition = Math.max(lastAgentNode.yPosition, userNodeY); - currentPhase.maxY = Math.max(currentPhase.maxY, newMaxY, lastAgentNode.yPosition + NODE_HEIGHT); - manager.nextAvailableGlobalY = currentPhase.maxY + VERTICAL_SPACING; - - // The agent receiving the request is the target. - const targetAgentHandle = isOrchestratorAgent(targetAgentName) ? "orch-left-input" : "peer-left-input"; - - createTimelineEdge( - userNodeInstance.id, - lastAgentNode.id, - step, - edges, - manager, - edgeAnimationService, - processedSteps, - "user-right-output", // Source from the new right handle - targetAgentHandle // Target the top of the agent - ); - } else { - // Original behavior: create a new phase for the user request. - const phase = createNewMainPhase(manager, targetAgentName, step, nodes); - - const userNodeId = generateNodeId(manager, `User_${phase.id}`); - const userNode: Node = { - id: userNodeId, - type: "userNode", - position: { x: LANE_X_POSITIONS.USER, y: phase.orchestratorAgent.yPosition - USER_NODE_Y_OFFSET }, - data: { label: "User", visualizerStepId: step.id, isTopNode: true }, - }; - addNode(nodes, manager.allCreatedNodeIds, userNode); - manager.nodePositions.set(userNodeId, userNode.position); - - const userNodeInstance = { id: userNodeId, yPosition: userNode.position.y, height: NODE_HEIGHT, width: NODE_WIDTH }; - phase.userNodes.push(userNodeInstance); // Add to userNodes array - manager.allUserNodes.push(userNodeInstance); // Add to global tracking - manager.userNodeCounter++; - - phase.maxY = Math.max(phase.maxY, userNode.position.y + NODE_HEIGHT); - manager.nextAvailableGlobalY = phase.maxY + VERTICAL_SPACING; - - createTimelineEdge( - userNodeId, - phase.orchestratorAgent.id, - step, - edges, - manager, - edgeAnimationService, - processedSteps, - "user-bottom-output", // UserNode output - "orch-top-input" // OrchestratorAgent input from user - ); - } -} - -function handleLLMCall(step: VisualizerStep, manager: TimelineLayoutManager, nodes: Node[], edges: Edge[], edgeAnimationService: EdgeAnimationService, processedSteps: VisualizerStep[]): void { - const currentPhase = getCurrentPhase(manager); - if (!currentPhase) return; - - // Use enhanced context resolution - const subflow = resolveSubflowContext(manager, step); - - const sourceAgentNodeId = subflow ? subflow.peerAgent.id : currentPhase.orchestratorAgent.id; - const llmToolInstance = createNewToolNodeInContext(manager, "LLM", "llmNode", step, nodes, subflow, true); - - if (llmToolInstance) { - createTimelineEdge( - sourceAgentNodeId, - llmToolInstance.id, - step, - edges, - manager, - edgeAnimationService, - processedSteps, - subflow ? "peer-right-output-tools" : "orch-right-output-tools", // Agent output to LLM - "llm-left-input" // LLM input - ); - } -} - -function handleLLMResponseToAgent(step: VisualizerStep, manager: TimelineLayoutManager, nodes: Node[], edges: Edge[], edgeAnimationService: EdgeAnimationService, processedSteps: VisualizerStep[]): void { - // If this is a parallel tool decision with multiple peer agents delegation, set up the parallel flow context - if (step.type === "AGENT_LLM_RESPONSE_TOOL_DECISION" && step.data.toolDecision?.isParallel) { - const parallelFlowId = `parallel-${step.id}`; - if (step.data.toolDecision.decisions.filter(d => d.isPeerDelegation).length > 1) { - manager.parallelFlows.set(parallelFlowId, { - subflowFunctionCallIds: step.data.toolDecision.decisions.filter(d => d.isPeerDelegation).map(d => d.functionCallId), - completedSubflows: new Set(), - startX: LANE_X_POSITIONS.MAIN_FLOW - 50, - startY: manager.nextAvailableGlobalY, - currentXOffset: 0, - maxHeight: 0, - }); - } - } - - const currentPhase = getCurrentPhase(manager); - if (!currentPhase) return; - - // Use enhanced context resolution - const subflow = resolveSubflowContext(manager, step); - - let llmNodeId: string | undefined; - // LLM node should exist from a previous AGENT_LLM_CALL - // Find the most recent LLM instance within the correct context - const context = subflow || currentPhase; - - const llmInstance = findToolInstanceByNameEnhanced(context.toolInstances, "LLM", nodes, step.functionCallId); - - if (llmInstance) { - llmNodeId = llmInstance.id; - } else { - console.error(`[Timeline] LLM node not found for step type ${step.type}: ${step.id}. Cannot create edge.`); - return; - } - - // Target is the agent that received the response - const targetAgentName = step.target || "UnknownAgent"; - let targetAgentNodeId: string | undefined; - let targetAgentHandleId: string | undefined; - - if (subflow) { - targetAgentNodeId = subflow.peerAgent.id; - targetAgentHandleId = "peer-right-input-tools"; - } else if (currentPhase.orchestratorAgent.id.startsWith(targetAgentName.replace(/[^a-zA-Z0-9_]/g, "_") + "_")) { - targetAgentNodeId = currentPhase.orchestratorAgent.id; - targetAgentHandleId = "orch-right-input-tools"; - } - - if (llmNodeId && targetAgentNodeId && targetAgentHandleId) { - createTimelineEdge( - llmNodeId, - targetAgentNodeId, - step, - edges, - manager, - edgeAnimationService, - processedSteps, - "llm-bottom-output", // LLM's bottom output handle - targetAgentHandleId // Agent's right input handle - ); - } else { - console.error(`[Timeline] Could not determine target agent node ID or handle for step type ${step.type}: ${step.id}. Target agent name: ${targetAgentName}. Edge will be missing.`); - } -} - -function handleToolInvocationStart(step: VisualizerStep, manager: TimelineLayoutManager, nodes: Node[], edges: Edge[], edgeAnimationService: EdgeAnimationService, processedSteps: VisualizerStep[]): void { - const currentPhase = getCurrentPhase(manager); - if (!currentPhase) return; - - const sourceName = step.source || "UnknownSource"; - const targetToolName = step.target || "UnknownTool"; - - const isPeerDelegation = step.data.toolInvocationStart?.isPeerInvocation || targetToolName.startsWith("peer_"); - - if (isPeerDelegation) { - const peerAgentName = targetToolName.startsWith("peer_") ? targetToolName.substring(5) : targetToolName; - - // Instead of relying on the current subflow context, which can be polluted by the - // first parallel node, we find the source agent directly from the registry. - const sourceAgentInfo = manager.agentRegistry.findAgentByName(sourceName); - if (!sourceAgentInfo) { - console.error(`[Timeline] Could not find source agent in registry: ${sourceName} for step ${step.id}`); - return; - } - - const sourceAgent = sourceAgentInfo.nodeInstance; - // All agent-to-agent delegations use the bottom-to-top handles. - const sourceHandle = getAgentHandle(sourceAgentInfo.type, "output", "bottom"); - - const isParallel = isParallelFlow(step, manager); - - const subflowContext = startNewSubflow(manager, peerAgentName, step, nodes, isParallel); - if (subflowContext) { - createTimelineEdge(sourceAgent.id, subflowContext.peerAgent.id, step, edges, manager, edgeAnimationService, processedSteps, sourceHandle, "peer-top-input"); - } - } else { - // Regular tool call - const subflow = resolveSubflowContext(manager, step); - let sourceNodeId: string; - let sourceHandle: string; - - if (subflow) { - sourceNodeId = subflow.peerAgent.id; - sourceHandle = "peer-right-output-tools"; - } else { - const sourceAgent = manager.agentRegistry.findAgentByName(sourceName); - if (sourceAgent) { - sourceNodeId = sourceAgent.id; - sourceHandle = getAgentHandle(sourceAgent.type, "output", "right"); - } else { - sourceNodeId = currentPhase.orchestratorAgent.id; - sourceHandle = "orch-right-output-tools"; - } - } - - const toolInstance = createNewToolNodeInContext(manager, targetToolName, "genericToolNode", step, nodes, subflow); - if (toolInstance) { - createTimelineEdge(sourceNodeId, toolInstance.id, step, edges, manager, edgeAnimationService, processedSteps, sourceHandle, `${toolInstance.id}-tool-left-input`); - } - } -} - -function handleToolExecutionResult(step: VisualizerStep, manager: TimelineLayoutManager, nodes: Node[], edges: Edge[], edgeAnimationService: EdgeAnimationService, processedSteps: VisualizerStep[]): void { - const currentPhase = getCurrentPhase(manager); - if (!currentPhase) return; - - const stepSource = step.source || "UnknownSource"; - const targetAgentName = step.target || "OrchestratorAgent"; - - if (step.data.toolResult?.isPeerResponse) { - const returningFunctionCallId = step.data.toolResult?.functionCallId; - - // 1. FIRST, check if this return belongs to any active parallel flow. - const parallelFlowEntry = Array.from(manager.parallelFlows.entries()).find( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ([_id, pf]) => pf.subflowFunctionCallIds.includes(returningFunctionCallId || "") - ); - - if (parallelFlowEntry) { - // It's a parallel return. Handle the special join logic. - const [parallelFlowId, parallelFlow] = parallelFlowEntry; - - parallelFlow.completedSubflows.add(returningFunctionCallId || ""); - - if (parallelFlow.completedSubflows.size < parallelFlow.subflowFunctionCallIds.length) { - // Not all parallel tasks are done yet. Just record completion and wait. - return; - } - - // 2. ALL parallel tasks are done. Create a SINGLE "join" node. - const sourceSubflows = currentPhase.subflows.filter(sf => parallelFlow.subflowFunctionCallIds.includes(sf.functionCallId)); - - const joinTargetAgentName = step.target || "OrchestratorAgent"; - let joinNode: NodeInstance; - let joinNodeHandle: string; - - if (isOrchestratorAgent(joinTargetAgentName)) { - // The parallel tasks are returning to the main orchestrator. - manager.indentationLevel = 0; - const newOrchestratorPhase = createNewMainPhase(manager, joinTargetAgentName, step, nodes); - joinNode = newOrchestratorPhase.orchestratorAgent; - joinNodeHandle = "orch-top-input"; - manager.currentSubflowIndex = -1; // Return to main flow context - } else { - // The parallel tasks are returning to a PEER agent (nested parallel). - // Create ONE new instance of that peer agent for them to join to. - manager.indentationLevel = Math.max(0, manager.indentationLevel - 1); - const newSubflowForJoin = startNewSubflow(manager, joinTargetAgentName, step, nodes, false); - if (!newSubflowForJoin) return; - joinNode = newSubflowForJoin.peerAgent; - joinNodeHandle = "peer-top-input"; - } - - // 3. Connect ALL completed parallel agents to this single join node. - sourceSubflows.forEach(subflow => { - createTimelineEdge( - subflow.lastSubflow?.peerAgent.id ?? subflow.peerAgent.id, - joinNode.id, - step, // Use the final step as the representative event for the join - edges, - manager, - edgeAnimationService, - processedSteps, - "peer-bottom-output", - joinNodeHandle - ); - }); - - // 4. Clean up the completed parallel flow to prevent reuse. - manager.parallelFlows.delete(parallelFlowId); - - return; // Exit after handling the parallel join. - } - - // If we reach here, it's a NON-PARALLEL (sequential) peer return. - const sourceAgent = manager.agentRegistry.findAgentByName(stepSource.startsWith("peer_") ? stepSource.substring(5) : stepSource); - if (!sourceAgent) { - console.error(`[Timeline] Source peer agent not found for peer response: ${stepSource}.`); - return; - } - - if (isOrchestratorAgent(targetAgentName)) { - manager.indentationLevel = 0; - const newOrchestratorPhase = createNewMainPhase(manager, targetAgentName, step, nodes); - createTimelineEdge(sourceAgent.id, newOrchestratorPhase.orchestratorAgent.id, step, edges, manager, edgeAnimationService, processedSteps, "peer-bottom-output", "orch-top-input"); - manager.currentSubflowIndex = -1; - } else { - // Peer-to-peer sequential return. - manager.indentationLevel = Math.max(0, manager.indentationLevel - 1); - - // Check if we need to consider parallel flow context for this return - const isWithinParallelContext = isParallelFlow(step, manager) || Array.from(manager.parallelFlows.values()).some(pf => pf.subflowFunctionCallIds.some(id => currentPhase.subflows.some(sf => sf.functionCallId === id))); - - const newSubflow = startNewSubflow(manager, targetAgentName, step, nodes, isWithinParallelContext); - if (newSubflow) { - createTimelineEdge(sourceAgent.id, newSubflow.peerAgent.id, step, edges, manager, edgeAnimationService, processedSteps, "peer-bottom-output", "peer-top-input"); - } - } - } else { - // Regular tool (non-peer) returning result - let toolNodeId: string | undefined; - const subflow = resolveSubflowContext(manager, step); - const context = subflow || currentPhase; - const toolInstance = findToolInstanceByNameEnhanced(context.toolInstances, stepSource, nodes, step.functionCallId); - - if (toolInstance) { - toolNodeId = toolInstance.id; - } - - if (toolNodeId) { - let receivingAgentNodeId: string; - let targetHandle: string; - - if (subflow) { - receivingAgentNodeId = subflow.peerAgent.id; - targetHandle = "peer-right-input-tools"; - } else { - const targetAgent = manager.agentRegistry.findAgentByName(targetAgentName); - if (targetAgent) { - receivingAgentNodeId = targetAgent.id; - targetHandle = getAgentHandle(targetAgent.type, "input", "right"); - } else { - receivingAgentNodeId = currentPhase.orchestratorAgent.id; - targetHandle = "orch-right-input-tools"; - } - } - - createTimelineEdge(toolNodeId, receivingAgentNodeId, step, edges, manager, edgeAnimationService, processedSteps, stepSource === "LLM" ? "llm-bottom-output" : `${toolNodeId}-tool-bottom-output`, targetHandle); - } else { - console.error(`[Timeline] Could not find source tool node for regular tool result: ${step.id}. Step source (tool name): ${stepSource}.`); - } - } -} - -function handleArtifactNotification(step: VisualizerStep, manager: TimelineLayoutManager, nodes: Node[], edges: Edge[], edgeAnimationService: EdgeAnimationService, processedSteps: VisualizerStep[]): void { - const currentPhase = getCurrentPhase(manager); - if (!currentPhase) return; - - const artifactData = step.data.artifactNotification; - const artifactName = artifactData?.artifactName || "Unnamed Artifact"; - - const subflow = resolveSubflowContext(manager, step); - - // Find the tool node that created this artifact - // We look for a tool that was invoked with the same functionCallId or recently executed - const context = subflow || currentPhase; - let sourceToolNode: NodeInstance | undefined; - - if (step.functionCallId) { - sourceToolNode = findToolInstanceByNameEnhanced(context.toolInstances, "", nodes, step.functionCallId) ?? undefined; - } - - if (!sourceToolNode && context.toolInstances.length > 0) { - sourceToolNode = context.toolInstances[context.toolInstances.length - 1]; - } - - let sourceNodeId: string; - let sourceHandle: string; - - if (sourceToolNode) { - sourceNodeId = sourceToolNode.id; - sourceHandle = `${sourceToolNode.id}-tool-right-output-artifact`; - } else return; // Cannot create artifact node without a source tool - - // Create artifact node positioned to the RIGHT of the tool node - const artifactNodeId = generateNodeId(manager, `Artifact_${artifactName}_${step.id}`); - const parentGroupId = subflow ? subflow.groupNode.id : undefined; - - let artifactX: number; - let artifactY: number; - - const ARTIFACT_X_OFFSET = 300; // Horizontal distance from tool to artifact - const ARTIFACT_DIFFERENCE_X = 100; - - if (subflow) { - // For artifacts in a subflow, position relative to the group (like tools do) - const toolNode = nodes.find(n => n.id === sourceToolNode.id); - let relativeToolX: number; - let relativeToolY: number; - - if (toolNode) { - // toolNode.position is already relative to the group - relativeToolX = toolNode.position.x; - relativeToolY = toolNode.position.y; - } else { - // Fallback: calculate relative position from absolute position - const groupX = subflow.groupNode.xPosition ?? LANE_X_POSITIONS.TOOLS; - relativeToolX = (sourceToolNode.xPosition ?? LANE_X_POSITIONS.TOOLS) - groupX; - relativeToolY = (sourceToolNode.yPosition ?? manager.nextAvailableGlobalY) - (subflow.groupNode.yPosition ?? manager.nextAvailableGlobalY); - } - - // Position artifact relative to group, offset from the tool node - artifactX = relativeToolX + ARTIFACT_X_OFFSET; - artifactY = relativeToolY; - } else { - // For main flow, use absolute positioning (like tools do) - artifactX = (sourceToolNode.xPosition ?? LANE_X_POSITIONS.TOOLS) + ARTIFACT_X_OFFSET; - artifactY = sourceToolNode.yPosition ?? manager.nextAvailableGlobalY; - } - - const artifactNode: Node = { - id: artifactNodeId, - type: "artifactNode", - position: { x: artifactX, y: artifactY }, - data: { - label: "Artifact", - visualizerStepId: step.id, - }, - parentId: parentGroupId, - }; - - addNode(nodes, manager.allCreatedNodeIds, artifactNode); - manager.nodePositions.set(artifactNodeId, { x: artifactX, y: artifactY }); - - const artifactInstance: NodeInstance = { - id: artifactNodeId, - xPosition: artifactX, - yPosition: artifactY, - height: NODE_HEIGHT, - width: NODE_WIDTH, - }; - - createTimelineEdge(sourceNodeId, artifactInstance.id, step, edges, manager, edgeAnimationService, processedSteps, sourceHandle, `${artifactInstance.id}-artifact-left-input`); - - // Update maxY and maxContentXRelative to ensure group accommodates the artifact - const artifactBottom = artifactY + NODE_HEIGHT; - const artifactRight = artifactX + NODE_WIDTH; - - if (subflow) { - subflow.maxY = Math.max(subflow.maxY, artifactBottom); - - // Update maxContentXRelative to include artifact node - subflow.maxContentXRelative = Math.max(subflow.maxContentXRelative, artifactRight - ARTIFACT_DIFFERENCE_X); - - // Update group dimensions - const requiredGroupWidth = subflow.maxContentXRelative + GROUP_PADDING_X; - subflow.groupNode.width = Math.max(subflow.groupNode.width || 0, requiredGroupWidth); - - // Update the actual group node in the nodes array - const groupNodeData = nodes.find(n => n.id === subflow.groupNode.id); - if (groupNodeData && groupNodeData.style) { - groupNodeData.style.width = `${subflow.groupNode.width}px`; - } - } - currentPhase.maxY = Math.max(currentPhase.maxY, artifactBottom); -} - -function handleAgentResponseText(step: VisualizerStep, manager: TimelineLayoutManager, nodes: Node[], edges: Edge[], edgeAnimationService: EdgeAnimationService, processedSteps: VisualizerStep[]): void { - const currentPhase = getCurrentPhase(manager); - // When step.isSubTaskStep is true, it indicates this is a response from Agent to Orchestrator (as a user) - if (!currentPhase || step.isSubTaskStep) return; - - const sourceAgentNodeId = currentPhase.orchestratorAgent.id; - - // Always create a new UserNode at the bottom of the chart for each response - const userNodeInstance = createNewUserNodeAtBottom(manager, currentPhase, step, nodes); - - createTimelineEdge( - sourceAgentNodeId, // OrchestratorAgent - userNodeInstance.id, // UserNode - step, - edges, - manager, - edgeAnimationService, - processedSteps, - "orch-bottom-output", // Orchestrator output to user - "user-top-input" // User input from orchestrator - ); -} - -function handleTaskCompleted(step: VisualizerStep, manager: TimelineLayoutManager, nodes: Node[], edges: Edge[], edgeAnimationService: EdgeAnimationService, processedSteps: VisualizerStep[]): void { - const currentPhase = getCurrentPhase(manager); - if (!currentPhase) return; - - const parallelFlow = Array.from(manager.parallelFlows.values()).find(p => p.subflowFunctionCallIds.includes(step.functionCallId || "")); - - if (parallelFlow) { - parallelFlow.completedSubflows.add(step.functionCallId || ""); - if (parallelFlow.completedSubflows.size === parallelFlow.subflowFunctionCallIds.length) { - // All parallel flows are complete, create a join node - manager.indentationLevel = 0; - const newOrchestratorPhase = createNewMainPhase(manager, "OrchestratorAgent", step, nodes); - - // Connect all completed subflows to the new orchestrator node - currentPhase.subflows.forEach(subflow => { - if (parallelFlow.subflowFunctionCallIds.includes(subflow.functionCallId)) { - createTimelineEdge(subflow.peerAgent.id, newOrchestratorPhase.orchestratorAgent.id, step, edges, manager, edgeAnimationService, processedSteps, "peer-bottom-output", "orch-top-input"); - } - }); - manager.currentSubflowIndex = -1; - } - return; - } - - if (!step.isSubTaskStep) { - return; - } - - const subflow = getCurrentSubflow(manager); - if (!subflow) { - console.warn(`[Timeline] TASK_COMPLETED with isSubTaskStep=true but no active subflow. Step ID: ${step.id}`); - return; - } - - if (!currentPhase) { - console.error(`[Timeline] No current phase found for TASK_COMPLETED. Step ID: ${step.id}`); - return; - } - - const sourcePeerAgent = subflow.peerAgent; - - // Check if an orchestrator node exists anywhere in the flow - const hasOrchestrator = nodes.some(node => typeof node.data.label === "string" && isOrchestratorAgent(node.data.label)); - - let targetNodeId: string; - let targetHandleId: string; - - if (hasOrchestrator) { - // Subtask is completing and returning to the orchestrator. - // Create a new phase for the orchestrator to continue. - manager.indentationLevel = 0; - // We need the orchestrator's name. Let's assume it's 'OrchestratorAgent'. - const newOrchestratorPhase = createNewMainPhase(manager, "OrchestratorAgent", step, nodes); - targetNodeId = newOrchestratorPhase.orchestratorAgent.id; - targetHandleId = "orch-top-input"; - } else { - // No orchestrator found, treat the return as a response to the User. - const userNodeInstance = createNewUserNodeAtBottom(manager, currentPhase, step, nodes); - targetNodeId = userNodeInstance.id; - targetHandleId = "user-top-input"; - } - - createTimelineEdge(sourcePeerAgent.id, targetNodeId, step, edges, manager, edgeAnimationService, processedSteps, "peer-bottom-output", targetHandleId); - - manager.currentSubflowIndex = -1; -} - -function handleTaskFailed(step: VisualizerStep, manager: TimelineLayoutManager, nodes: Node[], edges: Edge[]): void { - const currentPhase = getCurrentPhase(manager); - if (!currentPhase) return; - - const sourceName = step.source || "UnknownSource"; - const targetName = step.target || "User"; - - // Find the last agent node from the agents in current phase that matches the source - let sourceAgentNode: NodeInstance | undefined; - let sourceHandle = "orch-bottom-output"; // Default handle - - // Check if source is in current subflow - const currentSubflow = getCurrentSubflow(manager); - if (currentSubflow && currentSubflow.peerAgent.id.includes(sourceName.replace(/[^a-zA-Z0-9_]/g, "_"))) { - sourceAgentNode = currentSubflow.peerAgent; - sourceHandle = "peer-bottom-output"; - } else { - // Check if source matches orchestrator agent - if (currentPhase.orchestratorAgent.id.includes(sourceName.replace(/[^a-zA-Z0-9_]/g, "_"))) { - sourceAgentNode = currentPhase.orchestratorAgent; - sourceHandle = "orch-bottom-output"; - } else { - // Look for any peer agent in subflows that matches the source - for (const subflow of currentPhase.subflows) { - if (subflow.peerAgent.id.includes(sourceName.replace(/[^a-zA-Z0-9_]/g, "_"))) { - sourceAgentNode = subflow.peerAgent; - sourceHandle = "peer-bottom-output"; - break; - } - } - } - } - - if (!sourceAgentNode) { - console.error(`[Timeline] Could not find source agent node for TASK_FAILED: ${sourceName}`); - return; - } - - // Create a new target node with error state - let targetNodeId: string; - let targetHandleId: string; - - if (isOrchestratorAgent(targetName)) { - // Create a new orchestrator phase for error handling - manager.indentationLevel = 0; - const newOrchestratorPhase = createNewMainPhase(manager, targetName, step, nodes); - - targetNodeId = newOrchestratorPhase.orchestratorAgent.id; - targetHandleId = "orch-top-input"; - manager.currentSubflowIndex = -1; - } else { - // Create a new user node at the bottom for error notification - const userNodeInstance = createNewUserNodeAtBottom(manager, currentPhase, step, nodes); - - targetNodeId = userNodeInstance.id; - targetHandleId = "user-top-input"; - } - - // Create an error edge (red color) between source and target - createErrorEdge(sourceAgentNode.id, targetNodeId, step, edges, manager, sourceHandle, targetHandleId); -} - -// Helper function to create error edges with error state data -function createErrorEdge(sourceNodeId: string, targetNodeId: string, step: VisualizerStep, edges: Edge[], manager: TimelineLayoutManager, sourceHandleId?: string, targetHandleId?: string): void { - if (!sourceNodeId || !targetNodeId || sourceNodeId === targetNodeId) { - return; - } - - // Validate that source and target nodes exist - const sourceExists = manager.allCreatedNodeIds.has(sourceNodeId); - const targetExists = manager.allCreatedNodeIds.has(targetNodeId); - - if (!sourceExists || !targetExists) { - return; - } - - const edgeId = `error-edge-${sourceNodeId}${sourceHandleId || ""}-to-${targetNodeId}${targetHandleId || ""}-${step.id}`; - - const edgeExists = edges.some(e => e.id === edgeId); - - if (!edgeExists) { - const errorMessage = step.data.errorDetails?.message || "Task failed"; - const label = errorMessage.length > 30 ? "Error" : errorMessage; - - const newEdge: Edge = { - id: edgeId, - source: sourceNodeId, - target: targetNodeId, - label: label, - type: "defaultFlowEdge", - data: { - visualizerStepId: step.id, - isAnimated: false, - animationType: "static", - isError: true, - errorMessage: errorMessage, - } as unknown as Record, - }; - - // Only add handles if they are provided and valid - if (sourceHandleId) { - newEdge.sourceHandle = sourceHandleId; - } - if (targetHandleId) { - newEdge.targetHandle = targetHandleId; - } - - edges.push(newEdge); - } -} - -// Main transformation function -export const transformProcessedStepsToTimelineFlow = (processedSteps: VisualizerStep[], agentNameMap: Record = {}): FlowData => { - const newNodes: Node[] = []; - const newEdges: Edge[] = []; - - if (!processedSteps || processedSteps.length === 0) { - return { nodes: newNodes, edges: newEdges }; - } - - // Initialize edge animation service - const edgeAnimationService = new EdgeAnimationService(); - - const manager: TimelineLayoutManager = { - phases: [], - currentPhaseIndex: -1, - currentSubflowIndex: -1, - parallelFlows: new Map(), - nextAvailableGlobalY: Y_START, - nodeIdCounter: 0, - allCreatedNodeIds: new Set(), - nodePositions: new Map(), - allUserNodes: [], - userNodeCounter: 0, - agentRegistry: createAgentRegistry(), - indentationLevel: 0, - indentationStep: 50, // Pixels to indent per level - agentNameMap: agentNameMap, - }; - - const filteredSteps = processedSteps.filter(step => RELEVANT_STEP_TYPES.includes(step.type)); - - // Ensure the first USER_REQUEST step is processed first - const firstUserRequestIndex = filteredSteps.findIndex(step => step.type === "USER_REQUEST"); - let reorderedSteps = filteredSteps; - - if (firstUserRequestIndex > 0) { - // Move the first USER_REQUEST to the beginning - const firstUserRequest = filteredSteps[firstUserRequestIndex]; - reorderedSteps = [firstUserRequest, ...filteredSteps.slice(0, firstUserRequestIndex), ...filteredSteps.slice(firstUserRequestIndex + 1)]; - } - - for (const step of reorderedSteps) { - // Special handling for AGENT_LLM_RESPONSE_TOOL_DECISION if it's a peer delegation trigger - // This step often precedes AGENT_TOOL_INVOCATION_START for peers. - // The plan implies AGENT_TOOL_INVOCATION_START is the primary trigger for peer delegation. - // For now, we rely on AGENT_TOOL_INVOCATION_START to have enough info. - - switch (step.type) { - case "USER_REQUEST": - handleUserRequest(step, manager, newNodes, newEdges, edgeAnimationService, processedSteps); - break; - case "AGENT_LLM_CALL": - handleLLMCall(step, manager, newNodes, newEdges, edgeAnimationService, processedSteps); - break; - case "AGENT_LLM_RESPONSE_TO_AGENT": - case "AGENT_LLM_RESPONSE_TOOL_DECISION": - handleLLMResponseToAgent(step, manager, newNodes, newEdges, edgeAnimationService, processedSteps); - break; - case "AGENT_TOOL_INVOCATION_START": - handleToolInvocationStart(step, manager, newNodes, newEdges, edgeAnimationService, processedSteps); - break; - case "AGENT_TOOL_EXECUTION_RESULT": - handleToolExecutionResult(step, manager, newNodes, newEdges, edgeAnimationService, processedSteps); - break; - case "AGENT_RESPONSE_TEXT": - handleAgentResponseText(step, manager, newNodes, newEdges, edgeAnimationService, processedSteps); - break; - case "AGENT_ARTIFACT_NOTIFICATION": - handleArtifactNotification(step, manager, newNodes, newEdges, edgeAnimationService, processedSteps); - break; - case "TASK_COMPLETED": - handleTaskCompleted(step, manager, newNodes, newEdges, edgeAnimationService, processedSteps); - break; - case "TASK_FAILED": - handleTaskFailed(step, manager, newNodes, newEdges); - break; - } - } - - // Update group node heights based on final maxYInSubflow - manager.phases.forEach(phase => { - phase.subflows.forEach(subflow => { - const groupNodeData = newNodes.find(n => n.id === subflow.groupNode.id); - if (groupNodeData && groupNodeData.style) { - // Update Height - // peerAgent.yPosition is absolute, subflow.maxY is absolute. - // groupNode.yPosition is absolute. - // Content height is from top of first element (peerAgent) to bottom of last element in subflow. - // Relative Y of peer agent is GROUP_PADDING_Y. - // Max Y of content relative to group top = subflow.maxY - subflow.groupNode.yPosition - const contentMaxYRelative = subflow.maxY - subflow.groupNode.yPosition; - const requiredGroupHeight = contentMaxYRelative + GROUP_PADDING_Y; // Add bottom padding - groupNodeData.style.height = `${Math.max(NODE_HEIGHT + 2 * GROUP_PADDING_Y, requiredGroupHeight)}px`; - - // Update Width - // Ensure the group width is sufficient to contain all indented tool nodes - const requiredGroupWidth = subflow.maxContentXRelative + GROUP_PADDING_X; - - // Add extra padding to ensure the group is wide enough for indented tools - const minRequiredWidth = NODE_WIDTH + 2 * GROUP_PADDING_X + manager.indentationLevel * manager.indentationStep; - - groupNodeData.style.width = `${Math.max(requiredGroupWidth, minRequiredWidth)}px`; - } - }); - }); - - return { nodes: newNodes, edges: newEdges }; -}; diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/utils/layoutEngine.ts b/client/webui/frontend/src/lib/components/activities/FlowChart/utils/layoutEngine.ts new file mode 100644 index 000000000..27117ee63 --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/utils/layoutEngine.ts @@ -0,0 +1,1545 @@ +import type { VisualizerStep } from "@/lib/types"; +import type { LayoutNode, Edge, BuildContext, LayoutResult } from "./types"; + +// Layout constants +const NODE_WIDTHS = { + AGENT: 220, + TOOL: 180, + LLM: 180, + USER: 140, + SWITCH: 120, + LOOP: 120, + MAP: 120, + MIN_AGENT_CONTENT: 200, +}; + +const NODE_HEIGHTS = { + AGENT_HEADER: 50, + TOOL: 50, + LLM: 50, + USER: 50, + SWITCH: 80, + LOOP: 80, + MAP: 80, +}; + +const SPACING = { + VERTICAL: 16, // Space between children within agent + HORIZONTAL: 20, // Space between parallel branches + AGENT_VERTICAL: 60, // Space between top-level agents + PADDING: 20, // Padding inside agent nodes +}; + +/** + * Main entry point: Process VisualizerSteps into layout tree + */ +export function processSteps(steps: VisualizerStep[], agentNameMap: Record = {}): LayoutResult { + const context: BuildContext = { + steps, + stepIndex: 0, + nodeCounter: 0, + taskToNodeMap: new Map(), + functionCallToNodeMap: new Map(), + currentAgentNode: null, + rootNodes: [], + agentNameMap, + parallelContainerMap: new Map(), + currentBranchMap: new Map(), + hasTopUserNode: false, + hasBottomUserNode: false, + parallelPeerGroupMap: new Map(), + parallelBlockMap: new Map(), + subWorkflowParentMap: new Map(), + }; + + // Process all steps to build tree structure + for (let i = 0; i < steps.length; i++) { + context.stepIndex = i; + const step = steps[i]; + processStep(step, context); + } + + // DEBUG: Print tree structure + console.log('=== LAYOUT ENGINE DEBUG ==='); + console.log('Steps processed:', steps.length); + console.log('Root nodes:', context.rootNodes.length); + console.log('taskToNodeMap keys:', Array.from(context.taskToNodeMap.keys())); + console.log('subWorkflowParentMap keys:', Array.from(context.subWorkflowParentMap.keys())); + + const printTree = (node: LayoutNode, indent: string = '') => { + console.log(`${indent}[${node.type}] ${node.data.label} (id=${node.id}, owningTaskId=${node.owningTaskId || 'none'})`); + for (const child of node.children) { + printTree(child, indent + ' '); + } + }; + + console.log('Tree structure:'); + for (const node of context.rootNodes) { + printTree(node); + } + console.log('=== END DEBUG ==='); + + // Calculate layout (positions and dimensions) + const nodes = calculateLayout(context.rootNodes); + + // Calculate edges between top-level nodes + const edges = calculateEdges(nodes, steps); + + // Calculate total canvas size + const { totalWidth, totalHeight } = calculateCanvasSize(nodes); + + return { + nodes, + edges, + totalWidth, + totalHeight, + }; +} + +/** + * Process a single VisualizerStep + */ +function processStep(step: VisualizerStep, context: BuildContext): void { + // Log workflow-related steps + if (step.type.startsWith('WORKFLOW')) { + console.log('[processStep]', step.type, 'owningTaskId=', step.owningTaskId, 'data=', step.data); + } + + switch (step.type) { + case "USER_REQUEST": + handleUserRequest(step, context); + break; + case "AGENT_LLM_CALL": + handleLLMCall(step, context); + break; + case "AGENT_TOOL_INVOCATION_START": + handleToolInvocation(step, context); + break; + case "AGENT_TOOL_EXECUTION_RESULT": + handleToolResult(step, context); + break; + case "AGENT_LLM_RESPONSE_TO_AGENT": + case "AGENT_LLM_RESPONSE_TOOL_DECISION": + handleLLMResponse(step, context); + break; + case "AGENT_RESPONSE_TEXT": + handleAgentResponse(step, context); + break; + case "WORKFLOW_EXECUTION_START": + handleWorkflowStart(step, context); + break; + case "WORKFLOW_NODE_EXECUTION_START": + handleWorkflowNodeStart(step, context); + break; + case "WORKFLOW_EXECUTION_RESULT": + handleWorkflowExecutionResult(step, context); + break; + case "WORKFLOW_NODE_EXECUTION_RESULT": + handleWorkflowNodeResult(step, context); + break; + case "AGENT_ARTIFACT_NOTIFICATION": + handleArtifactNotification(step, context); + break; + // Add other cases as needed + } +} + +/** + * Handle USER_REQUEST step - creates User node + Agent node + */ +function handleUserRequest(step: VisualizerStep, context: BuildContext): void { + // Only create top User node once, and only for top-level requests + if (!context.hasTopUserNode && step.nestingLevel === 0) { + const userNode = createNode( + context, + 'user', + { + label: 'User', + visualizerStepId: step.id, + isTopNode: true, + }, + step.owningTaskId + ); + context.rootNodes.push(userNode); + context.hasTopUserNode = true; + } + + // Create Agent node + const agentName = step.target || 'Agent'; + const displayName = context.agentNameMap[agentName] || agentName; + + const agentNode = createNode( + context, + 'agent', + { + label: displayName, + visualizerStepId: step.id, + }, + step.owningTaskId + ); + + // Add agent to root nodes + context.rootNodes.push(agentNode); + + // Set as current agent + context.currentAgentNode = agentNode; + + // Map task ID to this agent + if (step.owningTaskId) { + context.taskToNodeMap.set(step.owningTaskId, agentNode); + } +} + +/** + * Handle AGENT_LLM_CALL - adds LLM child to current agent + */ +function handleLLMCall(step: VisualizerStep, context: BuildContext): void { + const agentNode = findAgentForStep(step, context); + if (!agentNode) return; + + const llmNode = createNode( + context, + 'llm', + { + label: 'LLM', + visualizerStepId: step.id, + status: 'in-progress', + }, + step.owningTaskId + ); + + // Add as child + agentNode.children.push(llmNode); + + // Track by functionCallId for result matching + if (step.functionCallId) { + context.functionCallToNodeMap.set(step.functionCallId, llmNode); + } +} + +/** + * Handle AGENT_LLM_RESPONSE_TO_AGENT or AGENT_LLM_RESPONSE_TOOL_DECISION - marks LLM as completed + */ +function handleLLMResponse(step: VisualizerStep, context: BuildContext): void { + const agentNode = findAgentForStep(step, context); + if (!agentNode) { + return; + } + + // Find the most recent LLM node in this agent and mark it as completed + let foundInProgressLLM = false; + for (let i = agentNode.children.length - 1; i >= 0; i--) { + const child = agentNode.children[i]; + if (child.type === 'llm' && child.data.status === 'in-progress') { + child.data.status = 'completed'; + foundInProgressLLM = true; + break; + } + } + + // If no in-progress LLM was found, it means we received an LLM response without + // a corresponding AGENT_LLM_CALL. This can happen if the llm_invocation signal + // wasn't emitted. Create a synthetic LLM node to represent this call. + if (!foundInProgressLLM) { + const syntheticLlmNode = createNode( + context, + 'llm', + { + label: 'LLM', + visualizerStepId: step.id, // Link to the response step since we don't have a call step + status: 'completed', + }, + step.owningTaskId + ); + + // Insert the LLM node before any parallel blocks (which are created by TOOL_DECISION) + // Find the position before the first parallelBlock child + let insertIndex = agentNode.children.length; + for (let i = 0; i < agentNode.children.length; i++) { + if (agentNode.children[i].type === 'parallelBlock') { + insertIndex = i; + break; + } + } + agentNode.children.splice(insertIndex, 0, syntheticLlmNode); + } + + // Check for parallel tool calls in TOOL_DECISION + if (step.type === 'AGENT_LLM_RESPONSE_TOOL_DECISION') { + const toolDecision = step.data.toolDecision; + if (toolDecision?.isParallel && toolDecision.decisions) { + // Filter for peer delegations + const peerDecisions = toolDecision.decisions.filter(d => d.isPeerDelegation); + + // Filter for workflow calls (non-peer, toolName contains 'workflow_') + const workflowDecisions = toolDecision.decisions.filter( + d => !d.isPeerDelegation && d.toolName.includes('workflow_') + ); + + // Handle parallel peer delegations + if (peerDecisions.length > 1) { + const groupKey = `${step.owningTaskId}:parallel-peer:${step.id}`; + const functionCallIds = new Set(peerDecisions.map(d => d.functionCallId)); + + context.parallelPeerGroupMap.set(groupKey, functionCallIds); + + const parallelBlockNode = createNode( + context, + 'parallelBlock', + { + label: 'Parallel', + visualizerStepId: step.id, + }, + step.owningTaskId + ); + + agentNode.children.push(parallelBlockNode); + context.parallelBlockMap.set(groupKey, parallelBlockNode); + } + + // Handle parallel workflow calls + if (workflowDecisions.length > 1) { + const groupKey = `${step.owningTaskId}:parallel-workflow:${step.id}`; + const functionCallIds = new Set(workflowDecisions.map(d => d.functionCallId)); + + context.parallelPeerGroupMap.set(groupKey, functionCallIds); + + const parallelBlockNode = createNode( + context, + 'parallelBlock', + { + label: 'Parallel', + visualizerStepId: step.id, + }, + step.owningTaskId + ); + + agentNode.children.push(parallelBlockNode); + context.parallelBlockMap.set(groupKey, parallelBlockNode); + } + } + } +} + +/** + * Handle AGENT_TOOL_INVOCATION_START + */ +function handleToolInvocation(step: VisualizerStep, context: BuildContext): void { + const isPeer = step.data.toolInvocationStart?.isPeerInvocation || step.target?.startsWith('peer_'); + const target = step.target || ''; + const toolName = step.data.toolInvocationStart?.toolName || target; + const parallelGroupId = step.data.toolInvocationStart?.parallelGroupId; + + // Skip workflow tools (handled separately) + if (target.includes('workflow_') || toolName.includes('workflow_')) { + return; + } + + const agentNode = findAgentForStep(step, context); + if (!agentNode) return; + + if (isPeer) { + // Create nested agent node + const peerName = target.startsWith('peer_') ? target.substring(5) : target; + const displayName = context.agentNameMap[peerName] || peerName; + + const subAgentNode = createNode( + context, + 'agent', + { + label: displayName, + visualizerStepId: step.id, + }, + step.delegationInfo?.[0]?.subTaskId || step.owningTaskId + ); + + // Check if this peer invocation is part of a parallel group + // First check for backend-provided parallelGroupId, then fall back to legacy detection + const functionCallId = step.data.toolInvocationStart?.functionCallId || step.functionCallId; + let addedToParallelBlock = false; + + // Use parallelGroupId from backend if available + if (parallelGroupId) { + let parallelBlock = context.parallelBlockMap.get(parallelGroupId); + if (!parallelBlock) { + // Create a new parallel block for this group + parallelBlock = createNode( + context, + 'parallelBlock', + { + label: 'Parallel', + visualizerStepId: step.id, + }, + step.owningTaskId + ); + context.parallelBlockMap.set(parallelGroupId, parallelBlock); + agentNode.children.push(parallelBlock); + } + parallelBlock.children.push(subAgentNode); + addedToParallelBlock = true; + } else if (functionCallId) { + // Fall back to legacy parallel peer group detection + for (const [groupKey, functionCallIds] of context.parallelPeerGroupMap.entries()) { + if (functionCallIds.has(functionCallId)) { + // This peer invocation is part of a parallel group + const parallelBlock = context.parallelBlockMap.get(groupKey); + if (parallelBlock) { + // Add the sub-agent as a child of the parallelBlock + parallelBlock.children.push(subAgentNode); + addedToParallelBlock = true; + } + break; + } + } + } + + // If not part of a parallel group, add as regular child + if (!addedToParallelBlock) { + agentNode.children.push(subAgentNode); + } + + // Map sub-task to this new agent + const subTaskId = step.delegationInfo?.[0]?.subTaskId; + if (subTaskId) { + context.taskToNodeMap.set(subTaskId, subAgentNode); + } + + // Track by functionCallId + if (functionCallId) { + context.functionCallToNodeMap.set(functionCallId, subAgentNode); + } + } else { + // Regular tool + const toolNode = createNode( + context, + 'tool', + { + label: toolName, + visualizerStepId: step.id, + status: 'in-progress', + }, + step.owningTaskId + ); + + // Check if this tool is part of a parallel group + if (parallelGroupId) { + let parallelBlock = context.parallelBlockMap.get(parallelGroupId); + if (!parallelBlock) { + // Create a new parallel block for this group + parallelBlock = createNode( + context, + 'parallelBlock', + { + label: 'Parallel Tools', + visualizerStepId: step.id, + }, + step.owningTaskId + ); + context.parallelBlockMap.set(parallelGroupId, parallelBlock); + agentNode.children.push(parallelBlock); + } + parallelBlock.children.push(toolNode); + } else { + agentNode.children.push(toolNode); + } + + // Use the tool's actual functionCallId from the data (preferred) for matching with tool_result + // The step.functionCallId is the parent tracking ID for sub-task relationships + const functionCallId = step.data.toolInvocationStart?.functionCallId || step.functionCallId; + if (functionCallId) { + context.functionCallToNodeMap.set(functionCallId, toolNode); + } + } +} + +/** + * Handle AGENT_TOOL_EXECUTION_RESULT - update status + */ +function handleToolResult(step: VisualizerStep, context: BuildContext): void { + const functionCallId = step.data.toolResult?.functionCallId || step.functionCallId; + if (!functionCallId) return; + + const node = context.functionCallToNodeMap.get(functionCallId); + if (node) { + node.data.status = 'completed'; + } +} + +/** + * Handle AGENT_ARTIFACT_NOTIFICATION - associate artifact with the tool that created it + */ +function handleArtifactNotification(step: VisualizerStep, context: BuildContext): void { + const functionCallId = step.functionCallId; + if (!functionCallId) return; + + const node = context.functionCallToNodeMap.get(functionCallId); + if (node && step.data.artifactNotification) { + if (!node.data.createdArtifacts) { + node.data.createdArtifacts = []; + } + node.data.createdArtifacts.push({ + filename: step.data.artifactNotification.artifactName, + version: step.data.artifactNotification.version, + mimeType: step.data.artifactNotification.mimeType, + description: step.data.artifactNotification.description, + }); + } +} + +/** + * Handle AGENT_RESPONSE_TEXT - create bottom User node (only once at the end) + */ +function handleAgentResponse(step: VisualizerStep, context: BuildContext): void { + // Only for top-level tasks + if (step.nestingLevel && step.nestingLevel > 0) return; + + // Only create bottom user node once, and only for the last response + // We'll check if this is the last top-level AGENT_RESPONSE_TEXT + const remainingSteps = context.steps.slice(context.stepIndex + 1); + const hasMoreTopLevelResponses = remainingSteps.some( + s => s.type === 'AGENT_RESPONSE_TEXT' && s.nestingLevel === 0 + ); + + if (!hasMoreTopLevelResponses && !context.hasBottomUserNode) { + const userNode = createNode( + context, + 'user', + { + label: 'User', + visualizerStepId: step.id, + isBottomNode: true, + }, + step.owningTaskId + ); + + context.rootNodes.push(userNode); + context.hasBottomUserNode = true; + } +} + +/** + * Handle WORKFLOW_EXECUTION_START + */ +function handleWorkflowStart(step: VisualizerStep, context: BuildContext): void { + const workflowName = step.data.workflowExecutionStart?.workflowName || 'Workflow'; + const displayName = context.agentNameMap[workflowName] || workflowName; + const executionId = step.data.workflowExecutionStart?.executionId; + + console.log('[handleWorkflowStart] workflowName=', workflowName, 'executionId=', executionId, 'owningTaskId=', step.owningTaskId, 'parentTaskId=', step.parentTaskId); + + // Check if this is a sub-workflow invoked by a parent workflow's 'workflow' node type + // The parent relationship is recorded in subWorkflowParentMap by handleWorkflowNodeType + const parentFromWorkflowNode = step.owningTaskId + ? context.subWorkflowParentMap.get(step.owningTaskId) + : null; + + console.log('[handleWorkflowStart] parentFromWorkflowNode=', parentFromWorkflowNode?.data.label, parentFromWorkflowNode?.id); + + // Find the calling agent - prefer the recorded parent from workflow node, + // then try parentTaskId lookup, then fall back to current agent + let callingAgent: LayoutNode | null = parentFromWorkflowNode || null; + if (!callingAgent && step.parentTaskId) { + callingAgent = context.taskToNodeMap.get(step.parentTaskId) || null; + console.log('[handleWorkflowStart] callingAgent from parentTaskId lookup=', callingAgent?.data.label); + } + if (!callingAgent) { + callingAgent = context.currentAgentNode; + console.log('[handleWorkflowStart] callingAgent from currentAgentNode=', callingAgent?.data.label); + } + + console.log('[handleWorkflowStart] Final callingAgent=', callingAgent?.data.label, callingAgent?.id); + + // Create group container + const groupNode = createNode( + context, + 'group', + { + label: displayName, + visualizerStepId: step.id, + }, + executionId || step.owningTaskId + ); + + // Create Start node inside group + const startNode = createNode( + context, + 'agent', + { + label: 'Start', + variant: 'pill', + visualizerStepId: step.id, + }, + step.owningTaskId + ); + + groupNode.children.push(startNode); + + // Check if this workflow is part of a parallel group + const functionCallId = step.functionCallId; + let addedToParallelBlock = false; + + if (functionCallId) { + // Search through all parallel groups to find if this functionCallId belongs to one + for (const [groupKey, functionCallIds] of context.parallelPeerGroupMap.entries()) { + if (functionCallIds.has(functionCallId)) { + // This workflow is part of a parallel group + const parallelBlock = context.parallelBlockMap.get(groupKey); + if (parallelBlock) { + parallelBlock.children.push(groupNode); + addedToParallelBlock = true; + } + break; + } + } + } + + // If not part of a parallel group, add to calling agent or root + if (!addedToParallelBlock) { + if (callingAgent) { + callingAgent.children.push(groupNode); + } else { + context.rootNodes.push(groupNode); + } + } + + // Map execution ID to group for workflow nodes + if (executionId) { + context.taskToNodeMap.set(executionId, groupNode); + } + + // Also map by owningTaskId so findAgentForStep can find the group + // when workflow node steps use the event's task_id as their owningTaskId + if (step.owningTaskId && step.owningTaskId !== executionId) { + context.taskToNodeMap.set(step.owningTaskId, groupNode); + } +} + +/** + * Handle WORKFLOW_NODE_EXECUTION_START + */ +function handleWorkflowNodeStart(step: VisualizerStep, context: BuildContext): void { + const nodeType = step.data.workflowNodeExecutionStart?.nodeType; + const nodeId = step.data.workflowNodeExecutionStart?.nodeId || 'unknown'; + const agentName = step.data.workflowNodeExecutionStart?.agentName; + const parentNodeId = step.data.workflowNodeExecutionStart?.parentNodeId; + const parallelGroupId = step.data.workflowNodeExecutionStart?.parallelGroupId; + const taskId = step.owningTaskId; + + // Check if this node is a child of a Map/Loop (parallel execution with parentNodeId) + // For Map/Loop children, use parallelGroupId if available, otherwise fall back to parentNodeId + // NOTE: For implicit parallel agents (no parentNodeId), we don't look up in parallelContainerMap + // because they find their container via parallelBlockMap using parallelGroupId directly. + const isMapOrLoopChild = parentNodeId !== undefined && parentNodeId !== null; + const parallelContainerKey = isMapOrLoopChild + ? (parallelGroupId || `${taskId}:${parentNodeId}`) + : null; + const parallelContainer = parallelContainerKey ? context.parallelContainerMap.get(parallelContainerKey) : null; + + // Handle workflow nodes specially - they invoke sub-workflows and need group styling + if (nodeType === 'workflow') { + handleWorkflowNodeType(step, context); + return; + } + + // Determine node type and variant + let type: LayoutNode['type'] = 'agent'; + const variant: 'default' | 'pill' = 'default'; + let label: string; + + if (nodeType === 'switch') { + type = 'switch'; + label = 'Switch'; + } else if (nodeType === 'loop') { + type = 'loop'; + label = 'Loop'; + } else if (nodeType === 'map') { + type = 'map'; + label = 'Map'; + } else { + // Agent nodes use their actual name + label = agentName || nodeId; + } + + const workflowNodeData = step.data.workflowNodeExecutionStart; + const workflowNode = createNode( + context, + type, + { + label, + variant, + visualizerStepId: step.id, + // Conditional node fields + condition: workflowNodeData?.condition, + trueBranch: workflowNodeData?.trueBranch, + falseBranch: workflowNodeData?.falseBranch, + // Switch node fields + cases: workflowNodeData?.cases, + defaultBranch: workflowNodeData?.defaultBranch, + // Loop node fields + maxIterations: workflowNodeData?.maxIterations, + loopDelay: workflowNodeData?.loopDelay, + // Store the original nodeId for reference when clicked + nodeId, + }, + step.owningTaskId + ); + + // For agent nodes within workflows, create a sub-task context + if (nodeType === 'agent') { + const subTaskId = step.data.workflowNodeExecutionStart?.subTaskId; + if (subTaskId) { + context.taskToNodeMap.set(subTaskId, workflowNode); + } + } + + // Handle Map nodes - these create parallel branches + if (nodeType === 'map') { + // Find parent group + const groupNode = findAgentForStep(step, context); + if (!groupNode) return; + + // Store in parallel container map for child nodes to find + // Use parallelGroupId from backend if available, otherwise use legacy key format + const containerKey = parallelGroupId || `${taskId}:${nodeId}`; + context.parallelContainerMap.set(containerKey, workflowNode); + + // Add to parent group + groupNode.children.push(workflowNode); + } + // Handle Loop nodes - these contain sequential iterations + else if (nodeType === 'loop') { + // Find parent group + const groupNode = findAgentForStep(step, context); + if (!groupNode) return; + + // Store in parallel container map so child nodes can find their parent + // Loop children will be added sequentially to this node's children array + const containerKey = `${taskId}:${nodeId}`; + context.parallelContainerMap.set(containerKey, workflowNode); + + // Add to parent group + groupNode.children.push(workflowNode); + } + // Handle nodes that are children of Map/Loop + else if (parallelContainer) { + // Check if parent is a loop (sequential children) or map (parallel branches) + if (parallelContainer.type === 'loop') { + // Loop iterations are sequential - add as direct children + parallelContainer.children.push(workflowNode); + } else { + // Map have parallel branches - store in children with iterationIndex metadata + const iterationIndex = step.data.workflowNodeExecutionStart?.iterationIndex ?? 0; + workflowNode.data.iterationIndex = iterationIndex; + parallelContainer.children.push(workflowNode); + } + } + // Handle implicit parallel agent nodes (from backend parallel_group_id) + else if (nodeType === 'agent' && parallelGroupId) { + // Find parent group + const groupNode = findAgentForStep(step, context); + if (!groupNode) return; + + // Check if we already have a parallel block for this group + let implicitParallelBlock = context.parallelBlockMap.get(parallelGroupId); + if (!implicitParallelBlock) { + // Create a new parallel block container for this implicit fork + implicitParallelBlock = createNode( + context, + 'parallelBlock', + { + label: 'Parallel', + visualizerStepId: step.id, + }, + step.owningTaskId + ); + context.parallelBlockMap.set(parallelGroupId, implicitParallelBlock); + // NOTE: Do NOT store in parallelContainerMap - that's only for Map/Loop containers + // where children have parentNodeId relationship. Implicit parallel agents find + // their container via parallelBlockMap using parallelGroupId. + groupNode.children.push(implicitParallelBlock); + } + + // Add this agent node to the parallel block with its branch index + const iterationIndex = step.data.workflowNodeExecutionStart?.iterationIndex ?? 0; + workflowNode.data.iterationIndex = iterationIndex; + implicitParallelBlock.children.push(workflowNode); + } + // Regular workflow node (not in parallel context) + else { + // Find parent group + const groupNode = findAgentForStep(step, context); + if (!groupNode) return; + + groupNode.children.push(workflowNode); + } +} + +/** + * Handle workflow node type - records parent relationship for sub-workflow invocation + * The actual group creation happens in handleWorkflowStart when the sub-workflow starts + */ +function handleWorkflowNodeType(step: VisualizerStep, context: BuildContext): void { + const workflowNodeData = step.data.workflowNodeExecutionStart; + const subTaskId = workflowNodeData?.subTaskId; + + console.log('[handleWorkflowNodeType] nodeType=workflow, subTaskId=', subTaskId, 'owningTaskId=', step.owningTaskId); + + if (!subTaskId) { + console.log('[handleWorkflowNodeType] No subTaskId, returning'); + return; + } + + // Find the parent workflow group + const parentGroup = findAgentForStep(step, context); + console.log('[handleWorkflowNodeType] parentGroup=', parentGroup?.data.label, parentGroup?.id); + if (!parentGroup) { + console.log('[handleWorkflowNodeType] No parent group found, returning'); + return; + } + + // Record the parent relationship so handleWorkflowStart can use it + // This allows the sub-workflow's WORKFLOW_EXECUTION_START to find the correct parent + context.subWorkflowParentMap.set(subTaskId, parentGroup); + console.log('[handleWorkflowNodeType] Recorded mapping:', subTaskId, '->', parentGroup.data.label); +} + +/** + * Handle WORKFLOW_EXECUTION_RESULT - creates Finish node + */ +function handleWorkflowExecutionResult(step: VisualizerStep, context: BuildContext): void { + // Find the workflow group node by owningTaskId (which should be the execution ID) + const groupNode = findAgentForStep(step, context); + if (!groupNode) return; + + // Get the execution result to determine status + const resultData = step.data.workflowExecutionResult; + // Backend may send 'error' or 'failure' for failures, 'success' for success + const isError = resultData?.status === 'error' || resultData?.status === 'failure'; + const nodeStatus = isError ? 'error' : 'completed'; + + // Create Finish node with status + const finishNode = createNode( + context, + 'agent', + { + label: 'Finish', + variant: 'pill', + visualizerStepId: step.id, + status: nodeStatus, + }, + step.owningTaskId + ); + + groupNode.children.push(finishNode); +} + +/** + * Handle WORKFLOW_NODE_EXECUTION_RESULT - cleanup, update node status, and add Join node + */ +function handleWorkflowNodeResult(step: VisualizerStep, context: BuildContext): void { + const resultData = step.data.workflowNodeExecutionResult; + const nodeId = resultData?.nodeId; + const taskId = step.owningTaskId; + + if (!nodeId) return; + + const containerKey = `${taskId}:${nodeId}`; + const parallelContainer = context.parallelContainerMap.get(containerKey); + + // Find the workflow node that matches this result and update its data + const groupNode = findAgentForStep(step, context); + if (groupNode) { + const targetNode = findNodeById(groupNode, nodeId); + if (targetNode) { + // Update status + targetNode.data.status = resultData?.status === 'success' ? 'completed' : + resultData?.status === 'failure' ? 'error' : 'completed'; + + // Update switch node with selected branch + if (targetNode.type === 'switch') { + const selectedBranch = resultData?.metadata?.selected_branch; + const selectedCaseIndex = resultData?.metadata?.selected_case_index; + if (selectedBranch !== undefined) { + targetNode.data.selectedBranch = selectedBranch; + } + if (selectedCaseIndex !== undefined) { + targetNode.data.selectedCaseIndex = selectedCaseIndex; + } + } + } + } + + // If this result is for a parallel container (Map/Fork/Loop), clean up tracking + if (parallelContainer) { + context.parallelContainerMap.delete(containerKey); + } + + // For agent node results, mark any remaining in-progress LLM nodes as completed + // This handles the case where the final LLM response doesn't emit a separate event + const nodeType = resultData?.metadata?.node_type; + if (nodeType === 'agent' || !parallelContainer) { + // Find the agent node for this workflow node by looking for it in the task map + // The agent node was registered with its subTaskId + for (const [subTaskId, agentNode] of context.taskToNodeMap.entries()) { + // Check if this subTaskId matches the pattern for this nodeId + if (subTaskId.includes(nodeId) || agentNode.data.nodeId === nodeId) { + // Mark all in-progress LLM children as completed + for (const child of agentNode.children) { + if (child.type === 'llm' && child.data.status === 'in-progress') { + child.data.status = 'completed'; + } + } + break; + } + } + } +} + +/** + * Find a node by its nodeId within a tree + */ +function findNodeById(root: LayoutNode, nodeId: string): LayoutNode | null { + // Check if this node matches + if (root.data.nodeId === nodeId) { + return root; + } + + // Search children + for (const child of root.children) { + const found = findNodeById(child, nodeId); + if (found) return found; + } + + // Search parallel branches + if (root.parallelBranches) { + for (const branch of root.parallelBranches) { + for (const branchNode of branch) { + const found = findNodeById(branchNode, nodeId); + if (found) return found; + } + } + } + + return null; +} + +/** + * Find the appropriate agent node for a step + */ +function findAgentForStep(step: VisualizerStep, context: BuildContext): LayoutNode | null { + // Try owningTaskId first + if (step.owningTaskId) { + const node = context.taskToNodeMap.get(step.owningTaskId); + if (node) return node; + } + + // Fallback to current agent + return context.currentAgentNode; +} + +/** + * Create a new node + */ +function createNode( + context: BuildContext, + type: LayoutNode['type'], + data: LayoutNode['data'], + owningTaskId?: string +): LayoutNode { + const id = `${type}_${context.nodeCounter++}`; + + return { + id, + type, + data, + x: 0, + y: 0, + width: 0, + height: 0, + children: [], + owningTaskId, + }; +} + +/** + * Calculate layout (positions and dimensions) for all nodes + */ +function calculateLayout(rootNodes: LayoutNode[]): LayoutNode[] { + // First pass: measure all nodes to find max width + let maxWidth = 0; + for (const node of rootNodes) { + measureNode(node); + maxWidth = Math.max(maxWidth, node.width); + } + + // Calculate center X position based on max width + const centerX = maxWidth / 2 + 100; // Add margin + + // Second pass: position nodes centered + let currentY = 50; // Start with offset from top + + for (let i = 0; i < rootNodes.length; i++) { + const node = rootNodes[i]; + const nextNode = rootNodes[i + 1]; + + // Center each node horizontally + node.x = centerX - node.width / 2; + node.y = currentY; + positionNode(node); + + // Use smaller spacing for User nodes (connector line spacing) + // Use larger spacing between agents + let spacing = SPACING.AGENT_VERTICAL; + if (node.type === 'user' || (nextNode && nextNode.type === 'user')) { + spacing = SPACING.VERTICAL; + } + + currentY = node.y + node.height + spacing; + } + + return rootNodes; +} + +/** + * Measure node dimensions (recursive, bottom-up) + */ +function measureNode(node: LayoutNode): void { + // First, measure all children + for (const child of node.children) { + measureNode(child); + } + + // Handle parallel branches + if (node.parallelBranches) { + for (const branch of node.parallelBranches) { + for (const branchNode of branch) { + measureNode(branchNode); + } + } + } + + // Calculate this node's dimensions based on type + switch (node.type) { + case 'agent': + measureAgentNode(node); + break; + case 'tool': + node.width = NODE_WIDTHS.TOOL; + node.height = NODE_HEIGHTS.TOOL; + break; + case 'llm': + node.width = NODE_WIDTHS.LLM; + node.height = NODE_HEIGHTS.LLM; + break; + case 'user': + node.width = NODE_WIDTHS.USER; + node.height = NODE_HEIGHTS.USER; + break; + case 'switch': + node.width = NODE_WIDTHS.SWITCH; + node.height = NODE_HEIGHTS.SWITCH; + break; + case 'loop': + measureLoopNode(node); + break; + case 'map': + measureMapNode(node); + break; + case 'group': + measureGroupNode(node); + break; + case 'parallelBlock': + measureParallelBlockNode(node); + break; + } +} + +/** + * Measure agent node (container with children) + */ +function measureAgentNode(node: LayoutNode): void { + let contentWidth = NODE_WIDTHS.MIN_AGENT_CONTENT; + let contentHeight = 0; + + // If it's a pill variant (Start/Finish/Join), use smaller dimensions + if (node.data.variant === 'pill') { + node.width = 100; + node.height = 40; + return; + } + + // Measure sequential children + if (node.children.length > 0) { + for (const child of node.children) { + contentWidth = Math.max(contentWidth, child.width); + contentHeight += child.height + SPACING.VERTICAL; + } + // Remove last spacing + contentHeight -= SPACING.VERTICAL; + } + + // Measure parallel branches + if (node.parallelBranches && node.parallelBranches.length > 0) { + // Add spacing between children and parallel branches if both exist + if (node.children.length > 0) { + contentHeight += SPACING.VERTICAL; + } + + let branchWidth = 0; + let maxBranchHeight = 0; + + for (const branch of node.parallelBranches) { + let branchHeight = 0; + let branchMaxWidth = 0; + + for (const branchNode of branch) { + branchHeight += branchNode.height + SPACING.VERTICAL; + branchMaxWidth = Math.max(branchMaxWidth, branchNode.width); + } + + // Remove last spacing from branch height + if (branch.length > 0) { + branchHeight -= SPACING.VERTICAL; + } + + branchWidth += branchMaxWidth + SPACING.HORIZONTAL; + maxBranchHeight = Math.max(maxBranchHeight, branchHeight); + } + + // Remove last horizontal spacing + if (node.parallelBranches.length > 0) { + branchWidth -= SPACING.HORIZONTAL; + } + + contentWidth = Math.max(contentWidth, branchWidth); + contentHeight += maxBranchHeight; + } + + // Add header height and padding + node.width = contentWidth + (SPACING.PADDING * 2); + node.height = NODE_HEIGHTS.AGENT_HEADER + contentHeight + SPACING.PADDING; +} + +/** + * Measure loop node - can be a badge or a container with children + */ +function measureLoopNode(node: LayoutNode): void { + // If no children, use badge dimensions + if (node.children.length === 0) { + node.width = NODE_WIDTHS.LOOP; + node.height = NODE_HEIGHTS.LOOP; + return; + } + + // Has children - measure as a container + let contentWidth = 200; + let contentHeight = 0; + + // Account for iteration labels (about 16px per iteration for the label) + const iterationLabelHeight = 16; + + for (const child of node.children) { + contentWidth = Math.max(contentWidth, child.width); + contentHeight += iterationLabelHeight + child.height + SPACING.VERTICAL; + } + + if (node.children.length > 0) { + contentHeight -= SPACING.VERTICAL; + } + + // Loop uses p-4 pt-3 (16px padding, 12px top) + const loopPadding = 16; + const topLabelOffset = -4; // pt-3 is less than p-4, so negative offset + node.width = contentWidth + (loopPadding * 2); + node.height = contentHeight + loopPadding + topLabelOffset + loopPadding; +} + +/** + * Measure map node - can be a badge or a container with parallel branches + * Children are stored in node.children with iterationIndex in their data + */ +function measureMapNode(node: LayoutNode): void { + // Group children by iterationIndex + const branches = new Map(); + for (const child of node.children) { + const iterationIndex = child.data.iterationIndex ?? 0; + if (!branches.has(iterationIndex)) { + branches.set(iterationIndex, []); + } + branches.get(iterationIndex)!.push(child); + } + + // If no children, use badge dimensions + if (branches.size === 0) { + node.width = NODE_WIDTHS.MAP; + node.height = NODE_HEIGHTS.MAP; + return; + } + + // Has children - measure as a container with side-by-side branches + let totalWidth = 0; + let maxBranchHeight = 0; + + // Account for iteration labels (about 20px per branch for the label) + const iterationLabelHeight = 20; + + // Sort branches by iteration index for consistent ordering + const sortedBranches = Array.from(branches.entries()).sort((a, b) => a[0] - b[0]); + + for (const [, branchChildren] of sortedBranches) { + let branchWidth = 0; + let branchHeight = iterationLabelHeight; // Start with label height + + for (const child of branchChildren) { + branchWidth = Math.max(branchWidth, child.width); + branchHeight += child.height + SPACING.VERTICAL; + } + + // Remove last spacing from branch + if (branchChildren.length > 0) { + branchHeight -= SPACING.VERTICAL; + } + + totalWidth += branchWidth + SPACING.HORIZONTAL; + maxBranchHeight = Math.max(maxBranchHeight, branchHeight); + } + + // Remove last horizontal spacing + if (sortedBranches.length > 0) { + totalWidth -= SPACING.HORIZONTAL; + } + + // Map uses p-4 pt-3 (16px padding, 12px top) + const containerPadding = 16; + const topLabelOffset = -4; // pt-3 is less than p-4, so negative offset + node.width = totalWidth + (containerPadding * 2); + node.height = maxBranchHeight + containerPadding + topLabelOffset + containerPadding; +} + +/** + * Measure group node + */ +function measureGroupNode(node: LayoutNode): void { + let contentWidth = 200; + let contentHeight = 0; + + for (const child of node.children) { + contentWidth = Math.max(contentWidth, child.width); + contentHeight += child.height + SPACING.VERTICAL; + } + + if (node.children.length > 0) { + contentHeight -= SPACING.VERTICAL; + } + + // Group uses p-6 (24px) padding in WorkflowGroup + const groupPadding = 24; + node.width = contentWidth + (groupPadding * 2); + node.height = contentHeight + (groupPadding * 2); +} + +/** + * Measure parallel block node - children are displayed side-by-side with bounding box + * Children are grouped by iterationIndex (branch index) for proper chain visualization + */ +function measureParallelBlockNode(node: LayoutNode): void { + // Group children by iterationIndex to form branch chains + const branches = new Map(); + for (const child of node.children) { + const branchIdx = child.data.iterationIndex ?? 0; + if (!branches.has(branchIdx)) { + branches.set(branchIdx, []); + } + branches.get(branchIdx)!.push(child); + } + + // If only one branch or no iterationIndex grouping, fall back to side-by-side + if (branches.size <= 1 && node.children.every(c => c.data.iterationIndex === undefined)) { + let totalWidth = 0; + let maxHeight = 0; + + for (const child of node.children) { + totalWidth += child.width + SPACING.HORIZONTAL; + maxHeight = Math.max(maxHeight, child.height); + } + + if (node.children.length > 0) { + totalWidth -= SPACING.HORIZONTAL; + } + + const blockPadding = 16; + node.width = totalWidth + (blockPadding * 2); + node.height = maxHeight + (blockPadding * 2); + return; + } + + // Multiple branches - measure each branch (stacked vertically) and place side-by-side + let totalWidth = 0; + let maxBranchHeight = 0; + + const sortedBranches = Array.from(branches.entries()).sort((a, b) => a[0] - b[0]); + for (const [, branchChildren] of sortedBranches) { + let branchWidth = 0; + let branchHeight = 0; + + for (const child of branchChildren) { + branchWidth = Math.max(branchWidth, child.width); + branchHeight += child.height + SPACING.VERTICAL; + } + + if (branchChildren.length > 0) { + branchHeight -= SPACING.VERTICAL; + } + + totalWidth += branchWidth + SPACING.HORIZONTAL; + maxBranchHeight = Math.max(maxBranchHeight, branchHeight); + } + + if (sortedBranches.length > 0) { + totalWidth -= SPACING.HORIZONTAL; + } + + const blockPadding = 16; + node.width = totalWidth + (blockPadding * 2); + node.height = maxBranchHeight + (blockPadding * 2); +} + +/** + * Position children within node (recursive, top-down) + */ +function positionNode(node: LayoutNode): void { + if (node.type === 'agent' && node.data.variant !== 'pill') { + // Position children inside agent + let currentY = node.y + NODE_HEIGHTS.AGENT_HEADER + SPACING.PADDING; + const centerX = node.x + node.width / 2; + + for (const child of node.children) { + child.x = centerX - child.width / 2; // Center horizontally + child.y = currentY; + positionNode(child); // Recursive + currentY += child.height + SPACING.VERTICAL; + } + + // Position parallel branches side-by-side + if (node.parallelBranches) { + let branchX = node.x + SPACING.PADDING; + + for (const branch of node.parallelBranches) { + let branchMaxWidth = 0; + let branchY = currentY; + + for (const branchNode of branch) { + branchNode.x = branchX; + branchNode.y = branchY; + positionNode(branchNode); + branchY += branchNode.height + SPACING.VERTICAL; + branchMaxWidth = Math.max(branchMaxWidth, branchNode.width); + } + + branchX += branchMaxWidth + SPACING.HORIZONTAL; + } + } + } else if (node.type === 'group') { + // Position children inside group + let currentY = node.y + SPACING.PADDING + 30; // Offset for label + const centerX = node.x + node.width / 2; + + for (const child of node.children) { + child.x = centerX - child.width / 2; + child.y = currentY; + positionNode(child); + currentY += child.height + SPACING.VERTICAL; + } + } else if (node.type === 'parallelBlock') { + // Group children by iterationIndex to form branch chains + const branches = new Map(); + for (const child of node.children) { + const branchIdx = child.data.iterationIndex ?? 0; + if (!branches.has(branchIdx)) { + branches.set(branchIdx, []); + } + branches.get(branchIdx)!.push(child); + } + + const blockPadding = 16; + + // If only one branch or no iterationIndex grouping, position side-by-side + if (branches.size <= 1 && node.children.every(c => c.data.iterationIndex === undefined)) { + let currentX = node.x + blockPadding; + for (const child of node.children) { + child.x = currentX; + child.y = node.y + blockPadding; + positionNode(child); + currentX += child.width + SPACING.HORIZONTAL; + } + } else { + // Multiple branches - position each branch vertically, branches side-by-side + const sortedBranches = Array.from(branches.entries()).sort((a, b) => a[0] - b[0]); + let currentX = node.x + blockPadding; + + for (const [, branchChildren] of sortedBranches) { + let currentY = node.y + blockPadding; + let branchMaxWidth = 0; + + for (const child of branchChildren) { + child.x = currentX; + child.y = currentY; + positionNode(child); + currentY += child.height + SPACING.VERTICAL; + branchMaxWidth = Math.max(branchMaxWidth, child.width); + } + + currentX += branchMaxWidth + SPACING.HORIZONTAL; + } + } + } else if (node.type === 'loop' && node.children.length > 0) { + // Position children inside loop container + const loopPadding = 16; + const topLabelOffset = -4; // pt-3 is less than p-4 + const iterationLabelHeight = 16; + let currentY = node.y + loopPadding + topLabelOffset; + const centerX = node.x + node.width / 2; + + for (const child of node.children) { + // Account for iteration label + currentY += iterationLabelHeight; + child.x = centerX - child.width / 2; + child.y = currentY; + positionNode(child); + currentY += child.height + SPACING.VERTICAL; + } + } else if (node.type === 'map' && node.children.length > 0) { + // Group children by iterationIndex for positioning + const branches = new Map(); + for (const child of node.children) { + const iterationIndex = child.data.iterationIndex ?? 0; + if (!branches.has(iterationIndex)) { + branches.set(iterationIndex, []); + } + branches.get(iterationIndex)!.push(child); + } + + // Position parallel branches side-by-side inside map container + const containerPadding = 16; + const topLabelOffset = -4; // pt-3 is less than p-4 + const iterationLabelHeight = 20; + let currentX = node.x + containerPadding; + const startY = node.y + containerPadding + topLabelOffset; + + // Sort branches by iteration index for consistent ordering + const sortedBranches = Array.from(branches.entries()).sort((a, b) => a[0] - b[0]); + + for (const [, branchChildren] of sortedBranches) { + let branchMaxWidth = 0; + let currentY = startY + iterationLabelHeight; // Start below iteration label + + for (const child of branchChildren) { + child.x = currentX; + child.y = currentY; + positionNode(child); + currentY += child.height + SPACING.VERTICAL; + branchMaxWidth = Math.max(branchMaxWidth, child.width); + } + + currentX += branchMaxWidth + SPACING.HORIZONTAL; + } + } +} + +/** + * Calculate edges between nodes + */ +function calculateEdges(nodes: LayoutNode[], _steps: VisualizerStep[]): Edge[] { + const edges: Edge[] = []; + const flatNodes = flattenNodes(nodes); + + // Create edges between sequential top-level nodes + for (let i = 0; i < flatNodes.length - 1; i++) { + const source = flatNodes[i]; + const target = flatNodes[i + 1]; + + // Only connect nodes at the same level (not nested) + if (shouldConnectNodes(source, target)) { + edges.push({ + id: `edge_${source.id}_${target.id}`, + source: source.id, + target: target.id, + sourceX: source.x + source.width / 2, + sourceY: source.y + source.height, + targetX: target.x + target.width / 2, + targetY: target.y, + }); + } + } + + return edges; +} + +/** + * Flatten node tree into array + */ +function flattenNodes(nodes: LayoutNode[]): LayoutNode[] { + const result: LayoutNode[] = []; + + function traverse(node: LayoutNode) { + result.push(node); + for (const child of node.children) { + traverse(child); + } + if (node.parallelBranches) { + for (const branch of node.parallelBranches) { + for (const branchNode of branch) { + traverse(branchNode); + } + } + } + } + + for (const node of nodes) { + traverse(node); + } + + return result; +} + +/** + * Determine if two nodes should be connected + */ +function shouldConnectNodes(source: LayoutNode, target: LayoutNode): boolean { + // Connect User → Agent + if (source.type === 'user' && source.data.isTopNode && target.type === 'agent') { + return true; + } + + // Connect Agent → User (bottom) + if (source.type === 'agent' && target.type === 'user' && target.data.isBottomNode) { + return true; + } + + // Connect Agent → Agent (for delegation returns) + if (source.type === 'agent' && target.type === 'agent') { + return true; + } + + return false; +} + +/** + * Calculate total canvas size + */ +function calculateCanvasSize(nodes: LayoutNode[]): { totalWidth: number; totalHeight: number } { + let maxX = 0; + let maxY = 0; + + const flatNodes = flattenNodes(nodes); + + for (const node of flatNodes) { + maxX = Math.max(maxX, node.x + node.width); + maxY = Math.max(maxY, node.y + node.height); + } + + return { + totalWidth: maxX + 100, // Add margin + totalHeight: maxY + 100, + }; +} diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/utils/nodeDetailsHelper.ts b/client/webui/frontend/src/lib/components/activities/FlowChart/utils/nodeDetailsHelper.ts new file mode 100644 index 000000000..6f35c8c33 --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/utils/nodeDetailsHelper.ts @@ -0,0 +1,406 @@ +import type { VisualizerStep } from "@/lib/types"; +import type { LayoutNode } from "./types"; + +/** + * Represents an artifact created by a tool + */ +export interface CreatedArtifact { + filename: string; + version?: number; + mimeType?: string; + description?: string; +} + +/** + * Represents the request and result information for a node + */ +export interface NodeDetails { + nodeType: LayoutNode['type']; + label: string; + description?: string; // NP-4: Node description to display under the name + requestStep?: VisualizerStep; + resultStep?: VisualizerStep; + outputArtifactStep?: VisualizerStep; // For workflow nodes - the WORKFLOW_NODE_EXECUTION_RESULT with output artifact + relatedSteps?: VisualizerStep[]; // For additional context + createdArtifacts?: CreatedArtifact[]; // For tool nodes - artifacts created by this tool +} + +/** + * Find all steps related to a given node and organize them into request/result pairs + */ +export function findNodeDetails( + node: LayoutNode, + allSteps: VisualizerStep[] +): NodeDetails { + const visualizerStepId = node.data.visualizerStepId; + + if (!visualizerStepId) { + return { + nodeType: node.type, + label: node.data.label, + description: node.data.description, + }; + } + + // Find the primary step for this node + const primaryStep = allSteps.find(s => s.id === visualizerStepId); + + if (!primaryStep) { + return { + nodeType: node.type, + label: node.data.label, + description: node.data.description, + }; + } + + switch (node.type) { + case 'user': + return findUserNodeDetails(node, primaryStep, allSteps); + case 'agent': + return findAgentNodeDetails(node, primaryStep, allSteps); + case 'llm': + return findLLMNodeDetails(node, primaryStep, allSteps); + case 'tool': + return findToolNodeDetails(node, primaryStep, allSteps); + case 'switch': + return findSwitchNodeDetails(node, primaryStep, allSteps); + case 'loop': + return findLoopNodeDetails(node, primaryStep, allSteps); + case 'group': + return findWorkflowGroupDetails(node, primaryStep, allSteps); + default: + return { + nodeType: node.type, + label: node.data.label, + description: node.data.description, + requestStep: primaryStep, + }; + } +} + +/** + * Find details for User nodes + */ +function findUserNodeDetails( + node: LayoutNode, + primaryStep: VisualizerStep, + allSteps: VisualizerStep[] +): NodeDetails { + // Top user node: show initial request + if (node.data.isTopNode) { + return { + nodeType: 'user', + label: 'User Input', + requestStep: primaryStep, + }; + } + + // Bottom user node: show final response + if (node.data.isBottomNode) { + // Find the last AGENT_RESPONSE_TEXT at nesting level 0 + const finalResponse = [...allSteps] + .reverse() + .find(s => s.type === 'AGENT_RESPONSE_TEXT' && s.nestingLevel === 0); + + return { + nodeType: 'user', + label: 'Final Output', + resultStep: finalResponse, + }; + } + + return { + nodeType: 'user', + label: node.data.label, + requestStep: primaryStep, + }; +} + +/** + * Find details for Agent nodes + */ +function findAgentNodeDetails( + node: LayoutNode, + primaryStep: VisualizerStep, + allSteps: VisualizerStep[] +): NodeDetails { + const isWorkflowAgent = primaryStep.type === 'WORKFLOW_NODE_EXECUTION_START'; + const workflowNodeData = isWorkflowAgent ? primaryStep.data.workflowNodeExecutionStart : undefined; + const subTaskId = workflowNodeData?.subTaskId; + + // For workflow agents, we need to search in the subTaskId's events + // For regular agents, use the node's owningTaskId + const agentTaskId = subTaskId || node.owningTaskId; + + // Find the request step + let requestStep: VisualizerStep | undefined; + + // First try to find a USER_REQUEST step (exists for root-level agents) + const userRequest = allSteps.find( + s => s.owningTaskId === agentTaskId && s.type === 'USER_REQUEST' + ); + + if (userRequest) { + requestStep = userRequest; + } else if (isWorkflowAgent) { + // For workflow agents, look for the WORKFLOW_AGENT_REQUEST step which contains the actual input + const workflowAgentRequest = allSteps.find( + s => s.owningTaskId === agentTaskId && s.type === 'WORKFLOW_AGENT_REQUEST' + ); + // Fall back to WORKFLOW_NODE_EXECUTION_START if no WORKFLOW_AGENT_REQUEST found + requestStep = workflowAgentRequest || primaryStep; + } else { + // Check if this is a sub-agent created via tool invocation + const toolInvocation = allSteps.find( + s => s.owningTaskId === agentTaskId && s.type === 'AGENT_TOOL_INVOCATION_START' + ); + + if (toolInvocation && toolInvocation.parentTaskId) { + // Try to find the USER_REQUEST from the parent task + const parentUserRequest = allSteps.find( + s => s.owningTaskId === toolInvocation.parentTaskId && s.type === 'USER_REQUEST' + ); + requestStep = parentUserRequest || toolInvocation; + } else { + // Fallback to primaryStep + requestStep = primaryStep; + } + } + + // Find the response (AGENT_RESPONSE_TEXT for this agent's task) + const responseStep = allSteps.find( + s => s.owningTaskId === agentTaskId && s.type === 'AGENT_RESPONSE_TEXT' + ); + + // For workflow agents, find the WORKFLOW_NODE_EXECUTION_RESULT which contains output artifact + let outputArtifactStep: VisualizerStep | undefined; + if (isWorkflowAgent && workflowNodeData?.nodeId) { + // The result step is at the workflow level, not the agent's task level + // Find by matching BOTH nodeId AND owningTaskId to handle parallel workflow executions + const workflowExecutionId = primaryStep.owningTaskId; + outputArtifactStep = allSteps.find( + s => s.type === 'WORKFLOW_NODE_EXECUTION_RESULT' && + s.owningTaskId === workflowExecutionId && + s.data.workflowNodeExecutionResult?.nodeId === workflowNodeData.nodeId + ); + } + + // Find all steps for this agent's task for additional context + const relatedSteps = allSteps.filter(s => s.owningTaskId === agentTaskId); + + return { + nodeType: 'agent' as const, + label: node.data.label, + description: node.data.description, + requestStep, + resultStep: responseStep, + outputArtifactStep, + relatedSteps, + }; +} + +/** + * Find details for LLM nodes + */ +function findLLMNodeDetails( + node: LayoutNode, + primaryStep: VisualizerStep, + allSteps: VisualizerStep[] +): NodeDetails { + // Primary step could be AGENT_LLM_CALL or AGENT_LLM_RESPONSE_TOOL_DECISION (for synthetic LLM nodes) + let requestStep: VisualizerStep | undefined; + let resultStep: VisualizerStep | undefined; + + if (primaryStep.type === 'AGENT_LLM_CALL') { + // Normal case: we have the LLM call step + requestStep = primaryStep; + + const owningTaskId = requestStep.owningTaskId; + const requestIndex = allSteps.indexOf(requestStep); + + // Look for the next LLM response in the same task (either type) + resultStep = allSteps + .slice(requestIndex + 1) + .find(s => + s.owningTaskId === owningTaskId && + (s.type === 'AGENT_LLM_RESPONSE_TO_AGENT' || s.type === 'AGENT_LLM_RESPONSE_TOOL_DECISION') + ); + } else if (primaryStep.type === 'AGENT_LLM_RESPONSE_TOOL_DECISION' || primaryStep.type === 'AGENT_LLM_RESPONSE_TO_AGENT') { + // Synthetic LLM node case: we only have the response step + // Try to find the preceding AGENT_LLM_CALL for this task + const owningTaskId = primaryStep.owningTaskId; + const responseIndex = allSteps.indexOf(primaryStep); + + // Look backwards for the most recent AGENT_LLM_CALL in the same task + for (let i = responseIndex - 1; i >= 0; i--) { + const s = allSteps[i]; + if (s.owningTaskId === owningTaskId && s.type === 'AGENT_LLM_CALL') { + requestStep = s; + break; + } + } + + // The result step is the primary step itself + resultStep = primaryStep; + } + + return { + nodeType: 'llm', + label: node.data.label, + description: node.data.description, + requestStep, + resultStep, + }; +} + +/** + * Find details for Tool nodes + */ +function findToolNodeDetails( + node: LayoutNode, + primaryStep: VisualizerStep, + allSteps: VisualizerStep[] +): NodeDetails { + // Primary step should be AGENT_TOOL_INVOCATION_START + const requestStep = primaryStep.type === 'AGENT_TOOL_INVOCATION_START' ? primaryStep : undefined; + + // Find the result by matching functionCallId + // Check both the step's functionCallId and the data's functionCallId + let resultStep: VisualizerStep | undefined; + + const functionCallId = requestStep?.functionCallId || requestStep?.data.toolInvocationStart?.functionCallId; + + if (functionCallId) { + resultStep = allSteps.find( + s => s.type === 'AGENT_TOOL_EXECUTION_RESULT' && + s.data.toolResult?.functionCallId === functionCallId + ); + } + + // Get created artifacts from node data (populated by layoutEngine) + const createdArtifacts = node.data.createdArtifacts; + + return { + nodeType: 'tool', + label: node.data.label, + description: node.data.description, + requestStep, + resultStep, + createdArtifacts, + }; +} + +/** + * Find details for Switch nodes + */ +function findSwitchNodeDetails( + node: LayoutNode, + primaryStep: VisualizerStep, + allSteps: VisualizerStep[] +): NodeDetails { + // Primary step should be WORKFLOW_NODE_EXECUTION_START with nodeType: switch + const requestStep = primaryStep.type === 'WORKFLOW_NODE_EXECUTION_START' ? primaryStep : undefined; + + // Find the result by matching nodeId + let resultStep: VisualizerStep | undefined; + + if (requestStep?.data.workflowNodeExecutionStart) { + const nodeId = requestStep.data.workflowNodeExecutionStart.nodeId; + const owningTaskId = requestStep.owningTaskId; + + resultStep = allSteps.find( + s => s.type === 'WORKFLOW_NODE_EXECUTION_RESULT' && + s.owningTaskId === owningTaskId && + s.data.workflowNodeExecutionResult?.nodeId === nodeId + ); + } + + return { + nodeType: 'switch', + label: node.data.label, + description: node.data.description, + requestStep, + resultStep, + }; +} + +/** + * Find details for Loop nodes + */ +function findLoopNodeDetails( + node: LayoutNode, + primaryStep: VisualizerStep, + allSteps: VisualizerStep[] +): NodeDetails { + // Primary step should be WORKFLOW_NODE_EXECUTION_START with nodeType: loop + const requestStep = primaryStep.type === 'WORKFLOW_NODE_EXECUTION_START' ? primaryStep : undefined; + + // Find the result by matching nodeId + let resultStep: VisualizerStep | undefined; + + if (requestStep?.data.workflowNodeExecutionStart) { + const nodeId = requestStep.data.workflowNodeExecutionStart.nodeId; + const owningTaskId = requestStep.owningTaskId; + + resultStep = allSteps.find( + s => s.type === 'WORKFLOW_NODE_EXECUTION_RESULT' && + s.owningTaskId === owningTaskId && + s.data.workflowNodeExecutionResult?.nodeId === nodeId + ); + } + + // Find related steps for loop iterations + const relatedSteps = requestStep ? allSteps.filter( + s => s.owningTaskId === requestStep.owningTaskId && + s.type === 'WORKFLOW_NODE_EXECUTION_START' && + s.data.workflowNodeExecutionStart?.parentNodeId === requestStep.data.workflowNodeExecutionStart?.nodeId + ) : undefined; + + return { + nodeType: 'loop', + label: node.data.label, + description: node.data.description, + requestStep, + resultStep, + relatedSteps, + }; +} + +/** + * Find details for Workflow Group nodes + */ +function findWorkflowGroupDetails( + node: LayoutNode, + primaryStep: VisualizerStep, + allSteps: VisualizerStep[] +): NodeDetails { + // Primary step should be WORKFLOW_EXECUTION_START + const requestStep = primaryStep.type === 'WORKFLOW_EXECUTION_START' ? primaryStep : undefined; + + // Find the result by matching executionId + let resultStep: VisualizerStep | undefined; + + if (requestStep?.data.workflowExecutionStart) { + const executionId = requestStep.data.workflowExecutionStart.executionId; + + resultStep = allSteps.find( + s => s.type === 'WORKFLOW_EXECUTION_RESULT' && + s.owningTaskId === executionId + ); + } + + // Find all workflow node steps for context + const relatedSteps = allSteps.filter( + s => s.owningTaskId === requestStep?.data.workflowExecutionStart?.executionId && + (s.type.startsWith('WORKFLOW_NODE_') || s.type === 'WORKFLOW_MAP_PROGRESS') + ); + + return { + nodeType: 'group', + label: node.data.label, + description: node.data.description, + requestStep, + resultStep, + relatedSteps, + }; +} diff --git a/client/webui/frontend/src/lib/components/activities/FlowChart/utils/types.ts b/client/webui/frontend/src/lib/components/activities/FlowChart/utils/types.ts new file mode 100644 index 000000000..aaa87791f --- /dev/null +++ b/client/webui/frontend/src/lib/components/activities/FlowChart/utils/types.ts @@ -0,0 +1,132 @@ +import type { VisualizerStep } from "@/lib/types"; + +/** + * Represents a node in the layout tree structure. + * Nodes can contain children (tools/LLMs/sub-agents) and have calculated positions/dimensions. + */ +export interface LayoutNode { + id: string; + type: 'agent' | 'tool' | 'llm' | 'user' | 'switch' | 'loop' | 'map' | 'group' | 'workflow' | 'parallelBlock'; + data: { + label: string; + visualizerStepId?: string; + description?: string; + status?: string; + variant?: 'default' | 'pill'; + // Switch node fields + condition?: string; + cases?: { condition: string; node: string }[]; + defaultBranch?: string; + selectedBranch?: string; + selectedCaseIndex?: number; + // Join node fields + waitFor?: string[]; + joinStrategy?: string; + joinN?: number; + // Loop node fields + maxIterations?: number; + loopDelay?: string; + currentIteration?: number; + // Map/Fork node fields + iterationIndex?: number; + // Tool node fields - artifacts created by this tool + createdArtifacts?: Array<{ + filename: string; + version?: number; + mimeType?: string; + description?: string; + }>; + // Common fields + isTopNode?: boolean; + isBottomNode?: boolean; + isSkipped?: boolean; + [key: string]: any; + }; + + // Layout properties + x: number; // Absolute X position + y: number; // Absolute Y position + width: number; // Calculated width + height: number; // Calculated height + + // Hierarchy + children: LayoutNode[]; // Sequential children (tools, LLMs, sub-agents) + parallelBranches?: LayoutNode[][]; // For Map/Fork - each array is a parallel branch + + // Context + owningTaskId?: string; + parentTaskId?: string; + functionCallId?: string; +} + +/** + * Represents an edge between two nodes in the visualization. + */ +export interface Edge { + id: string; + source: string; // Node ID + target: string; // Node ID + sourceX: number; + sourceY: number; + targetX: number; + targetY: number; + visualizerStepId?: string; + label?: string; + isError?: boolean; + isSelected?: boolean; +} + +/** + * Context for building the layout tree from VisualizerSteps + */ +export interface BuildContext { + steps: VisualizerStep[]; + stepIndex: number; + nodeCounter: number; + + // Map task IDs to their container nodes + taskToNodeMap: Map; + + // Map function call IDs to nodes (for tool results) + functionCallToNodeMap: Map; + + // Current agent node being built + currentAgentNode: LayoutNode | null; + + // Root nodes (top-level user/agent pairs) + rootNodes: LayoutNode[]; + + // Agent name display map + agentNameMap: Record; + + // Map workflow nodeId to Map/Fork node for parallel branch tracking + parallelContainerMap: Map; + + // Track current branch within a parallel container + currentBranchMap: Map; + + // Track if we've created top/bottom user nodes (only one each for entire flow) + hasTopUserNode: boolean; + hasBottomUserNode: boolean; + + // Track parallel peer delegation groups: maps a unique group key to the set of functionCallIds + // Key format: `${owningTaskId}:parallel:${stepId}` where stepId is from the TOOL_DECISION step + parallelPeerGroupMap: Map>; + + // Track the parallelBlock node for each parallel peer group (for adding peer agents to it) + parallelBlockMap: Map; + + // Track sub-workflow -> parent group relationships (for workflow node types) + // Maps subTaskId -> parent workflow group node + subWorkflowParentMap: Map; +} + +/** + * Layout calculation result + */ +export interface LayoutResult { + nodes: LayoutNode[]; + edges: Edge[]; + totalWidth: number; + totalHeight: number; +} diff --git a/client/webui/frontend/src/lib/components/activities/FlowChartPanel.tsx b/client/webui/frontend/src/lib/components/activities/FlowChartPanel.tsx deleted file mode 100644 index c4ae98d11..000000000 --- a/client/webui/frontend/src/lib/components/activities/FlowChartPanel.tsx +++ /dev/null @@ -1,401 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; - -import { Background, Controls, MarkerType, Panel, ReactFlow, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow } from "@xyflow/react"; -import type { Edge, Node } from "@xyflow/react"; -import "@xyflow/react/dist/style.css"; - -import { PopoverManual } from "@/lib/components/ui"; -import { useChatContext, useTaskContext } from "@/lib/hooks"; -import type { VisualizerStep } from "@/lib/types"; -import { getThemeButtonHtmlStyles } from "@/lib/utils"; - -import { EdgeAnimationService } from "./FlowChart/edgeAnimationService"; -import { GROUP_PADDING_X, GROUP_PADDING_Y, NODE_HEIGHT, NODE_WIDTH } from "./FlowChart/taskToFlowData.helpers"; -import { transformProcessedStepsToTimelineFlow } from "./FlowChart/taskToFlowData"; -import GenericFlowEdge, { type AnimatedEdgeData } from "./FlowChart/customEdges/GenericFlowEdge"; -import GenericAgentNode from "./FlowChart/customNodes/GenericAgentNode"; -import GenericToolNode from "./FlowChart/customNodes/GenericToolNode"; -import LLMNode from "./FlowChart/customNodes/LLMNode"; -import OrchestratorAgentNode from "./FlowChart/customNodes/OrchestratorAgentNode"; -import UserNode from "./FlowChart/customNodes/UserNode"; -import ArtifactNode from "./FlowChart/customNodes/GenericArtifactNode"; -import { VisualizerStepCard } from "./VisualizerStepCard"; - -const nodeTypes = { - genericAgentNode: GenericAgentNode, - userNode: UserNode, - llmNode: LLMNode, - orchestratorNode: OrchestratorAgentNode, - genericToolNode: GenericToolNode, - artifactNode: ArtifactNode, -}; - -const edgeTypes = { - defaultFlowEdge: GenericFlowEdge, -}; - -interface FlowChartPanelProps { - processedSteps: VisualizerStep[]; - isRightPanelVisible?: boolean; - isSidePanelTransitioning?: boolean; -} - -// Stable offset object to prevent unnecessary re-renders -const POPOVER_OFFSET = { x: 16, y: 0 }; - -// Internal component to house the React Flow logic -const FlowRenderer: React.FC = ({ processedSteps, isRightPanelVisible = false, isSidePanelTransitioning = false }) => { - const [nodes, setNodes, onNodesChange] = useNodesState([]); - const [edges, setEdges, onEdgesChange] = useEdgesState([]); - const { fitView } = useReactFlow(); - const { highlightedStepId, setHighlightedStepId } = useTaskContext(); - const { taskIdInSidePanel, agentNameDisplayNameMap } = useChatContext(); - - const prevProcessedStepsRef = useRef([]); - const [hasUserInteracted, setHasUserInteracted] = useState(false); - - // Popover state for edge clicks - const [selectedStep, setSelectedStep] = useState(null); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const popoverAnchorRef = useRef(null); - - // Track selected edge for highlighting - const [selectedEdgeId, setSelectedEdgeId] = useState(null); - - const edgeAnimationServiceRef = useRef(new EdgeAnimationService()); - - const memoizedFlowData = useMemo(() => { - if (!processedSteps || processedSteps.length === 0) { - return { nodes: [], edges: [] }; - } - return transformProcessedStepsToTimelineFlow(processedSteps, agentNameDisplayNameMap); - }, [processedSteps, agentNameDisplayNameMap]); - - // Consolidated edge computation - const computedEdges = useMemo(() => { - if (!memoizedFlowData.edges.length) return []; - - return memoizedFlowData.edges.map(edge => { - const edgeData = edge.data as unknown as AnimatedEdgeData; - - // Determine animation state - let animationState = { isAnimated: false, animationType: "none" }; - if (edgeData?.visualizerStepId) { - const stepIndex = processedSteps.length - 1; - animationState = edgeAnimationServiceRef.current.getEdgeAnimationState(edgeData.visualizerStepId, stepIndex, processedSteps); - } - - // Determine if this edge should be selected - let isSelected = edge.id === selectedEdgeId; - - // If highlightedStepId is set, also select the corresponding edge - if (highlightedStepId && edgeData?.visualizerStepId === highlightedStepId) { - isSelected = true; - } - - return { - ...edge, - animated: animationState.isAnimated, - data: { - ...edgeData, - isAnimated: animationState.isAnimated, - animationType: animationState.animationType, - isSelected, - } as unknown as Record, - }; - }); - }, [memoizedFlowData.edges, processedSteps, selectedEdgeId, highlightedStepId]); - - const updateGroupNodeSizes = useCallback(() => { - setNodes(currentNodes => { - return currentNodes.map(node => { - if (node.type !== "group") return node; - - // Find all child nodes of this group - const childNodes = currentNodes.filter(n => n.parentId === node.id); - if (childNodes.length === 0) return node; - - // Calculate required width and height based on child positions - let maxX = 0; - let maxY = 0; - - childNodes.forEach(child => { - const childRight = child.position.x + NODE_WIDTH; - const childBottom = child.position.y + NODE_HEIGHT; - maxX = Math.max(maxX, childRight); - maxY = Math.max(maxY, childBottom); - }); - - // Add padding - const requiredWidth = maxX + GROUP_PADDING_X; - const requiredHeight = maxY + GROUP_PADDING_Y; - - // Ensure minimum width for indented groups - // Extract indentation level from group ID if possible - let indentationLevel = 0; - const groupIdParts = node.id.split("_"); - if (groupIdParts.length > 2) { - // Try to extract the subflow number which correlates with indentation level - const subflowPart = groupIdParts.find(part => part.startsWith("subflow")); - if (subflowPart) { - const subflowNum = parseInt(subflowPart.replace("subflow", "")); - if (!isNaN(subflowNum)) { - indentationLevel = subflowNum; - } - } - } - - // Add extra space for indented tool nodes - const minRequiredWidth = NODE_WIDTH + 2 * GROUP_PADDING_X + indentationLevel * 50; - const finalRequiredWidth = Math.max(requiredWidth, minRequiredWidth); - - // Update group node style if needed - const currentWidth = parseInt(node.style?.width?.toString().replace("px", "") || "0"); - const currentHeight = parseInt(node.style?.height?.toString().replace("px", "") || "0"); - - if (currentWidth !== finalRequiredWidth || currentHeight !== requiredHeight) { - return { - ...node, - style: { - ...node.style, - width: `${finalRequiredWidth}px`, - height: `${requiredHeight}px`, - }, - }; - } - - return node; - }); - }); - }, [setNodes]); - - useEffect(() => { - setNodes(memoizedFlowData.nodes); - setEdges(computedEdges); - updateGroupNodeSizes(); - }, [memoizedFlowData.nodes, computedEdges, setNodes, setEdges, updateGroupNodeSizes]); - - const findEdgeBySourceAndHandle = useCallback( - (sourceNodeId: string, sourceHandleId?: string): Edge | null => { - return edges.find(edge => edge.source === sourceNodeId && (sourceHandleId ? edge.sourceHandle === sourceHandleId : true)) || null; - }, - [edges] - ); - - const handleEdgeClick = useCallback( - (_event: React.MouseEvent, edge: Edge) => { - setHasUserInteracted(true); - - const stepId = edge.data?.visualizerStepId as string; - if (stepId) { - const step = processedSteps.find(s => s.id === stepId); - if (step) { - setSelectedEdgeId(edge.id); - - if (isRightPanelVisible) { - setHighlightedStepId(stepId); - } else { - setHighlightedStepId(stepId); - setSelectedStep(step); - setIsPopoverOpen(true); - } - } - } - }, - [processedSteps, isRightPanelVisible, setHighlightedStepId] - ); - - const getNodeSourceHandles = useCallback((node: Node): string[] => { - switch (node.type) { - case "userNode": { - const userData = node.data as { isTopNode?: boolean; isBottomNode?: boolean }; - if (userData?.isTopNode) return ["user-bottom-output"]; - if (userData?.isBottomNode) return ["user-top-input"]; - return ["user-right-output"]; - } - case "orchestratorNode": - return ["orch-right-output-tools", "orch-bottom-output"]; - - case "genericAgentNode": - return ["peer-right-output-tools", "peer-bottom-output"]; - - case "llmNode": - return ["llm-bottom-output"]; - - case "genericToolNode": - return [`${node.id}-tool-bottom-output`]; - - default: - return []; - } - }, []); - - const handlePopoverClose = useCallback(() => { - setIsPopoverOpen(false); - setSelectedStep(null); - }, []); - - const handleNodeClick = useCallback( - (_event: React.MouseEvent, node: Node) => { - setHasUserInteracted(true); - - // If clicking on a group container, treat it like clicking on empty space - if (node.type === "group") { - setHighlightedStepId(null); - setSelectedEdgeId(null); - handlePopoverClose(); - return; - } - - const sourceHandles = getNodeSourceHandles(node); - - let targetEdge: Edge | null = null; - for (const handleId of sourceHandles) { - targetEdge = findEdgeBySourceAndHandle(node.id, handleId); - - if (targetEdge) break; - } - - // Special case for bottom UserNode - check for incoming edges instead - if (!targetEdge && node.type === "userNode") { - const userData = node.data as { isBottomNode?: boolean }; - if (userData?.isBottomNode) { - targetEdge = edges.find(edge => edge.target === node.id) || null; - } - } - - if (!targetEdge && node.type === "artifactNode") { - // For artifact nodes, find the tool that created it - targetEdge = edges.find(edge => edge.target === node.id) || null; - } - - if (targetEdge) { - handleEdgeClick(_event, targetEdge); - } - }, - [getNodeSourceHandles, setHighlightedStepId, handlePopoverClose, findEdgeBySourceAndHandle, edges, handleEdgeClick] - ); - - const handleUserMove = useCallback((event: MouseEvent | TouchEvent | null) => { - if (!event?.isTrusted) return; // Ignore synthetic events - setHasUserInteracted(true); - }, []); - - // Reset user interaction state when taskIdInSidePanel changes (new task loaded) - useEffect(() => { - setHasUserInteracted(false); - }, [taskIdInSidePanel]); - - useEffect(() => { - // Only run fitView if the panel is NOT transitioning AND user hasn't interacted - if (!isSidePanelTransitioning && fitView && nodes.length > 0) { - const shouldFitView = prevProcessedStepsRef.current !== processedSteps && hasUserInteracted === false; - if (shouldFitView) { - fitView({ - duration: 200, - padding: 0.1, - maxZoom: 1.2, - }); - - prevProcessedStepsRef.current = processedSteps; - } - } - }, [nodes.length, fitView, processedSteps, isSidePanelTransitioning, hasUserInteracted]); - - // Combined effect for node highlighting and edge selection based on highlightedStepId - useEffect(() => { - // Update node highlighting - setNodes(currentFlowNodes => - currentFlowNodes.map(flowNode => { - const isHighlighted = flowNode.data?.visualizerStepId && flowNode.data.visualizerStepId === highlightedStepId; - - // Find the original node from memoizedFlowData to get its base style - const originalNode = memoizedFlowData.nodes.find(n => n.id === flowNode.id); - const baseStyle = originalNode?.style || {}; - - return { - ...flowNode, - style: { - ...baseStyle, - boxShadow: isHighlighted ? "0px 4px 12px rgba(0, 0, 0, 0.2)" : baseStyle.boxShadow || "none", - transition: "box-shadow 0.2s ease-in-out", - }, - }; - }) - ); - - // Update selected edge - if (highlightedStepId) { - const relatedEdge = computedEdges.find(edge => { - const edgeData = edge.data as unknown as AnimatedEdgeData; - return edgeData?.visualizerStepId === highlightedStepId; - }); - - if (relatedEdge) { - setSelectedEdgeId(relatedEdge.id); - } - } else { - setSelectedEdgeId(null); - } - }, [highlightedStepId, setNodes, memoizedFlowData.nodes, computedEdges]); - - if (!processedSteps || processedSteps.length === 0) { - return
{Object.keys(processedSteps).length > 0 ? "Processing flow data..." : "No steps to display in flow chart."}
; - } - - if (memoizedFlowData.nodes.length === 0 && processedSteps.length > 0) { - return
Generating flow chart...
; - } - - return ( -
- ({ - ...edge, - markerEnd: { type: MarkerType.ArrowClosed, color: "#888" }, - }))} - onNodesChange={onNodesChange} - onEdgesChange={onEdgesChange} - onEdgeClick={handleEdgeClick} - onNodeClick={handleNodeClick} - onPaneClick={() => { - setHighlightedStepId(null); - setSelectedEdgeId(null); - handlePopoverClose(); - }} - onMoveStart={handleUserMove} - nodeTypes={nodeTypes} - edgeTypes={edgeTypes} - fitViewOptions={{ padding: 0.1 }} - className={"bg-gray-50 dark:bg-gray-900 [&>button]:dark:bg-gray-700"} - proOptions={{ hideAttribution: true }} - nodesDraggable={false} - elementsSelectable={false} - nodesConnectable={false} - minZoom={0.2} - > - - - -
- - - - {/* Edge Information Popover */} - - {selectedStep && } - -
- ); -}; - -const FlowChartPanel: React.FC = props => { - return ( - - - - ); -}; - -export { FlowChartPanel }; diff --git a/client/webui/frontend/src/lib/components/activities/VisualizerStepCard.tsx b/client/webui/frontend/src/lib/components/activities/VisualizerStepCard.tsx index 636ce4c07..3cee8a538 100644 --- a/client/webui/frontend/src/lib/components/activities/VisualizerStepCard.tsx +++ b/client/webui/frontend/src/lib/components/activities/VisualizerStepCard.tsx @@ -1,10 +1,24 @@ -import React from "react"; +import { useState, type FC, type ReactNode, type MouseEvent } from "react"; -import { CheckCircle, FileText, HardDrive, Link, MessageSquare, Share2, Terminal, User, XCircle, Zap, ExternalLink } from "lucide-react"; +import { CheckCircle, ExternalLink, FileText, GitCommit, GitMerge, HardDrive, Link, List, MessageSquare, Share2, Split, Terminal, User, Workflow, XCircle, Zap } from "lucide-react"; import { JSONViewer, MarkdownHTMLConverter } from "@/lib/components"; import { useChatContext } from "@/lib/hooks"; -import type { ArtifactNotificationData, LLMCallData, LLMResponseToAgentData, ToolDecisionData, ToolInvocationStartData, ToolResultData, VisualizerStep } from "@/lib/types"; +import { ImageSearchGrid } from "@/lib/components/research"; +import type { + ArtifactNotificationData, + LLMCallData, + LLMResponseToAgentData, + ToolDecisionData, + ToolInvocationStartData, + ToolResultData, + VisualizerStep, + WorkflowExecutionResultData, + WorkflowExecutionStartData, + WorkflowNodeExecutionResultData, + WorkflowNodeExecutionStartData, +} from "@/lib/types"; +import { isString } from "@/lib/utils"; interface VisualizerStepCardProps { step: VisualizerStep; @@ -13,7 +27,7 @@ interface VisualizerStepCardProps { variant?: "list" | "popover"; } -const VisualizerStepCard: React.FC = ({ step, isHighlighted, onClick, variant = "list" }) => { +const VisualizerStepCard: FC = ({ step, isHighlighted, onClick, variant = "list" }) => { const { artifacts, setPreviewArtifact, setActiveSidePanelTab, setIsSidePanelCollapsed, navigateArtifactVersion } = useChatContext(); const getStepIcon = () => { @@ -42,6 +56,18 @@ const VisualizerStepCard: React.FC = ({ step, isHighlig return ; case "AGENT_ARTIFACT_NOTIFICATION": return ; + case "WORKFLOW_EXECUTION_START": + case "WORKFLOW_EXECUTION_RESULT": + return ; + case "WORKFLOW_NODE_EXECUTION_START": + if (step.data.workflowNodeExecutionStart?.nodeType === "map") return ; + if (step.data.workflowNodeExecutionStart?.nodeType === "fork") return ; + if (step.data.workflowNodeExecutionStart?.nodeType === "switch") return ; + return ; + case "WORKFLOW_NODE_EXECUTION_RESULT": + return ; + case "WORKFLOW_MAP_PROGRESS": + return ; default: return ; } @@ -67,10 +93,10 @@ const VisualizerStepCard: React.FC = ({ step, isHighlig
); - const LLMResponseToAgentDetails: React.FC<{ data: LLMResponseToAgentData }> = ({ data }) => { - const [expanded, setExpanded] = React.useState(false); + const LLMResponseToAgentDetails: FC<{ data: LLMResponseToAgentData }> = ({ data }) => { + const [expanded, setExpanded] = useState(false); - const toggleExpand = (e: React.MouseEvent) => { + const toggleExpand = (e: MouseEvent) => { e.stopPropagation(); setExpanded(!expanded); }; @@ -146,21 +172,73 @@ const VisualizerStepCard: React.FC = ({ step, isHighlig
); - const renderToolResultData = (data: ToolResultData) => ( -
-

- Tool: {data.toolName} -

-

- Result: -

-
- {typeof data.resultData === "object" ? :
{String(data.resultData)}
} + /** + * Renders result data as either a JSON viewer (for objects) or a preformatted text block (for primitives). + * Abstracts the common pattern of displaying tool result data. + */ + const renderResultData = (resultData: unknown): ReactNode => { + if (typeof resultData === "object") { + // Cast is safe here as JSONViewer handles null and object types + return [0]["data"]} />; + } + return
{String(resultData)}
; + }; + + const renderToolResultData = (data: ToolResultData) => { + // Check if this is a web search result with images + let parsedResult = null; + let hasImages = false; + + try { + // Try to parse the result if it's a string + if (isString(data.resultData)) { + parsedResult = JSON.parse(data.resultData); + } else if (typeof data.resultData === "object") { + parsedResult = data.resultData; + } + + // Check if the result has an images array (from web search tools) + if (parsedResult?.result) { + const innerResult = isString(parsedResult.result) ? JSON.parse(parsedResult.result) : parsedResult.result; + + if (innerResult?.images && Array.isArray(innerResult.images) && innerResult.images.length > 0) { + hasImages = true; + } + } + } catch { + // Not JSON or parsing failed, will display as normal + } + + return ( +
+

+ Tool: {data.toolName} +

+ + {hasImages && parsedResult?.result ? ( + <> +

+ Image Results: +

+ +
+ Show full result data +
{renderResultData(data.resultData)}
+
+ + ) : ( + <> +

+ Result: +

+
{renderResultData(data.resultData)}
+ + )}
-
- ); + ); + }; const renderArtifactNotificationData = (data: ArtifactNotificationData) => { - const handleViewFile = async (e: React.MouseEvent) => { + const handleViewFile = async (e: MouseEvent) => { e.stopPropagation(); // Find the artifact by filename @@ -213,6 +291,102 @@ const VisualizerStepCard: React.FC = ({ step, isHighlig ); }; + const renderWorkflowNodeStartData = (data: WorkflowNodeExecutionStartData) => ( +
+
+ {data.nodeType} Node + {(data.iterationIndex !== undefined && data.iterationIndex !== null && typeof data.iterationIndex === 'number') && Iter #{data.iterationIndex}} +
+ + {data.condition && ( +
+

Condition:

+ {data.condition} +
+ )} + {data.trueBranch && ( +

+ True Branch: {data.trueBranch} +

+ )} + {data.falseBranch && ( +

+ False Branch: {data.falseBranch} +

+ )} +
+ ); + + const renderWorkflowNodeResultData = (data: WorkflowNodeExecutionResultData) => ( +
+

+ Status: {data.status} +

+ {data.metadata?.condition && ( +
+

Condition:

+ {data.metadata.condition} +
+ )} + {data.metadata?.condition_result !== undefined && ( +

+ Condition Result: {data.metadata.condition_result ? "True" : "False"} +

+ )} + {data.outputArtifactRef && ( +

+ Output: {data.outputArtifactRef.name} (v{data.outputArtifactRef.version}) +

+ )} + {data.errorMessage && ( +

+ Error: {data.errorMessage} +

+ )} +
+ ); + + const renderWorkflowExecutionStartData = (data: WorkflowExecutionStartData) => ( +
+

+ Workflow: {data.workflowName} +

+ {data.workflowInput && ( +
+

+ Input: +

+
+ +
+
+ )} +
+ ); + + const renderWorkflowExecutionResultData = (data: WorkflowExecutionResultData) => ( +
+

+ Status: {data.status} +

+ {data.workflowOutput && ( +
+

+ Output: +

+
+ +
+
+ )} + {data.errorMessage && ( +

+ Error: {data.errorMessage} +

+ )} +
+ ); + // Calculate indentation based on nesting level - only apply in list variant const indentationStyle = variant === "list" && step.nestingLevel && step.nestingLevel > 0 @@ -302,6 +476,10 @@ const VisualizerStepCard: React.FC = ({ step, isHighlig {step.data.toolInvocationStart && renderToolInvocationStartData(step.data.toolInvocationStart)} {step.data.toolResult && renderToolResultData(step.data.toolResult)} {step.data.artifactNotification && renderArtifactNotificationData(step.data.artifactNotification)} + {step.data.workflowExecutionStart && renderWorkflowExecutionStartData(step.data.workflowExecutionStart)} + {step.data.workflowNodeExecutionStart && renderWorkflowNodeStartData(step.data.workflowNodeExecutionStart)} + {step.data.workflowNodeExecutionResult && renderWorkflowNodeResultData(step.data.workflowNodeExecutionResult)} + {step.data.workflowExecutionResult && renderWorkflowExecutionResultData(step.data.workflowExecutionResult)}
); }; diff --git a/client/webui/frontend/src/lib/components/activities/index.ts b/client/webui/frontend/src/lib/components/activities/index.ts index f17ec3b96..af201c74f 100644 --- a/client/webui/frontend/src/lib/components/activities/index.ts +++ b/client/webui/frontend/src/lib/components/activities/index.ts @@ -1,4 +1,4 @@ export * from "./FlowChartDetails"; -export * from "./FlowChartPanel"; +export * from "./FlowChart"; export * from "./VisualizerStepCard"; export * from "./taskVisualizerProcessor"; diff --git a/client/webui/frontend/src/lib/components/activities/taskVisualizerProcessor.ts b/client/webui/frontend/src/lib/components/activities/taskVisualizerProcessor.ts index 859b01212..5a5a11c1b 100644 --- a/client/webui/frontend/src/lib/components/activities/taskVisualizerProcessor.ts +++ b/client/webui/frontend/src/lib/components/activities/taskVisualizerProcessor.ts @@ -25,6 +25,19 @@ import type { VisualizedTask, } from "@/lib/types"; +/** + * Checks if an artifact is an intermediate web content artifact from deep research. + * These are temporary files that should not be shown in the workflow visualization. + * + * @param artifactName The name of the artifact to check. + * @returns True if the artifact is an intermediate web content artifact. + */ +const isIntermediateWebContentArtifact = (artifactName: string | undefined): boolean => { + if (!artifactName) return false; + // Skip web_content_ artifacts (temporary files from deep research) + return artifactName.startsWith("web_content_"); +}; + /** * Helper function to get parentTaskId from a TaskFE object. * It first checks the direct `parentTaskId` field. If not present, @@ -57,9 +70,21 @@ const getEventTimestamp = (event: A2AEventSSEPayload): string => { * @param allMonitoredTasks A record of all monitored tasks. * @param taskNestingLevels A map to store the nesting level of each task ID. * @param currentLevel The current nesting level for currentTaskId. + * @param visitedTaskIds A set of task IDs that have already been visited to prevent cycles/duplication. * @returns An array of A2AEventSSEPayload objects from the task and its descendants. */ -const collectAllDescendantEvents = (currentTaskId: string, allMonitoredTasks: Record, taskNestingLevels: Map, currentLevel: number): A2AEventSSEPayload[] => { +const collectAllDescendantEvents = ( + currentTaskId: string, + allMonitoredTasks: Record, + taskNestingLevels: Map, + currentLevel: number, + visitedTaskIds: Set = new Set() +): A2AEventSSEPayload[] => { + if (visitedTaskIds.has(currentTaskId)) { + return []; + } + visitedTaskIds.add(currentTaskId); + const task = allMonitoredTasks[currentTaskId]; if (!task) { console.warn(`[collectAllDescendantEvents] Task not found in allMonitoredTasks: ${currentTaskId}`); @@ -78,7 +103,7 @@ const collectAllDescendantEvents = (currentTaskId: string, allMonitoredTasks: Re const childsParentId = getParentTaskIdFromTaskObject(potentialChildTask); if (childsParentId === currentTaskId) { - events = events.concat(collectAllDescendantEvents(potentialChildTask.taskId, allMonitoredTasks, taskNestingLevels, currentLevel + 1)); + events = events.concat(collectAllDescendantEvents(potentialChildTask.taskId, allMonitoredTasks, taskNestingLevels, currentLevel + 1, visitedTaskIds)); } } return events; @@ -210,7 +235,8 @@ export const processTaskForVisualization = ( parentTaskObject.taskId, allMonitoredTasks, taskNestingLevels, - 0 // Root task is at level 0 + 0, // Root task is at level 0 + new Set() // visitedTaskIds ); if (combinedEvents.length === 0) { @@ -239,6 +265,7 @@ export const processTaskForVisualization = ( const subTaskToFunctionCallIdMap = new Map(); const functionCallIdToDelegationInfoMap = new Map(); const activeFunctionCallIdByTask = new Map(); + const processedWorkflowEvents = new Set(); const flushAggregatedTextStep = (currentEventOwningTaskId?: string) => { if (currentAggregatedText.trim() && aggregatedTextSourceAgent && aggregatedTextTimestamp) { @@ -257,6 +284,9 @@ export const processTaskForVisualization = ( const nestingLevelForFlush = taskNestingLevels.get(owningTaskIdForFlush) ?? 0; const functionCallIdForStep = subTaskToFunctionCallIdMap.get(owningTaskIdForFlush) || activeFunctionCallIdByTask.get(owningTaskIdForFlush); + const taskForFlush = allMonitoredTasks[owningTaskIdForFlush]; + const parentTaskIdForFlush = getParentTaskIdFromTaskObject(taskForFlush); + visualizerSteps.push({ id: `vstep-agenttext-${visualizerSteps.length}-${aggregatedRawEventIds[0] || "unknown"}`, type: "AGENT_RESPONSE_TEXT", @@ -269,6 +299,7 @@ export const processTaskForVisualization = ( isSubTaskStep: nestingLevelForFlush > 0, nestingLevel: nestingLevelForFlush, owningTaskId: owningTaskIdForFlush, + parentTaskId: parentTaskIdForFlush || undefined, functionCallId: functionCallIdForStep, }); lastFlushedAgentResponseText = textToFlush; @@ -288,6 +319,9 @@ export const processTaskForVisualization = ( const currentEventOwningTaskId = event.task_id || parentTaskObject.taskId; const currentEventNestingLevel = taskNestingLevels.get(currentEventOwningTaskId) ?? 0; + const currentTask = allMonitoredTasks[currentEventOwningTaskId]; + const parentTaskId = getParentTaskIdFromTaskObject(currentTask); + // Determine agent name let eventAgentName = event.source_entity || "UnknownAgent"; if (payload?.params?.message?.metadata?.agent_name) { @@ -316,8 +350,10 @@ export const processTaskForVisualization = ( // Handle sub-task creation requests to establish the mapping early if (event.direction === "request" && currentEventNestingLevel > 0) { - const metadata = payload.params?.metadata as any; - const functionCallId = metadata?.function_call_id; + // Note: metadata can be at params level (for tool delegation) or message level (for workflow agent calls) + const paramsMetadata = payload.params?.metadata as any; + const messageMetadata = payload.params?.message?.metadata as any; + const functionCallId = paramsMetadata?.function_call_id || messageMetadata?.function_call_id; const subTaskId = event.task_id; if (subTaskId && functionCallId) { @@ -326,6 +362,95 @@ export const processTaskForVisualization = ( // It doesn't create a visual step itself, so we return. return; } + + // Check if this is a workflow agent request (has workflow_name in message metadata) + const workflowName = messageMetadata?.workflow_name; + const nodeId = messageMetadata?.node_id; + if (workflowName && nodeId) { + // This is a workflow agent invocation - create a WORKFLOW_AGENT_REQUEST step + const params = payload.params as any; + let inputText: string | undefined; + let instruction: string | undefined; + let inputArtifactRef: { name: string; version?: number; uri?: string; mimeType?: string } | undefined; + let inputSchema: Record | undefined; + let outputSchema: Record | undefined; + let suggestedOutputFilename: string | undefined; + + if (params?.message?.parts) { + // Extract structured_invocation_request data part + const structuredInvocationPart = params.message.parts.find((p: any) => + p.kind === "data" && p.data?.type === "structured_invocation_request" + ); + if (structuredInvocationPart) { + const invocationData = structuredInvocationPart.data; + inputSchema = invocationData.input_schema; + outputSchema = invocationData.output_schema; + suggestedOutputFilename = invocationData.suggested_output_filename; + } + + // Extract text parts (skip the reminder text) + // The first non-reminder text part is the instruction if present + const textParts = params.message.parts.filter((p: any) => + p.kind === "text" && p.text && !p.text.includes("REMINDER:") + ); + if (textParts.length > 0) { + // First text part is the instruction + instruction = textParts[0].text; + } + + // Extract file parts (artifact references) - this is the structured input + const fileParts = params.message.parts.filter((p: any) => + p.kind === "file" && p.file + ); + if (fileParts.length > 0) { + const file = fileParts[0].file; + inputArtifactRef = { + name: file.name || "input", + version: file.version, + uri: file.uri, + mimeType: file.mimeType, + }; + } + + // If no file part but text parts exist after instruction, that's the inputText + // (for simple text schemas where input is sent as text, not artifact) + if (!inputArtifactRef && textParts.length > 1) { + inputText = textParts[1].text; + } else if (!inputArtifactRef && textParts.length === 1 && !instruction) { + // Single text part that's not instruction - it's the input + inputText = textParts[0].text; + } + } + + const stepData = { + id: `vstep-wfagentreq-${visualizerSteps.length}-${eventId}`, + type: "WORKFLOW_AGENT_REQUEST" as const, + timestamp: eventTimestamp, + title: `Workflow Request to ${event.target_entity || eventAgentName}`, + source: "Workflow", + target: event.target_entity || eventAgentName, + data: { + workflowAgentRequest: { + agentName: event.target_entity || eventAgentName || "Unknown", + nodeId, + workflowName, + inputText, + inputArtifactRef, + instruction, + inputSchema, + outputSchema, + suggestedOutputFilename, + }, + }, + rawEventIds: [eventId], + isSubTaskStep: true, + nestingLevel: currentEventNestingLevel, + owningTaskId: currentEventOwningTaskId, + parentTaskId: parentTaskId || undefined, + }; + visualizerSteps.push(stepData); + return; + } } // Skip rendering of cancel requests as not enough information is present yet to link to original task @@ -340,8 +465,22 @@ export const processTaskForVisualization = ( let userText = "User request"; if (params?.message?.parts) { const textParts = params.message.parts.filter((p: any) => p.kind === "text" && p.text); - if (textParts.length > 0) { - userText = textParts[textParts.length - 1].text; + // Filter out gateway timestamp parts (they appear like "Request received by gateway at: 2025-12-19T22:46:16.994017+00:00") + // The gateway prepends this as the first part, so we can skip parts that match this pattern + const gatewayTimestampPattern = /^Request received by gateway at:/; + const filteredParts = textParts.filter( + (p: any) => !gatewayTimestampPattern.test(p.text.trim()) + ); + if (filteredParts.length > 0) { + // Join remaining text parts + userText = filteredParts.map((p: any) => p.text).join("\n"); + } else if (textParts.length > 0) { + // Fallback: if all parts were filtered, use the last part but strip the gateway prefix + const lastPart = textParts[textParts.length - 1].text; + // Try to extract text after the timestamp line + const lines = lastPart.split('\n'); + const nonGatewayLines = lines.filter((line: string) => !gatewayTimestampPattern.test(line.trim())); + userText = nonGatewayLines.length > 0 ? nonGatewayLines.join('\n') : lastPart; } } visualizerSteps.push({ @@ -367,9 +506,11 @@ export const processTaskForVisualization = ( const messageMetadata = statusMessage?.metadata as any; let statusUpdateAgentName: string; - const isForwardedMessage = !!messageMetadata?.forwarded_from_peer; + // Check both message metadata and result metadata for forwarding flag + const isForwardedMessage = !!messageMetadata?.forwarded_from_peer || !!result.metadata?.forwarded_from_peer; + if (isForwardedMessage) { - statusUpdateAgentName = messageMetadata.forwarded_from_peer; + statusUpdateAgentName = messageMetadata?.forwarded_from_peer || result.metadata?.forwarded_from_peer; } else if (result.metadata?.agent_name) { statusUpdateAgentName = result.metadata.agent_name as string; } else if (messageMetadata?.agent_name) { @@ -397,6 +538,171 @@ export const processTaskForVisualization = ( const signalType = signalData?.type as string; switch (signalType) { + case "workflow_execution_start": { + const dedupKey = `start:${signalData.execution_id}`; + + if (processedWorkflowEvents.has(dedupKey)) { + break; + } + processedWorkflowEvents.add(dedupKey); + + const stepId = `vstep-wfstart-${visualizerSteps.length}-${eventId}`; + visualizerSteps.push({ + id: stepId, + type: "WORKFLOW_EXECUTION_START", + timestamp: eventTimestamp, + title: `Workflow Started: ${signalData.workflow_name}`, + source: "System", + target: "Workflow", + data: { + workflowExecutionStart: { + workflowName: signalData.workflow_name, + executionId: signalData.execution_id, + inputArtifactRef: signalData.input_artifact_ref, + workflowInput: signalData.workflow_input, + }, + }, + rawEventIds: [eventId], + isSubTaskStep: currentEventNestingLevel > 0, + nestingLevel: currentEventNestingLevel, + owningTaskId: currentEventOwningTaskId, + parentTaskId: parentTaskId || undefined, + functionCallId: functionCallIdForStep, + }); + break; + } + case "workflow_node_execution_start": { + const dedupKey = `node_start:${currentEventOwningTaskId}:${signalData.sub_task_id || signalData.node_id}:${signalData.iteration_index || 0}`; + if (processedWorkflowEvents.has(dedupKey)) break; + processedWorkflowEvents.add(dedupKey); + + visualizerSteps.push({ + id: `vstep-wfnode-start-${visualizerSteps.length}-${eventId}`, + type: "WORKFLOW_NODE_EXECUTION_START", + timestamp: eventTimestamp, + title: `Node Started: ${signalData.node_id} (${signalData.node_type})`, + source: "Workflow", + target: signalData.agent_name || signalData.node_id, + data: { + workflowNodeExecutionStart: { + nodeId: signalData.node_id, + nodeType: signalData.node_type, + agentName: signalData.agent_name, + inputArtifactRef: signalData.input_artifact_ref, + iterationIndex: signalData.iteration_index, + // Conditional node fields + condition: signalData.condition, + trueBranch: signalData.true_branch, + falseBranch: signalData.false_branch, + trueBranchLabel: signalData.true_branch_label, + falseBranchLabel: signalData.false_branch_label, + // Switch node fields + cases: signalData.cases, + defaultBranch: signalData.default_branch, + // Join node fields + waitFor: signalData.wait_for, + joinStrategy: signalData.join_strategy, + joinN: signalData.join_n, + // Loop node fields + maxIterations: signalData.max_iterations, + loopDelay: signalData.loop_delay, + // Common fields + subTaskId: signalData.sub_task_id, + parentNodeId: signalData.parent_node_id, + // Parallel grouping + parallelGroupId: signalData.parallel_group_id, + }, + }, + rawEventIds: [eventId], + isSubTaskStep: currentEventNestingLevel > 0, + nestingLevel: currentEventNestingLevel, + owningTaskId: currentEventOwningTaskId, + functionCallId: functionCallIdForStep, + }); + break; + } + case "workflow_node_execution_result": { + const dedupKey = `node_result:${currentEventOwningTaskId}:${signalData.node_id}:${signalData.status}`; + if (processedWorkflowEvents.has(dedupKey)) break; + processedWorkflowEvents.add(dedupKey); + + visualizerSteps.push({ + id: `vstep-wfnode-result-${visualizerSteps.length}-${eventId}`, + type: "WORKFLOW_NODE_EXECUTION_RESULT", + timestamp: eventTimestamp, + title: `Node Completed: ${signalData.node_id} (${signalData.status})`, + source: signalData.node_id, + target: "Workflow", + data: { + workflowNodeExecutionResult: { + nodeId: signalData.node_id, + status: signalData.status, + outputArtifactRef: signalData.output_artifact_ref, + errorMessage: signalData.error_message, + metadata: signalData.metadata, + conditionResult: signalData.metadata?.condition_result, + }, + }, + rawEventIds: [eventId], + isSubTaskStep: currentEventNestingLevel > 0, + nestingLevel: currentEventNestingLevel, + owningTaskId: currentEventOwningTaskId, + functionCallId: functionCallIdForStep, + }); + break; + } + case "workflow_map_progress": { + visualizerSteps.push({ + id: `vstep-wfmap-${visualizerSteps.length}-${eventId}`, + type: "WORKFLOW_MAP_PROGRESS", + timestamp: eventTimestamp, + title: `Map Progress: ${signalData.node_id} (${signalData.completed_items}/${signalData.total_items})`, + source: signalData.node_id, + target: "Workflow", + data: { + workflowMapProgress: { + nodeId: signalData.node_id, + totalItems: signalData.total_items, + completedItems: signalData.completed_items, + status: signalData.status, + }, + }, + rawEventIds: [eventId], + isSubTaskStep: currentEventNestingLevel > 0, + nestingLevel: currentEventNestingLevel, + owningTaskId: currentEventOwningTaskId, + functionCallId: functionCallIdForStep, + }); + break; + } + case "workflow_execution_result": { + const dedupKey = `result:${currentEventOwningTaskId}:${signalData.status}`; + if (processedWorkflowEvents.has(dedupKey)) break; + processedWorkflowEvents.add(dedupKey); + + visualizerSteps.push({ + id: `vstep-wfresult-${visualizerSteps.length}-${eventId}`, + type: "WORKFLOW_EXECUTION_RESULT", + timestamp: eventTimestamp, + title: `Workflow Finished: ${signalData.status}`, + source: "Workflow", + target: "User", + data: { + workflowExecutionResult: { + status: signalData.status, + outputArtifactRef: signalData.output_artifact_ref, + errorMessage: signalData.error_message, + workflowOutput: signalData.workflow_output, + }, + }, + rawEventIds: [eventId], + isSubTaskStep: currentEventNestingLevel > 0, + nestingLevel: currentEventNestingLevel, + owningTaskId: currentEventOwningTaskId, + functionCallId: functionCallIdForStep, + }); + break; + } case "agent_progress_update": { visualizerSteps.push({ id: `vstep-progress-${visualizerSteps.length}-${eventId}`, @@ -418,13 +724,86 @@ export const processTaskForVisualization = ( const llmData = signalData.request as any; let promptText = "System-initiated LLM call"; if (llmData?.contents && Array.isArray(llmData.contents) && llmData.contents.length > 0) { - // Find the last user message in the history to use as the prompt preview. - const lastUserContent = [...llmData.contents].reverse().find((c: any) => c.role === "user"); - if (lastUserContent && lastUserContent.parts) { - promptText = lastUserContent.parts - .map((p: any) => p.text || "") // Handle cases where text might be null/undefined - .join("\n") - .trim(); + // Get the last message in the conversation to understand what triggered this LLM call + const lastContent = llmData.contents[llmData.contents.length - 1]; + const lastRole = lastContent?.role; + + // Check if this LLM call is following tool results + // Tool results can be: role="tool", role="function", or parts with function_response + const toolResultContents = llmData.contents.filter((c: any) => + c.role === "tool" || c.role === "function" || + (c.parts && c.parts.some((p: any) => p.function_response)) + ); + const hasToolResults = toolResultContents.length > 0; + + if (hasToolResults && lastRole !== "user") { + // This is a follow-up LLM call after tool execution + // Count only the tool results from the CURRENT turn, not the entire conversation history + // Find the index of the last "model" role message (the LLM's response that called tools) + let lastModelIndex = -1; + for (let i = llmData.contents.length - 1; i >= 0; i--) { + if (llmData.contents[i].role === "model") { + lastModelIndex = i; + break; + } + } + + // Count function_response parts that come AFTER the last model message + // Also collect summaries of the tool results + let actualToolResultCount = 0; + const toolResultSummaries: string[] = []; + const startIndex = lastModelIndex + 1; + for (let i = startIndex; i < llmData.contents.length; i++) { + const content = llmData.contents[i]; + if (content.parts && Array.isArray(content.parts)) { + for (const part of content.parts) { + if (part.function_response) { + actualToolResultCount++; + const toolName = part.function_response.name || "unknown"; + const response = part.function_response.response; + // Create a brief summary of the response + let summary: string; + if (typeof response === "string") { + summary = response.substring(0, 200); + } else if (typeof response === "object" && response !== null) { + const jsonStr = JSON.stringify(response); + summary = jsonStr.substring(0, 200); + } else { + summary = String(response).substring(0, 200); + } + if (summary.length === 200) summary += "..."; + toolResultSummaries.push(`- ${toolName}: ${summary}`); + } + } + } + } + // If no function_response parts found after last model, fall back to 0 (shouldn't happen) + const toolResultCount = actualToolResultCount > 0 ? actualToolResultCount : 0; + + // Find the LAST user message (which is the current turn's request) + const lastUserContent = [...llmData.contents].reverse().find((c: any) => c.role === "user"); + const userPromptFull = lastUserContent?.parts + ?.map((p: any) => p.text || "") + .join(" ") + .trim() || ""; + const userPromptSnippet = userPromptFull.substring(0, 5000); + + // Build prompt text with tool result summaries + let toolResultsSection = ""; + if (toolResultSummaries.length > 0) { + toolResultsSection = "\n\nTool Results:\n" + toolResultSummaries.join("\n"); + } + + promptText = `[Following ${toolResultCount} tool result(s)]\nOriginal request: ${userPromptSnippet || "N/A"}${userPromptFull.length > 5000 ? "..." : ""}${toolResultsSection}`; + } else { + // Regular LLM call - find the last user message + const lastUserContent = [...llmData.contents].reverse().find((c: any) => c.role === "user"); + if (lastUserContent && lastUserContent.parts) { + promptText = lastUserContent.parts + .map((p: any) => p.text || "") // Handle cases where text might be null/undefined + .join("\n") + .trim(); + } } } const llmCallData: LLMCallData = { @@ -459,7 +838,7 @@ export const processTaskForVisualization = ( } const llmResponseData = signalData.data as any; - const contentParts = llmResponseData.content?.parts as any[]; + const contentParts = llmResponseData?.content?.parts as any[]; const functionCallParts = contentParts?.filter(p => p.function_call); if (functionCallParts && functionCallParts.length > 0) { @@ -471,7 +850,7 @@ export const processTaskForVisualization = ( functionCallId: p.function_call.id, toolName: p.function_call.name, toolArguments: p.function_call.args || {}, - isPeerDelegation: p.function_call.name?.startsWith("peer_"), + isPeerDelegation: p.function_call.name?.startsWith("peer_") || p.function_call.name?.startsWith("workflow_"), })); const toolDecisionData: ToolDecisionData = { decisions, isParallel: decisions.length > 1 }; @@ -479,7 +858,13 @@ export const processTaskForVisualization = ( const claimedSubTaskIds = new Set(); decisions.forEach(decision => { if (decision.isPeerDelegation) { - const peerAgentActualName = decision.toolName.substring(5); + let peerAgentActualName = decision.toolName; + if (decision.toolName.startsWith("peer_")) { + peerAgentActualName = decision.toolName.substring(5); + } else if (decision.toolName.startsWith("workflow_")) { + peerAgentActualName = decision.toolName.substring(9); + } + for (const stId in allMonitoredTasks) { const candSubTask = allMonitoredTasks[stId]; if (claimedSubTaskIds.has(candSubTask.taskId)) continue; @@ -507,7 +892,7 @@ export const processTaskForVisualization = ( } }); - const toolDecisionStep: VisualizerStep = { + const toolDecisionStep: VisualizerStep = { id: `vstep-tooldecision-${visualizerSteps.length}-${eventId}`, type: "AGENT_LLM_RESPONSE_TOOL_DECISION", timestamp: eventTimestamp, @@ -554,6 +939,7 @@ export const processTaskForVisualization = ( .join("\n") || ""; const llmResponseToAgentData: LLMResponseToAgentData = { responsePreview: llmResponseText.substring(0, 200) + (llmResponseText.length > 200 ? "..." : ""), + response: llmResponseText, // Store full response isFinalResponse: llmResponseData?.partial === false, }; visualizerSteps.push({ @@ -578,8 +964,12 @@ export const processTaskForVisualization = ( functionCallId: signalData.function_call_id, toolName: signalData.tool_name, toolArguments: signalData.tool_args, - isPeerInvocation: signalData.tool_name?.startsWith("peer_"), + isPeerInvocation: signalData.tool_name?.startsWith("peer_") || signalData.tool_name?.startsWith("workflow_"), + parallelGroupId: signalData.parallel_group_id, }; + + const delegationInfo = functionCallIdToDelegationInfoMap.get(signalData.function_call_id); + visualizerSteps.push({ id: `vstep-toolinvokestart-${visualizerSteps.length}-${eventId}`, type: "AGENT_TOOL_INVOCATION_START", @@ -589,6 +979,7 @@ export const processTaskForVisualization = ( target: invocationData.toolName, data: { toolInvocationStart: invocationData }, rawEventIds: [eventId], + delegationInfo: delegationInfo ? [delegationInfo] : undefined, isSubTaskStep: currentEventNestingLevel > 0, nestingLevel: currentEventNestingLevel, owningTaskId: currentEventOwningTaskId, @@ -604,12 +995,21 @@ export const processTaskForVisualization = ( const duration = new Date(eventTimestamp).getTime() - new Date(openToolCallForPerf.timestamp).getTime(); const invokingAgentMetrics = report.agents[openToolCallForPerf.invokingAgentInstanceId]; if (invokingAgentMetrics) { + let peerAgentName: string | undefined; + if (openToolCallForPerf.isPeer) { + if (openToolCallForPerf.toolName.startsWith("peer_")) { + peerAgentName = openToolCallForPerf.toolName.substring(5); + } else if (openToolCallForPerf.toolName.startsWith("workflow_")) { + peerAgentName = openToolCallForPerf.toolName.substring(9); + } + } + const toolCallPerf: ToolCallPerformance = { toolName: openToolCallForPerf.toolName, durationMs: duration, isPeer: openToolCallForPerf.isPeer, timestamp: openToolCallForPerf.timestamp, - peerAgentName: openToolCallForPerf.isPeer ? openToolCallForPerf.toolName.substring(5) : undefined, + peerAgentName: peerAgentName, subTaskId: openToolCallForPerf.subTaskId, parallelBlockId: openToolCallForPerf.parallelBlockId, }; @@ -623,7 +1023,7 @@ export const processTaskForVisualization = ( toolName: signalData.tool_name, functionCallId: functionCallId, resultData: signalData.result_data, - isPeerResponse: signalData.tool_name?.startsWith("peer_"), + isPeerResponse: signalData.tool_name?.startsWith("peer_") || signalData.tool_name?.startsWith("workflow_"), }; visualizerSteps.push({ id: `vstep-toolresult-${visualizerSteps.length}-${eventId}`, @@ -643,7 +1043,8 @@ export const processTaskForVisualization = ( // Check if this is _notify_artifact_save and we have a pending artifact if (signalData.tool_name === "_notify_artifact_save" && functionCallId) { const pendingArtifact = pendingArtifacts.get(functionCallId); - if (pendingArtifact) { + // Skip intermediate web content artifacts from deep research + if (pendingArtifact && !isIntermediateWebContentArtifact(pendingArtifact.filename)) { const artifactNotification: ArtifactNotificationData = { artifactName: pendingArtifact.filename, version: pendingArtifact.version, @@ -664,8 +1065,8 @@ export const processTaskForVisualization = ( owningTaskId: pendingArtifact.taskId, functionCallId: functionCallId, }); - pendingArtifacts.delete(functionCallId); } + pendingArtifacts.delete(functionCallId); } break; } @@ -676,13 +1077,20 @@ export const processTaskForVisualization = ( // Handle new artifact_saved event type flushAggregatedTextStep(currentEventOwningTaskId); + const artifactFilename = signalData.filename || "Unnamed Artifact"; + + // Skip intermediate web content artifacts from deep research + if (isIntermediateWebContentArtifact(artifactFilename)) { + break; + } + // Check if this has a function_call_id (from fenced blocks with _notify_artifact_save) const isSyntheticToolCall = signalData.function_call_id && signalData.function_call_id.startsWith("host-notify-"); if (isSyntheticToolCall) { // Queue this artifact - will be created when we see the _notify_artifact_save tool result pendingArtifacts.set(signalData.function_call_id, { - filename: signalData.filename || "Unnamed Artifact", + filename: artifactFilename, version: signalData.version, description: signalData.description, mimeType: signalData.mime_type, @@ -695,7 +1103,7 @@ export const processTaskForVisualization = ( } else { // Regular tool call - create node immediately const artifactNotification: ArtifactNotificationData = { - artifactName: signalData.filename || "Unnamed Artifact", + artifactName: artifactFilename, version: signalData.version, description: signalData.description, mimeType: signalData.mime_type, @@ -717,6 +1125,9 @@ export const processTaskForVisualization = ( } break; } + default: + console.log(`Received unknown data part type: ${signalType}`, signalData); + break; } } else if (part.kind === "text" && part.text) { if (aggregatedTextSourceAgent && aggregatedTextSourceAgent !== statusUpdateAgentName) { @@ -740,6 +1151,13 @@ export const processTaskForVisualization = ( if (event.direction === "artifact_update" && payload?.result?.artifact) { flushAggregatedTextStep(currentEventOwningTaskId); const artifactData = payload.result.artifact as Artifact; + const artifactName = artifactData.name || "Unnamed Artifact"; + + // Skip intermediate web content artifacts from deep research + if (isIntermediateWebContentArtifact(artifactName)) { + return; + } + const artifactAgentName = (artifactData.metadata?.agent_name as string) || event.source_entity || "Agent"; let mimeType: string | undefined = undefined; if (artifactData.parts && artifactData.parts.length > 0) { @@ -751,7 +1169,7 @@ export const processTaskForVisualization = ( } } const artifactNotification: ArtifactNotificationData = { - artifactName: artifactData.name || "Unnamed Artifact", + artifactName: artifactName, version: typeof artifactData.metadata?.version === "number" ? artifactData.metadata.version : undefined, description: artifactData.description || undefined, mimeType, @@ -783,7 +1201,7 @@ export const processTaskForVisualization = ( const finalState = result.status.state as string; const responseAgentName = result.metadata?.agent_name || result.status?.message?.metadata?.agent_name || event.source_entity || "Agent"; - if (["completed", "failed", "canceled"].includes(finalState) && currentEventNestingLevel == 0) { + if (["completed", "failed", "canceled"].includes(finalState)) { const stepType: VisualizerStepType = finalState === "completed" ? "TASK_COMPLETED" : "TASK_FAILED"; const title = `${responseAgentName}: Task ${finalState.charAt(0).toUpperCase() + finalState.slice(1)}`; let dataPayload: any = {}; diff --git a/client/webui/frontend/src/lib/components/agents/AgentDisplayCard.tsx b/client/webui/frontend/src/lib/components/agents/AgentDisplayCard.tsx index 6fb60aba3..db9b31dcf 100644 --- a/client/webui/frontend/src/lib/components/agents/AgentDisplayCard.tsx +++ b/client/webui/frontend/src/lib/components/agents/AgentDisplayCard.tsx @@ -1,9 +1,10 @@ import React from "react"; import type { ReactNode } from "react"; -import { GitMerge, Info, Book, Link, Paperclip, Box, Wrench, Key, Bot, Code } from "lucide-react"; +import { GitMerge, Info, Book, Link, Paperclip, Box, Wrench, Key, Bot, Code, Workflow } from "lucide-react"; import type { AgentCardInfo, AgentSkill } from "@/lib/types"; +import { isWorkflowAgent, getWorkflowNodeCount } from "@/lib/utils/agentUtils"; interface DetailItemProps { label: string; @@ -34,6 +35,9 @@ const DetailItem: React.FC = ({ label, value, icon, fullWidthVa }; export const AgentDisplayCard: React.FC = ({ agent, isExpanded, onToggleExpand }) => { + const isWorkflow = isWorkflowAgent(agent); + const nodeCount = isWorkflow ? getWorkflowNodeCount(agent) : 0; + const renderCapabilities = (capabilities?: { [key: string]: unknown } | null) => { if (!capabilities || Object.keys(capabilities).length === 0) return N/A; return ( @@ -90,8 +94,12 @@ export const AgentDisplayCard: React.FC = ({ agent, isExp
-
- +
+ {isWorkflow ? ( + + ) : ( + + )}

{agent.displayName || agent.name} @@ -146,6 +154,12 @@ export const AgentDisplayCard: React.FC = ({ agent, isExp } fullWidthValue /> } fullWidthValue /> } fullWidthValue /> + {isWorkflow && nodeCount > 0 && ( +
+

Workflow Information

+ } /> +
+ )}
diff --git a/client/webui/frontend/src/lib/components/chat/ChatInputArea.tsx b/client/webui/frontend/src/lib/components/chat/ChatInputArea.tsx index 8509eb886..d2c639f16 100644 --- a/client/webui/frontend/src/lib/components/chat/ChatInputArea.tsx +++ b/client/webui/frontend/src/lib/components/chat/ChatInputArea.tsx @@ -801,13 +801,19 @@ export const ChatInputArea: React.FC<{ agents: AgentCardInfo[]; scrollToBottom?: /> {/* Buttons */} -
+
Agent:
- { + handleAgentSelection(agentName); + }} + disabled={isResponding || agents.length === 0} + > @@ -827,7 +833,7 @@ export const ChatInputArea: React.FC<{ agents: AgentCardInfo[]; scrollToBottom?: {sttEnabled && settings.speechToText && } {isResponding && !isCancelling ? ( - diff --git a/client/webui/frontend/src/lib/components/chat/ChatMessage.tsx b/client/webui/frontend/src/lib/components/chat/ChatMessage.tsx index f7ce1b09a..eef014074 100644 --- a/client/webui/frontend/src/lib/components/chat/ChatMessage.tsx +++ b/client/webui/frontend/src/lib/components/chat/ChatMessage.tsx @@ -1,14 +1,21 @@ -import React, { useState } from "react"; +import React, { useState, useMemo } from "react"; import type { ReactNode } from "react"; import { AlertCircle, ThumbsDown, ThumbsUp } from "lucide-react"; -import { ChatBubble, ChatBubbleMessage, MarkdownHTMLConverter, MessageBanner } from "@/lib/components"; +import { ChatBubble, ChatBubbleMessage, MarkdownHTMLConverter, MarkdownWrapper, MessageBanner } from "@/lib/components"; import { Button } from "@/lib/components/ui"; import { ViewWorkflowButton } from "@/lib/components/ui/ViewWorkflowButton"; import { useChatContext } from "@/lib/hooks"; -import type { ArtifactPart, FileAttachment, FilePart, MessageFE, TextPart } from "@/lib/types"; +import type { ArtifactInfo, ArtifactPart, DataPart, FileAttachment, FilePart, MessageFE, RAGSearchResult, TextPart } from "@/lib/types"; import type { ChatContextValue } from "@/lib/contexts"; +import { InlineResearchProgress, type ResearchProgressData } from "@/lib/components/research/InlineResearchProgress"; +import { DeepResearchReportContent } from "@/lib/components/research/DeepResearchReportContent"; +import { Sources } from "@/lib/components/web/Sources"; +import { ImageSearchGrid } from "@/lib/components/research"; +import { isDeepResearchReportFilename } from "@/lib/utils/deepResearchUtils"; +import { TextWithCitations } from "./Citation"; +import { parseCitations } from "@/lib/utils/citations"; import { ArtifactMessage, FileMessage } from "./file"; import { FeedbackModal } from "./FeedbackModal"; @@ -28,7 +35,10 @@ const MessageActions: React.FC<{ showWorkflowButton: boolean; showFeedbackActions: boolean; handleViewWorkflowClick: () => void; -}> = ({ message, showWorkflowButton, showFeedbackActions, handleViewWorkflowClick }) => { + sourcesElement?: React.ReactNode; + /** Optional text content override */ + textContentOverride?: string; +}> = ({ message, showWorkflowButton, showFeedbackActions, handleViewWorkflowClick, sourcesElement, textContentOverride }) => { const { configCollectFeedback, submittedFeedback, handleFeedbackSubmit, addNotification } = useChatContext(); const [isFeedbackModalOpen, setIsFeedbackModalOpen] = useState(false); const [feedbackType, setFeedbackType] = useState<"up" | "down" | null>(null); @@ -55,7 +65,7 @@ const MessageActions: React.FC<{ const shouldShowFeedback = showFeedbackActions && configCollectFeedback; - if (!showWorkflowButton && !shouldShowFeedback) { + if (!showWorkflowButton && !shouldShowFeedback && !sourcesElement) { return null; } @@ -74,7 +84,8 @@ const MessageActions: React.FC<{
)} - + + {sourcesElement &&
{sourcesElement}
}

{feedbackType && } @@ -82,9 +93,40 @@ const MessageActions: React.FC<{ ); }; -const MessageContent = React.memo<{ message: MessageFE }>(({ message }) => { +/** + * Transform technical workflow error messages into user-friendly ones + */ +const getUserFriendlyErrorMessage = (technicalMessage: string): string => { + // Pattern: "Workflow failed: Node 'X' failed: Node execution error: No FilePart found in message for structured schema" + if (technicalMessage.includes("No FilePart found in message for structured schema")) { + return "This workflow requires a file to be uploaded. Please attach a file and try again."; + } + + // Pattern: "Workflow failed: Node 'X' failed: Node execution error: ..." + if (technicalMessage.includes("Workflow failed:") && technicalMessage.includes("Node execution error:")) { + const match = technicalMessage.match(/Node execution error:\s*(.+)$/); + if (match) { + return `The workflow encountered an error: ${match[1]}`; + } + } + + // Pattern: "Workflow failed: ..." + if (technicalMessage.startsWith("Workflow failed:")) { + return technicalMessage.replace(/^Workflow failed:\s*/, "The workflow encountered an error: "); + } + + // Pattern: Generic task failure + if (technicalMessage.toLowerCase().includes("task failed")) { + return "The request could not be completed. Please try again or contact support."; + } + + // Default: return the original message if no pattern matches + return technicalMessage; +}; + +const MessageContent = React.memo<{ message: MessageFE; isStreaming?: boolean }>(({ message, isStreaming }) => { const [renderError, setRenderError] = useState(null); - const { sessionId } = useChatContext(); + const { sessionId, ragData, openSidePanelTab, setTaskIdInSidePanel } = useChatContext(); // Extract text content from message parts const textContent = @@ -96,25 +138,64 @@ const MessageContent = React.memo<{ message: MessageFE }>(({ message }) => { // Trim text for user messages to prevent trailing whitespace issues const displayText = message.isUser ? textContent.trim() : textContent; - const renderContent = () => { - if (message.isError) { - return ( -
- - {displayText} -
- ); + // Parse citations from text and match to RAG sources + // Aggregate sources from ALL RAG entries for this task, not just the last one. + // When there are multiple web searches (multiple tool calls), each creates a separate RAG entry. + const taskRagData = useMemo(() => { + if (!message.taskId || !ragData) return undefined; + const matches = ragData.filter(r => r.taskId === message.taskId); + if (matches.length === 0) return undefined; + + // If only one entry, return it directly + if (matches.length === 1) return matches[0]; + + // Aggregate all sources from all matching RAG entries + // Use the last entry as the base (for query, title, etc.) but combine all sources + const lastEntry = matches[matches.length - 1]; + const allSources = matches.flatMap(r => r.sources || []); + + // Deduplicate sources by citationId (keep the first occurrence) + const seenCitationIds = new Set(); + const uniqueSources = allSources.filter(source => { + const citationId = source.citationId; + if (!citationId || seenCitationIds.has(citationId)) { + return false; + } + seenCitationIds.add(citationId); + return true; + }); + + return { + ...lastEntry, + sources: uniqueSources, + }; + }, [message.taskId, ragData]); + + const citations = useMemo(() => { + if (message.isUser) return []; + return parseCitations(displayText, taskRagData); + }, [displayText, taskRagData, message.isUser]); + + const handleCitationClick = () => { + // Open RAG panel when citation is clicked + if (message.taskId) { + setTaskIdInSidePanel(message.taskId); + openSidePanelTab("rag"); } + }; + + // Extract embedded content and compute modified text at component level + const embeddedContent = useMemo(() => extractEmbeddedContent(displayText), [displayText]); - const embeddedContent = extractEmbeddedContent(displayText); + const { modifiedText, contentElements } = useMemo(() => { if (embeddedContent.length === 0) { - return {displayText}; + return { modifiedText: displayText, contentElements: [] }; } - let modifiedText = displayText; - const contentElements: ReactNode[] = []; + let modText = displayText; + const elements: ReactNode[] = []; embeddedContent.forEach((item: ExtractedContent, index: number) => { - modifiedText = modifiedText.replace(item.originalMatch, ""); + modText = modText.replace(item.originalMatch, ""); if (item.type === "file") { const fileAttachment: FileAttachment = { @@ -122,7 +203,7 @@ const MessageContent = React.memo<{ message: MessageFE }>(({ message }) => { content: item.content, mime_type: item.mimeType, }; - contentElements.push( + elements.push(
downloadFile(fileAttachment, sessionId)} isEmbedded={true} />
@@ -130,19 +211,57 @@ const MessageContent = React.memo<{ message: MessageFE }>(({ message }) => { } else if (!RENDER_TYPES_WITH_RAW_CONTENT.includes(item.type)) { const finalContent = decodeBase64Content(item.content); if (finalContent) { - contentElements.push( + elements.push(
- +
); } } }); + return { modifiedText: modText, contentElements: elements }; + }, [embeddedContent, displayText, sessionId, setRenderError, taskRagData]); + + // Parse citations from modified text + const modifiedCitations = useMemo(() => { + if (message.isUser) return []; + return parseCitations(modifiedText, taskRagData); + }, [modifiedText, taskRagData, message.isUser]); + + const renderContent = () => { + if (message.isError) { + const friendlyErrorMessage = getUserFriendlyErrorMessage(displayText); + return ( +
+ + {friendlyErrorMessage} +
+ ); + } + + if (embeddedContent.length === 0) { + // Use MarkdownWrapper for streaming (smooth animation), TextWithCitations otherwise (citation support) + if (isStreaming) { + return ; + } + // Render text with citations if any exist + if (citations.length > 0) { + return ; + } + return {displayText}; + } + return (
{renderError && } - {modifiedText} + {isStreaming ? ( + + ) : modifiedCitations.length > 0 ? ( + + ) : ( + {modifiedText} + )} {contentElements}
); @@ -177,8 +296,40 @@ const getUploadedFiles = (message: MessageFE) => { return null; }; -const getChatBubble = (message: MessageFE, chatContext: ChatContextValue, isLastWithTaskId?: boolean) => { - const { openSidePanelTab, setTaskIdInSidePanel } = chatContext; +interface DeepResearchReportInfo { + artifact: ArtifactInfo; + sessionId: string; + ragData?: RAGSearchResult; +} + +// Component to render deep research report with TTS support +const DeepResearchReportBubble: React.FC<{ + deepResearchReportInfo: DeepResearchReportInfo; + message: MessageFE; + onContentLoaded?: (content: string) => void; +}> = ({ deepResearchReportInfo, message, onContentLoaded }) => { + return ( + + + + + + + + ); +}; + +const getChatBubble = ( + message: MessageFE, + chatContext: ChatContextValue, + isLastWithTaskId?: boolean, + isStreaming?: boolean, + sourcesElement?: React.ReactNode, + deepResearchReportInfo?: DeepResearchReportInfo, + onReportContentLoaded?: (content: string) => void, + reportContentOverride?: string +): React.ReactNode => { + const { openSidePanelTab, setTaskIdInSidePanel, ragData } = chatContext; if (message.isStatusBubble) { return null; @@ -188,6 +339,43 @@ const getChatBubble = (message: MessageFE, chatContext: ChatContextValue, isLast return ; } + // Check for deep research progress data + const progressPart = message.parts?.find(p => p.kind === "data") as DataPart | undefined; + const hasDeepResearchProgress = progressPart?.data && (progressPart.data as { type?: string }).type === "deep_research_progress"; + + // Show progress block at the top if we have progress data and research is not complete + if (hasDeepResearchProgress && !message.isComplete) { + const data = progressPart!.data as unknown as ResearchProgressData; + const taskRagData = ragData?.filter(r => r.taskId === message.taskId); + const hasOtherContent = message.parts?.some(p => (p.kind === "text" && (p as TextPart).text.trim()) || p.kind === "artifact" || p.kind === "file"); + + // Always show progress block for active research (before completion) + const progressBlock = ( +
+ +
+ ); + + // If this is progress-only (no other content), just return the progress block + if (!hasOtherContent) { + return progressBlock; + } + + // If there's other content, show progress block first, then the rest + // Create a new message without the progress data part to avoid infinite recursion + const messageWithoutProgress = { + ...message, + parts: message.parts?.filter(p => p.kind !== "data"), + }; + + return ( + <> + {progressBlock} + {getChatBubble(messageWithoutProgress, chatContext, isLastWithTaskId)} + + ); + } + // Group contiguous parts to handle interleaving of text and files const groupedParts: (TextPart | FilePart | ArtifactPart)[] = []; let currentTextGroup = ""; @@ -216,15 +404,28 @@ const getChatBubble = (message: MessageFE, chatContext: ChatContextValue, isLast const showWorkflowButton = !message.isUser && message.isComplete && !!message.taskId && !!isLastWithTaskId; const showFeedbackActions = !message.isUser && message.isComplete && !!message.taskId && !!isLastWithTaskId; + // Debug logging for error messages + if (message.isError) { + console.log('[ChatMessage] Error message debug:', { + isUser: message.isUser, + isComplete: message.isComplete, + taskId: message.taskId, + isLastWithTaskId: isLastWithTaskId, + showWorkflowButton, + showFeedbackActions, + parts: message.parts, + }); + } + const handleViewWorkflowClick = () => { if (message.taskId) { setTaskIdInSidePanel(message.taskId); - openSidePanelTab("workflow"); + openSidePanelTab("activity"); } }; // Helper function to render artifact/file parts - const renderArtifactOrFilePart = (part: ArtifactPart | FilePart, index: number) => { + const renderArtifactOrFilePart = (part: ArtifactPart | FilePart, index: number, isStreamingPart?: boolean) => { // Create unique key for expansion state using taskId (or messageId) + filename const uniqueKey = message.taskId ? `${message.taskId}-${part.kind === "file" ? (part as FilePart).file.name : (part as ArtifactPart).name}` @@ -244,17 +445,17 @@ const getChatBubble = (message: MessageFE, chatContext: ChatContextValue, isLast } else if ("uri" in fileInfo && fileInfo.uri) { attachment.uri = fileInfo.uri; } - return ; + return ; } if (part.kind === "artifact") { const artifactPart = part as ArtifactPart; switch (artifactPart.status) { case "completed": - return ; + return ; case "in-progress": - return ; + return ; case "failed": - return ; + return ; default: return null; } @@ -271,6 +472,7 @@ const getChatBubble = (message: MessageFE, chatContext: ChatContextValue, isLast {/* Render parts in their original order to preserve interleaving */} {groupedParts.map((part, index) => { const isLastPart = index === lastPartIndex; + const shouldStream = isStreaming && isLastPart; if (part.kind === "text") { // Skip rendering empty or whitespace-only text parts @@ -280,7 +482,13 @@ const getChatBubble = (message: MessageFE, chatContext: ChatContextValue, isLast if (isLastPart && (showWorkflowButton || showFeedbackActions)) { return (
- +
); } @@ -290,22 +498,41 @@ const getChatBubble = (message: MessageFE, chatContext: ChatContextValue, isLast return ( - + {/* Show actions on the last part if it's text */} - {isLastPart && } + {isLastPart && ( + + )} ); } else if (part.kind === "artifact" || part.kind === "file") { - return renderArtifactOrFilePart(part, index); + return renderArtifactOrFilePart(part, index, shouldStream); } return null; })} + {/* Show deep research report content inline (without References and Methodology sections) */} + {deepResearchReportInfo && } + {/* Show actions after artifacts if the last part is an artifact */} {lastPartKind === "artifact" || lastPartKind === "file" ? (
- +
) : null} @@ -318,15 +545,227 @@ const getChatBubble = (message: MessageFE, chatContext: ChatContextValue, isLast
); }; -export const ChatMessage: React.FC<{ message: MessageFE; isLastWithTaskId?: boolean }> = ({ message, isLastWithTaskId }) => { +export const ChatMessage: React.FC<{ message: MessageFE; isLastWithTaskId?: boolean; isStreaming?: boolean }> = ({ message, isLastWithTaskId, isStreaming }) => { const chatContext = useChatContext(); + const { ragData, openSidePanelTab, setTaskIdInSidePanel, artifacts, sessionId } = chatContext; + + // State to track deep research report content for message actions functionality + const [reportContent, setReportContent] = useState(null); + + // Get RAG metadata for this task + const taskRagData = useMemo(() => { + if (!message?.taskId || !ragData) return undefined; + return ragData.filter(r => r.taskId === message.taskId); + }, [message?.taskId, ragData]); + + // Find deep research report artifact in the message + const deepResearchReportArtifact = useMemo(() => { + if (!message) return null; + + // Check if this is a completed deep research message + const hasProgressPart = message.parts?.some(p => { + if (p.kind === "data") { + const data = (p as DataPart).data as unknown as ResearchProgressData; + return data?.type === "deep_research_progress"; + } + return false; + }); + + const hasRagSources = taskRagData && taskRagData.length > 0 && taskRagData.some(r => r.sources && r.sources.length > 0); + const hasDeepResearchRagData = taskRagData?.some(r => r.searchType === "deep_research"); + const isDeepResearchComplete = message.isComplete && (hasProgressPart || hasDeepResearchRagData) && hasRagSources; + + if (!isDeepResearchComplete || !isLastWithTaskId) return null; + + // Look for artifact parts in the message that match deep research report pattern + const artifactParts = message.parts?.filter(p => p.kind === "artifact") as ArtifactPart[] | undefined; + + // First priority: Find the report artifact from this message's artifact parts + // This ensures we get the correct report for this specific task + if (artifactParts && artifactParts.length > 0) { + for (const part of artifactParts) { + if (part.status === "completed" && isDeepResearchReportFilename(part.name)) { + const fullArtifact = artifacts.find(a => a.filename === part.name); + if (fullArtifact) { + return fullArtifact; + } + } + } + } + + // Second priority: Use artifact filename from RAG metadata + // The backend stores the artifact filename in the RAG metadata for this purpose + if (taskRagData && taskRagData.length > 0) { + // Get the last RAG data entry which should have the artifact filename + const lastRagData = taskRagData[taskRagData.length - 1]; + const artifactFilenameFromRag = lastRagData.metadata?.artifactFilename as string | undefined; + if (artifactFilenameFromRag) { + const matchedArtifact = artifacts.find(a => a.filename === artifactFilenameFromRag); + if (matchedArtifact) { + return matchedArtifact; + } + } + } + // Only use global artifacts list if there's exactly one report artifact + // This handles edge cases but avoids showing the wrong report when there are multiple + const allReportArtifacts = artifacts.filter(a => isDeepResearchReportFilename(a.filename)); + if (allReportArtifacts.length === 1) { + return allReportArtifacts[0]; + } + + // If there are multiple report artifacts and we couldn't find one, + // don't show any inline report to avoid showing the wrong one + return null; + }, [message, isLastWithTaskId, artifacts, taskRagData, sessionId]); + + // Get the last RAG data entry for this task (for citations in report) + const lastTaskRagData = useMemo(() => { + if (!taskRagData || taskRagData.length === 0) return undefined; + return taskRagData[taskRagData.length - 1]; + }, [taskRagData]); + + // Early return after all hooks if (!message) { return null; } + + // Check if this is a completed deep research message + // Check both for progress data part (during session) and ragData search_type (after refresh) + const hasProgressPart = message.parts?.some(p => { + if (p.kind === "data") { + const data = (p as DataPart).data as unknown as ResearchProgressData; + return data?.type === "deep_research_progress"; + } + return false; + }); + + const hasRagSources = taskRagData && taskRagData.length > 0 && taskRagData.some(r => r.sources && r.sources.length > 0); + + // Check if ragData indicates deep research + const hasDeepResearchRagData = taskRagData?.some(r => r.searchType === "deep_research"); + + const isDeepResearchComplete = message.isComplete && (hasProgressPart || hasDeepResearchRagData) && hasRagSources; + + // Check if this is a completed web search message (has web_search sources but not deep research) + const isWebSearchComplete = message.isComplete && !isDeepResearchComplete && hasRagSources && taskRagData?.some(r => r.searchType === "web_search"); + + // Handler for sources click (works for both deep research and web search) + const handleSourcesClick = () => { + if (message.taskId) { + setTaskIdInSidePanel(message.taskId); + openSidePanelTab("rag"); + } + }; + return ( <> - {getChatBubble(message, chatContext, isLastWithTaskId)} + {/* Show progress block at the top for completed deep research - only for the last message with this taskId */} + {isDeepResearchComplete && + hasRagSources && + isLastWithTaskId && + (() => { + // Filter to only show fetched sources (not snippets) + const allSources = taskRagData.flatMap(r => r.sources); + const fetchedSources = allSources.filter(source => { + const wasFetched = source.metadata?.fetched === true || source.metadata?.fetch_status === "success" || (source.contentPreview && source.contentPreview.includes("[Full Content Fetched]")); + return wasFetched; + }); + + return ( +
+ +
+ ); + })()} + {getChatBubble( + message, + chatContext, + isLastWithTaskId, + isStreaming, + // Show sources element for both deep research and web search (in message actions area) + !message.isUser && (isDeepResearchComplete || isWebSearchComplete) && hasRagSources + ? (() => { + const allSources = taskRagData.flatMap(r => r.sources); + + // For deep research: filter to only show fetched sources (not snippets) + // For web search: show all sources including images (images with source links will be shown) + const sourcesToShow = isDeepResearchComplete + ? allSources.filter(source => { + const sourceType = source.sourceType || "web"; + // For images in deep research: include if they have a source link + if (sourceType === "image") { + return source.sourceUrl || source.metadata?.link; + } + const wasFetched = source.metadata?.fetched === true || source.metadata?.fetch_status === "success" || (source.contentPreview && source.contentPreview.includes("[Full Content Fetched]")); + return wasFetched; + }) + : allSources.filter(source => { + const sourceType = source.sourceType || "web"; + // For images in web search: include if they have a source link + if (sourceType === "image") { + return source.sourceUrl || source.metadata?.link; + } + return true; + }); + + // Only render if we have sources + if (sourcesToShow.length === 0) return null; + + return ; + })() + : undefined, + // Pass deep research report info if available + isDeepResearchComplete && isLastWithTaskId && deepResearchReportArtifact && sessionId ? { artifact: deepResearchReportArtifact, sessionId, ragData: lastTaskRagData } : undefined, + // Callback to capture report content for TTS/copy + setReportContent, + // Pass report content to MessageActions for TTS/copy + reportContent || undefined + )} + + {/* Render images separately at the end for web search */} + {!message.isUser && + isWebSearchComplete && + hasRagSources && + (() => { + const allSources = taskRagData.flatMap(r => r.sources); + const imageResults = allSources + .filter(source => { + const sourceType = source.sourceType || "web"; + return sourceType === "image" && source.metadata?.imageUrl; + }) + .map(source => ({ + imageUrl: source.metadata!.imageUrl, + title: source.metadata?.title || source.filename, + link: source.sourceUrl || source.metadata?.link || source.metadata!.imageUrl, + })); + + if (imageResults.length > 0) { + return ( +
+ +
+ ); + } + return null; + })()} + {getUploadedFiles(message)} ); diff --git a/client/webui/frontend/src/lib/components/chat/ChatSidePanel.tsx b/client/webui/frontend/src/lib/components/chat/ChatSidePanel.tsx index 6be0255c5..f356b172c 100644 --- a/client/webui/frontend/src/lib/components/chat/ChatSidePanel.tsx +++ b/client/webui/frontend/src/lib/components/chat/ChatSidePanel.tsx @@ -1,14 +1,16 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; -import { PanelRightIcon, FileText, Network, RefreshCw } from "lucide-react"; +import { PanelRightIcon, FileText, Network, RefreshCw, Link2 } from "lucide-react"; import { Button, Tabs, TabsList, TabsTrigger, TabsContent } from "@/lib/components/ui"; import { useTaskContext, useChatContext } from "@/lib/hooks"; import { FlowChartPanel, processTaskForVisualization } from "@/lib/components/activities"; import type { VisualizedTask } from "@/lib/types"; +import { hasSourcesWithUrls } from "@/lib/utils"; import { ArtifactPanel } from "./artifact/ArtifactPanel"; import { FlowChartDetails } from "../activities/FlowChartDetails"; +import { RAGInfoPanel } from "./rag/RAGInfoPanel"; interface ChatSidePanelProps { onCollapsedToggle: (isSidePanelCollapsed: boolean) => void; @@ -18,7 +20,7 @@ interface ChatSidePanelProps { } export const ChatSidePanel: React.FC = ({ onCollapsedToggle, isSidePanelCollapsed, setIsSidePanelCollapsed, isSidePanelTransitioning }) => { - const { activeSidePanelTab, setActiveSidePanelTab, setPreviewArtifact, taskIdInSidePanel } = useChatContext(); + const { activeSidePanelTab, setActiveSidePanelTab, setPreviewArtifact, taskIdInSidePanel, ragData, ragEnabled } = useChatContext(); const { isReconnecting, isTaskMonitorConnecting, isTaskMonitorConnected, monitoredTasks, connectTaskMonitorStream, loadTaskFromBackend } = useTaskContext(); const [visualizedTask, setVisualizedTask] = useState(null); const [isLoadingTask, setIsLoadingTask] = useState(false); @@ -26,7 +28,10 @@ export const ChatSidePanel: React.FC = ({ onCollapsedToggle, // Track which task IDs we've already attempted to load to prevent duplicate loads const loadAttemptedRef = React.useRef>(new Set()); - // Process task data for visualization when the selected workflow task ID changes + // Check if there are any sources in the current session (web sources or deep research sources) + const hasSourcesInSession = useMemo(() => hasSourcesWithUrls(ragData), [ragData]); + + // Process task data for visualization when the selected activity task ID changes // or when monitoredTasks is updated with new data useEffect(() => { if (!taskIdInSidePanel) { @@ -96,11 +101,11 @@ export const ChatSidePanel: React.FC = ({ onCollapsedToggle, } }, [taskIdInSidePanel]); - // Helper function to determine what to display in the workflow panel - const getWorkflowPanelContent = () => { + // Helper function to determine what to display in the activity panel + const getActivityPanelContent = () => { if (isLoadingTask) { return { - message: "Loading workflow data...", + message: "Loading activity data...", showButton: false, }; } @@ -130,7 +135,7 @@ export const ChatSidePanel: React.FC = ({ onCollapsedToggle, if (!visualizedTask) { return { - message: "No workflow data available for the selected task", + message: "No activity data available for the selected task", showButton: false, }; } @@ -144,7 +149,7 @@ export const ChatSidePanel: React.FC = ({ onCollapsedToggle, onCollapsedToggle(newCollapsed); }; - const handleTabClick = (tab: "files" | "workflow") => { + const handleTabClick = (tab: "files" | "activity" | "rag") => { if (tab === "files") { setPreviewArtifact(null); } @@ -152,7 +157,7 @@ export const ChatSidePanel: React.FC = ({ onCollapsedToggle, setActiveSidePanelTab(tab); }; - const handleIconClick = (tab: "files" | "workflow") => { + const handleIconClick = (tab: "files" | "activity" | "rag") => { if (isSidePanelCollapsed) { setIsSidePanelCollapsed(false); onCollapsedToggle?.(false); @@ -175,9 +180,15 @@ export const ChatSidePanel: React.FC = ({ onCollapsedToggle, - + + {hasSourcesInSession && ( + + )}
); } @@ -186,29 +197,39 @@ export const ChatSidePanel: React.FC = ({ onCollapsedToggle, return (
- handleTabClick(value as "files" | "workflow")} className="flex h-full flex-col"> -
- - + setPreviewArtifact(null)} > - - Files + + Files - - Workflow + + Activity + {hasSourcesInSession && ( + + + Sources + + )}
@@ -218,10 +239,10 @@ export const ChatSidePanel: React.FC = ({ onCollapsedToggle,
- +
{(() => { - const emptyStateContent = getWorkflowPanelContent(); + const emptyStateContent = getActivityPanelContent(); if (!emptyStateContent && visualizedTask) { return ( @@ -236,7 +257,7 @@ export const ChatSidePanel: React.FC = ({ onCollapsedToggle,
-
Workflow
+
Activity
{emptyStateContent?.message}
{emptyStateContent?.showButton && (
@@ -256,6 +277,14 @@ export const ChatSidePanel: React.FC = ({ onCollapsedToggle, })()}
+ + {hasSourcesInSession && ( + +
+ r.taskId === taskIdInSidePanel) : ragData} enabled={ragEnabled} /> +
+
+ )}
diff --git a/client/webui/frontend/src/lib/components/chat/Citation.tsx b/client/webui/frontend/src/lib/components/chat/Citation.tsx new file mode 100644 index 000000000..1a7191120 --- /dev/null +++ b/client/webui/frontend/src/lib/components/chat/Citation.tsx @@ -0,0 +1,552 @@ +/** + * Citation component for displaying clickable source citations + */ +import React, { useState, useMemo, useRef, useCallback, type ReactNode } from "react"; +import DOMPurify from "dompurify"; +import { marked } from "marked"; +import parse, { type HTMLReactParserOptions, type DOMNode, Element, Text as DomText } from "html-react-parser"; +import type { Citation as CitationType } from "@/lib/utils/citations"; +import { getCitationTooltip, INDIVIDUAL_CITATION_PATTERN } from "@/lib/utils/citations"; +import { MarkdownHTMLConverter } from "@/lib/components"; +import { getThemeHtmlStyles } from "@/lib/utils/themeHtmlStyles"; +import { getSourceUrl } from "@/lib/utils/sourceUrlHelpers"; +import { Popover, PopoverContent, PopoverTrigger } from "@/lib/components/ui/popover"; +import { ExternalLink } from "lucide-react"; + +interface CitationProps { + citation: CitationType; + onClick?: (citation: CitationType) => void; + maxLength?: number; +} + +/** + * Truncate text to fit within maxLength, adding ellipsis if needed + */ +function truncateText(text: string, maxLength: number): string { + if (text.length <= maxLength) { + return text; + } + return text.substring(0, maxLength - 3) + "..."; +} + +/** + * Extract clean filename from file_id by removing session prefix if present + * Same logic as RAGInfoPanel, but only applies if the filename has the session pattern + */ +function extractFilename(filename: string): string { + // Check if this looks like a session-prefixed filename + const hasSessionPrefix = filename.includes("web-session-") || filename.startsWith("sam_dev_user_"); + + // If it doesn't have a session prefix, return as-is + if (!hasSessionPrefix) { + return filename; + } + + // The pattern is: sam_dev_user_web-session-{uuid}_{actual_filename}_v{version}.pdf + // We need to extract just the {actual_filename}.pdf part + + // First, remove the .pdf extension at the very end (added by backend) + let cleaned = filename.replace(/\.pdf$/, ""); + + // Remove the version suffix (_v0, _v1, etc.) + cleaned = cleaned.replace(/_v\d+$/, ""); + + // Now we have: sam_dev_user_web-session-{uuid}_{actual_filename} + // Find the pattern "web-session-{uuid}_" and remove everything before and including it + const sessionPattern = /^.*web-session-[a-f0-9-]+_/; + cleaned = cleaned.replace(sessionPattern, ""); + + // Add back the .pdf extension + return cleaned + ".pdf"; +} + +/** + * Get display text for citation (filename or URL) + */ +function getCitationDisplayText(citation: CitationType, maxLength: number = 30): string { + // For web search citations, try to extract domain name even without full source data + const isWebSearch = citation.source?.metadata?.type === "web_search" || citation.type === "search"; + + if (isWebSearch && citation.source?.sourceUrl) { + try { + const url = new URL(citation.source.sourceUrl); + const domain = url.hostname.replace(/^www\./, ""); + return truncateText(domain, maxLength); + } catch { + // If URL parsing fails, fall through to other methods + } + } + + // Check if source has a URL in metadata + if (citation.source?.metadata?.link) { + try { + const url = new URL(citation.source.metadata.link); + const domain = url.hostname.replace(/^www\./, ""); + return truncateText(domain, maxLength); + } catch { + // If URL parsing fails, continue + } + } + + // If no source data but it's a search citation, try to infer from citation type + if (!citation.source && citation.type === "search") { + // For search citations without source data, show a more descriptive label + return `Web Source ${citation.sourceId + 1}`; + } + + if (!citation.source) { + return `Source ${citation.sourceId + 1}`; + } + + // The filename field contains the original filename (not the temp path) + // The source_url field contains the temp path (not useful for display) + if (citation.source.filename) { + // For KB search, filename already contains the original name + // For file search, it might have session prefix that needs extraction + const hasSessionPrefix = citation.source.filename.includes("web-session-") || citation.source.filename.startsWith("sam_dev_user_"); + + const displayName = hasSessionPrefix ? extractFilename(citation.source.filename) : citation.source.filename; + + return truncateText(displayName, maxLength); + } + + // Fallback to source URL if no filename + if (citation.source.sourceUrl) { + // Try to extract domain name or filename from URL + try { + const url = new URL(citation.source.sourceUrl); + const domain = url.hostname.replace(/^www\./, ""); + return truncateText(domain, maxLength); + } catch { + // If URL parsing fails, try to extract filename + const filename = citation.source.sourceUrl.split("/").pop() || citation.source.sourceUrl; + return truncateText(filename, maxLength); + } + } + + return `Source ${citation.sourceId + 1}`; +} + +export function Citation({ citation, onClick, maxLength = 30 }: CitationProps) { + const displayText = getCitationDisplayText(citation, maxLength); + const tooltip = getCitationTooltip(citation); + + // Check if this is a web search or deep research citation with a URL + const { url: sourceUrl, sourceType } = getSourceUrl(citation.source); + const isWebSearch = sourceType === "web_search" || citation.type === "search"; + const isDeepResearch = sourceType === "deep_research" || citation.type === "research"; + const hasClickableUrl = (isWebSearch || isDeepResearch) && sourceUrl; + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // For web search and deep research citations with URLs, open the URL directly + if (hasClickableUrl) { + window.open(sourceUrl, "_blank", "noopener,noreferrer"); + return; + } + + // For RAG citations, use onClick handler (to open RAG panel) + if (onClick) { + onClick(citation); + } + }; + + return ( + + ); +} + +/** + * Bundled Citations Component + * Displays multiple citations grouped together at the end of a paragraph + * If only one citation, shows it as a regular citation badge + * If multiple, shows first citation name with "+X" in the same bubble + */ +interface BundledCitationsProps { + citations: CitationType[]; + onCitationClick?: (citation: CitationType) => void; +} + +export function BundledCitations({ citations, onCitationClick }: BundledCitationsProps) { + const [isDark, setIsDark] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const timeoutRef = useRef | null>(null); + const showTimeout = 150; + const hideTimeout = 150; + + // Detect dark mode + React.useEffect(() => { + const checkDarkMode = () => { + setIsDark(document.documentElement.classList.contains("dark")); + }; + + checkDarkMode(); + + const observer = new MutationObserver(checkDarkMode); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }); + + return () => observer.disconnect(); + }, []); + + // Cleanup timeout on unmount + React.useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + const handleMouseEnter = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + setIsOpen(true); + }, showTimeout); + }, []); + + const handleMouseLeave = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + setIsOpen(false); + }, hideTimeout); + }, []); + + const handleContentMouseEnter = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }, []); + + const handleContentMouseLeave = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + setIsOpen(false); + }, hideTimeout); + }, []); + + if (citations.length === 0) return null; + + // Get unique citations (deduplicate by sourceId) + const uniqueCitations = citations.filter((citation, index, self) => index === self.findIndex(c => c.sourceId === citation.sourceId && c.type === citation.type)); + + // If only one citation, render it as a regular citation badge + if (uniqueCitations.length === 1) { + return ; + } + + // Multiple citations - show first citation name + "+X" in same bubble + const firstCitation = uniqueCitations[0]; + const remainingCount = uniqueCitations.length - 1; + const firstDisplayText = getCitationDisplayText(firstCitation, 20); + const tooltip = getCitationTooltip(firstCitation); + + // Check if this is a web search or deep research citation + const { url: sourceUrl, sourceType } = getSourceUrl(firstCitation.source); + const isWebSearch = sourceType === "web_search" || firstCitation.type === "search"; + const isDeepResearch = sourceType === "deep_research" || firstCitation.type === "research"; + const hasClickableUrl = (isWebSearch || isDeepResearch) && sourceUrl; + + const handleFirstCitationClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // For web search and deep research citations, open the URL directly + if (hasClickableUrl && sourceUrl) { + window.open(sourceUrl, "_blank", "noopener,noreferrer"); + return; + } + + // For RAG citations, use onClick handler (to open RAG panel) + if (onCitationClick) { + onCitationClick(firstCitation); + } + }; + + return ( + + + + + +
+
+

All Sources · {uniqueCitations.length}

+
+ {uniqueCitations.map((citation, index) => { + const displayText = getCitationDisplayText(citation, 50); + const { url: sourceUrl, sourceType } = getSourceUrl(citation.source); + const isWebSearch = sourceType === "web_search" || citation.type === "search"; + const isDeepResearch = sourceType === "deep_research" || citation.type === "research"; + const hasClickableUrl = (isWebSearch || isDeepResearch) && sourceUrl; + + // Get favicon for web sources (both web search and deep research) + let favicon = null; + if ((isWebSearch || isDeepResearch) && sourceUrl) { + try { + const url = new URL(sourceUrl); + const domain = url.hostname; + favicon = `https://www.google.com/s2/favicons?domain=${domain}&sz=32`; + } catch { + // Ignore favicon errors + } + } + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (hasClickableUrl && sourceUrl) { + window.open(sourceUrl, "_blank", "noopener,noreferrer"); + } else if (onCitationClick) { + onCitationClick(citation); + } + }; + + return ( + + ); + })} +
+
+
+ ); +} + +/** + * Component to render text with embedded citations + */ +interface TextWithCitationsProps { + text: string; + citations: CitationType[]; + onCitationClick?: (citation: CitationType) => void; +} + +/** + * Parse a citation ID and return its components + * Handles both formats: + * - s{turn}r{index} (e.g., "s0r0", "s1r2") -> type: "search" + * - research{N} (e.g., "research0") -> type: "research" + */ +function parseCitationIdLocal(citationId: string): { type: "search" | "research"; sourceId: number } | null { + // Try sTrN format first + const searchMatch = citationId.match(/^s(\d+)r(\d+)$/); + if (searchMatch) { + return { + type: "search", + sourceId: parseInt(searchMatch[2], 10), // Use result index as sourceId + }; + } + + // Try research format + const researchMatch = citationId.match(/^research(\d+)$/); + if (researchMatch) { + return { + type: "research", + sourceId: parseInt(researchMatch[1], 10), + }; + } + + return null; +} + +/** + * Parse individual citations from a comma-separated content string + * Supports: s0r0, s1r2, research0, research1 + */ +function parseMultiCitationIds(content: string): Array<{ type: "search" | "research"; sourceId: number; citationId: string }> { + const results: Array<{ type: "search" | "research"; sourceId: number; citationId: string }> = []; + let individualMatch; + + INDIVIDUAL_CITATION_PATTERN.lastIndex = 0; + while ((individualMatch = INDIVIDUAL_CITATION_PATTERN.exec(content)) !== null) { + const citationId = individualMatch[1]; // The captured citation ID (s0r0 or research0) + const parsed = parseCitationIdLocal(citationId); + + if (parsed) { + results.push({ + type: parsed.type, + sourceId: parsed.sourceId, + citationId: citationId, + }); + } + } + + return results; +} + +/** + * Combined pattern that matches both single and multi-citation formats + * This ensures we process them in order of appearance + * Supports: s0r0, s1r2, research0, research1 + */ +const COMBINED_CITATION_PATTERN = /\[?\[cite:((?:s\d+r\d+|research\d+)(?:\s*,\s*(?:cite:)?(?:s\d+r\d+|research\d+))*)\]\]?/g; + +/** + * Process text node content to replace citation markers with React components + */ +function processTextWithCitations(textContent: string, citations: CitationType[], onCitationClick?: (citation: CitationType) => void): ReactNode[] { + const result: ReactNode[] = []; + let lastIndex = 0; + let match; + let pendingCitations: CitationType[] = []; + + // Reset regex + COMBINED_CITATION_PATTERN.lastIndex = 0; + + while ((match = COMBINED_CITATION_PATTERN.exec(textContent)) !== null) { + // Add text before citation + if (match.index > lastIndex) { + // Flush pending citations before text + if (pendingCitations.length > 0) { + result.push(); + pendingCitations = []; + } + result.push(textContent.substring(lastIndex, match.index)); + } + + // Parse the citation content (could be single or comma-separated) + const [, content] = match; + const citationIds = parseMultiCitationIds(content); + + for (const { citationId } of citationIds) { + // Look up by citationId (e.g., "s0r0" or "research0") + const citation = citations.find(c => c.citationId === citationId); + + if (citation) { + pendingCitations.push(citation); + } + } + + lastIndex = match.index + match[0].length; + } + + // Add remaining text + if (lastIndex < textContent.length) { + // Flush pending citations before remaining text + if (pendingCitations.length > 0) { + result.push(); + pendingCitations = []; + } + result.push(textContent.substring(lastIndex)); + } else if (pendingCitations.length > 0) { + // Flush any remaining citations at the end + result.push(); + } + + return result; +} + +export function TextWithCitations({ text, citations, onCitationClick }: TextWithCitationsProps) { + // Create parser options to process text nodes and replace citation markers + const parserOptions: HTMLReactParserOptions = useMemo( + () => ({ + replace: (domNode: DOMNode) => { + // Process text nodes to find and replace citation markers + if (domNode.type === "text" && domNode instanceof DomText) { + const textContent = domNode.data; + + // Check if this text contains citation markers (single or multi) + COMBINED_CITATION_PATTERN.lastIndex = 0; + if (COMBINED_CITATION_PATTERN.test(textContent)) { + COMBINED_CITATION_PATTERN.lastIndex = 0; + const processed = processTextWithCitations(textContent, citations, onCitationClick); + if (processed.length > 0) { + return <>{processed}; + } + } + } + + // Handle links - add target blank + if (domNode instanceof Element && domNode.name === "a") { + domNode.attribs.target = "_blank"; + domNode.attribs.rel = "noopener noreferrer"; + } + + return undefined; + }, + }), + [citations, onCitationClick] + ); + + if (citations.length === 0) { + return {text}; + } + + try { + // Convert markdown to HTML + const rawHtml = marked.parse(text, { gfm: true }) as string; + const cleanHtml = DOMPurify.sanitize(rawHtml, { USE_PROFILES: { html: true } }); + + // Parse HTML and inject citations + const reactElements = parse(cleanHtml, parserOptions); + + return
{reactElements}
; + } catch { + return {text}; + } +} diff --git a/client/webui/frontend/src/lib/components/chat/ConnectionRequiredModal.tsx b/client/webui/frontend/src/lib/components/chat/ConnectionRequiredModal.tsx new file mode 100644 index 000000000..18ce86817 --- /dev/null +++ b/client/webui/frontend/src/lib/components/chat/ConnectionRequiredModal.tsx @@ -0,0 +1,57 @@ +/** + * Connection Required Modal Component + * + * Shows when user tries to select an enterprise source that requires authentication + * Provides option to navigate to connections panel to authenticate + */ + +import React from "react"; +import { AlertCircle, ExternalLink } from "lucide-react"; +import { Button } from "@/lib/components/ui/button"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/lib/components/ui/dialog"; + +interface ConnectionRequiredModalProps { + isOpen: boolean; + onClose: () => void; + sourceName: string; + onNavigateToConnections: () => void; +} + +export const ConnectionRequiredModal: React.FC = ({ isOpen, onClose, sourceName, onNavigateToConnections }) => { + return ( + !open && onClose()}> + + + + + Connection Required + + You need to connect your {sourceName} account before using it in deep research. + + + {/* Content */} +
+

+ How to connect: +

+
    +
  1. Go to the Connections panel
  2. +
  3. Click "Connect" next to {sourceName}
  4. +
  5. Authenticate with your account
  6. +
  7. Return here to enable the source
  8. +
+
+ + + + + +
+
+ ); +}; diff --git a/client/webui/frontend/src/lib/components/chat/MessageHoverButtons.tsx b/client/webui/frontend/src/lib/components/chat/MessageHoverButtons.tsx index cdec8b4a3..a6320756b 100644 --- a/client/webui/frontend/src/lib/components/chat/MessageHoverButtons.tsx +++ b/client/webui/frontend/src/lib/components/chat/MessageHoverButtons.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef, useCallback } from "react"; +import React, { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { Copy, Check } from "lucide-react"; import { Button } from "@/lib/components/ui"; import { cn } from "@/lib/utils"; @@ -9,21 +9,26 @@ import { TTSButton } from "./TTSButton"; interface MessageHoverButtonsProps { message: MessageFE; className?: string; + /** Optional text content override */ + textContentOverride?: string; } -export const MessageHoverButtons: React.FC = ({ message, className }) => { +export const MessageHoverButtons: React.FC = ({ message, className, textContentOverride }) => { const { addNotification } = useChatContext(); const [isCopied, setIsCopied] = useState(false); const buttonRef = useRef(null); - // Extract text content from message parts + // Extract text content from message parts, or use override if provided const getTextContent = useCallback((): string => { + if (textContentOverride) { + return textContentOverride; + } if (!message.parts || message.parts.length === 0) { return ""; } const textParts = message.parts.filter(p => p.kind === "text") as TextPart[]; return textParts.map(p => p.text).join(""); - }, [message.parts]); + }, [message.parts, textContentOverride]); // Copy functionality const handleCopy = useCallback(() => { @@ -76,6 +81,17 @@ export const MessageHoverButtons: React.FC = ({ messag }; }, [handleCopy]); + // Create a synthetic message with text content override + const messageForTTS = useMemo((): MessageFE => { + if (!textContentOverride) { + return message; + } + return { + ...message, + parts: [{ kind: "text", text: textContentOverride }], + }; + }, [message, textContentOverride]); + // Don't show buttons for status messages if (message.isStatusBubble || message.isStatusMessage) { return null; @@ -84,7 +100,7 @@ export const MessageHoverButtons: React.FC = ({ messag return (
{/* TTS Button - for AI messages */} - {!message.isUser && } + {!message.isUser && } {/* Copy button - all messages */}
)} diff --git a/client/webui/frontend/src/lib/components/chat/SessionSearch.tsx b/client/webui/frontend/src/lib/components/chat/SessionSearch.tsx index 2b4ee1953..0322c9694 100644 --- a/client/webui/frontend/src/lib/components/chat/SessionSearch.tsx +++ b/client/webui/frontend/src/lib/components/chat/SessionSearch.tsx @@ -1,11 +1,11 @@ import { useState, useCallback, useEffect } from "react"; import { Search, X } from "lucide-react"; -import { Input } from "@/lib/components/ui/input"; -import { Button } from "@/lib/components/ui/button"; -import { Badge } from "@/lib/components/ui/badge"; -import { useDebounce } from "@/lib/hooks/useDebounce"; -import type { Session } from "@/lib/types"; + import { api } from "@/lib/api"; +import { ProjectBadge } from "@/lib/components/chat"; +import { Button, Input } from "@/lib/components/ui"; +import { useDebounce } from "@/lib/hooks"; +import type { Session } from "@/lib/types"; interface SessionSearchProps { onSessionSelect: (sessionId: string) => void; @@ -98,11 +98,7 @@ export const SessionSearch = ({ onSessionSelect, projectId }: SessionSearchProps diff --git a/client/webui/frontend/src/lib/components/chat/artifact/ArtifactBar.tsx b/client/webui/frontend/src/lib/components/chat/artifact/ArtifactBar.tsx index 64f6e8c1a..d3ac56e6d 100644 --- a/client/webui/frontend/src/lib/components/chat/artifact/ArtifactBar.tsx +++ b/client/webui/frontend/src/lib/components/chat/artifact/ArtifactBar.tsx @@ -1,9 +1,10 @@ import React, { useState, useEffect } from "react"; -import { Download, ChevronDown, Trash, Info, ChevronUp, CircleAlert } from "lucide-react"; +import { Download, ChevronDown, Trash, Info, ChevronUp, CircleAlert, Pencil } from "lucide-react"; -import { Button, Spinner, Badge } from "@/lib/components/ui"; -import { FileIcon } from "../file/FileIcon"; -import { cn } from "@/lib/utils"; +import { Button, Spinner } from "@/lib/components/ui"; +import { cn, formatBytes } from "@/lib/utils"; + +import { FileIcon, ProjectBadge } from "../file"; const ErrorState: React.FC<{ message: string }> = ({ message }) => (
@@ -26,6 +27,7 @@ export interface ArtifactBarProps { onDelete?: () => void; onInfo?: () => void; onExpand?: () => void; + onEdit?: () => void; }; // For creation progress bytesTransferred?: number; @@ -108,7 +110,7 @@ export const ArtifactBar: React.FC = ({ switch (status) { case "in-progress": return { - text: bytesTransferred ? `Creating... ${(bytesTransferred / 1024).toFixed(1)}KB` : "Creating...", + text: bytesTransferred ? `Creating... ${formatBytes(bytesTransferred)}` : "Creating...", className: "text-[var(--color-info-wMain)]", }; case "failed": @@ -118,7 +120,7 @@ export const ArtifactBar: React.FC = ({ }; case "completed": return { - text: size ? `${(size / 1024).toFixed(1)}KB` : "", + text: size ? formatBytes(size) : "", }; default: return { @@ -211,11 +213,7 @@ export const ArtifactBar: React.FC = ({ {hasDescription ? displayDescription : filename.length > 50 ? `${filename.substring(0, 47)}...` : filename}
{/* Project badge */} - {source === "project" && ( - - Project - - )} + {source === "project" && }
{/* Secondary line: Filename (if description shown) or status */} @@ -298,6 +296,24 @@ export const ArtifactBar: React.FC = ({ )} + {status === "completed" && actions?.onEdit && !isDeleted && ( + + )} + {status === "completed" && actions?.onDelete && !isDeleted && (
@@ -417,32 +435,7 @@ export const ArtifactMessage: React.FC = props => { const infoContent = useMemo(() => { if (!isInfoExpanded || !artifact) return null; - return ( -
- {artifact.description && ( -
- Description: -
{artifact.description}
-
- )} -
-
- Size: -
{formatBytes(artifact.size)}
-
-
- Modified: -
{formatRelativeTime(artifact.last_modified)}
-
-
- {artifact.mime_type && ( -
- Type: -
{artifact.mime_type}
-
- )} -
- ); + return ; }, [isInfoExpanded, artifact]); // Determine what content to show in expanded area - can show both info and content @@ -479,7 +472,7 @@ export const ArtifactMessage: React.FC = props => { mimeType={fileMimeType} size={fileAttachment?.size} status={props.status} - expandable={isExpandable && context === "chat"} // Allow expansion in chat context for user-controllable files + expandable={isExpandable && context === "chat" && !isDeepResearchReportFilename(fileName)} // Allow expansion in chat context for user-controllable files, but not for deep research reports (shown inline) expanded={isExpanded || isInfoExpanded} onToggleExpand={isExpandable && context === "chat" ? toggleExpanded : undefined} actions={actions} diff --git a/client/webui/frontend/src/lib/components/chat/file/FileDetails.tsx b/client/webui/frontend/src/lib/components/chat/file/FileDetails.tsx new file mode 100644 index 000000000..7824656c8 --- /dev/null +++ b/client/webui/frontend/src/lib/components/chat/file/FileDetails.tsx @@ -0,0 +1,39 @@ +import React from "react"; + +import { formatBytes, formatRelativeTime } from "@/lib/utils/format"; + +interface FileDetailsProps { + description?: string; + size: number; + lastModified: string; + mimeType?: string; +} + +export const FileDetails: React.FC = ({ description, size, lastModified, mimeType }) => { + return ( +
+ {description && ( +
+ Description: +
{description}
+
+ )} +
+
+ Size: +
{formatBytes(size)}
+
+
+ Modified: +
{formatRelativeTime(lastModified)}
+
+
+ {mimeType && ( +
+ Type: +
{mimeType}
+
+ )} +
+ ); +}; diff --git a/client/webui/frontend/src/lib/components/chat/file/FileLabel.tsx b/client/webui/frontend/src/lib/components/chat/file/FileLabel.tsx new file mode 100644 index 000000000..9fe5bce43 --- /dev/null +++ b/client/webui/frontend/src/lib/components/chat/file/FileLabel.tsx @@ -0,0 +1,16 @@ +import { formatBytes } from "@/lib/utils"; +import { File } from "lucide-react"; + +export const FileLabel = ({ fileName, fileSize }: { fileName: string; fileSize: number }) => { + return ( +
+ +
+
+ {fileName} +
+
{formatBytes(fileSize)}
+
+
+ ); +}; diff --git a/client/webui/frontend/src/lib/components/chat/file/ProjectBadge.tsx b/client/webui/frontend/src/lib/components/chat/file/ProjectBadge.tsx new file mode 100644 index 000000000..e05f2853b --- /dev/null +++ b/client/webui/frontend/src/lib/components/chat/file/ProjectBadge.tsx @@ -0,0 +1,14 @@ +import { Badge, Tooltip, TooltipContent, TooltipTrigger } from "@/lib"; + +export const ProjectBadge = ({ text = "Project", className = "" }: { text?: string; className?: string }) => { + return ( + + + + {text} + + + {text} + + ); +}; diff --git a/client/webui/frontend/src/lib/components/chat/file/index.ts b/client/webui/frontend/src/lib/components/chat/file/index.ts index 4e13b04ab..b5c8fc5d9 100644 --- a/client/webui/frontend/src/lib/components/chat/file/index.ts +++ b/client/webui/frontend/src/lib/components/chat/file/index.ts @@ -1,5 +1,7 @@ export * from "./ArtifactMessage"; export * from "./FileBadge"; +export * from "./FileDetails"; export * from "./FileIcon"; export * from "./FileMessage"; export * from "./fileUtils"; +export * from "./ProjectBadge"; diff --git a/client/webui/frontend/src/lib/components/chat/index.ts b/client/webui/frontend/src/lib/components/chat/index.ts index 87785d710..6d0ccfe6d 100644 --- a/client/webui/frontend/src/lib/components/chat/index.ts +++ b/client/webui/frontend/src/lib/components/chat/index.ts @@ -1,6 +1,8 @@ export { AudioRecorder } from "./AudioRecorder"; export { ChatInputArea } from "./ChatInputArea"; export { ChatMessage } from "./ChatMessage"; +export { ChatSessionDeleteDialog } from "./ChatSessionDeleteDialog"; +export { ChatSessionDialog } from "./ChatSessionDialog"; export { ChatSessions } from "./ChatSessions"; export { ChatSidePanel } from "./ChatSidePanel"; export { LoadingMessageRow } from "./LoadingMessageRow"; @@ -9,4 +11,5 @@ export { MoveSessionDialog } from "./MoveSessionDialog"; export { VariableDialog } from "./VariableDialog"; export { SessionSearch } from "./SessionSearch"; export { MessageHoverButtons } from "./MessageHoverButtons"; +export * from "./file"; export * from "./selection"; diff --git a/client/webui/frontend/src/lib/components/chat/preview/ContentRenderer.tsx b/client/webui/frontend/src/lib/components/chat/preview/ContentRenderer.tsx index f17c057bc..a701b027c 100644 --- a/client/webui/frontend/src/lib/components/chat/preview/ContentRenderer.tsx +++ b/client/webui/frontend/src/lib/components/chat/preview/ContentRenderer.tsx @@ -1,15 +1,18 @@ import React from "react"; import { AudioRenderer, CsvRenderer, HtmlRenderer, ImageRenderer, MarkdownRenderer, MermaidRenderer, StructuredDataRenderer, TextRenderer } from "./Renderers"; +import type { RAGSearchResult } from "@/lib/types"; interface ContentRendererProps { content: string; rendererType: string; mime_type?: string; setRenderError: (error: string | null) => void; + isStreaming?: boolean; + ragData?: RAGSearchResult; } -export const ContentRenderer: React.FC = ({ content, rendererType, mime_type, setRenderError }) => { +export const ContentRenderer: React.FC = ({ content, rendererType, mime_type, setRenderError, isStreaming, ragData }) => { switch (rendererType) { case "csv": return ; @@ -23,10 +26,10 @@ export const ContentRenderer: React.FC = ({ content, rende case "image": return ; case "markdown": - return ; + return ; case "audio": return ; default: - return ; + return ; } }; diff --git a/client/webui/frontend/src/lib/components/chat/preview/Renderers/MarkdownRenderer.tsx b/client/webui/frontend/src/lib/components/chat/preview/Renderers/MarkdownRenderer.tsx index 1e99929d5..13c01d922 100644 --- a/client/webui/frontend/src/lib/components/chat/preview/Renderers/MarkdownRenderer.tsx +++ b/client/webui/frontend/src/lib/components/chat/preview/Renderers/MarkdownRenderer.tsx @@ -1,16 +1,41 @@ -import React from "react"; +import React, { useMemo } from "react"; -import { MarkdownHTMLConverter } from "@/lib/components"; +import { MarkdownWrapper } from "@/lib/components"; import type { BaseRendererProps } from "."; import { useCopy } from "../../../../hooks/useCopy"; +import { getThemeHtmlStyles } from "@/lib/utils/themeHtmlStyles"; +import type { RAGSearchResult } from "@/lib/types"; +import { parseCitations } from "@/lib/utils/citations"; +import { TextWithCitations } from "@/lib/components/chat/Citation"; -export const MarkdownRenderer: React.FC = ({ content }) => { +interface MarkdownRendererProps extends BaseRendererProps { + ragData?: RAGSearchResult; +} + +/** + * MarkdownRenderer - Renders markdown content with citation support + * + * Uses MarkdownWrapper for streaming content (smooth text animation) + * Uses TextWithCitations for non-streaming content (citation support) + */ +export const MarkdownRenderer: React.FC = ({ content, ragData, isStreaming }) => { const { ref, handleKeyDown } = useCopy(); + // Parse citations from content using ragData + const citations = useMemo(() => { + return parseCitations(content, ragData); + }, [content, ragData]); + return (
- {content} + {isStreaming ? ( + + ) : ( +
+ +
+ )}
); diff --git a/client/webui/frontend/src/lib/components/chat/preview/Renderers/TextRenderer.tsx b/client/webui/frontend/src/lib/components/chat/preview/Renderers/TextRenderer.tsx index 14423d432..04e18dd04 100644 --- a/client/webui/frontend/src/lib/components/chat/preview/Renderers/TextRenderer.tsx +++ b/client/webui/frontend/src/lib/components/chat/preview/Renderers/TextRenderer.tsx @@ -1,13 +1,25 @@ import type { BaseRendererProps } from "."; import { useCopy } from "../../../../hooks/useCopy"; +import { StreamingMarkdown } from "@/lib/components"; interface TextRendererProps extends BaseRendererProps { className?: string; } -export const TextRenderer: React.FC = ({ content, className = "" }) => { +export const TextRenderer: React.FC = ({ content, className = "", isStreaming }) => { const { ref, handleKeyDown } = useCopy(); + if (isStreaming) { + // Use StreamingMarkdown for smooth rendering effect, even though it might interpret markdown. + return ( +
+
} className="whitespace-pre-wrap select-text focus-visible:outline-none" tabIndex={0} onKeyDown={handleKeyDown}> + +
+
+ ); + } + return (
diff --git a/client/webui/frontend/src/lib/components/chat/preview/Renderers/index.ts b/client/webui/frontend/src/lib/components/chat/preview/Renderers/index.ts
index f171e1857..46ae046b6 100644
--- a/client/webui/frontend/src/lib/components/chat/preview/Renderers/index.ts
+++ b/client/webui/frontend/src/lib/components/chat/preview/Renderers/index.ts
@@ -6,6 +6,7 @@ export interface BaseRendererProps {
     rendererType?: string; // Optional, for structured data renderers
     mime_type?: string; // Optional MIME type for specific renderers
     setRenderError: (error: string | null) => void; // Function to set error state
+    isStreaming?: boolean;
 }
 
 export { AudioRenderer } from "./AudioRenderer";
diff --git a/client/webui/frontend/src/lib/components/chat/preview/previewUtils.ts b/client/webui/frontend/src/lib/components/chat/preview/previewUtils.ts
index 1caaef038..0b640edf6 100644
--- a/client/webui/frontend/src/lib/components/chat/preview/previewUtils.ts
+++ b/client/webui/frontend/src/lib/components/chat/preview/previewUtils.ts
@@ -314,7 +314,6 @@ export const getFileContent = (file: FileAttachment | null) => {
     // Check if content is already plain text (from streaming)
     // @ts-expect-error - Custom property added during streaming
     if (file.isPlainText) {
-        console.log("Content is plain text from streaming, returning as-is");
         return file.content;
     }
 
diff --git a/client/webui/frontend/src/lib/components/chat/rag/RAGInfoPanel.tsx b/client/webui/frontend/src/lib/components/chat/rag/RAGInfoPanel.tsx
new file mode 100644
index 000000000..3191f7ab1
--- /dev/null
+++ b/client/webui/frontend/src/lib/components/chat/rag/RAGInfoPanel.tsx
@@ -0,0 +1,541 @@
+import React from "react";
+import { FileText, TrendingUp, Search, Link2, ChevronDown, ChevronUp, Brain, Globe, ExternalLink } from "lucide-react";
+// Web-only version - enterprise icons removed
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/lib/components/ui/tabs";
+import type { RAGSearchResult } from "@/lib/types";
+
+interface TimelineEvent {
+    type: "thinking" | "search" | "read";
+    timestamp: string;
+    content: string;
+    url?: string;
+    favicon?: string;
+    title?: string;
+    source_type?: string;
+}
+
+interface RAGInfoPanelProps {
+    ragData: RAGSearchResult[] | null;
+    enabled: boolean;
+}
+
+/**
+ * Extract clean filename from file_id by removing session prefix
+ * Example: "sam_dev_user_web-session-xxx_filename.pdf_v0.pdf" -> "filename.pdf"
+ */
+const extractFilename = (filename: string | undefined): string => {
+    if (!filename) return "Unknown";
+
+    // The pattern is: sam_dev_user_web-session-{uuid}_{actual_filename}_v{version}.pdf
+    // We need to extract just the {actual_filename}.pdf part
+
+    // First, remove the .pdf extension at the very end (added by backend)
+    let cleaned = filename.replace(/\.pdf$/, "");
+
+    // Remove the version suffix (_v0, _v1, etc.)
+    cleaned = cleaned.replace(/_v\d+$/, "");
+
+    // Now we have: sam_dev_user_web-session-{uuid}_{actual_filename}
+    // Find the pattern "web-session-{uuid}_" and remove everything before and including it
+    const sessionPattern = /^.*web-session-[a-f0-9-]+_/;
+    cleaned = cleaned.replace(sessionPattern, "");
+
+    // Add back the .pdf extension
+    return cleaned + ".pdf";
+};
+
+const SourceCard: React.FC<{
+    source: RAGSearchResult["sources"][0];
+}> = ({ source }) => {
+    const [isExpanded, setIsExpanded] = React.useState(false);
+    const contentPreview = source.contentPreview;
+    const sourceType = source.sourceType || "web";
+
+    // For image sources, use the source page link (not the imageUrl)
+    let sourceUrl: string;
+    let displayTitle: string;
+
+    if (sourceType === "image") {
+        sourceUrl = source.sourceUrl || source.metadata?.link || "";
+        displayTitle = source.metadata?.title || source.filename || "Image source";
+    } else {
+        sourceUrl = source.sourceUrl || source.url || "";
+        displayTitle = source.title || source.filename || extractFilename(source.fileId);
+    }
+
+    // Don't show content preview if it's just "Reading..." placeholder
+    const hasRealContent = contentPreview && contentPreview !== "Reading...";
+    const shouldTruncate = hasRealContent && contentPreview.length > 200;
+    const displayContent = shouldTruncate && !isExpanded ? contentPreview.substring(0, 200) + "..." : contentPreview;
+
+    // Only show score if it's a real relevance score (not the default 1.0 from deep research)
+    const showScore = source.relevanceScore !== 1.0;
+
+    return (
+        
+ {/* Source Header */} +
+
+ + {sourceUrl ? ( + + {displayTitle} + + + ) : ( + + {displayTitle} + + )} +
+ {showScore && ( +
+ + Score: {source.relevanceScore.toFixed(2)} +
+ )} +
+ + {/* Content Preview - Fixed height when collapsed - Only show if we have real content */} + {hasRealContent &&
{displayContent}
} + + {/* Expand/Collapse Button */} + {shouldTruncate && ( + + )} + + {/* Metadata (if available) */} + {source.metadata && Object.keys(source.metadata).length > 0 && ( +
+
+ Metadata +
+ {Object.entries(source.metadata).map(([key, value]) => ( +
+ {key}: + {typeof value === "object" ? JSON.stringify(value) : String(value)} +
+ ))} +
+
+
+ )} +
+ ); +}; + +export const RAGInfoPanel: React.FC = ({ ragData, enabled }) => { + if (!enabled) { + return ( +
+
+ +
RAG Sources
+
RAG source visibility is disabled in settings
+
+
+ ); + } + + if (!ragData || ragData.length === 0) { + return ( +
+
+ +
Sources
+
No sources available yet
+
Sources from web research will appear here after completion
+
+
+ ); + } + + const isAllDeepResearch = ragData.every(search => search.searchType === "deep_research" || search.searchType === "web_search"); + + // Calculate total sources across all searches (including images with valid source links) + const totalSources = ragData.reduce((sum, search) => { + const validSources = search.sources.filter(s => { + const sourceType = s.sourceType || "web"; + // For images, only count if they have a source link (not just imageUrl) + if (sourceType === "image") { + return s.sourceUrl || s.metadata?.link; + } + return true; + }); + return sum + validSources.length; + }, 0); + + // Simple source item component for deep research + const SimpleSourceItem: React.FC<{ source: RAGSearchResult["sources"][0] }> = ({ source }) => { + const sourceType = source.sourceType || "web"; + + // For image sources, use the source page link (not the imageUrl) + let url: string; + let title: string; + + if (sourceType === "image") { + url = source.sourceUrl || source.metadata?.link || ""; + title = source.metadata?.title || source.filename || "Image source"; + } else { + url = source.url || source.sourceUrl || ""; + title = source.title || source.filename || "Unknown"; + } + + const favicon = source.metadata?.favicon || (url ? `https://www.google.com/s2/favicons?domain=${url}&sz=32` : ""); + + return ( +
+ {favicon && ( + { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + )} + {url ? ( + + {title} + + + ) : ( + + {title} + + )} +
+ ); + }; + + // Helper function to check if a source was fully fetched + const isSourceFullyFetched = (source: RAGSearchResult["sources"][0]): boolean => { + return source.metadata?.fetched === true || source.metadata?.fetch_status === "success" || (source.contentPreview ? source.contentPreview.includes("[Full Content Fetched]") : false); + }; + + // Get all unique sources grouped by fully read vs snippets (for deep research) + const { fullyReadSources, snippetSources, allUniqueSources } = (() => { + if (!isAllDeepResearch) return { fullyReadSources: [], snippetSources: [], allUniqueSources: [] }; + + const fullyReadMap = new Map(); + const snippetMap = new Map(); + + // Check if this is web_search (no fetched metadata) or deep_research (has fetched metadata) + const isWebSearch = ragData.some(search => search.searchType === "web_search"); + const isDeepResearch = ragData.some(search => search.searchType === "deep_research"); + + ragData.forEach(search => { + search.sources.forEach(source => { + const sourceType = source.sourceType || "web"; + + // For image sources: include if they have a source link (not just imageUrl) + if (sourceType === "image") { + const sourceLink = source.sourceUrl || source.metadata?.link; + if (!sourceLink) { + return; // Skip images without source links + } + // Images are always considered "fully read" if they have a source link + if (!fullyReadMap.has(sourceLink)) { + fullyReadMap.set(sourceLink, source); + } + return; + } + + const key = source.url || source.sourceUrl || source.title || ""; + if (!key) return; + + // For web_search: all sources go to fully read (no distinction) + if (isWebSearch && !isDeepResearch) { + if (!fullyReadMap.has(key)) { + fullyReadMap.set(key, source); + } + return; + } + + // For deep_research: separate into fully read vs snippets + const wasFetched = isSourceFullyFetched(source); + if (wasFetched) { + if (!fullyReadMap.has(key)) { + fullyReadMap.set(key, source); + } + // Remove from snippets if it was previously added there + snippetMap.delete(key); + } else { + // Only add to snippets if not already in fully read + if (!fullyReadMap.has(key) && !snippetMap.has(key)) { + snippetMap.set(key, source); + } + } + }); + }); + + const fullyRead = Array.from(fullyReadMap.values()); + const snippets = Array.from(snippetMap.values()); + const all = [...fullyRead, ...snippets]; + + console.log("[RAGInfoPanel] Source filtering:", { + isWebSearch, + isDeepResearch, + totalSourcesBeforeFilter: ragData.reduce((sum, s) => sum + s.sources.length, 0), + fullyReadSources: fullyRead.length, + snippetSources: snippets.length, + sampleFullyRead: fullyRead.slice(0, 3).map(s => ({ + url: s.url, + title: s.title, + fetched: s.metadata?.fetched, + fetch_status: s.metadata?.fetch_status, + contentPreview: s.contentPreview?.substring(0, 100), + hasMarker: s.contentPreview?.includes("[Full Content Fetched]"), + })), + sampleSnippets: snippets.slice(0, 3).map(s => ({ + url: s.url, + title: s.title, + fetched: s.metadata?.fetched, + fetch_status: s.metadata?.fetch_status, + contentPreview: s.contentPreview?.substring(0, 100), + hasMarker: s.contentPreview?.includes("[Full Content Fetched]"), + })), + }); + + return { fullyReadSources: fullyRead, snippetSources: snippets, allUniqueSources: all }; + })(); + + // Check if we should show grouped view (only for deep_research with both types) + const isDeepResearch = ragData.some(search => search.searchType === "deep_research"); + const showGroupedSources = isDeepResearch && (fullyReadSources.length > 0 || snippetSources.length > 0); + + // Get the title from the first ragData entry (prefer LLM-generated title, fallback to query) + const panelTitle = ragData && ragData.length > 0 ? ragData[0].title || ragData[0].query : ""; + + // Check if research is complete by looking for sources with fetched metadata + const hasAnyFetchedSources = isDeepResearch && ragData.some(search => search.sources.some(s => s.metadata?.fetched === true || s.metadata?.fetch_status === "success")); + + return ( +
+ {isAllDeepResearch ? ( + // Deep research: Show sources grouped by fully read vs snippets (only when complete) +
+
+ {/* Title section showing research question or query */} + {panelTitle && ( +
+

{panelTitle}

+
+ )} + + {/* Show grouped sources ONLY when research is complete (has fetched sources) */} + {showGroupedSources && hasAnyFetchedSources ? ( + <> + {/* Fully Read Sources Section */} + {fullyReadSources.length > 0 && ( +
+
+

+ {fullyReadSources.length} Fully Read Source{fullyReadSources.length !== 1 ? "s" : ""} +

+
+
+ {fullyReadSources.map((source, idx) => ( + + ))} +
+
+ )} + + {/* Partially Read Sources Section */} + {snippetSources.length > 0 && ( +
+
+

+ {snippetSources.length} Partially Read Source{snippetSources.length !== 1 ? "s" : ""} +

+

Search result snippets

+
+
+ {snippetSources.map((source, idx) => ( + + ))} +
+
+ )} + + ) : ( + <> +
+

{isDeepResearch && !hasAnyFetchedSources ? "Sources Explored So Far" : `${allUniqueSources.length} Sources`}

+ {isDeepResearch && !hasAnyFetchedSources &&

Research in progress...

} +
+
+ {allUniqueSources.map((source, idx) => ( + + ))} +
+ + )} +
+
+ ) : ( + // Regular RAG/web search: Show both Activity and Sources tabs + +
+ + Activity + {totalSources} Sources + +
+ + +
+

Timeline of Research Activity

+

+ {ragData.length} search{ragData.length !== 1 ? "es" : ""} performed +

+
+ +
+ {ragData.map((search, searchIdx) => { + // Build timeline events for this search + const events: TimelineEvent[] = []; + + // Add search event + events.push({ + type: "search", + timestamp: search.timestamp, + content: search.query, + }); + + // Add read events for sources that were fetched/analyzed + search.sources.forEach(source => { + if (source.url || source.title) { + const sourceType = source.metadata?.source_type || "web"; + events.push({ + type: "read", + timestamp: source.retrievedAt || search.timestamp, + content: source.title || source.url || "Unknown", + url: source.url, + favicon: source.metadata?.favicon || (source.url ? `https://www.google.com/s2/favicons?domain=${source.url}&sz=32` : ""), + title: source.title, + source_type: sourceType, + }); + } + }); + + return ( + + {events.map((event, eventIdx) => ( +
+ {/* Icon */} +
+ {event.type === "thinking" && } + {event.type === "search" && } + {event.type === "read" && + (() => { + // Web-only version - only web sources + if (event.favicon && event.favicon.trim() !== "") { + // Web source with favicon + return ( + { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + ); + } else { + // Web source without favicon or unknown + return ; + } + })()} +
+ + {/* Content */} +
+ {event.type === "search" && ( +
+ Searched for + {event.content} +
+ )} + {event.type === "read" && ( +
+ Read + {event.url ? ( + + {event.title || new URL(event.url).hostname} + + + ) : ( + {event.content} + )} +
+ )} + {event.type === "thinking" &&
{event.content}
} +
+
+ ))} +
+ ); + })} +
+
+ + +
+

All Sources

+

+ {totalSources} source{totalSources !== 1 ? "s" : ""} found across {ragData.length} search{ragData.length !== 1 ? "es" : ""} +

+
+ +
+ {ragData.map((search, searchIdx) => + search.sources + .filter(source => { + const sourceType = source.sourceType || "web"; + // Include images only if they have a source link + if (sourceType === "image") { + return source.sourceUrl || source.metadata?.link; + } + return true; + }) + .map((source, sourceIdx) => ) + )} +
+
+
+ )} +
+ ); +}; diff --git a/client/webui/frontend/src/lib/components/chat/selection/SelectableMessageContent.tsx b/client/webui/frontend/src/lib/components/chat/selection/SelectableMessageContent.tsx index 444d249ff..46e6fa664 100644 --- a/client/webui/frontend/src/lib/components/chat/selection/SelectableMessageContent.tsx +++ b/client/webui/frontend/src/lib/components/chat/selection/SelectableMessageContent.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useRef } from "react"; -import { useTextSelection } from "./TextSelectionProvider"; +import { useTextSelection } from "./useTextSelection"; import { getSelectedText, getSelectionRange, getSelectionBoundingRect, calculateMenuPosition, isValidSelection } from "./selectionUtils"; import type { SelectableMessageContentProps } from "./types"; diff --git a/client/webui/frontend/src/lib/components/chat/selection/TextSelectionContext.tsx b/client/webui/frontend/src/lib/components/chat/selection/TextSelectionContext.tsx new file mode 100644 index 000000000..3e80a59a8 --- /dev/null +++ b/client/webui/frontend/src/lib/components/chat/selection/TextSelectionContext.tsx @@ -0,0 +1,4 @@ +import { createContext } from "react"; +import type { SelectionContextValue } from "./types"; + +export const TextSelectionContext = createContext(undefined); diff --git a/client/webui/frontend/src/lib/components/chat/selection/TextSelectionProvider.tsx b/client/webui/frontend/src/lib/components/chat/selection/TextSelectionProvider.tsx index 3ed9fc4f7..93fd47fa8 100644 --- a/client/webui/frontend/src/lib/components/chat/selection/TextSelectionProvider.tsx +++ b/client/webui/frontend/src/lib/components/chat/selection/TextSelectionProvider.tsx @@ -1,16 +1,7 @@ -import React, { createContext, useContext, useState, useCallback, type ReactNode } from "react"; +import React, { useState, useCallback, type ReactNode } from "react"; import type { SelectionState, SelectionContextValue } from "./types"; - -export const TextSelectionContext = createContext(undefined); - -export const useTextSelection = () => { - const context = useContext(TextSelectionContext); - if (!context) { - throw new Error("useTextSelection must be used within TextSelectionProvider"); - } - return context; -}; +import { TextSelectionContext } from "./TextSelectionContext"; interface TextSelectionProviderProps { children: ReactNode; diff --git a/client/webui/frontend/src/lib/components/chat/selection/index.ts b/client/webui/frontend/src/lib/components/chat/selection/index.ts index a0912b0da..48eb79af3 100644 --- a/client/webui/frontend/src/lib/components/chat/selection/index.ts +++ b/client/webui/frontend/src/lib/components/chat/selection/index.ts @@ -1,5 +1,7 @@ -export { TextSelectionProvider, useTextSelection } from "./TextSelectionProvider"; +export { TextSelectionProvider } from "./TextSelectionProvider"; +export { useTextSelection } from "./useTextSelection"; +export { TextSelectionContext } from "./TextSelectionContext"; export { SelectionContextMenu } from "./SelectionContextMenu"; export { SelectableMessageContent } from "./SelectableMessageContent"; export * from "./types"; -export * from "./selectionUtils"; \ No newline at end of file +export * from "./selectionUtils"; diff --git a/client/webui/frontend/src/lib/components/chat/selection/useTextSelection.tsx b/client/webui/frontend/src/lib/components/chat/selection/useTextSelection.tsx new file mode 100644 index 000000000..72e8ed264 --- /dev/null +++ b/client/webui/frontend/src/lib/components/chat/selection/useTextSelection.tsx @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import { TextSelectionContext } from "./TextSelectionContext"; + +export const useTextSelection = () => { + const context = useContext(TextSelectionContext); + if (!context) { + throw new Error("useTextSelection must be used within TextSelectionProvider"); + } + return context; +}; diff --git a/client/webui/frontend/src/lib/components/common/ConfirmationDialog.tsx b/client/webui/frontend/src/lib/components/common/ConfirmationDialog.tsx index 9ee107df6..3bc28142b 100644 --- a/client/webui/frontend/src/lib/components/common/ConfirmationDialog.tsx +++ b/client/webui/frontend/src/lib/components/common/ConfirmationDialog.tsx @@ -14,8 +14,9 @@ export interface ConfirmationDialogProps { content?: React.ReactNode; description?: string; - // optional loading state for confirm action + // optional loading and enabled state for confirm action isLoading?: boolean; + isEnabled?: boolean; // optional custom action labels actionLabels?: { @@ -27,7 +28,7 @@ export interface ConfirmationDialogProps { trigger?: React.ReactNode; } -export const ConfirmationDialog: React.FC = ({ open, title, content, description, actionLabels, trigger, isLoading, onOpenChange, onConfirm, onCancel }) => { +export const ConfirmationDialog: React.FC = ({ open, title, content, description, actionLabels, trigger, isLoading, isEnabled = true, onOpenChange, onConfirm, onCancel }) => { const cancelTitle = actionLabels?.cancel ?? "Cancel"; const confirmTitle = actionLabels?.confirm ?? "Confirm"; @@ -63,7 +64,7 @@ export const ConfirmationDialog: React.FC = ({ open, ti await onConfirm(); onOpenChange(false); }} - disabled={isLoading} + disabled={isLoading || !isEnabled} > {confirmTitle} diff --git a/client/webui/frontend/src/lib/components/common/ErrorLabel.tsx b/client/webui/frontend/src/lib/components/common/ErrorLabel.tsx new file mode 100644 index 000000000..caded7230 --- /dev/null +++ b/client/webui/frontend/src/lib/components/common/ErrorLabel.tsx @@ -0,0 +1,3 @@ +export const ErrorLabel = ({ message, className }: { message?: string; className?: string }) => { + return message ?
{message}
: null; +}; diff --git a/client/webui/frontend/src/lib/components/common/FileUpload.tsx b/client/webui/frontend/src/lib/components/common/FileUpload.tsx new file mode 100644 index 000000000..aaaeeb8b4 --- /dev/null +++ b/client/webui/frontend/src/lib/components/common/FileUpload.tsx @@ -0,0 +1,183 @@ +import { useState, useRef, useEffect, type DragEvent, type ChangeEvent } from "react"; +import { X } from "lucide-react"; + +import { Button } from "@/lib/components"; +import { MessageBanner } from "@/lib/components/common"; + +/** + * Removes a file at the specified index from a FileList. + * @param prevFiles the FileList + * @param indexToRemove the index of the file to remove + * @returns new FileList with the file removed, or null if no files remain + */ +const removeAtIndex = (prevFiles: FileList | null, indexToRemove: number): FileList | null => { + if (!prevFiles) return null; + const filesArray = Array.from(prevFiles); + filesArray.splice(indexToRemove, 1); + if (filesArray.length === 0) { + return null; + } + const dataTransfer = new DataTransfer(); + filesArray.forEach(file => dataTransfer.items.add(file)); + return dataTransfer.files; +}; + +export interface FileUploadProps { + name: string; + accept: string; + multiple?: boolean; + disabled?: boolean; + testid?: string; + value?: FileList | null; + onChange: (file: FileList | null) => void; + onValidate?: (files: FileList) => { valid: boolean; error?: string }; +} + +function FileUpload({ name, accept, multiple = false, disabled = false, testid = "", value = null, onChange, onValidate }: FileUploadProps) { + const [uploadedFiles, setUploadedFiles] = useState(value); + const [isDragging, setIsDragging] = useState(false); + const [validationError, setValidationError] = useState(null); + const fileInputRef = useRef(null); + + // Sync internal state with value prop to handle external clearing + useEffect(() => { + setUploadedFiles(value); + }, [value]); + + const setSelectedFiles = (files: FileList | null) => { + if (files && files.length > 0) { + // Validate files if validation function is provided + if (onValidate) { + const validation = onValidate(files); + if (!validation.valid) { + setValidationError(validation.error || "File validation failed."); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + return; + } + } + + setValidationError(null); + setUploadedFiles(files); + onChange(files); + } else { + setValidationError(null); + setUploadedFiles(null); + onChange(null); + fileInputRef.current!.value = ""; + } + }; + + const handleDragEnter = (e: DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = (e: DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }; + + const handleDragOver = (e: DragEvent) => { + e.preventDefault(); + if (disabled) { + e.currentTarget.style.cursor = "not-allowed"; + } else { + e.currentTarget.style.cursor = "default"; + } + }; + + const handleDrop = (e: DragEvent) => { + e.preventDefault(); + setIsDragging(false); + + if (!disabled) { + let files = e.dataTransfer.files; + + // If multiple is false and more than one file is dropped, only take the first file + if (!multiple && files.length > 1) { + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(files[0]); + files = dataTransfer.files; + } + + setSelectedFiles(files); + } + }; + + const handleFileChange = (e: ChangeEvent) => { + const files = e.target.files; + setSelectedFiles(files); + }; + + const handleDropZoneClick = (e: React.MouseEvent) => { + e.preventDefault(); + fileInputRef.current?.click(); + }; + + const handleClearValidationError = () => { + setValidationError(null); + }; + + const handleRemoveFile = (index: number) => { + const newFiles = removeAtIndex(uploadedFiles, index); + setUploadedFiles(newFiles); + onChange(newFiles); + + // Clear the input so the same file can be re-selected + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + return ( +
+ {validationError && ( +
+ +
+ )} + + {uploadedFiles ? ( + Array.from(uploadedFiles).map((file, index) => ( +
+
{file.name}
+ +
+ )) + ) : ( +
+ {isDragging && !disabled ? ( +
Drop file here
+ ) : ( +
+
Drag and drop file here
+
+
+
OR
+
+
+
+ +
+
+ )} +
+ )} +
+ ); +} + +export { FileUpload }; diff --git a/client/webui/frontend/src/lib/components/common/MarkdownWrapper.tsx b/client/webui/frontend/src/lib/components/common/MarkdownWrapper.tsx new file mode 100644 index 000000000..d19ab5337 --- /dev/null +++ b/client/webui/frontend/src/lib/components/common/MarkdownWrapper.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { MarkdownHTMLConverter } from "./MarkdownHTMLConverter"; +import { StreamingMarkdown } from "./StreamingMarkdown"; + +interface MarkdownWrapperProps { + content: string; + isStreaming?: boolean; + className?: string; +} + +/** + * A wrapper component that automatically chooses between StreamingMarkdown + * (for smooth animated rendering during streaming) and MarkdownHTMLConverter + * (for static content). + */ +const MarkdownWrapper: React.FC = ({ content, isStreaming, className }) => { + if (isStreaming) { + return ; + } + + return {content}; +}; + +export { MarkdownWrapper }; diff --git a/client/webui/frontend/src/lib/components/common/StreamingMarkdown.tsx b/client/webui/frontend/src/lib/components/common/StreamingMarkdown.tsx new file mode 100644 index 000000000..3ea856552 --- /dev/null +++ b/client/webui/frontend/src/lib/components/common/StreamingMarkdown.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { MarkdownHTMLConverter } from "./MarkdownHTMLConverter"; +import { useStreamingSpeed, useStreamingAnimation } from "@/lib/hooks"; + +interface StreamingMarkdownProps { + content: string; + className?: string; +} + +const StreamingMarkdown: React.FC = ({ content, className }) => { + const { state, contentRef } = useStreamingSpeed(content); + const displayedContent = useStreamingAnimation(state, contentRef); + + return {displayedContent}; +}; + +export { StreamingMarkdown }; diff --git a/client/webui/frontend/src/lib/components/common/index.ts b/client/webui/frontend/src/lib/components/common/index.ts index be1f4bcd6..f67ef53cc 100644 --- a/client/webui/frontend/src/lib/components/common/index.ts +++ b/client/webui/frontend/src/lib/components/common/index.ts @@ -1,9 +1,13 @@ export { ConfirmationDialog } from "./ConfirmationDialog"; export { EmptyState } from "./EmptyState"; export { ErrorDialog } from "./ErrorDialog"; +export { ErrorLabel } from "./ErrorLabel"; +export { FileUpload } from "./FileUpload"; export { Footer } from "./Footer"; export { GridCard } from "./GridCard"; export { LoadingBlocker } from "./LoadingBlocker"; export { MarkdownHTMLConverter } from "./MarkdownHTMLConverter"; +export * from "./MarkdownWrapper"; export { MessageBanner } from "./MessageBanner"; export * from "./messageColourVariants"; +export * from "./StreamingMarkdown"; diff --git a/client/webui/frontend/src/lib/components/index.ts b/client/webui/frontend/src/lib/components/index.ts index fc321bc25..f534928b6 100644 --- a/client/webui/frontend/src/lib/components/index.ts +++ b/client/webui/frontend/src/lib/components/index.ts @@ -7,10 +7,33 @@ export * from "./navigation"; export * from "./chat"; export * from "./settings"; -export { MarkdownHTMLConverter, MessageBanner, EmptyState, ErrorDialog, ConfirmationDialog, LoadingBlocker, messageColourVariants } from "./common"; +export { MarkdownHTMLConverter, MarkdownWrapper, MessageBanner, EmptyState, ErrorDialog, ConfirmationDialog, LoadingBlocker, messageColourVariants, StreamingMarkdown } from "./common"; export * from "./header"; export * from "./pages"; export * from "./agents"; +export * from "./workflows"; +// Export workflow visualization components (selective to avoid conflicts with activities) +export { + WorkflowVisualizationPage, + buildWorkflowNavigationUrl, + WorkflowDiagram, + WorkflowNodeRenderer, + WorkflowNodeDetailPanel, + WorkflowDetailsSidePanel, + StartNode, + EndNode, + AgentNode, + WorkflowRefNode, + MapNode, + LoopNode, + SwitchNode, + ConditionPillNode, + EdgeLayer, + processWorkflowConfig, + LAYOUT_CONSTANTS, +} from "./workflowVisualization"; +export type { WorkflowPanelView, NodeProps, WorkflowVisualNodeType } from "./workflowVisualization"; +// Note: LayoutNode, Edge, LayoutResult types not exported here to avoid conflicts with activities export * from "./jsonViewer"; export * from "./projects"; diff --git a/client/webui/frontend/src/lib/components/jsonViewer/JSONViewer.tsx b/client/webui/frontend/src/lib/components/jsonViewer/JSONViewer.tsx index 8451a4346..b7d541ea8 100644 --- a/client/webui/frontend/src/lib/components/jsonViewer/JSONViewer.tsx +++ b/client/webui/frontend/src/lib/components/jsonViewer/JSONViewer.tsx @@ -41,9 +41,11 @@ interface JSONViewerProps { data: JSONValue; maxDepth?: number; className?: string; + /** Root name label. Set to empty string to hide. Defaults to empty (hidden). */ + rootName?: string; } -export const JSONViewer: React.FC = ({ data, maxDepth = 2, className = "" }) => { +export const JSONViewer: React.FC = ({ data, maxDepth = 2, className = "", rootName = "" }) => { const { currentTheme } = useThemeContext(); const jsonEditorTheme = useMemo(() => { @@ -78,7 +80,7 @@ export const JSONViewer: React.FC = ({ data, maxDepth = 2, clas return (
- +
); }; diff --git a/client/webui/frontend/src/lib/components/navigation/NavigationButton.tsx b/client/webui/frontend/src/lib/components/navigation/NavigationButton.tsx index a66e4c5fe..91bdf4cc3 100644 --- a/client/webui/frontend/src/lib/components/navigation/NavigationButton.tsx +++ b/client/webui/frontend/src/lib/components/navigation/NavigationButton.tsx @@ -47,7 +47,7 @@ export const NavigationButton: React.FC = ({ item, isActive {label} {badge && ( - + {badge} )} diff --git a/client/webui/frontend/src/lib/components/navigation/NavigationList.tsx b/client/webui/frontend/src/lib/components/navigation/NavigationList.tsx index ede0979a3..47ff4bbfc 100644 --- a/client/webui/frontend/src/lib/components/navigation/NavigationList.tsx +++ b/client/webui/frontend/src/lib/components/navigation/NavigationList.tsx @@ -21,15 +21,17 @@ export const NavigationList: React.FC = ({ items, bottomIte // When authorization is enabled, show menu with user info and settings/logout const { configUseAuthorization, configFeatureEnablement } = useConfigContext(); const logoutEnabled = configUseAuthorization && configFeatureEnablement?.logout ? true : false; + const { userInfo, logout } = useAuthContext(); + const userName = typeof userInfo?.username === "string" ? userInfo.username : "Guest"; const handleSettingsClick = () => { setMenuOpen(false); setSettingsDialogOpen(true); }; - const handleLogoutClick = () => { + const handleLogoutClick = async () => { setMenuOpen(false); - logout(); + await logout(); }; return ( @@ -76,8 +78,10 @@ export const NavigationList: React.FC = ({ items, bottomIte
- - {typeof userInfo?.username === "string" ? userInfo.username : "Guest"} + +
+ {userName} +
): N }, { id: "agentMesh", - label: "Agents", + label: "Agent Mesh", icon: Bot, }, ]; @@ -58,7 +58,7 @@ export const topNavigationItems: NavigationItem[] = [ }, { id: "agentMesh", - label: "Agents", + label: "Agent Mesh", icon: Bot, }, { diff --git a/client/webui/frontend/src/lib/components/pages/AgentMeshPage.tsx b/client/webui/frontend/src/lib/components/pages/AgentMeshPage.tsx index e14af9d1e..f3e966136 100644 --- a/client/webui/frontend/src/lib/components/pages/AgentMeshPage.tsx +++ b/client/webui/frontend/src/lib/components/pages/AgentMeshPage.tsx @@ -1,30 +1,73 @@ +import { useMemo } from "react"; +import { useSearchParams } from "react-router-dom"; + import { Button, EmptyState, Header } from "@/lib/components"; import { AgentMeshCards } from "@/lib/components/agents"; +import { WorkflowList } from "@/lib/components/workflows"; import { useChatContext } from "@/lib/hooks"; +import { isWorkflowAgent } from "@/lib/utils/agentUtils"; import { RefreshCcw } from "lucide-react"; +type AgentMeshTab = "agents" | "workflows"; + export function AgentMeshPage() { const { agents, agentsLoading, agentsError, agentsRefetch } = useChatContext(); + const [searchParams, setSearchParams] = useSearchParams(); + + // Read active tab from URL, default to "agents" + const activeTab: AgentMeshTab = (searchParams.get("tab") as AgentMeshTab) || "agents"; + + const setActiveTab = (tab: AgentMeshTab) => { + if (tab === "agents") { + // Remove tab param for default tab + searchParams.delete("tab"); + } else { + searchParams.set("tab", tab); + } + setSearchParams(searchParams); + }; + + const { regularAgents, workflowAgents } = useMemo(() => { + const regular = agents.filter(agent => !isWorkflowAgent(agent)); + const workflows = agents.filter(agent => isWorkflowAgent(agent)); + return { regularAgents: regular, workflowAgents: workflows }; + }, [agents]); + + const tabs = [ + { + id: "agents", + label: "Agents", + isActive: activeTab === "agents", + onClick: () => setActiveTab("agents"), + }, + { + id: "workflows", + label: "Workflows", + isActive: activeTab === "workflows", + onClick: () => setActiveTab("workflows"), + }, + ]; return (
agentsRefetch()}> + , ]} /> {agentsLoading ? ( - + ) : agentsError ? ( - + ) : (
- + {activeTab === "agents" ? : }
)}
diff --git a/client/webui/frontend/src/lib/components/pages/ChatPage.tsx b/client/webui/frontend/src/lib/components/pages/ChatPage.tsx index 1968f1605..2f4dbc117 100644 --- a/client/webui/frontend/src/lib/components/pages/ChatPage.tsx +++ b/client/webui/frontend/src/lib/components/pages/ChatPage.tsx @@ -4,20 +4,12 @@ import { PanelLeftIcon } from "lucide-react"; import type { ImperativePanelHandle } from "react-resizable-panels"; import { Header } from "@/lib/components/header"; -import { ChatInputArea, ChatMessage, LoadingMessageRow } from "@/lib/components/chat"; -import type { TextPart } from "@/lib/types"; -import { Button, ChatMessageList, CHAT_STYLES, Badge } from "@/lib/components/ui"; -import { Spinner } from "@/lib/components/ui/spinner"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/lib/components/ui/tooltip"; -import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/lib/components/ui/resizable"; import { useChatContext, useTaskContext, useThemeContext } from "@/lib/hooks"; import { useProjectContext } from "@/lib/providers"; - -import { ChatSidePanel } from "../chat/ChatSidePanel"; -import { ChatSessionDialog } from "../chat/ChatSessionDialog"; -import { SessionSidePanel } from "../chat/SessionSidePanel"; -import { ChatSessionDeleteDialog } from "../chat/ChatSessionDeleteDialog"; -import type { ChatMessageListRef } from "../ui/chat/chat-message-list"; +import type { TextPart } from "@/lib/types"; +import { ChatInputArea, ChatMessage, ChatSessionDialog, ChatSessionDeleteDialog, ChatSidePanel, LoadingMessageRow, ProjectBadge, SessionSidePanel } from "@/lib/components/chat"; +import { Button, ChatMessageList, CHAT_STYLES, ResizablePanelGroup, ResizablePanel, ResizableHandle, Spinner, Tooltip, TooltipContent, TooltipTrigger } from "@/lib/components/ui"; +import type { ChatMessageListRef } from "@/lib/components/ui/chat/chat-message-list"; // Constants for sidepanel behavior const COLLAPSED_SIZE = 4; // icon-only mode size @@ -165,7 +157,7 @@ export function ChatPage() { return () => { setTaskIdInSidePanel(currentTaskId); - openSidePanelTab("workflow"); + openSidePanelTab("activity"); }; }, [currentTaskId, setTaskIdInSidePanel, openSidePanelTab]); @@ -201,11 +193,7 @@ export function ChatPage() {

{pageTitle}

- {activeProject && ( - - {activeProject.name} - - )} + {activeProject && }
} breadcrumbs={breadcrumbs} @@ -247,7 +235,9 @@ export function ChatPage() { {messages.map((message, index) => { const isLastWithTaskId = !!(message.taskId && lastMessageIndexByTaskId.get(message.taskId) === index); const messageKey = message.metadata?.messageId || `temp-${index}`; - return ; + const isLastMessage = index === messages.length - 1; + const shouldStream = isLastMessage && isResponding && !message.isUser; + return ; })}
diff --git a/client/webui/frontend/src/lib/components/pages/PromptsPage.tsx b/client/webui/frontend/src/lib/components/pages/PromptsPage.tsx index 1c245fb08..33a332872 100644 --- a/client/webui/frontend/src/lib/components/pages/PromptsPage.tsx +++ b/client/webui/frontend/src/lib/components/pages/PromptsPage.tsx @@ -60,7 +60,7 @@ export const PromptsPage: React.FC = () => { try { const data = await api.webui.get(`/api/v1/prompts/groups/${loaderData.promptId}`); setEditingGroup(data); - setBuilderInitialMode("manual"); + setBuilderInitialMode("ai-assisted"); // Always start in AI-assisted mode for editing setShowBuilder(true); } catch (error) { displayError({ title: "Failed to Edit Prompt", error: getErrorMessage(error, "An error occurred while fetching prompt.") }); diff --git a/client/webui/frontend/src/lib/components/projects/AddProjectFilesDialog.tsx b/client/webui/frontend/src/lib/components/projects/AddProjectFilesDialog.tsx index 82aa7196e..ce317c8f2 100644 --- a/client/webui/frontend/src/lib/components/projects/AddProjectFilesDialog.tsx +++ b/client/webui/frontend/src/lib/components/projects/AddProjectFilesDialog.tsx @@ -1,10 +1,8 @@ import React, { useState, useCallback, useEffect } from "react"; -import { FileText } from "lucide-react"; -import { Button, Card, Textarea } from "@/lib/components/ui"; -import { CardContent } from "@/lib/components/ui/card"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/lib/components/ui/dialog"; -import { MessageBanner } from "@/lib/components/common"; +import { Textarea } from "@/lib/components/ui"; +import { MessageBanner, ConfirmationDialog } from "@/lib/components/common"; +import { FileLabel } from "../chat/file/FileLabel"; interface AddProjectFilesDialogProps { isOpen: boolean; @@ -38,7 +36,7 @@ export const AddProjectFilesDialog: React.FC = ({ is })); }, []); - const handleConfirmClick = useCallback(() => { + const handleConfirmClick = useCallback(async () => { if (!files) return; const formData = new FormData(); @@ -55,52 +53,43 @@ export const AddProjectFilesDialog: React.FC = ({ is formData.append("fileMetadata", JSON.stringify(metadataPayload)); } - onConfirm(formData); + await onConfirm(formData); }, [files, fileDescriptions, onConfirm]); const fileList = files ? Array.from(files) : []; - return ( - !open && handleClose()}> - - - Upload Files to Project - Add descriptions for each file. This helps Solace Agent Mesh understand the file's purpose. - -
- {error && } - {fileList.length > 0 ? ( -
- {fileList.map((file, index) => ( - - -
- -
-

- {file.name} -

-

{(file.size / 1024).toFixed(1)} KB

-
-
-