diff --git a/.github/workflows/platform-frontend-ci.yml b/.github/workflows/platform-frontend-ci.yml index fb7a55055e..499bb03170 100644 --- a/.github/workflows/platform-frontend-ci.yml +++ b/.github/workflows/platform-frontend-ci.yml @@ -128,7 +128,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} exitOnceUploaded: true - test: + e2e_test: runs-on: big-boi needs: setup strategy: @@ -258,3 +258,39 @@ jobs: - name: Print Final Docker Compose logs if: always() run: docker compose -f ../docker-compose.yml logs + + integration_test: + runs-on: ubuntu-latest + needs: setup + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22.18.0" + + - name: Enable corepack + run: corepack enable + + - name: Restore dependencies cache + uses: actions/cache@v4 + with: + path: ~/.pnpm-store + key: ${{ needs.setup.outputs.cache-key }} + restore-keys: | + ${{ runner.os }}-pnpm-${{ hashFiles('autogpt_platform/frontend/pnpm-lock.yaml') }} + ${{ runner.os }}-pnpm- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Generate API client + run: pnpm generate:api + + - name: Run Integration Tests + run: pnpm test:unit diff --git a/AGENTS.md b/AGENTS.md index d31bc92f8c..cd176f8a2d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,32 @@ See `docs/content/platform/getting-started.md` for setup instructions. - Format Python code with `poetry run format`. - Format frontend code using `pnpm format`. + +## Frontend guidelines: + +See `/frontend/CONTRIBUTING.md` for complete patterns. Quick reference: + +1. **Pages**: Create in `src/app/(platform)/feature-name/page.tsx` + - Add `usePageName.ts` hook for logic + - Put sub-components in local `components/` folder +2. **Components**: Structure as `ComponentName/ComponentName.tsx` + `useComponentName.ts` + `helpers.ts` + - Use design system components from `src/components/` (atoms, molecules, organisms) + - Never use `src/components/__legacy__/*` +3. **Data fetching**: Use generated API hooks from `@/app/api/__generated__/endpoints/` + - Regenerate with `pnpm generate:api` + - Pattern: `use{Method}{Version}{OperationName}` +4. **Styling**: Tailwind CSS only, use design tokens, Phosphor Icons only +5. **Testing**: Add Storybook stories for new components, Playwright for E2E +6. **Code conventions**: Function declarations (not arrow functions) for components/handlers +- Component props should be `interface Props { ... }` (not exported) unless the interface needs to be used outside the component +- Separate render logic from business logic (component.tsx + useComponent.ts + helpers.ts) +- Colocate state when possible and avoid creating large components, use sub-components ( local `/components` folder next to the parent component ) when sensible +- Avoid large hooks, abstract logic into `helpers.ts` files when sensible +- Use function declarations for components, arrow functions only for callbacks +- No barrel files or `index.ts` re-exports +- Do not use `useCallback` or `useMemo` unless strictly needed +- Avoid comments at all times unless the code is very complex + ## Testing - Backend: `poetry run test` (runs pytest with a docker based postgres + prisma). diff --git a/autogpt_platform/CLAUDE.md b/autogpt_platform/CLAUDE.md index df1f3314aa..2c76e7db80 100644 --- a/autogpt_platform/CLAUDE.md +++ b/autogpt_platform/CLAUDE.md @@ -201,7 +201,7 @@ If you get any pushback or hit complex block conditions check the new_blocks gui 3. Write tests alongside the route file 4. Run `poetry run test` to verify -**Frontend feature development:** +### Frontend guidelines: See `/frontend/CONTRIBUTING.md` for complete patterns. Quick reference: @@ -217,6 +217,14 @@ See `/frontend/CONTRIBUTING.md` for complete patterns. Quick reference: 4. **Styling**: Tailwind CSS only, use design tokens, Phosphor Icons only 5. **Testing**: Add Storybook stories for new components, Playwright for E2E 6. **Code conventions**: Function declarations (not arrow functions) for components/handlers +- Component props should be `interface Props { ... }` (not exported) unless the interface needs to be used outside the component +- Separate render logic from business logic (component.tsx + useComponent.ts + helpers.ts) +- Colocate state when possible and avoid creating large components, use sub-components ( local `/components` folder next to the parent component ) when sensible +- Avoid large hooks, abstract logic into `helpers.ts` files when sensible +- Use function declarations for components, arrow functions only for callbacks +- No barrel files or `index.ts` re-exports +- Do not use `useCallback` or `useMemo` unless strictly needed +- Avoid comments at all times unless the code is very complex ### Security Implementation diff --git a/autogpt_platform/backend/backend/api/features/chat/model.py b/autogpt_platform/backend/backend/api/features/chat/model.py index ec4cf1fc8b..75bda11127 100644 --- a/autogpt_platform/backend/backend/api/features/chat/model.py +++ b/autogpt_platform/backend/backend/api/features/chat/model.py @@ -290,6 +290,11 @@ async def _cache_session(session: ChatSession) -> None: await async_redis.setex(redis_key, config.session_ttl, session.model_dump_json()) +async def cache_chat_session(session: ChatSession) -> None: + """Cache a chat session without persisting to the database.""" + await _cache_session(session) + + async def _get_session_from_db(session_id: str) -> ChatSession | None: """Get a chat session from the database.""" prisma_session = await chat_db.get_chat_session(session_id) diff --git a/autogpt_platform/backend/backend/api/features/chat/routes.py b/autogpt_platform/backend/backend/api/features/chat/routes.py index 58b017ad5e..cab51543b1 100644 --- a/autogpt_platform/backend/backend/api/features/chat/routes.py +++ b/autogpt_platform/backend/backend/api/features/chat/routes.py @@ -172,12 +172,12 @@ async def get_session( user_id: The optional authenticated user ID, or None for anonymous access. Returns: - SessionDetailResponse: Details for the requested session; raises NotFoundError if not found. + SessionDetailResponse: Details for the requested session, or None if not found. """ session = await get_chat_session(session_id, user_id) if not session: - raise NotFoundError(f"Session {session_id} not found") + raise NotFoundError(f"Session {session_id} not found.") messages = [message.model_dump() for message in session.messages] logger.info( @@ -222,6 +222,8 @@ async def stream_chat_post( session = await _validate_and_get_session(session_id, user_id) async def event_generator() -> AsyncGenerator[str, None]: + chunk_count = 0 + first_chunk_type: str | None = None async for chunk in chat_service.stream_chat_completion( session_id, request.message, @@ -230,7 +232,26 @@ async def stream_chat_post( session=session, # Pass pre-fetched session to avoid double-fetch context=request.context, ): + if chunk_count < 3: + logger.info( + "Chat stream chunk", + extra={ + "session_id": session_id, + "chunk_type": str(chunk.type), + }, + ) + if not first_chunk_type: + first_chunk_type = str(chunk.type) + chunk_count += 1 yield chunk.to_sse() + logger.info( + "Chat stream completed", + extra={ + "session_id": session_id, + "chunk_count": chunk_count, + "first_chunk_type": first_chunk_type, + }, + ) # AI SDK protocol termination yield "data: [DONE]\n\n" @@ -275,6 +296,8 @@ async def stream_chat_get( session = await _validate_and_get_session(session_id, user_id) async def event_generator() -> AsyncGenerator[str, None]: + chunk_count = 0 + first_chunk_type: str | None = None async for chunk in chat_service.stream_chat_completion( session_id, message, @@ -282,7 +305,26 @@ async def stream_chat_get( user_id=user_id, session=session, # Pass pre-fetched session to avoid double-fetch ): + if chunk_count < 3: + logger.info( + "Chat stream chunk", + extra={ + "session_id": session_id, + "chunk_type": str(chunk.type), + }, + ) + if not first_chunk_type: + first_chunk_type = str(chunk.type) + chunk_count += 1 yield chunk.to_sse() + logger.info( + "Chat stream completed", + extra={ + "session_id": session_id, + "chunk_count": chunk_count, + "first_chunk_type": first_chunk_type, + }, + ) # AI SDK protocol termination yield "data: [DONE]\n\n" diff --git a/autogpt_platform/backend/backend/api/features/chat/service.py b/autogpt_platform/backend/backend/api/features/chat/service.py index 93634c47e3..3daf378f65 100644 --- a/autogpt_platform/backend/backend/api/features/chat/service.py +++ b/autogpt_platform/backend/backend/api/features/chat/service.py @@ -1,12 +1,20 @@ import asyncio import logging +import time +from asyncio import CancelledError from collections.abc import AsyncGenerator from typing import Any import orjson from langfuse import get_client, propagate_attributes from langfuse.openai import openai # type: ignore -from openai import APIConnectionError, APIError, APIStatusError, RateLimitError +from openai import ( + APIConnectionError, + APIError, + APIStatusError, + PermissionDeniedError, + RateLimitError, +) from openai.types.chat import ChatCompletionChunk, ChatCompletionToolParam from backend.data.understanding import ( @@ -21,6 +29,7 @@ from .model import ( ChatMessage, ChatSession, Usage, + cache_chat_session, get_chat_session, update_session_title, upsert_chat_session, @@ -296,6 +305,10 @@ async def stream_chat_completion( content="", ) accumulated_tool_calls: list[dict[str, Any]] = [] + has_saved_assistant_message = False + has_appended_streaming_message = False + last_cache_time = 0.0 + last_cache_content_len = 0 # Wrap main logic in try/finally to ensure Langfuse observations are always ended has_yielded_end = False @@ -332,6 +345,23 @@ async def stream_chat_completion( assert assistant_response.content is not None assistant_response.content += delta has_received_text = True + if not has_appended_streaming_message: + session.messages.append(assistant_response) + has_appended_streaming_message = True + current_time = time.monotonic() + content_len = len(assistant_response.content) + if ( + current_time - last_cache_time >= 1.0 + and content_len > last_cache_content_len + ): + try: + await cache_chat_session(session) + except Exception as e: + logger.warning( + f"Failed to cache partial session {session.session_id}: {e}" + ) + last_cache_time = current_time + last_cache_content_len = content_len yield chunk elif isinstance(chunk, StreamTextEnd): # Emit text-end after text completes @@ -390,10 +420,42 @@ async def stream_chat_completion( if has_received_text and not text_streaming_ended: yield StreamTextEnd(id=text_block_id) text_streaming_ended = True + + # Save assistant message before yielding finish to ensure it's persisted + # even if client disconnects immediately after receiving StreamFinish + if not has_saved_assistant_message: + messages_to_save_early: list[ChatMessage] = [] + if accumulated_tool_calls: + assistant_response.tool_calls = ( + accumulated_tool_calls + ) + if not has_appended_streaming_message and ( + assistant_response.content + or assistant_response.tool_calls + ): + messages_to_save_early.append(assistant_response) + messages_to_save_early.extend(tool_response_messages) + + if messages_to_save_early: + session.messages.extend(messages_to_save_early) + logger.info( + f"Saving assistant message before StreamFinish: " + f"content_len={len(assistant_response.content or '')}, " + f"tool_calls={len(assistant_response.tool_calls or [])}, " + f"tool_responses={len(tool_response_messages)}" + ) + if ( + messages_to_save_early + or has_appended_streaming_message + ): + await upsert_chat_session(session) + has_saved_assistant_message = True + has_yielded_end = True yield chunk elif isinstance(chunk, StreamError): has_yielded_error = True + yield chunk elif isinstance(chunk, StreamUsage): session.usage.append( Usage( @@ -413,6 +475,27 @@ async def stream_chat_completion( langfuse.update_current_trace(output=str(tool_response_messages)) langfuse.update_current_span(output=str(tool_response_messages)) + except CancelledError: + if not has_saved_assistant_message: + if accumulated_tool_calls: + assistant_response.tool_calls = accumulated_tool_calls + if assistant_response.content: + assistant_response.content = ( + f"{assistant_response.content}\n\n[interrupted]" + ) + else: + assistant_response.content = "[interrupted]" + if not has_appended_streaming_message: + session.messages.append(assistant_response) + if tool_response_messages: + session.messages.extend(tool_response_messages) + try: + await upsert_chat_session(session) + except Exception as e: + logger.warning( + f"Failed to save interrupted session {session.session_id}: {e}" + ) + raise except Exception as e: logger.error(f"Error during stream: {e!s}", exc_info=True) @@ -434,14 +517,19 @@ async def stream_chat_completion( # Add assistant message if it has content or tool calls if accumulated_tool_calls: assistant_response.tool_calls = accumulated_tool_calls - if assistant_response.content or assistant_response.tool_calls: + if not has_appended_streaming_message and ( + assistant_response.content or assistant_response.tool_calls + ): messages_to_save.append(assistant_response) # Add tool response messages after assistant message messages_to_save.extend(tool_response_messages) - session.messages.extend(messages_to_save) - await upsert_chat_session(session) + if not has_saved_assistant_message: + if messages_to_save: + session.messages.extend(messages_to_save) + if messages_to_save or has_appended_streaming_message: + await upsert_chat_session(session) if not has_yielded_error: error_message = str(e) @@ -472,38 +560,49 @@ async def stream_chat_completion( return # Exit after retry to avoid double-saving in finally block # Normal completion path - save session and handle tool call continuation - logger.info( - f"Normal completion path: session={session.session_id}, " - f"current message_count={len(session.messages)}" - ) - - # Build the messages list in the correct order - messages_to_save: list[ChatMessage] = [] - - # Add assistant message with tool_calls if any - if accumulated_tool_calls: - assistant_response.tool_calls = accumulated_tool_calls + # Only save if we haven't already saved when StreamFinish was received + if not has_saved_assistant_message: logger.info( - f"Added {len(accumulated_tool_calls)} tool calls to assistant message" - ) - if assistant_response.content or assistant_response.tool_calls: - messages_to_save.append(assistant_response) - logger.info( - f"Saving assistant message with content_len={len(assistant_response.content or '')}, tool_calls={len(assistant_response.tool_calls or [])}" + f"Normal completion path: session={session.session_id}, " + f"current message_count={len(session.messages)}" ) - # Add tool response messages after assistant message - messages_to_save.extend(tool_response_messages) - logger.info( - f"Saving {len(tool_response_messages)} tool response messages, " - f"total_to_save={len(messages_to_save)}" - ) + # Build the messages list in the correct order + messages_to_save: list[ChatMessage] = [] - session.messages.extend(messages_to_save) - logger.info( - f"Extended session messages, new message_count={len(session.messages)}" - ) - await upsert_chat_session(session) + # Add assistant message with tool_calls if any + if accumulated_tool_calls: + assistant_response.tool_calls = accumulated_tool_calls + logger.info( + f"Added {len(accumulated_tool_calls)} tool calls to assistant message" + ) + if not has_appended_streaming_message and ( + assistant_response.content or assistant_response.tool_calls + ): + messages_to_save.append(assistant_response) + logger.info( + f"Saving assistant message with content_len={len(assistant_response.content or '')}, tool_calls={len(assistant_response.tool_calls or [])}" + ) + + # Add tool response messages after assistant message + messages_to_save.extend(tool_response_messages) + logger.info( + f"Saving {len(tool_response_messages)} tool response messages, " + f"total_to_save={len(messages_to_save)}" + ) + + if messages_to_save: + session.messages.extend(messages_to_save) + logger.info( + f"Extended session messages, new message_count={len(session.messages)}" + ) + if messages_to_save or has_appended_streaming_message: + await upsert_chat_session(session) + else: + logger.info( + "Assistant message already saved when StreamFinish was received, " + "skipping duplicate save" + ) # If we did a tool call, stream the chat completion again to get the next response if has_done_tool_call: @@ -545,6 +644,12 @@ def _is_retryable_error(error: Exception) -> bool: return False +def _is_region_blocked_error(error: Exception) -> bool: + if isinstance(error, PermissionDeniedError): + return "not available in your region" in str(error).lower() + return "not available in your region" in str(error).lower() + + async def _stream_chat_chunks( session: ChatSession, tools: list[ChatCompletionToolParam], @@ -737,7 +842,18 @@ async def _stream_chat_chunks( f"Error in stream (not retrying): {e!s}", exc_info=True, ) - error_response = StreamError(errorText=str(e)) + error_code = None + error_text = str(e) + if _is_region_blocked_error(e): + error_code = "MODEL_NOT_AVAILABLE_REGION" + error_text = ( + "This model is not available in your region. " + "Please connect via VPN and try again." + ) + error_response = StreamError( + errorText=error_text, + code=error_code, + ) yield error_response yield StreamFinish() return diff --git a/autogpt_platform/backend/migrations/20260109181714_add_docs_embedding/migration.sql b/autogpt_platform/backend/migrations/20260109181714_add_docs_embedding/migration.sql index 855fe36933..a839070a28 100644 --- a/autogpt_platform/backend/migrations/20260109181714_add_docs_embedding/migration.sql +++ b/autogpt_platform/backend/migrations/20260109181714_add_docs_embedding/migration.sql @@ -1,12 +1,37 @@ -- CreateExtension -- Supabase: pgvector must be enabled via Dashboard → Database → Extensions first --- Creates extension in current schema (determined by search_path from DATABASE_URL ?schema= param) +-- Ensures vector extension is in the current schema (from DATABASE_URL ?schema= param) +-- If it exists in a different schema (e.g., public), we drop and recreate it in the current schema -- This ensures vector type is in the same schema as tables, making ::vector work without explicit qualification DO $$ +DECLARE + current_schema_name text; + vector_schema text; BEGIN - CREATE EXTENSION IF NOT EXISTS "vector"; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'vector extension not available or already exists, skipping'; + -- Get the current schema from search_path + SELECT current_schema() INTO current_schema_name; + + -- Check if vector extension exists and which schema it's in + SELECT n.nspname INTO vector_schema + FROM pg_extension e + JOIN pg_namespace n ON e.extnamespace = n.oid + WHERE e.extname = 'vector'; + + -- Handle removal if in wrong schema + IF vector_schema IS NOT NULL AND vector_schema != current_schema_name THEN + BEGIN + -- Vector exists in a different schema, drop it first + RAISE WARNING 'pgvector found in schema "%" but need it in "%". Dropping and reinstalling...', + vector_schema, current_schema_name; + EXECUTE 'DROP EXTENSION IF EXISTS vector CASCADE'; + EXCEPTION WHEN OTHERS THEN + RAISE EXCEPTION 'Failed to drop pgvector from schema "%": %. You may need to drop it manually.', + vector_schema, SQLERRM; + END; + END IF; + + -- Create extension in current schema (let it fail naturally if not available) + EXECUTE format('CREATE EXTENSION IF NOT EXISTS vector SCHEMA %I', current_schema_name); END $$; -- CreateEnum diff --git a/autogpt_platform/backend/migrations/20260112173500_add_supabase_extensions_to_platform_schema/migration.sql b/autogpt_platform/backend/migrations/20260112173500_add_supabase_extensions_to_platform_schema/migration.sql deleted file mode 100644 index ca91bc5cab..0000000000 --- a/autogpt_platform/backend/migrations/20260112173500_add_supabase_extensions_to_platform_schema/migration.sql +++ /dev/null @@ -1,71 +0,0 @@ --- Acknowledge Supabase-managed extensions to prevent drift warnings --- These extensions are pre-installed by Supabase in specific schemas --- This migration ensures they exist where available (Supabase) or skips gracefully (CI) - --- Create schemas (safe in both CI and Supabase) -CREATE SCHEMA IF NOT EXISTS "extensions"; - --- Extensions that exist in both CI and Supabase -DO $$ -BEGIN - CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions"; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'pgcrypto extension not available, skipping'; -END $$; - -DO $$ -BEGIN - CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions"; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'uuid-ossp extension not available, skipping'; -END $$; - --- Supabase-specific extensions (skip gracefully in CI) -DO $$ -BEGIN - CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions"; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'pg_stat_statements extension not available, skipping'; -END $$; - -DO $$ -BEGIN - CREATE EXTENSION IF NOT EXISTS "pg_net" WITH SCHEMA "extensions"; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'pg_net extension not available, skipping'; -END $$; - -DO $$ -BEGIN - CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions"; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'pgjwt extension not available, skipping'; -END $$; - -DO $$ -BEGIN - CREATE SCHEMA IF NOT EXISTS "graphql"; - CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql"; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'pg_graphql extension not available, skipping'; -END $$; - -DO $$ -BEGIN - CREATE SCHEMA IF NOT EXISTS "pgsodium"; - CREATE EXTENSION IF NOT EXISTS "pgsodium" WITH SCHEMA "pgsodium"; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'pgsodium extension not available, skipping'; -END $$; - -DO $$ -BEGIN - CREATE SCHEMA IF NOT EXISTS "vault"; - CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault"; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'supabase_vault extension not available, skipping'; -END $$; - - --- Return to platform -CREATE SCHEMA IF NOT EXISTS "platform"; \ No newline at end of file diff --git a/autogpt_platform/frontend/.env.default b/autogpt_platform/frontend/.env.default index dc3f67efab..197a37e8bb 100644 --- a/autogpt_platform/frontend/.env.default +++ b/autogpt_platform/frontend/.env.default @@ -29,4 +29,4 @@ NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY= NEXT_PUBLIC_TURNSTILE=disabled # PR previews -NEXT_PUBLIC_PREVIEW_STEALING_DEV= \ No newline at end of file +NEXT_PUBLIC_PREVIEW_STEALING_DEV= diff --git a/autogpt_platform/frontend/CONTRIBUTING.md b/autogpt_platform/frontend/CONTRIBUTING.md index 1b2b810986..649bb1ca92 100644 --- a/autogpt_platform/frontend/CONTRIBUTING.md +++ b/autogpt_platform/frontend/CONTRIBUTING.md @@ -175,6 +175,8 @@ While server components and actions are cool and cutting-edge, they introduce a - Prefer [React Query](https://tanstack.com/query/latest/docs/framework/react/overview) for server state, colocated near consumers (see [state colocation](https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster)) - Co-locate UI state inside components/hooks; keep global state minimal +- Avoid `useMemo` and `useCallback` unless you have a measured performance issue +- Do not abuse `useEffect`; prefer state colocation and derive values directly when possible ### Styling and components @@ -549,9 +551,48 @@ Files: Types: - Prefer `interface` for object shapes -- Component props should be `interface Props { ... }` +- Component props should be `interface Props { ... }` (not exported) +- Only use specific exported names (e.g., `export interface MyComponentProps`) when the interface needs to be used outside the component +- Keep type definitions inline with the component - do not create separate `types.ts` files unless types are shared across multiple files - Use precise types; avoid `any` and unsafe casts +**Props naming examples:** + +```tsx +// ✅ Good - internal props, not exported +interface Props { + title: string; + onClose: () => void; +} + +export function Modal({ title, onClose }: Props) { + // ... +} + +// ✅ Good - exported when needed externally +export interface ModalProps { + title: string; + onClose: () => void; +} + +export function Modal({ title, onClose }: ModalProps) { + // ... +} + +// ❌ Bad - unnecessarily specific name for internal use +interface ModalComponentProps { + title: string; + onClose: () => void; +} + +// ❌ Bad - separate types.ts file for single component +// types.ts +export interface ModalProps { ... } + +// Modal.tsx +import type { ModalProps } from './types'; +``` + Parameters: - If more than one parameter is needed, pass a single `Args` object for clarity diff --git a/autogpt_platform/frontend/orval.config.ts b/autogpt_platform/frontend/orval.config.ts index dff857e1b6..9e09467518 100644 --- a/autogpt_platform/frontend/orval.config.ts +++ b/autogpt_platform/frontend/orval.config.ts @@ -16,6 +16,12 @@ export default defineConfig({ client: "react-query", httpClient: "fetch", indexFiles: false, + mock: { + type: "msw", + baseUrl: "http://localhost:3000/api/proxy", + generateEachHttpStatus: true, + delay: 0, + }, override: { mutator: { path: "./mutators/custom-mutator.ts", diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index 0823400c87..bc1e2d7443 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -15,6 +15,8 @@ "types": "tsc --noEmit", "test": "NEXT_PUBLIC_PW_TEST=true next build --turbo && playwright test", "test-ui": "NEXT_PUBLIC_PW_TEST=true next build --turbo && playwright test --ui", + "test:unit": "vitest run", + "test:unit:watch": "vitest", "test:no-build": "playwright test", "gentests": "playwright codegen http://localhost:3000", "storybook": "storybook dev -p 6006", @@ -118,6 +120,7 @@ }, "devDependencies": { "@chromatic-com/storybook": "4.1.2", + "happy-dom": "20.3.4", "@opentelemetry/instrumentation": "0.209.0", "@playwright/test": "1.56.1", "@storybook/addon-a11y": "9.1.5", @@ -127,6 +130,8 @@ "@storybook/nextjs": "9.1.5", "@tanstack/eslint-plugin-query": "5.91.2", "@tanstack/react-query-devtools": "5.90.2", + "@testing-library/dom": "10.4.1", + "@testing-library/react": "16.3.2", "@types/canvas-confetti": "1.9.0", "@types/lodash": "4.17.20", "@types/negotiator": "0.6.4", @@ -135,6 +140,7 @@ "@types/react-dom": "18.3.5", "@types/react-modal": "3.16.3", "@types/react-window": "1.8.8", + "@vitejs/plugin-react": "5.1.2", "axe-playwright": "2.2.2", "chromatic": "13.3.3", "concurrently": "9.2.1", @@ -153,7 +159,9 @@ "require-in-the-middle": "8.0.1", "storybook": "9.1.5", "tailwindcss": "3.4.17", - "typescript": "5.9.3" + "typescript": "5.9.3", + "vite-tsconfig-paths": "6.0.4", + "vitest": "4.0.17" }, "msw": { "workerDirectory": [ diff --git a/autogpt_platform/frontend/pnpm-lock.yaml b/autogpt_platform/frontend/pnpm-lock.yaml index f503c23ea8..8e83289f03 100644 --- a/autogpt_platform/frontend/pnpm-lock.yaml +++ b/autogpt_platform/frontend/pnpm-lock.yaml @@ -275,7 +275,7 @@ importers: devDependencies: '@chromatic-com/storybook': specifier: 4.1.2 - version: 4.1.2(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)) + version: 4.1.2(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))) '@opentelemetry/instrumentation': specifier: 0.209.0 version: 0.209.0(@opentelemetry/api@1.9.0) @@ -284,25 +284,31 @@ importers: version: 1.56.1 '@storybook/addon-a11y': specifier: 9.1.5 - version: 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)) + version: 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))) '@storybook/addon-docs': specifier: 9.1.5 - version: 9.1.5(@types/react@18.3.17)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)) + version: 9.1.5(@types/react@18.3.17)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))) '@storybook/addon-links': specifier: 9.1.5 - version: 9.1.5(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)) + version: 9.1.5(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))) '@storybook/addon-onboarding': specifier: 9.1.5 - version: 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)) + version: 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))) '@storybook/nextjs': specifier: 9.1.5 - version: 9.1.5(esbuild@0.25.12)(next@15.4.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(type-fest@4.41.0)(typescript@5.9.3)(webpack-hot-middleware@2.26.1)(webpack@5.104.1(esbuild@0.25.12)) + version: 9.1.5(esbuild@0.25.12)(next@15.4.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))(type-fest@4.41.0)(typescript@5.9.3)(webpack-hot-middleware@2.26.1)(webpack@5.104.1(esbuild@0.25.12)) '@tanstack/eslint-plugin-query': specifier: 5.91.2 version: 5.91.2(eslint@8.57.1)(typescript@5.9.3) '@tanstack/react-query-devtools': specifier: 5.90.2 version: 5.90.2(@tanstack/react-query@5.90.6(react@18.3.1))(react@18.3.1) + '@testing-library/dom': + specifier: 10.4.1 + version: 10.4.1 + '@testing-library/react': + specifier: 16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/canvas-confetti': specifier: 1.9.0 version: 1.9.0 @@ -327,6 +333,9 @@ importers: '@types/react-window': specifier: 1.8.8 version: 1.8.8 + '@vitejs/plugin-react': + specifier: 5.1.2 + version: 5.1.2(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) axe-playwright: specifier: 2.2.2 version: 2.2.2(playwright@1.56.1) @@ -347,7 +356,10 @@ importers: version: 15.5.7(eslint@8.57.1)(typescript@5.9.3) eslint-plugin-storybook: specifier: 9.1.5 - version: 9.1.5(eslint@8.57.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(typescript@5.9.3) + version: 9.1.5(eslint@8.57.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))(typescript@5.9.3) + happy-dom: + specifier: 20.3.4 + version: 20.3.4 import-in-the-middle: specifier: 2.0.2 version: 2.0.2 @@ -377,16 +389,25 @@ importers: version: 8.0.1 storybook: specifier: 9.1.5 - version: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2) + version: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) tailwindcss: specifier: 3.4.17 version: 3.4.17 typescript: specifier: 5.9.3 version: 5.9.3 + vite-tsconfig-paths: + specifier: 6.0.4 + version: 6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) + vitest: + specifier: 4.0.17 + version: 4.0.17(@opentelemetry/api@1.9.0)(@types/node@24.10.0)(happy-dom@20.3.4)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(terser@5.44.1)(yaml@2.8.2) packages: + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} @@ -416,6 +437,15 @@ packages: '@apm-js-collab/tracing-hooks@0.3.1': resolution: {integrity: sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw==} + '@asamuzakjp/css-color@4.1.1': + resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==} + + '@asamuzakjp/dom-selector@6.7.6': + resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@asyncapi/specs@6.10.0': resolution: {integrity: sha512-vB5oKLsdrLUORIZ5BXortZTlVyGWWMC1Nud/0LtgxQ3Yn2738HigAD6EVqScvpPsDUI/bcLVsYEXN4dtXQHVng==} @@ -849,6 +879,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx@7.27.1': resolution: {integrity: sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==} engines: {node: '>=6.9.0'} @@ -995,6 +1037,38 @@ packages: peerDependencies: commander: ~14.0.0 + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.25': + resolution: {integrity: sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q==} + engines: {node: '>=18'} + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@date-fns/tz@1.4.1': resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} @@ -1022,156 +1096,312 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1190,6 +1420,15 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@exodus/bytes@1.9.0': + resolution: {integrity: sha512-lagqsvnk09NKogQaN/XrtlWeUF8SRhT12odMvbTIIaVObqzwAogL6jhR4DAp0gPuKoM1AOVrKUshJpRdpMFrww==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@exodus/schemasafe@1.3.0': resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} @@ -2407,6 +2646,9 @@ packages: peerDependencies: '@rjsf/utils': ^6.x + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + '@rollup/plugin-commonjs@28.0.1': resolution: {integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==} engines: {node: '>=16.0.0 || 14 >= 14.17'} @@ -2965,6 +3207,21 @@ packages: resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + 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 + '@testing-library/user-event@14.6.1': resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} engines: {node: '>=12', npm: '>=6'} @@ -3156,6 +3413,9 @@ packages: '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -3365,9 +3625,18 @@ packages: vue-router: optional: true + '@vitejs/plugin-react@5.1.2': + resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/expect@4.0.17': + resolution: {integrity: sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==} + '@vitest/mocker@3.2.4': resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} peerDependencies: @@ -3379,15 +3648,41 @@ packages: vite: optional: true + '@vitest/mocker@4.0.17': + resolution: {integrity: sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@4.0.17': + resolution: {integrity: sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==} + + '@vitest/runner@4.0.17': + resolution: {integrity: sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==} + + '@vitest/snapshot@4.0.17': + resolution: {integrity: sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==} + '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@4.0.17': + resolution: {integrity: sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==} + '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@4.0.17': + resolution: {integrity: sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -3484,6 +3779,10 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -3702,6 +4001,9 @@ packages: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} @@ -3811,6 +4113,10 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -4025,6 +4331,10 @@ packages: css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} @@ -4037,6 +4347,10 @@ packages: engines: {node: '>=4'} hasBin: true + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -4109,6 +4423,10 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + data-urls@6.0.1: + resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} + engines: {node: '>=20'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -4147,6 +4465,9 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} @@ -4388,6 +4709,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -4562,6 +4888,10 @@ packages: exenv@1.2.2: resolution: {integrity: sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -4802,6 +5132,9 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -4816,6 +5149,10 @@ packages: resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + happy-dom@20.3.4: + resolution: {integrity: sha512-rfbiwB6OKxZFIFQ7SRnCPB2WL9WhyXsFoTfecYgeCeFSOBxvkWLaXsdv5ehzJrfqwXQmDephAKWLRQoFoJwrew==} + engines: {node: '>=20.0.0'} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -4907,6 +5244,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-entities@2.6.0: resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} @@ -4933,6 +5274,10 @@ packages: htmlparser2@6.1.0: resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + http2-client@1.3.5: resolution: {integrity: sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==} @@ -4943,6 +5288,10 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -5128,6 +5477,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -5217,6 +5569,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsep@1.4.0: resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} engines: {node: '>= 10.16.0'} @@ -5403,6 +5764,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -5500,6 +5865,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -5862,6 +6230,9 @@ packages: objectorarray@1.0.5: resolution: {integrity: sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg==} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -5952,6 +6323,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + party-js@2.2.0: resolution: {integrity: sha512-50hGuALCpvDTrQLPQ1fgUgxKIWAH28ShVkmeK/3zhO0YJyCqkhrZhQEkWPxDYLvbFJ7YAXyROmFEu35gKpZLtQ==} @@ -5991,6 +6365,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} @@ -6359,6 +6736,10 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -6627,6 +7008,10 @@ packages: webpack: optional: true + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -6724,6 +7109,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -6766,6 +7154,9 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} @@ -6777,6 +7168,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -6930,6 +7324,9 @@ packages: resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} hasBin: true + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tailwind-merge@2.6.0: resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} @@ -6994,6 +7391,13 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -7002,6 +7406,10 @@ packages: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + tinyspy@4.0.4: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} @@ -7028,6 +7436,10 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -7061,6 +7473,16 @@ packages: typescript: optional: true + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + tsconfig-paths-webpack-plugin@4.2.0: resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} engines: {node: '>=10.13.0'} @@ -7300,9 +7722,95 @@ packages: victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite-tsconfig-paths@6.0.4: + resolution: {integrity: sha512-iIsEJ+ek5KqRTK17pmxtgIxXtqr3qDdE6OxrP9mVeGhVDNXRJTKN/l9oMbujTQNzMLe6XZ8qmpztfbkPu2TiFQ==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.0.17: + resolution: {integrity: sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.17 + '@vitest/browser-preview': 4.0.17 + '@vitest/browser-webdriverio': 4.0.17 + '@vitest/ui': 4.0.17 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + warning@4.0.3: resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} @@ -7316,6 +7824,10 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-dev-middleware@6.1.3: resolution: {integrity: sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw==} engines: {node: '>= 14.15.0'} @@ -7348,6 +7860,22 @@ packages: webpack-cli: optional: true + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -7372,6 +7900,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -7403,10 +7936,17 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + xmlbuilder@15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -7488,6 +8028,9 @@ packages: snapshots: + '@acemir/cssom@0.9.31': + optional: true + '@adobe/css-tools@4.4.4': {} '@alloc/quick-lru@5.2.0': {} @@ -7521,6 +8064,27 @@ snapshots: transitivePeerDependencies: - supports-color + '@asamuzakjp/css-color@4.1.1': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.4 + optional: true + + '@asamuzakjp/dom-selector@6.7.6': + 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 + optional: true + + '@asamuzakjp/nwsapi@2.3.9': + optional: true + '@asyncapi/specs@6.10.0': dependencies: '@types/json-schema': 7.0.15 @@ -8036,6 +8600,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -8274,13 +8848,13 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@chromatic-com/storybook@4.1.2(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))': + '@chromatic-com/storybook@4.1.2(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 12.2.0 filesize: 10.1.6 jsonfile: 6.2.0 - storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2) + storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) strip-ansi: 7.1.2 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -8290,6 +8864,34 @@ snapshots: dependencies: commander: 14.0.2 + '@csstools/color-helpers@5.1.0': + optional: true + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-syntax-patches-for-csstree@1.0.25': + optional: true + + '@csstools/css-tokenizer@3.0.4': + optional: true + '@date-fns/tz@1.4.1': {} '@emnapi/core@1.8.1': @@ -8321,81 +8923,159 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true + '@esbuild/aix-ppc64@0.27.2': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true + '@esbuild/android-arm64@0.27.2': + optional: true + '@esbuild/android-arm@0.25.12': optional: true + '@esbuild/android-arm@0.27.2': + optional: true + '@esbuild/android-x64@0.25.12': optional: true + '@esbuild/android-x64@0.27.2': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true + '@esbuild/darwin-arm64@0.27.2': + optional: true + '@esbuild/darwin-x64@0.25.12': optional: true + '@esbuild/darwin-x64@0.27.2': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true + '@esbuild/freebsd-arm64@0.27.2': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true + '@esbuild/freebsd-x64@0.27.2': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true + '@esbuild/linux-arm64@0.27.2': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true + '@esbuild/linux-arm@0.27.2': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true + '@esbuild/linux-ia32@0.27.2': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true + '@esbuild/linux-loong64@0.27.2': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true + '@esbuild/linux-mips64el@0.27.2': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true + '@esbuild/linux-ppc64@0.27.2': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true + '@esbuild/linux-riscv64@0.27.2': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true + '@esbuild/linux-s390x@0.27.2': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true + '@esbuild/linux-x64@0.27.2': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true + '@esbuild/netbsd-arm64@0.27.2': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true + '@esbuild/netbsd-x64@0.27.2': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true + '@esbuild/openbsd-arm64@0.27.2': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true + '@esbuild/openbsd-x64@0.27.2': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true + '@esbuild/openharmony-arm64@0.27.2': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true + '@esbuild/sunos-x64@0.27.2': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true + '@esbuild/win32-arm64@0.27.2': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true + '@esbuild/win32-ia32@0.27.2': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true + '@esbuild/win32-x64@0.27.2': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': dependencies: eslint: 8.57.1 @@ -8419,6 +9099,9 @@ snapshots: '@eslint/js@8.57.1': {} + '@exodus/bytes@1.9.0': + optional: true + '@exodus/schemasafe@1.3.0': {} '@faker-js/faker@10.0.0': {} @@ -9755,6 +10438,8 @@ snapshots: lodash: 4.17.21 lodash-es: 4.17.22 + '@rolldown/pluginutils@1.0.0-beta.53': {} + '@rollup/plugin-commonjs@28.0.1(rollup@4.55.1)': dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.55.1) @@ -10247,39 +10932,39 @@ snapshots: '@stoplight/yaml-ast-parser': 0.0.50 tslib: 2.8.1 - '@storybook/addon-a11y@9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))': + '@storybook/addon-a11y@9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.11.1 - storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2) + storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) - '@storybook/addon-docs@9.1.5(@types/react@18.3.17)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))': + '@storybook/addon-docs@9.1.5(@types/react@18.3.17)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))': dependencies: '@mdx-js/react': 3.1.1(@types/react@18.3.17)(react@18.3.1) - '@storybook/csf-plugin': 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)) + '@storybook/csf-plugin': 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))) '@storybook/icons': 1.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/react-dom-shim': 9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)) + '@storybook/react-dom-shim': 9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2) + storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-links@9.1.5(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))': + '@storybook/addon-links@9.1.5(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))': dependencies: '@storybook/global': 5.0.0 - storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2) + storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) optionalDependencies: react: 18.3.1 - '@storybook/addon-onboarding@9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))': + '@storybook/addon-onboarding@9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))': dependencies: - storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2) + storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) - '@storybook/builder-webpack5@9.1.5(esbuild@0.25.12)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(typescript@5.9.3)': + '@storybook/builder-webpack5@9.1.5(esbuild@0.25.12)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))(typescript@5.9.3)': dependencies: - '@storybook/core-webpack': 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)) + '@storybook/core-webpack': 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))) case-sensitive-paths-webpack-plugin: 2.4.0 cjs-module-lexer: 1.4.3 css-loader: 6.11.0(webpack@5.104.1(esbuild@0.25.12)) @@ -10287,7 +10972,7 @@ snapshots: fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.9.3)(webpack@5.104.1(esbuild@0.25.12)) html-webpack-plugin: 5.6.5(webpack@5.104.1(esbuild@0.25.12)) magic-string: 0.30.21 - storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2) + storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) style-loader: 3.3.4(webpack@5.104.1(esbuild@0.25.12)) terser-webpack-plugin: 5.3.16(esbuild@0.25.12)(webpack@5.104.1(esbuild@0.25.12)) ts-dedent: 2.2.0 @@ -10304,14 +10989,14 @@ snapshots: - uglify-js - webpack-cli - '@storybook/core-webpack@9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))': + '@storybook/core-webpack@9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))': dependencies: - storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2) + storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) ts-dedent: 2.2.0 - '@storybook/csf-plugin@9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))': + '@storybook/csf-plugin@9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))': dependencies: - storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2) + storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) unplugin: 1.16.1 '@storybook/global@5.0.0': {} @@ -10321,7 +11006,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/nextjs@9.1.5(esbuild@0.25.12)(next@15.4.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(type-fest@4.41.0)(typescript@5.9.3)(webpack-hot-middleware@2.26.1)(webpack@5.104.1(esbuild@0.25.12))': + '@storybook/nextjs@9.1.5(esbuild@0.25.12)(next@15.4.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))(type-fest@4.41.0)(typescript@5.9.3)(webpack-hot-middleware@2.26.1)(webpack@5.104.1(esbuild@0.25.12))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.5) @@ -10337,9 +11022,9 @@ snapshots: '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) '@babel/runtime': 7.28.4 '@pmmmwh/react-refresh-webpack-plugin': 0.5.17(react-refresh@0.14.2)(type-fest@4.41.0)(webpack-hot-middleware@2.26.1)(webpack@5.104.1(esbuild@0.25.12)) - '@storybook/builder-webpack5': 9.1.5(esbuild@0.25.12)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(typescript@5.9.3) - '@storybook/preset-react-webpack': 9.1.5(esbuild@0.25.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(typescript@5.9.3) - '@storybook/react': 9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(typescript@5.9.3) + '@storybook/builder-webpack5': 9.1.5(esbuild@0.25.12)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))(typescript@5.9.3) + '@storybook/preset-react-webpack': 9.1.5(esbuild@0.25.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))(typescript@5.9.3) + '@storybook/react': 9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))(typescript@5.9.3) '@types/semver': 7.7.1 babel-loader: 9.2.1(@babel/core@7.28.5)(webpack@5.104.1(esbuild@0.25.12)) css-loader: 6.11.0(webpack@5.104.1(esbuild@0.25.12)) @@ -10355,7 +11040,7 @@ snapshots: resolve-url-loader: 5.0.0 sass-loader: 16.0.6(webpack@5.104.1(esbuild@0.25.12)) semver: 7.7.3 - storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2) + storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) style-loader: 3.3.4(webpack@5.104.1(esbuild@0.25.12)) styled-jsx: 5.1.7(@babel/core@7.28.5)(react@18.3.1) tsconfig-paths: 4.2.0 @@ -10381,9 +11066,9 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@storybook/preset-react-webpack@9.1.5(esbuild@0.25.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(typescript@5.9.3)': + '@storybook/preset-react-webpack@9.1.5(esbuild@0.25.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))(typescript@5.9.3)': dependencies: - '@storybook/core-webpack': 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)) + '@storybook/core-webpack': 9.1.5(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))) '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.9.3)(webpack@5.104.1(esbuild@0.25.12)) '@types/semver': 7.7.1 find-up: 7.0.0 @@ -10393,7 +11078,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) resolve: 1.22.11 semver: 7.7.3 - storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2) + storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) tsconfig-paths: 4.2.0 webpack: 5.104.1(esbuild@0.25.12) optionalDependencies: @@ -10419,19 +11104,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/react-dom-shim@9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))': + '@storybook/react-dom-shim@9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2) + storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) - '@storybook/react@9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(typescript@5.9.3)': + '@storybook/react@9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))(typescript@5.9.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)) + '@storybook/react-dom-shim': 9.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2) + storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) optionalDependencies: typescript: 5.9.3 @@ -10542,6 +11227,16 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.4 + '@testing-library/dom': 10.4.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.17 + '@types/react-dom': 18.3.5(@types/react@18.3.17) + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 @@ -10739,6 +11434,8 @@ snapshots: '@types/use-sync-external-store@0.0.6': {} + '@types/whatwg-mimetype@3.0.2': {} + '@types/ws@8.18.1': dependencies: '@types/node': 24.10.0 @@ -10905,6 +11602,18 @@ snapshots: next: 15.4.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 + '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.53 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.3 @@ -10913,28 +11622,69 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))': + '@vitest/expect@4.0.17': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.17 + '@vitest/utils': 4.0.17 + chai: 6.2.2 + tinyrainbow: 3.0.3 + + '@vitest/mocker@3.2.4(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.11.6(@types/node@24.10.0)(typescript@5.9.3) + vite: 7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2) + + '@vitest/mocker@4.0.17(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.0.17 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.11.6(@types/node@24.10.0)(typescript@5.9.3) + vite: 7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 + '@vitest/pretty-format@4.0.17': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.17': + dependencies: + '@vitest/utils': 4.0.17 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.17': + dependencies: + '@vitest/pretty-format': 4.0.17 + magic-string: 0.30.21 + pathe: 2.0.3 + '@vitest/spy@3.2.4': dependencies: tinyspy: 4.0.4 + '@vitest/spy@4.0.17': {} + '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 loupe: 3.2.1 tinyrainbow: 2.0.0 + '@vitest/utils@4.0.17': + dependencies: + '@vitest/pretty-format': 4.0.17 + tinyrainbow: 3.0.3 + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -11071,6 +11821,9 @@ snapshots: transitivePeerDependencies: - supports-color + agent-base@7.1.4: + optional: true + ajv-draft-04@1.0.0(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -11305,6 +12058,11 @@ snapshots: dependencies: open: 8.4.2 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + optional: true + big.js@5.2.2: {} binary-extensions@2.3.0: {} @@ -11437,6 +12195,8 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -11655,12 +12415,26 @@ snapshots: domutils: 2.8.0 nth-check: 2.1.1 + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + optional: true + css-what@6.2.2: {} css.escape@1.5.1: {} cssesc@3.0.0: {} + cssstyle@5.3.7: + dependencies: + '@asamuzakjp/css-color': 4.1.1 + '@csstools/css-syntax-patches-for-csstree': 1.0.25 + css-tree: 3.1.0 + lru-cache: 11.2.4 + optional: true + csstype@3.2.3: {} d3-array@3.2.4: @@ -11729,6 +12503,12 @@ snapshots: damerau-levenshtein@1.0.8: {} + data-urls@6.0.1: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 15.1.0 + optional: true + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -11761,6 +12541,9 @@ snapshots: decimal.js-light@2.5.1: {} + decimal.js@10.6.0: + optional: true + decode-named-character-reference@1.2.0: dependencies: character-entities: 2.0.2 @@ -12094,6 +12877,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -12228,11 +13040,11 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-storybook@9.1.5(eslint@8.57.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2))(typescript@5.9.3): + eslint-plugin-storybook@9.1.5(eslint@8.57.1)(storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)))(typescript@5.9.3): dependencies: '@typescript-eslint/utils': 8.52.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 - storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2) + storybook: 9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) transitivePeerDependencies: - supports-color - typescript @@ -12349,6 +13161,8 @@ snapshots: exenv@1.2.2: {} + expect-type@1.3.0: {} + extend@3.0.2: {} fast-deep-equal@2.0.1: {} @@ -12612,6 +13426,8 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + globrex@0.1.2: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -12620,6 +13436,18 @@ snapshots: graphql@16.12.0: {} + happy-dom@20.3.4: + dependencies: + '@types/node': 24.10.0 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + entities: 4.5.0 + whatwg-mimetype: 3.0.0 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -12763,6 +13591,13 @@ snapshots: dependencies: react-is: 16.13.1 + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.9.0 + transitivePeerDependencies: + - '@noble/hashes' + optional: true + html-entities@2.6.0: {} html-minifier-terser@6.1.0: @@ -12794,6 +13629,14 @@ snapshots: domutils: 2.8.0 entities: 2.2.0 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + http2-client@1.3.5: {} https-browserify@1.0.0: {} @@ -12805,6 +13648,14 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + human-signals@2.1.0: {} icss-utils@5.1.0(postcss@8.5.6): @@ -12970,6 +13821,9 @@ snapshots: is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: + optional: true + is-reference@1.2.1: dependencies: '@types/estree': 1.0.8 @@ -13062,6 +13916,35 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@27.4.0: + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.7.6 + '@exodus/bytes': 1.9.0 + cssstyle: 5.3.7 + data-urls: 6.0.1 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.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 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - supports-color + - utf-8-validate + optional: true + jsep@1.4.0: {} jsesc@3.1.0: {} @@ -13229,6 +14112,9 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.4: + optional: true + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -13441,6 +14327,9 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + mdn-data@2.12.2: + optional: true + mdurl@2.0.0: {} memfs@3.5.3: @@ -13943,6 +14832,8 @@ snapshots: objectorarray@1.0.5: {} + obug@2.1.1: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -14085,6 +14976,11 @@ snapshots: dependencies: entities: 6.0.1 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + optional: true + party-js@2.2.0: {} pascal-case@3.1.2: @@ -14113,6 +15009,8 @@ snapshots: path-type@4.0.0: {} + pathe@2.0.3: {} + pathval@2.0.1: {} pbkdf2@3.1.5: @@ -14414,6 +15312,8 @@ snapshots: react-refresh@0.14.2: {} + react-refresh@0.18.0: {} + react-remove-scroll-bar@2.3.8(@types/react@18.3.17)(react@18.3.1): dependencies: react: 18.3.1 @@ -14791,6 +15691,11 @@ snapshots: optionalDependencies: webpack: 5.104.1(esbuild@0.25.12) + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + optional: true + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -14946,6 +15851,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -14976,6 +15883,8 @@ snapshots: stable-hash@0.0.5: {} + stackback@0.0.2: {} + stackframe@1.3.4: {} stacktrace-parser@0.1.11: @@ -14984,18 +15893,20 @@ snapshots: statuses@2.0.2: {} + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2): + storybook@9.1.5(@testing-library/dom@10.4.1)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(prettier@3.6.2)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)): dependencies: '@storybook/global': 5.0.0 '@testing-library/jest-dom': 6.9.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3)) + '@vitest/mocker': 3.2.4(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) '@vitest/spy': 3.2.4 better-opn: 3.0.2 esbuild: 0.25.12 @@ -15186,6 +16097,9 @@ snapshots: transitivePeerDependencies: - encoding + symbol-tree@3.2.4: + optional: true + tailwind-merge@2.6.0: {} tailwind-scrollbar@3.1.0(tailwindcss@3.4.17): @@ -15261,6 +16175,10 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + + tinyexec@1.0.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -15268,6 +16186,8 @@ snapshots: tinyrainbow@2.0.0: {} + tinyrainbow@3.0.3: {} + tinyspy@4.0.4: {} tldts-core@7.0.19: {} @@ -15292,6 +16212,11 @@ snapshots: tr46@0.0.3: {} + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + optional: true + tree-kill@1.2.2: {} trim-lines@3.0.1: {} @@ -15310,6 +16235,10 @@ snapshots: optionalDependencies: typescript: 5.9.3 + tsconfck@3.1.6(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 @@ -15606,8 +16535,79 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite-tsconfig-paths@6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)): + dependencies: + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.3) + optionalDependencies: + vite: 7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + - typescript + + vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.55.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.0 + fsevents: 2.3.3 + jiti: 2.6.1 + terser: 5.44.1 + yaml: 2.8.2 + + vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@24.10.0)(happy-dom@20.3.4)(jiti@2.6.1)(jsdom@27.4.0)(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(terser@5.44.1)(yaml@2.8.2): + dependencies: + '@vitest/expect': 4.0.17 + '@vitest/mocker': 4.0.17(msw@2.11.6(@types/node@24.10.0)(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) + '@vitest/pretty-format': 4.0.17 + '@vitest/runner': 4.0.17 + '@vitest/snapshot': 4.0.17 + '@vitest/spy': 4.0.17 + '@vitest/utils': 4.0.17 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.3.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 24.10.0 + happy-dom: 20.3.4 + jsdom: 27.4.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + vm-browserify@1.1.2: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + optional: true + warning@4.0.3: dependencies: loose-envify: 1.4.0 @@ -15621,6 +16621,9 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@8.0.1: + optional: true + webpack-dev-middleware@6.1.3(webpack@5.104.1(esbuild@0.25.12)): dependencies: colorette: 2.0.20 @@ -15675,6 +16678,20 @@ snapshots: - esbuild - uglify-js + whatwg-mimetype@3.0.0: {} + + whatwg-mimetype@4.0.0: + optional: true + + whatwg-mimetype@5.0.0: + optional: true + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + optional: true + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -15725,6 +16742,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@6.2.0: @@ -15749,8 +16771,14 @@ snapshots: ws@8.19.0: {} + xml-name-validator@5.0.0: + optional: true + xmlbuilder@15.1.1: {} + xmlchars@2.2.0: + optional: true + xtend@4.0.2: {} y18n@5.0.8: {} diff --git a/autogpt_platform/frontend/src/app/(no-navbar)/logout/page.tsx b/autogpt_platform/frontend/src/app/(no-navbar)/logout/page.tsx new file mode 100644 index 0000000000..ef3dc03f1a --- /dev/null +++ b/autogpt_platform/frontend/src/app/(no-navbar)/logout/page.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; +import { Text } from "@/components/atoms/Text/Text"; +import { useToast } from "@/components/molecules/Toast/use-toast"; +import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; +import { useRouter } from "next/navigation"; +import { useEffect, useRef } from "react"; + +const LOGOUT_REDIRECT_DELAY_MS = 400; + +function wait(ms: number): Promise { + return new Promise(function resolveAfterDelay(resolve) { + setTimeout(resolve, ms); + }); +} + +export default function LogoutPage() { + const { logOut } = useSupabase(); + const { toast } = useToast(); + const router = useRouter(); + const hasStartedRef = useRef(false); + + useEffect( + function handleLogoutEffect() { + if (hasStartedRef.current) return; + hasStartedRef.current = true; + + async function runLogout() { + try { + await logOut(); + } catch { + toast({ + title: "Failed to log out. Redirecting to login.", + variant: "destructive", + }); + } finally { + await wait(LOGOUT_REDIRECT_DELAY_MS); + router.replace("/login"); + } + } + + void runLogout(); + }, + [logOut, router, toast], + ); + + return ( +
+
+ + + Logging you out... + +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts b/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts index 13f8d988fe..a6a07a703f 100644 --- a/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts +++ b/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts @@ -9,7 +9,7 @@ export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url); const code = searchParams.get("code"); - let next = "/marketplace"; + let next = "/"; if (code) { const supabase = await getServerSupabase(); diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/Chat.tsx b/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/Chat.tsx deleted file mode 100644 index 461c885dc3..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/Chat.tsx +++ /dev/null @@ -1,134 +0,0 @@ -"use client"; - -import { Button } from "@/components/atoms/Button/Button"; -import { Text } from "@/components/atoms/Text/Text"; -import { cn } from "@/lib/utils"; -import { List } from "@phosphor-icons/react"; -import React, { useState } from "react"; -import { ChatContainer } from "./components/ChatContainer/ChatContainer"; -import { ChatErrorState } from "./components/ChatErrorState/ChatErrorState"; -import { ChatLoadingState } from "./components/ChatLoadingState/ChatLoadingState"; -import { SessionsDrawer } from "./components/SessionsDrawer/SessionsDrawer"; -import { useChat } from "./useChat"; - -export interface ChatProps { - className?: string; - headerTitle?: React.ReactNode; - showHeader?: boolean; - showSessionInfo?: boolean; - showNewChatButton?: boolean; - onNewChat?: () => void; - headerActions?: React.ReactNode; -} - -export function Chat({ - className, - headerTitle = "AutoGPT Copilot", - showHeader = true, - showSessionInfo = true, - showNewChatButton = true, - onNewChat, - headerActions, -}: ChatProps) { - const { - messages, - isLoading, - isCreating, - error, - sessionId, - createSession, - clearSession, - loadSession, - } = useChat(); - - const [isSessionsDrawerOpen, setIsSessionsDrawerOpen] = useState(false); - - const handleNewChat = () => { - clearSession(); - onNewChat?.(); - }; - - const handleSelectSession = async (sessionId: string) => { - try { - await loadSession(sessionId); - } catch (err) { - console.error("Failed to load session:", err); - } - }; - - return ( -
- {/* Header */} - {showHeader && ( -
-
-
- - {typeof headerTitle === "string" ? ( - - {headerTitle} - - ) : ( - headerTitle - )} -
-
- {showSessionInfo && sessionId && ( - <> - {showNewChatButton && ( - - )} - - )} - {headerActions} -
-
-
- )} - - {/* Main Content */} -
- {/* Loading State - show when explicitly loading/creating OR when we don't have a session yet and no error */} - {(isLoading || isCreating || (!sessionId && !error)) && ( - - )} - - {/* Error State */} - {error && !isLoading && ( - - )} - - {/* Session Content */} - {sessionId && !isLoading && !error && ( - - )} -
- - {/* Sessions Drawer */} - setIsSessionsDrawerOpen(false)} - onSelectSession={handleSelectSession} - currentSessionId={sessionId} - /> -
- ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/ChatContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/ChatContainer.tsx deleted file mode 100644 index 6f7a0e8f51..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/ChatContainer.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse"; -import { cn } from "@/lib/utils"; -import { useCallback } from "react"; -import { usePageContext } from "../../usePageContext"; -import { ChatInput } from "../ChatInput/ChatInput"; -import { MessageList } from "../MessageList/MessageList"; -import { QuickActionsWelcome } from "../QuickActionsWelcome/QuickActionsWelcome"; -import { useChatContainer } from "./useChatContainer"; - -export interface ChatContainerProps { - sessionId: string | null; - initialMessages: SessionDetailResponse["messages"]; - className?: string; -} - -export function ChatContainer({ - sessionId, - initialMessages, - className, -}: ChatContainerProps) { - const { messages, streamingChunks, isStreaming, sendMessage } = - useChatContainer({ - sessionId, - initialMessages, - }); - const { capturePageContext } = usePageContext(); - - // Wrap sendMessage to automatically capture page context - const sendMessageWithContext = useCallback( - async (content: string, isUserMessage: boolean = true) => { - const context = capturePageContext(); - await sendMessage(content, isUserMessage, context); - }, - [sendMessage, capturePageContext], - ); - - const quickActions = [ - "Find agents for social media management", - "Show me agents for content creation", - "Help me automate my business", - "What can you help me with?", - ]; - - return ( -
- {/* Messages or Welcome Screen */} -
- {messages.length === 0 ? ( - - ) : ( - - )} -
- - {/* Input - Always visible */} -
- -
-
- ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatInput/ChatInput.tsx b/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatInput/ChatInput.tsx deleted file mode 100644 index 3101174a11..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatInput/ChatInput.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Input } from "@/components/atoms/Input/Input"; -import { cn } from "@/lib/utils"; -import { ArrowUpIcon } from "@phosphor-icons/react"; -import { useChatInput } from "./useChatInput"; - -export interface ChatInputProps { - onSend: (message: string) => void; - disabled?: boolean; - placeholder?: string; - className?: string; -} - -export function ChatInput({ - onSend, - disabled = false, - placeholder = "Type your message...", - className, -}: ChatInputProps) { - const inputId = "chat-input"; - const { value, setValue, handleKeyDown, handleSend } = useChatInput({ - onSend, - disabled, - maxRows: 5, - inputId, - }); - - return ( -
- setValue(e.target.value)} - onKeyDown={handleKeyDown} - placeholder={placeholder} - disabled={disabled} - rows={1} - wrapperClassName="mb-0 relative" - className="pr-12" - /> - - Press Enter to send, Shift+Enter for new line - - - -
- ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatInput/useChatInput.ts b/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatInput/useChatInput.ts deleted file mode 100644 index 08cf565daa..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatInput/useChatInput.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { KeyboardEvent, useCallback, useEffect, useState } from "react"; - -interface UseChatInputArgs { - onSend: (message: string) => void; - disabled?: boolean; - maxRows?: number; - inputId?: string; -} - -export function useChatInput({ - onSend, - disabled = false, - maxRows = 5, - inputId = "chat-input", -}: UseChatInputArgs) { - const [value, setValue] = useState(""); - - useEffect(() => { - const textarea = document.getElementById(inputId) as HTMLTextAreaElement; - if (!textarea) return; - textarea.style.height = "auto"; - const lineHeight = parseInt( - window.getComputedStyle(textarea).lineHeight, - 10, - ); - const maxHeight = lineHeight * maxRows; - const newHeight = Math.min(textarea.scrollHeight, maxHeight); - textarea.style.height = `${newHeight}px`; - textarea.style.overflowY = - textarea.scrollHeight > maxHeight ? "auto" : "hidden"; - }, [value, maxRows, inputId]); - - const handleSend = useCallback(() => { - if (disabled || !value.trim()) return; - onSend(value.trim()); - setValue(""); - const textarea = document.getElementById(inputId) as HTMLTextAreaElement; - if (textarea) { - textarea.style.height = "auto"; - } - }, [value, onSend, disabled, inputId]); - - const handleKeyDown = useCallback( - (event: KeyboardEvent) => { - if (event.key === "Enter" && !event.shiftKey) { - event.preventDefault(); - handleSend(); - } - // Shift+Enter allows default behavior (new line) - no need to handle explicitly - }, - [handleSend], - ); - - return { - value, - setValue, - handleKeyDown, - handleSend, - }; -} diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/MessageList/MessageList.tsx b/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/MessageList/MessageList.tsx deleted file mode 100644 index 22b51c0a92..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/MessageList/MessageList.tsx +++ /dev/null @@ -1,121 +0,0 @@ -"use client"; - -import { cn } from "@/lib/utils"; -import { ChatMessage } from "../ChatMessage/ChatMessage"; -import type { ChatMessageData } from "../ChatMessage/useChatMessage"; -import { StreamingMessage } from "../StreamingMessage/StreamingMessage"; -import { ThinkingMessage } from "../ThinkingMessage/ThinkingMessage"; -import { useMessageList } from "./useMessageList"; - -export interface MessageListProps { - messages: ChatMessageData[]; - streamingChunks?: string[]; - isStreaming?: boolean; - className?: string; - onStreamComplete?: () => void; - onSendMessage?: (content: string) => void; -} - -export function MessageList({ - messages, - streamingChunks = [], - isStreaming = false, - className, - onStreamComplete, - onSendMessage, -}: MessageListProps) { - const { messagesEndRef, messagesContainerRef } = useMessageList({ - messageCount: messages.length, - isStreaming, - }); - - return ( -
-
- {/* Render all persisted messages */} - {messages.map((message, index) => { - // Check if current message is an agent_output tool_response - // and if previous message is an assistant message - let agentOutput: ChatMessageData | undefined; - - if (message.type === "tool_response" && message.result) { - let parsedResult: Record | null = null; - try { - parsedResult = - typeof message.result === "string" - ? JSON.parse(message.result) - : (message.result as Record); - } catch { - parsedResult = null; - } - if (parsedResult?.type === "agent_output") { - const prevMessage = messages[index - 1]; - if ( - prevMessage && - prevMessage.type === "message" && - prevMessage.role === "assistant" - ) { - // This agent output will be rendered inside the previous assistant message - // Skip rendering this message separately - return null; - } - } - } - - // Check if next message is an agent_output tool_response to include in current assistant message - if (message.type === "message" && message.role === "assistant") { - const nextMessage = messages[index + 1]; - if ( - nextMessage && - nextMessage.type === "tool_response" && - nextMessage.result - ) { - let parsedResult: Record | null = null; - try { - parsedResult = - typeof nextMessage.result === "string" - ? JSON.parse(nextMessage.result) - : (nextMessage.result as Record); - } catch { - parsedResult = null; - } - if (parsedResult?.type === "agent_output") { - agentOutput = nextMessage; - } - } - } - - return ( - - ); - })} - - {/* Render thinking message when streaming but no chunks yet */} - {isStreaming && streamingChunks.length === 0 && } - - {/* Render streaming message if active */} - {isStreaming && streamingChunks.length > 0 && ( - - )} - - {/* Invisible div to scroll to */} -
-
-
- ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ToolCallMessage/ToolCallMessage.tsx b/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ToolCallMessage/ToolCallMessage.tsx deleted file mode 100644 index 97590ae0cf..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ToolCallMessage/ToolCallMessage.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Text } from "@/components/atoms/Text/Text"; -import { cn } from "@/lib/utils"; -import { WrenchIcon } from "@phosphor-icons/react"; -import { getToolActionPhrase } from "../../helpers"; - -export interface ToolCallMessageProps { - toolName: string; - className?: string; -} - -export function ToolCallMessage({ toolName, className }: ToolCallMessageProps) { - return ( -
- - - {getToolActionPhrase(toolName)}... - -
- ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ToolResponseMessage/ToolResponseMessage.tsx b/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ToolResponseMessage/ToolResponseMessage.tsx deleted file mode 100644 index b84204c3ff..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ToolResponseMessage/ToolResponseMessage.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import { Text } from "@/components/atoms/Text/Text"; -import "@/components/contextual/OutputRenderers"; -import { - globalRegistry, - OutputItem, -} from "@/components/contextual/OutputRenderers"; -import { cn } from "@/lib/utils"; -import type { ToolResult } from "@/types/chat"; -import { WrenchIcon } from "@phosphor-icons/react"; -import { getToolActionPhrase } from "../../helpers"; - -export interface ToolResponseMessageProps { - toolName: string; - result?: ToolResult; - success?: boolean; - className?: string; -} - -export function ToolResponseMessage({ - toolName, - result, - success: _success = true, - className, -}: ToolResponseMessageProps) { - if (!result) { - return ( -
- - - {getToolActionPhrase(toolName)}... - -
- ); - } - - let parsedResult: Record | null = null; - try { - parsedResult = - typeof result === "string" - ? JSON.parse(result) - : (result as Record); - } catch { - parsedResult = null; - } - - if (parsedResult && typeof parsedResult === "object") { - const responseType = parsedResult.type as string | undefined; - - if (responseType === "agent_output") { - const execution = parsedResult.execution as - | { - outputs?: Record; - } - | null - | undefined; - const outputs = execution?.outputs || {}; - const message = parsedResult.message as string | undefined; - - return ( -
-
- - - {getToolActionPhrase(toolName)} - -
- {message && ( -
- - {message} - -
- )} - {Object.keys(outputs).length > 0 && ( -
- {Object.entries(outputs).map(([outputName, values]) => - values.map((value, index) => { - const renderer = globalRegistry.getRenderer(value); - if (renderer) { - return ( - - ); - } - return ( -
- - {outputName} - -
-                        {JSON.stringify(value, null, 2)}
-                      
-
- ); - }), - )} -
- )} -
- ); - } - - if (responseType === "block_output" && parsedResult.outputs) { - const outputs = parsedResult.outputs as Record; - - return ( -
-
- - - {getToolActionPhrase(toolName)} - -
-
- {Object.entries(outputs).map(([outputName, values]) => - values.map((value, index) => { - const renderer = globalRegistry.getRenderer(value); - if (renderer) { - return ( - - ); - } - return ( -
- - {outputName} - -
-                      {JSON.stringify(value, null, 2)}
-                    
-
- ); - }), - )} -
-
- ); - } - - // Handle other response types with a message field (e.g., understanding_updated) - if (parsedResult.message && typeof parsedResult.message === "string") { - // Format tool name from snake_case to Title Case - const formattedToolName = toolName - .split("_") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "); - - // Clean up message - remove incomplete user_name references - let cleanedMessage = parsedResult.message; - // Remove "Updated understanding with: user_name" pattern if user_name is just a placeholder - cleanedMessage = cleanedMessage.replace( - /Updated understanding with:\s*user_name\.?\s*/gi, - "", - ); - // Remove standalone user_name references - cleanedMessage = cleanedMessage.replace(/\buser_name\b\.?\s*/gi, ""); - cleanedMessage = cleanedMessage.trim(); - - // Only show message if it has content after cleaning - if (!cleanedMessage) { - return ( -
- - - {formattedToolName} - -
- ); - } - - return ( -
-
- - - {formattedToolName} - -
-
- - {cleanedMessage} - -
-
- ); - } - } - - const renderer = globalRegistry.getRenderer(result); - if (renderer) { - return ( -
-
- - - {getToolActionPhrase(toolName)} - -
- -
- ); - } - - return ( -
- - - {getToolActionPhrase(toolName)}... - -
- ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/helpers.ts deleted file mode 100644 index 0fade56b73..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/helpers.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Maps internal tool names to user-friendly display names with emojis. - * @deprecated Use getToolActionPhrase or getToolCompletionPhrase for status messages - * - * @param toolName - The internal tool name from the backend - * @returns A user-friendly display name with an emoji prefix - */ -export function getToolDisplayName(toolName: string): string { - const toolDisplayNames: Record = { - find_agent: "🔍 Search Marketplace", - get_agent_details: "📋 Get Agent Details", - check_credentials: "🔑 Check Credentials", - setup_agent: "⚙️ Setup Agent", - run_agent: "▶️ Run Agent", - get_required_setup_info: "📝 Get Setup Requirements", - }; - return toolDisplayNames[toolName] || toolName; -} - -/** - * Maps internal tool names to human-friendly action phrases (present continuous). - * Used for tool call messages to indicate what action is currently happening. - * - * @param toolName - The internal tool name from the backend - * @returns A human-friendly action phrase in present continuous tense - */ -export function getToolActionPhrase(toolName: string): string { - const toolActionPhrases: Record = { - find_agent: "Looking for agents in the marketplace", - agent_carousel: "Looking for agents in the marketplace", - get_agent_details: "Learning about the agent", - check_credentials: "Checking your credentials", - setup_agent: "Setting up the agent", - execution_started: "Running the agent", - run_agent: "Running the agent", - get_required_setup_info: "Getting setup requirements", - schedule_agent: "Scheduling the agent to run", - }; - - // Return mapped phrase or generate human-friendly fallback - return toolActionPhrases[toolName] || toolName; -} - -/** - * Maps internal tool names to human-friendly completion phrases (past tense). - * Used for tool response messages to indicate what action was completed. - * - * @param toolName - The internal tool name from the backend - * @returns A human-friendly completion phrase in past tense - */ -export function getToolCompletionPhrase(toolName: string): string { - const toolCompletionPhrases: Record = { - find_agent: "Finished searching the marketplace", - get_agent_details: "Got agent details", - check_credentials: "Checked credentials", - setup_agent: "Agent setup complete", - run_agent: "Agent execution started", - get_required_setup_info: "Got setup requirements", - }; - - // Return mapped phrase or generate human-friendly fallback - return ( - toolCompletionPhrases[toolName] || - `Finished ${toolName.replace(/_/g, " ").replace("...", "")}` - ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/useChatSession.ts b/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/useChatSession.ts deleted file mode 100644 index a54dc9e32a..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/useChatSession.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { - getGetV2GetSessionQueryKey, - getGetV2GetSessionQueryOptions, - postV2CreateSession, - useGetV2GetSession, - usePatchV2SessionAssignUser, - usePostV2CreateSession, -} from "@/app/api/__generated__/endpoints/chat/chat"; -import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse"; -import { okData } from "@/app/api/helpers"; -import { isValidUUID } from "@/lib/utils"; -import { Key, storage } from "@/services/storage/local-storage"; -import { useQueryClient } from "@tanstack/react-query"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { toast } from "sonner"; - -interface UseChatSessionArgs { - urlSessionId?: string | null; - autoCreate?: boolean; -} - -export function useChatSession({ - urlSessionId, - autoCreate = false, -}: UseChatSessionArgs = {}) { - const queryClient = useQueryClient(); - const [sessionId, setSessionId] = useState(null); - const [error, setError] = useState(null); - const justCreatedSessionIdRef = useRef(null); - - useEffect(() => { - if (urlSessionId) { - if (!isValidUUID(urlSessionId)) { - console.error("Invalid session ID format:", urlSessionId); - toast.error("Invalid session ID", { - description: - "The session ID in the URL is not valid. Starting a new session...", - }); - setSessionId(null); - storage.clean(Key.CHAT_SESSION_ID); - return; - } - setSessionId(urlSessionId); - storage.set(Key.CHAT_SESSION_ID, urlSessionId); - } else { - const storedSessionId = storage.get(Key.CHAT_SESSION_ID); - if (storedSessionId) { - if (!isValidUUID(storedSessionId)) { - console.error("Invalid stored session ID:", storedSessionId); - storage.clean(Key.CHAT_SESSION_ID); - setSessionId(null); - } else { - setSessionId(storedSessionId); - } - } else if (autoCreate) { - setSessionId(null); - } - } - }, [urlSessionId, autoCreate]); - - const { - mutateAsync: createSessionMutation, - isPending: isCreating, - error: createError, - } = usePostV2CreateSession(); - - const { - data: sessionData, - isLoading: isLoadingSession, - error: loadError, - refetch, - } = useGetV2GetSession(sessionId || "", { - query: { - enabled: !!sessionId, - select: okData, - staleTime: Infinity, // Never mark as stale - refetchOnMount: false, // Don't refetch on component mount - refetchOnWindowFocus: false, // Don't refetch when window regains focus - refetchOnReconnect: false, // Don't refetch when network reconnects - retry: 1, - }, - }); - - const { mutateAsync: claimSessionMutation } = usePatchV2SessionAssignUser(); - - const session = useMemo(() => { - if (sessionData) return sessionData; - - if (sessionId && justCreatedSessionIdRef.current === sessionId) { - return { - id: sessionId, - user_id: null, - messages: [], - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - } as SessionDetailResponse; - } - return null; - }, [sessionData, sessionId]); - - const messages = session?.messages || []; - const isLoading = isCreating || isLoadingSession; - - useEffect(() => { - if (createError) { - setError( - createError instanceof Error - ? createError - : new Error("Failed to create session"), - ); - } else if (loadError) { - setError( - loadError instanceof Error - ? loadError - : new Error("Failed to load session"), - ); - } else { - setError(null); - } - }, [createError, loadError]); - - const createSession = useCallback( - async function createSession() { - try { - setError(null); - const response = await postV2CreateSession({ - body: JSON.stringify({}), - }); - if (response.status !== 200) { - throw new Error("Failed to create session"); - } - const newSessionId = response.data.id; - setSessionId(newSessionId); - storage.set(Key.CHAT_SESSION_ID, newSessionId); - justCreatedSessionIdRef.current = newSessionId; - setTimeout(() => { - if (justCreatedSessionIdRef.current === newSessionId) { - justCreatedSessionIdRef.current = null; - } - }, 10000); - return newSessionId; - } catch (err) { - const error = - err instanceof Error ? err : new Error("Failed to create session"); - setError(error); - toast.error("Failed to create chat session", { - description: error.message, - }); - throw error; - } - }, - [createSessionMutation], - ); - - const loadSession = useCallback( - async function loadSession(id: string) { - try { - setError(null); - // Invalidate the query cache for this session to force a fresh fetch - await queryClient.invalidateQueries({ - queryKey: getGetV2GetSessionQueryKey(id), - }); - // Set sessionId after invalidation to ensure the hook refetches - setSessionId(id); - storage.set(Key.CHAT_SESSION_ID, id); - // Force fetch with fresh data (bypass cache) - const queryOptions = getGetV2GetSessionQueryOptions(id, { - query: { - staleTime: 0, // Force fresh fetch - retry: 1, - }, - }); - const result = await queryClient.fetchQuery(queryOptions); - if (!result || ("status" in result && result.status !== 200)) { - console.warn("Session not found on server, clearing local state"); - storage.clean(Key.CHAT_SESSION_ID); - setSessionId(null); - throw new Error("Session not found"); - } - } catch (err) { - const error = - err instanceof Error ? err : new Error("Failed to load session"); - setError(error); - throw error; - } - }, - [queryClient], - ); - - const refreshSession = useCallback( - async function refreshSession() { - if (!sessionId) { - console.log("[refreshSession] Skipping - no session ID"); - return; - } - try { - setError(null); - await refetch(); - } catch (err) { - const error = - err instanceof Error ? err : new Error("Failed to refresh session"); - setError(error); - throw error; - } - }, - [sessionId, refetch], - ); - - const claimSession = useCallback( - async function claimSession(id: string) { - try { - setError(null); - await claimSessionMutation({ sessionId: id }); - if (justCreatedSessionIdRef.current === id) { - justCreatedSessionIdRef.current = null; - } - await queryClient.invalidateQueries({ - queryKey: getGetV2GetSessionQueryKey(id), - }); - await refetch(); - toast.success("Session claimed successfully", { - description: "Your chat history has been saved to your account", - }); - } catch (err: unknown) { - const error = - err instanceof Error ? err : new Error("Failed to claim session"); - const is404 = - (typeof err === "object" && - err !== null && - "status" in err && - err.status === 404) || - (typeof err === "object" && - err !== null && - "response" in err && - typeof err.response === "object" && - err.response !== null && - "status" in err.response && - err.response.status === 404); - if (!is404) { - setError(error); - toast.error("Failed to claim session", { - description: error.message || "Unable to claim session", - }); - } - throw error; - } - }, - [claimSessionMutation, queryClient, refetch], - ); - - const clearSession = useCallback(function clearSession() { - setSessionId(null); - setError(null); - storage.clean(Key.CHAT_SESSION_ID); - justCreatedSessionIdRef.current = null; - }, []); - - return { - session, - sessionId, - messages, - isLoading, - isCreating, - error, - createSession, - loadSession, - refreshSession, - claimSession, - clearSession, - }; -} diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/page.tsx b/autogpt_platform/frontend/src/app/(platform)/chat/page.tsx deleted file mode 100644 index 9c04e40594..0000000000 --- a/autogpt_platform/frontend/src/app/(platform)/chat/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; -import { Chat } from "./components/Chat/Chat"; - -export default function ChatPage() { - const isChatEnabled = useGetFlag(Flag.CHAT); - const router = useRouter(); - - useEffect(() => { - if (isChatEnabled === false) { - router.push("/marketplace"); - } - }, [isChatEnabled, router]); - - if (isChatEnabled === null || isChatEnabled === false) { - return null; - } - - return ( -
- -
- ); -} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/CopilotShell.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/CopilotShell.tsx new file mode 100644 index 0000000000..03a2ff5db0 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/CopilotShell.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; +import { NAVBAR_HEIGHT_PX } from "@/lib/constants"; +import type { ReactNode } from "react"; +import { DesktopSidebar } from "./components/DesktopSidebar/DesktopSidebar"; +import { LoadingState } from "./components/LoadingState/LoadingState"; +import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer"; +import { MobileHeader } from "./components/MobileHeader/MobileHeader"; +import { useCopilotShell } from "./useCopilotShell"; + +interface Props { + children: ReactNode; +} + +export function CopilotShell({ children }: Props) { + const { + isMobile, + isDrawerOpen, + isLoading, + isLoggedIn, + hasActiveSession, + sessions, + currentSessionId, + handleSelectSession, + handleOpenDrawer, + handleCloseDrawer, + handleDrawerOpenChange, + handleNewChat, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + isReadyToShowContent, + } = useCopilotShell(); + + if (!isLoggedIn) { + return ( +
+ +
+ ); + } + + return ( +
+ {!isMobile && ( + + )} + +
+ {isMobile && } +
+ {isReadyToShowContent ? children : } +
+
+ + {isMobile && ( + + )} +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/DesktopSidebar/DesktopSidebar.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/DesktopSidebar/DesktopSidebar.tsx new file mode 100644 index 0000000000..122a09a02f --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/DesktopSidebar/DesktopSidebar.tsx @@ -0,0 +1,70 @@ +import type { SessionSummaryResponse } from "@/app/api/__generated__/models/sessionSummaryResponse"; +import { Button } from "@/components/atoms/Button/Button"; +import { Text } from "@/components/atoms/Text/Text"; +import { scrollbarStyles } from "@/components/styles/scrollbars"; +import { cn } from "@/lib/utils"; +import { Plus } from "@phosphor-icons/react"; +import { SessionsList } from "../SessionsList/SessionsList"; + +interface Props { + sessions: SessionSummaryResponse[]; + currentSessionId: string | null; + isLoading: boolean; + hasNextPage: boolean; + isFetchingNextPage: boolean; + onSelectSession: (sessionId: string) => void; + onFetchNextPage: () => void; + onNewChat: () => void; + hasActiveSession: boolean; +} + +export function DesktopSidebar({ + sessions, + currentSessionId, + isLoading, + hasNextPage, + isFetchingNextPage, + onSelectSession, + onFetchNextPage, + onNewChat, + hasActiveSession, +}: Props) { + return ( + + ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/LoadingState/LoadingState.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/LoadingState/LoadingState.tsx new file mode 100644 index 0000000000..21b1663916 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/LoadingState/LoadingState.tsx @@ -0,0 +1,15 @@ +import { Text } from "@/components/atoms/Text/Text"; +import { ChatLoader } from "@/components/contextual/Chat/components/ChatLoader/ChatLoader"; + +export function LoadingState() { + return ( +
+
+ + + Loading your chats... + +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/MobileDrawer/MobileDrawer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/MobileDrawer/MobileDrawer.tsx new file mode 100644 index 0000000000..ea3b39f829 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/MobileDrawer/MobileDrawer.tsx @@ -0,0 +1,91 @@ +import type { SessionSummaryResponse } from "@/app/api/__generated__/models/sessionSummaryResponse"; +import { Button } from "@/components/atoms/Button/Button"; +import { scrollbarStyles } from "@/components/styles/scrollbars"; +import { cn } from "@/lib/utils"; +import { PlusIcon, X } from "@phosphor-icons/react"; +import { Drawer } from "vaul"; +import { SessionsList } from "../SessionsList/SessionsList"; + +interface Props { + isOpen: boolean; + sessions: SessionSummaryResponse[]; + currentSessionId: string | null; + isLoading: boolean; + hasNextPage: boolean; + isFetchingNextPage: boolean; + onSelectSession: (sessionId: string) => void; + onFetchNextPage: () => void; + onNewChat: () => void; + onClose: () => void; + onOpenChange: (open: boolean) => void; + hasActiveSession: boolean; +} + +export function MobileDrawer({ + isOpen, + sessions, + currentSessionId, + isLoading, + hasNextPage, + isFetchingNextPage, + onSelectSession, + onFetchNextPage, + onNewChat, + onClose, + onOpenChange, + hasActiveSession, +}: Props) { + return ( + + + + +
+
+ + Your chats + + +
+
+
+ +
+ {hasActiveSession && ( +
+ +
+ )} +
+
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/MobileDrawer/useMobileDrawer.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/MobileDrawer/useMobileDrawer.ts new file mode 100644 index 0000000000..c9504e49a9 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/MobileDrawer/useMobileDrawer.ts @@ -0,0 +1,24 @@ +import { useState } from "react"; + +export function useMobileDrawer() { + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + + function handleOpenDrawer() { + setIsDrawerOpen(true); + } + + function handleCloseDrawer() { + setIsDrawerOpen(false); + } + + function handleDrawerOpenChange(open: boolean) { + setIsDrawerOpen(open); + } + + return { + isDrawerOpen, + handleOpenDrawer, + handleCloseDrawer, + handleDrawerOpenChange, + }; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/MobileHeader/MobileHeader.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/MobileHeader/MobileHeader.tsx new file mode 100644 index 0000000000..e0d6161744 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/MobileHeader/MobileHeader.tsx @@ -0,0 +1,22 @@ +import { Button } from "@/components/atoms/Button/Button"; +import { NAVBAR_HEIGHT_PX } from "@/lib/constants"; +import { ListIcon } from "@phosphor-icons/react"; + +interface Props { + onOpenDrawer: () => void; +} + +export function MobileHeader({ onOpenDrawer }: Props) { + return ( + + ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/SessionsList/SessionsList.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/SessionsList/SessionsList.tsx new file mode 100644 index 0000000000..ef63e1aff4 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/SessionsList/SessionsList.tsx @@ -0,0 +1,80 @@ +import type { SessionSummaryResponse } from "@/app/api/__generated__/models/sessionSummaryResponse"; +import { Skeleton } from "@/components/__legacy__/ui/skeleton"; +import { Text } from "@/components/atoms/Text/Text"; +import { InfiniteList } from "@/components/molecules/InfiniteList/InfiniteList"; +import { cn } from "@/lib/utils"; +import { getSessionTitle } from "../../helpers"; + +interface Props { + sessions: SessionSummaryResponse[]; + currentSessionId: string | null; + isLoading: boolean; + hasNextPage: boolean; + isFetchingNextPage: boolean; + onSelectSession: (sessionId: string) => void; + onFetchNextPage: () => void; +} + +export function SessionsList({ + sessions, + currentSessionId, + isLoading, + hasNextPage, + isFetchingNextPage, + onSelectSession, + onFetchNextPage, +}: Props) { + if (isLoading) { + return ( +
+ {Array.from({ length: 5 }).map((_, i) => ( +
+ +
+ ))} +
+ ); + } + + if (sessions.length === 0) { + return ( +
+ + You don't have previous chats + +
+ ); + } + + return ( + { + const isActive = session.id === currentSessionId; + return ( + + ); + }} + /> + ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/SessionsList/useSessionsPagination.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/SessionsList/useSessionsPagination.ts new file mode 100644 index 0000000000..8833a419c1 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/SessionsList/useSessionsPagination.ts @@ -0,0 +1,92 @@ +import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat"; +import type { SessionSummaryResponse } from "@/app/api/__generated__/models/sessionSummaryResponse"; +import { okData } from "@/app/api/helpers"; +import { useEffect, useMemo, useState } from "react"; + +const PAGE_SIZE = 50; + +export interface UseSessionsPaginationArgs { + enabled: boolean; +} + +export function useSessionsPagination({ enabled }: UseSessionsPaginationArgs) { + const [offset, setOffset] = useState(0); + const [accumulatedSessions, setAccumulatedSessions] = useState< + SessionSummaryResponse[] + >([]); + const [totalCount, setTotalCount] = useState(null); + + const { data, isLoading, isFetching, isError } = useGetV2ListSessions( + { limit: PAGE_SIZE, offset }, + { + query: { + enabled: enabled && offset >= 0, + }, + }, + ); + + useEffect(() => { + const responseData = okData(data); + if (responseData) { + const newSessions = responseData.sessions; + const total = responseData.total; + setTotalCount(total); + + if (offset === 0) { + setAccumulatedSessions(newSessions); + } else { + setAccumulatedSessions((prev) => [...prev, ...newSessions]); + } + } else if (!enabled) { + setAccumulatedSessions([]); + setTotalCount(null); + } + }, [data, offset, enabled]); + + const hasNextPage = useMemo(() => { + if (totalCount === null) return false; + return accumulatedSessions.length < totalCount; + }, [accumulatedSessions.length, totalCount]); + + const areAllSessionsLoaded = useMemo(() => { + if (totalCount === null) return false; + return ( + accumulatedSessions.length >= totalCount && !isFetching && !isLoading + ); + }, [accumulatedSessions.length, totalCount, isFetching, isLoading]); + + useEffect(() => { + if ( + hasNextPage && + !isFetching && + !isLoading && + !isError && + totalCount !== null + ) { + setOffset((prev) => prev + PAGE_SIZE); + } + }, [hasNextPage, isFetching, isLoading, isError, totalCount]); + + function fetchNextPage() { + if (hasNextPage && !isFetching) { + setOffset((prev) => prev + PAGE_SIZE); + } + } + + function reset() { + setOffset(0); + setAccumulatedSessions([]); + setTotalCount(null); + } + + return { + sessions: accumulatedSessions, + isLoading, + isFetching, + hasNextPage, + areAllSessionsLoaded, + totalCount, + fetchNextPage, + reset, + }; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/helpers.ts new file mode 100644 index 0000000000..bf4eb70ccb --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/helpers.ts @@ -0,0 +1,165 @@ +import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse"; +import type { SessionSummaryResponse } from "@/app/api/__generated__/models/sessionSummaryResponse"; +import { format, formatDistanceToNow, isToday } from "date-fns"; + +export function convertSessionDetailToSummary( + session: SessionDetailResponse, +): SessionSummaryResponse { + return { + id: session.id, + created_at: session.created_at, + updated_at: session.updated_at, + title: undefined, + }; +} + +export function filterVisibleSessions( + sessions: SessionSummaryResponse[], +): SessionSummaryResponse[] { + return sessions.filter( + (session) => session.updated_at !== session.created_at, + ); +} + +export function getSessionTitle(session: SessionSummaryResponse): string { + if (session.title) return session.title; + const isNewSession = session.updated_at === session.created_at; + if (isNewSession) { + const createdDate = new Date(session.created_at); + if (isToday(createdDate)) { + return "Today"; + } + return format(createdDate, "MMM d, yyyy"); + } + return "Untitled Chat"; +} + +export function getSessionUpdatedLabel( + session: SessionSummaryResponse, +): string { + if (!session.updated_at) return ""; + return formatDistanceToNow(new Date(session.updated_at), { addSuffix: true }); +} + +export function mergeCurrentSessionIntoList( + accumulatedSessions: SessionSummaryResponse[], + currentSessionId: string | null, + currentSessionData: SessionDetailResponse | null | undefined, +): SessionSummaryResponse[] { + const filteredSessions: SessionSummaryResponse[] = []; + + if (accumulatedSessions.length > 0) { + const visibleSessions = filterVisibleSessions(accumulatedSessions); + + if (currentSessionId) { + const currentInAll = accumulatedSessions.find( + (s) => s.id === currentSessionId, + ); + if (currentInAll) { + const isInVisible = visibleSessions.some( + (s) => s.id === currentSessionId, + ); + if (!isInVisible) { + filteredSessions.push(currentInAll); + } + } + } + + filteredSessions.push(...visibleSessions); + } + + if (currentSessionId && currentSessionData) { + const isCurrentInList = filteredSessions.some( + (s) => s.id === currentSessionId, + ); + if (!isCurrentInList) { + const summarySession = convertSessionDetailToSummary(currentSessionData); + filteredSessions.unshift(summarySession); + } + } + + return filteredSessions; +} + +export function getCurrentSessionId( + searchParams: URLSearchParams, +): string | null { + return searchParams.get("sessionId"); +} + +export function shouldAutoSelectSession( + areAllSessionsLoaded: boolean, + hasAutoSelectedSession: boolean, + paramSessionId: string | null, + visibleSessions: SessionSummaryResponse[], + accumulatedSessions: SessionSummaryResponse[], + isLoading: boolean, + totalCount: number | null, +): { + shouldSelect: boolean; + sessionIdToSelect: string | null; + shouldCreate: boolean; +} { + if (!areAllSessionsLoaded || hasAutoSelectedSession) { + return { + shouldSelect: false, + sessionIdToSelect: null, + shouldCreate: false, + }; + } + + if (paramSessionId) { + return { + shouldSelect: false, + sessionIdToSelect: null, + shouldCreate: false, + }; + } + + if (visibleSessions.length > 0) { + return { + shouldSelect: true, + sessionIdToSelect: visibleSessions[0].id, + shouldCreate: false, + }; + } + + if (accumulatedSessions.length === 0 && !isLoading && totalCount === 0) { + return { shouldSelect: false, sessionIdToSelect: null, shouldCreate: true }; + } + + if (totalCount === 0) { + return { + shouldSelect: false, + sessionIdToSelect: null, + shouldCreate: false, + }; + } + + return { shouldSelect: false, sessionIdToSelect: null, shouldCreate: false }; +} + +export function checkReadyToShowContent( + areAllSessionsLoaded: boolean, + paramSessionId: string | null, + accumulatedSessions: SessionSummaryResponse[], + isCurrentSessionLoading: boolean, + currentSessionData: SessionDetailResponse | null | undefined, + hasAutoSelectedSession: boolean, +): boolean { + if (!areAllSessionsLoaded) return false; + + if (paramSessionId) { + const sessionFound = accumulatedSessions.some( + (s) => s.id === paramSessionId, + ); + return ( + sessionFound || + (!isCurrentSessionLoading && + currentSessionData !== undefined && + currentSessionData !== null) + ); + } + + return hasAutoSelectedSession; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/useCopilotShell.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/useCopilotShell.ts new file mode 100644 index 0000000000..6003c64b73 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/useCopilotShell.ts @@ -0,0 +1,170 @@ +"use client"; + +import { + getGetV2ListSessionsQueryKey, + useGetV2GetSession, +} from "@/app/api/__generated__/endpoints/chat/chat"; +import { okData } from "@/app/api/helpers"; +import { useBreakpoint } from "@/lib/hooks/useBreakpoint"; +import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; +import { useQueryClient } from "@tanstack/react-query"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; +import { useMobileDrawer } from "./components/MobileDrawer/useMobileDrawer"; +import { useSessionsPagination } from "./components/SessionsList/useSessionsPagination"; +import { + checkReadyToShowContent, + filterVisibleSessions, + getCurrentSessionId, + mergeCurrentSessionIntoList, +} from "./helpers"; + +export function useCopilotShell() { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const queryClient = useQueryClient(); + const breakpoint = useBreakpoint(); + const { isLoggedIn } = useSupabase(); + const isMobile = + breakpoint === "base" || breakpoint === "sm" || breakpoint === "md"; + + const isOnHomepage = pathname === "/copilot"; + const paramSessionId = searchParams.get("sessionId"); + + const { + isDrawerOpen, + handleOpenDrawer, + handleCloseDrawer, + handleDrawerOpenChange, + } = useMobileDrawer(); + + const paginationEnabled = !isMobile || isDrawerOpen || !!paramSessionId; + + const { + sessions: accumulatedSessions, + isLoading: isSessionsLoading, + isFetching: isSessionsFetching, + hasNextPage, + areAllSessionsLoaded, + fetchNextPage, + reset: resetPagination, + } = useSessionsPagination({ + enabled: paginationEnabled, + }); + + const currentSessionId = getCurrentSessionId(searchParams); + + const { data: currentSessionData, isLoading: isCurrentSessionLoading } = + useGetV2GetSession(currentSessionId || "", { + query: { + enabled: !!currentSessionId, + select: okData, + }, + }); + + const [hasAutoSelectedSession, setHasAutoSelectedSession] = useState(false); + const hasAutoSelectedRef = useRef(false); + + // Mark as auto-selected when sessionId is in URL + useEffect(() => { + if (paramSessionId && !hasAutoSelectedRef.current) { + hasAutoSelectedRef.current = true; + setHasAutoSelectedSession(true); + } + }, [paramSessionId]); + + // On homepage without sessionId, mark as ready immediately + useEffect(() => { + if (isOnHomepage && !paramSessionId && !hasAutoSelectedRef.current) { + hasAutoSelectedRef.current = true; + setHasAutoSelectedSession(true); + } + }, [isOnHomepage, paramSessionId]); + + // Invalidate sessions list when navigating to homepage (to show newly created sessions) + useEffect(() => { + if (isOnHomepage && !paramSessionId) { + queryClient.invalidateQueries({ + queryKey: getGetV2ListSessionsQueryKey(), + }); + } + }, [isOnHomepage, paramSessionId, queryClient]); + + // Reset pagination when query becomes disabled + const prevPaginationEnabledRef = useRef(paginationEnabled); + useEffect(() => { + if (prevPaginationEnabledRef.current && !paginationEnabled) { + resetPagination(); + resetAutoSelect(); + } + prevPaginationEnabledRef.current = paginationEnabled; + }, [paginationEnabled, resetPagination]); + + const sessions = mergeCurrentSessionIntoList( + accumulatedSessions, + currentSessionId, + currentSessionData, + ); + + const visibleSessions = filterVisibleSessions(sessions); + + const sidebarSelectedSessionId = + isOnHomepage && !paramSessionId ? null : currentSessionId; + + const isReadyToShowContent = isOnHomepage + ? true + : checkReadyToShowContent( + areAllSessionsLoaded, + paramSessionId, + accumulatedSessions, + isCurrentSessionLoading, + currentSessionData, + hasAutoSelectedSession, + ); + + function handleSelectSession(sessionId: string) { + // Navigate using replaceState to avoid full page reload + window.history.replaceState(null, "", `/copilot?sessionId=${sessionId}`); + // Force a re-render by updating the URL through router + router.replace(`/copilot?sessionId=${sessionId}`); + if (isMobile) handleCloseDrawer(); + } + + function handleNewChat() { + resetAutoSelect(); + resetPagination(); + // Invalidate and refetch sessions list to ensure newly created sessions appear + queryClient.invalidateQueries({ + queryKey: getGetV2ListSessionsQueryKey(), + }); + window.history.replaceState(null, "", "/copilot"); + router.replace("/copilot"); + if (isMobile) handleCloseDrawer(); + } + + function resetAutoSelect() { + hasAutoSelectedRef.current = false; + setHasAutoSelectedSession(false); + } + + return { + isMobile, + isDrawerOpen, + isLoggedIn, + hasActiveSession: + Boolean(currentSessionId) && (!isOnHomepage || Boolean(paramSessionId)), + isLoading: isSessionsLoading || !areAllSessionsLoaded, + sessions: visibleSessions, + currentSessionId: sidebarSelectedSessionId, + handleSelectSession, + handleOpenDrawer, + handleCloseDrawer, + handleDrawerOpenChange, + handleNewChat, + hasNextPage, + isFetchingNextPage: isSessionsFetching, + fetchNextPage, + isReadyToShowContent, + }; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts new file mode 100644 index 0000000000..692a5741f4 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts @@ -0,0 +1,33 @@ +import type { User } from "@supabase/supabase-js"; + +export function getGreetingName(user?: User | null): string { + if (!user) return "there"; + const metadata = user.user_metadata as Record | undefined; + const fullName = metadata?.full_name; + const name = metadata?.name; + if (typeof fullName === "string" && fullName.trim()) { + return fullName.split(" ")[0]; + } + if (typeof name === "string" && name.trim()) { + return name.split(" ")[0]; + } + if (user.email) { + return user.email.split("@")[0]; + } + return "there"; +} + +export function buildCopilotChatUrl(prompt: string): string { + const trimmed = prompt.trim(); + if (!trimmed) return "/copilot/chat"; + const encoded = encodeURIComponent(trimmed); + return `/copilot/chat?prompt=${encoded}`; +} + +export function getQuickActions(): string[] { + return [ + "Show me what I can automate", + "Design a custom workflow", + "Help me with content creation", + ]; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/layout.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/layout.tsx new file mode 100644 index 0000000000..89cf72e2ba --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/layout.tsx @@ -0,0 +1,6 @@ +import type { ReactNode } from "react"; +import { CopilotShell } from "./components/CopilotShell/CopilotShell"; + +export default function CopilotLayout({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx new file mode 100644 index 0000000000..add9504f9b --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx @@ -0,0 +1,228 @@ +"use client"; + +import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat"; +import { Skeleton } from "@/components/__legacy__/ui/skeleton"; +import { Button } from "@/components/atoms/Button/Button"; +import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; +import { Text } from "@/components/atoms/Text/Text"; +import { Chat } from "@/components/contextual/Chat/Chat"; +import { ChatInput } from "@/components/contextual/Chat/components/ChatInput/ChatInput"; +import { getHomepageRoute } from "@/lib/constants"; +import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; +import { + Flag, + type FlagValues, + useGetFlag, +} from "@/services/feature-flags/use-get-flag"; +import { useFlags } from "launchdarkly-react-client-sdk"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { getGreetingName, getQuickActions } from "./helpers"; + +type PageState = + | { type: "welcome" } + | { type: "creating"; prompt: string } + | { type: "chat"; sessionId: string; initialPrompt?: string }; + +export default function CopilotPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const { user, isLoggedIn, isUserLoading } = useSupabase(); + + const isChatEnabled = useGetFlag(Flag.CHAT); + const flags = useFlags(); + const homepageRoute = getHomepageRoute(isChatEnabled); + const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true"; + const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID; + const isLaunchDarklyConfigured = envEnabled && Boolean(clientId); + const isFlagReady = + !isLaunchDarklyConfigured || flags[Flag.CHAT] !== undefined; + + const [pageState, setPageState] = useState({ type: "welcome" }); + const initialPromptRef = useRef>(new Map()); + + const urlSessionId = searchParams.get("sessionId"); + + // Sync with URL sessionId (preserve initialPrompt from ref) + useEffect( + function syncSessionFromUrl() { + if (urlSessionId) { + // If we're already in chat state with this sessionId, don't overwrite + if (pageState.type === "chat" && pageState.sessionId === urlSessionId) { + return; + } + // Get initialPrompt from ref or current state + const storedInitialPrompt = initialPromptRef.current.get(urlSessionId); + const currentInitialPrompt = + storedInitialPrompt || + (pageState.type === "creating" + ? pageState.prompt + : pageState.type === "chat" + ? pageState.initialPrompt + : undefined); + if (currentInitialPrompt) { + initialPromptRef.current.set(urlSessionId, currentInitialPrompt); + } + setPageState({ + type: "chat", + sessionId: urlSessionId, + initialPrompt: currentInitialPrompt, + }); + } else if (pageState.type === "chat") { + setPageState({ type: "welcome" }); + } + }, + [urlSessionId], + ); + + useEffect( + function ensureAccess() { + if (!isFlagReady) return; + if (isChatEnabled === false) { + router.replace(homepageRoute); + } + }, + [homepageRoute, isChatEnabled, isFlagReady, router], + ); + + const greetingName = useMemo( + function getName() { + return getGreetingName(user); + }, + [user], + ); + + const quickActions = useMemo(function getActions() { + return getQuickActions(); + }, []); + + async function startChatWithPrompt(prompt: string) { + if (!prompt?.trim()) return; + if (pageState.type === "creating") return; + + const trimmedPrompt = prompt.trim(); + setPageState({ type: "creating", prompt: trimmedPrompt }); + + try { + // Create session + const sessionResponse = await postV2CreateSession({ + body: JSON.stringify({}), + }); + + if (sessionResponse.status !== 200 || !sessionResponse.data?.id) { + throw new Error("Failed to create session"); + } + + const sessionId = sessionResponse.data.id; + + // Store initialPrompt in ref so it persists across re-renders + initialPromptRef.current.set(sessionId, trimmedPrompt); + + // Update URL and show Chat with initial prompt + // Chat will handle sending the message and streaming + window.history.replaceState(null, "", `/copilot?sessionId=${sessionId}`); + setPageState({ type: "chat", sessionId, initialPrompt: trimmedPrompt }); + } catch (error) { + console.error("[CopilotPage] Failed to start chat:", error); + setPageState({ type: "welcome" }); + } + } + + function handleQuickAction(action: string) { + startChatWithPrompt(action); + } + + function handleSessionNotFound() { + router.replace("/copilot"); + } + + if (!isFlagReady || isChatEnabled === false || !isLoggedIn) { + return null; + } + + // Show Chat when we have an active session + if (pageState.type === "chat") { + return ( +
+ +
+ ); + } + + // Show loading state while creating session and sending first message + if (pageState.type === "creating") { + return ( +
+ + + Starting your chat... + +
+ ); + } + + // Show Welcome screen + const isLoading = isUserLoading; + + return ( +
+
+ {isLoading ? ( +
+ + +
+ +
+
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+
+ ) : ( + <> +
+ + Hey, {greetingName} + + + What do you want to automate? + + +
+ +
+
+
+ {quickActions.map((action) => ( + + ))} +
+ + )} +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/error/page.tsx b/autogpt_platform/frontend/src/app/(platform)/error/page.tsx index b7858787cf..b26ca4559b 100644 --- a/autogpt_platform/frontend/src/app/(platform)/error/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/error/page.tsx @@ -1,6 +1,8 @@ "use client"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; +import { getHomepageRoute } from "@/lib/constants"; +import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; import { useSearchParams } from "next/navigation"; import { Suspense } from "react"; import { getErrorDetails } from "./helpers"; @@ -9,6 +11,8 @@ function ErrorPageContent() { const searchParams = useSearchParams(); const errorMessage = searchParams.get("message"); const errorDetails = getErrorDetails(errorMessage); + const isChatEnabled = useGetFlag(Flag.CHAT); + const homepageRoute = getHomepageRoute(isChatEnabled); function handleRetry() { // Auth-related errors should redirect to login @@ -25,8 +29,8 @@ function ErrorPageContent() { window.location.reload(); }, 2000); } else { - // For server/network errors, go to marketplace - window.location.href = "/marketplace"; + // For server/network errors, go to home + window.location.href = homepageRoute; } } diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/RunAgentModal.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/RunAgentModal.tsx index cd0c666be6..d5ba9142ee 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/RunAgentModal.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/RunAgentModal/RunAgentModal.tsx @@ -180,7 +180,7 @@ export function RunAgentModal({ {/* Content */} {hasAnySetupFields ? ( -
+
{agent.description} @@ -40,6 +40,8 @@ export function ModalHeader({ agent }: ModalHeaderProps) { Tip +
+ For best results, run this agent{" "} {humanizeCronExpression( @@ -50,7 +52,7 @@ export function ModalHeader({ agent }: ModalHeaderProps) { ) : null} {agent.instructions ? ( -
+
Instructions diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts index 4232847226..87e9e9e9bc 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/useLibraryAgentCard.ts @@ -8,6 +8,8 @@ import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/ import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent"; import { okData } from "@/app/api/helpers"; import { useToast } from "@/components/molecules/Toast/use-toast"; +import { isLogoutInProgress } from "@/lib/autogpt-server-api/helpers"; +import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { updateFavoriteInQueries } from "./helpers"; interface Props { @@ -23,10 +25,14 @@ export function useLibraryAgentCard({ agent }: Props) { const { toast } = useToast(); const queryClient = getQueryClient(); const { mutateAsync: updateLibraryAgent } = usePatchV2UpdateLibraryAgent(); + const { user, isLoggedIn } = useSupabase(); + const logoutInProgress = isLogoutInProgress(); const { data: profile } = useGetV2GetUserProfile({ query: { select: okData, + enabled: isLoggedIn && !!user && !logoutInProgress, + queryKey: ["/api/store/profile", user?.id], }, }); diff --git a/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts b/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts index 656e1febc2..9bde570548 100644 --- a/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts @@ -1,6 +1,8 @@ import { useToast } from "@/components/molecules/Toast/use-toast"; +import { getHomepageRoute } from "@/lib/constants"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { environment } from "@/services/environment"; +import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; import { loginFormSchema, LoginProvider } from "@/types/auth"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter, useSearchParams } from "next/navigation"; @@ -20,15 +22,17 @@ export function useLoginPage() { const [isGoogleLoading, setIsGoogleLoading] = useState(false); const [showNotAllowedModal, setShowNotAllowedModal] = useState(false); const isCloudEnv = environment.isCloud(); + const isChatEnabled = useGetFlag(Flag.CHAT); + const homepageRoute = getHomepageRoute(isChatEnabled); // Get redirect destination from 'next' query parameter const nextUrl = searchParams.get("next"); useEffect(() => { if (isLoggedIn && !isLoggingIn) { - router.push(nextUrl || "/marketplace"); + router.push(nextUrl || homepageRoute); } - }, [isLoggedIn, isLoggingIn, nextUrl, router]); + }, [homepageRoute, isLoggedIn, isLoggingIn, nextUrl, router]); const form = useForm>({ resolver: zodResolver(loginFormSchema), @@ -98,7 +102,7 @@ export function useLoginPage() { } else if (result.onboarding) { router.replace("/onboarding"); } else { - router.replace("/marketplace"); + router.replace(homepageRoute); } } catch (error) { toast({ diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainMarketplacePage/__tests__/main.test.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainMarketplacePage/__tests__/main.test.tsx new file mode 100644 index 0000000000..bee227a7af --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainMarketplacePage/__tests__/main.test.tsx @@ -0,0 +1,15 @@ +import { expect, test } from "vitest"; +import { render, screen } from "@/tests/integrations/test-utils"; +import { MainMarkeplacePage } from "../MainMarketplacePage"; +import { server } from "@/mocks/mock-server"; +import { getDeleteV2DeleteStoreSubmissionMockHandler422 } from "@/app/api/__generated__/endpoints/store/store.msw"; + +// Only for CI testing purpose, will remove it in future PR +test("MainMarketplacePage", async () => { + server.use(getDeleteV2DeleteStoreSubmissionMockHandler422()); + + render(); + expect( + await screen.findByText("Featured agents", { exact: false }), + ).toBeDefined(); +}); diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/page.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/page.tsx index 260fbc0b52..979b113f55 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/page.tsx @@ -3,12 +3,14 @@ import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store"; import { ProfileInfoForm } from "@/components/__legacy__/ProfileInfoForm"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; +import { isLogoutInProgress } from "@/lib/autogpt-server-api/helpers"; import { ProfileDetails } from "@/lib/autogpt-server-api/types"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { ProfileLoading } from "./ProfileLoading"; export default function UserProfilePage() { const { user } = useSupabase(); + const logoutInProgress = isLogoutInProgress(); const { data: profile, @@ -18,7 +20,7 @@ export default function UserProfilePage() { refetch, } = useGetV2GetUserProfile({ query: { - enabled: !!user, + enabled: !!user && !logoutInProgress, select: (res) => { if (res.status === 200) { return { diff --git a/autogpt_platform/frontend/src/app/(platform)/signup/actions.ts b/autogpt_platform/frontend/src/app/(platform)/signup/actions.ts index 68f7ae10ec..6d68782e7a 100644 --- a/autogpt_platform/frontend/src/app/(platform)/signup/actions.ts +++ b/autogpt_platform/frontend/src/app/(platform)/signup/actions.ts @@ -1,5 +1,6 @@ "use server"; +import { getHomepageRoute } from "@/lib/constants"; import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; import { signupFormSchema } from "@/types/auth"; import * as Sentry from "@sentry/nextjs"; @@ -11,6 +12,7 @@ export async function signup( password: string, confirmPassword: string, agreeToTerms: boolean, + isChatEnabled: boolean, ) { try { const parsed = signupFormSchema.safeParse({ @@ -58,7 +60,9 @@ export async function signup( } const isOnboardingEnabled = await shouldShowOnboarding(); - const next = isOnboardingEnabled ? "/onboarding" : "/"; + const next = isOnboardingEnabled + ? "/onboarding" + : getHomepageRoute(isChatEnabled); return { success: true, next }; } catch (err) { diff --git a/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts b/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts index e6d7c68aef..5bd53ca846 100644 --- a/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts @@ -1,6 +1,8 @@ import { useToast } from "@/components/molecules/Toast/use-toast"; +import { getHomepageRoute } from "@/lib/constants"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { environment } from "@/services/environment"; +import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; import { LoginProvider, signupFormSchema } from "@/types/auth"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter, useSearchParams } from "next/navigation"; @@ -20,15 +22,17 @@ export function useSignupPage() { const [isGoogleLoading, setIsGoogleLoading] = useState(false); const [showNotAllowedModal, setShowNotAllowedModal] = useState(false); const isCloudEnv = environment.isCloud(); + const isChatEnabled = useGetFlag(Flag.CHAT); + const homepageRoute = getHomepageRoute(isChatEnabled); // Get redirect destination from 'next' query parameter const nextUrl = searchParams.get("next"); useEffect(() => { if (isLoggedIn && !isSigningUp) { - router.push(nextUrl || "/marketplace"); + router.push(nextUrl || homepageRoute); } - }, [isLoggedIn, isSigningUp, nextUrl, router]); + }, [homepageRoute, isLoggedIn, isSigningUp, nextUrl, router]); const form = useForm>({ resolver: zodResolver(signupFormSchema), @@ -104,6 +108,7 @@ export function useSignupPage() { data.password, data.confirmPassword, data.agreeToTerms, + isChatEnabled === true, ); setIsLoading(false); @@ -129,7 +134,7 @@ export function useSignupPage() { } // Prefer the URL's next parameter, then result.next (for onboarding), then default - const redirectTo = nextUrl || result.next || "/"; + const redirectTo = nextUrl || result.next || homepageRoute; router.replace(redirectTo); } catch (error) { setIsLoading(false); diff --git a/autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts b/autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts index 4578ac03fe..3c9eda7785 100644 --- a/autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts +++ b/autogpt_platform/frontend/src/app/api/mutators/custom-mutator.ts @@ -4,12 +4,12 @@ import { getServerAuthToken, } from "@/lib/autogpt-server-api/helpers"; -import { transformDates } from "./date-transformer"; -import { environment } from "@/services/environment"; import { IMPERSONATION_HEADER_NAME, IMPERSONATION_STORAGE_KEY, } from "@/lib/constants"; +import { environment } from "@/services/environment"; +import { transformDates } from "./date-transformer"; const FRONTEND_BASE_URL = process.env.NEXT_PUBLIC_FRONTEND_BASE_URL || "http://localhost:3000"; diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json index 5cd60fcb35..579bc3e454 100644 --- a/autogpt_platform/frontend/src/app/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -1022,7 +1022,7 @@ "get": { "tags": ["v2", "chat", "chat"], "summary": "Get Session", - "description": "Retrieve the details of a specific chat session.\n\nLooks up a chat session by ID for the given user (if authenticated) and returns all session data including messages.\n\nArgs:\n session_id: The unique identifier for the desired chat session.\n user_id: The optional authenticated user ID, or None for anonymous access.\n\nReturns:\n SessionDetailResponse: Details for the requested session; raises NotFoundError if not found.", + "description": "Retrieve the details of a specific chat session.\n\nLooks up a chat session by ID for the given user (if authenticated) and returns all session data including messages.\n\nArgs:\n session_id: The unique identifier for the desired chat session.\n user_id: The optional authenticated user ID, or None for anonymous access.\n\nReturns:\n SessionDetailResponse: Details for the requested session, or None if not found.", "operationId": "getV2GetSession", "security": [{ "HTTPBearerJWT": [] }], "parameters": [ diff --git a/autogpt_platform/frontend/src/app/globals.css b/autogpt_platform/frontend/src/app/globals.css index 0625c26082..1f782f753b 100644 --- a/autogpt_platform/frontend/src/app/globals.css +++ b/autogpt_platform/frontend/src/app/globals.css @@ -141,52 +141,6 @@ } } -@keyframes shimmer { - 0% { - background-position: -200% 0; - } - 100% { - background-position: 200% 0; - } -} - -@keyframes l3 { - 25% { - background-position: - 0 0, - 100% 100%, - 100% calc(100% - 5px); - } - 50% { - background-position: - 0 100%, - 100% 100%, - 0 calc(100% - 5px); - } - 75% { - background-position: - 0 100%, - 100% 0, - 100% 5px; - } -} - -.loader { - width: 80px; - height: 70px; - border: 5px solid rgb(241 245 249); - padding: 0 8px; - box-sizing: border-box; - background: - linear-gradient(rgb(15 23 42) 0 0) 0 0/8px 20px, - linear-gradient(rgb(15 23 42) 0 0) 100% 0/8px 20px, - radial-gradient(farthest-side, rgb(15 23 42) 90%, #0000) 0 5px/8px 8px - content-box, - transparent; - background-repeat: no-repeat; - animation: l3 2s infinite linear; -} - input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; diff --git a/autogpt_platform/frontend/src/app/page.tsx b/autogpt_platform/frontend/src/app/page.tsx index b499a40d71..dbfab49469 100644 --- a/autogpt_platform/frontend/src/app/page.tsx +++ b/autogpt_platform/frontend/src/app/page.tsx @@ -1,5 +1,27 @@ -import { redirect } from "next/navigation"; +"use client"; + +import { getHomepageRoute } from "@/lib/constants"; +import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; export default function Page() { - redirect("/marketplace"); + const isChatEnabled = useGetFlag(Flag.CHAT); + const router = useRouter(); + const homepageRoute = getHomepageRoute(isChatEnabled); + const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true"; + const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID; + const isLaunchDarklyConfigured = envEnabled && Boolean(clientId); + const isFlagReady = + !isLaunchDarklyConfigured || typeof isChatEnabled === "boolean"; + + useEffect( + function redirectToHomepage() { + if (!isFlagReady) return; + router.replace(homepageRoute); + }, + [homepageRoute, isFlagReady, router], + ); + + return null; } diff --git a/autogpt_platform/frontend/src/components/atoms/Badge/Badge.test.tsx b/autogpt_platform/frontend/src/components/atoms/Badge/Badge.test.tsx deleted file mode 100644 index cd8531375b..0000000000 --- a/autogpt_platform/frontend/src/components/atoms/Badge/Badge.test.tsx +++ /dev/null @@ -1,81 +0,0 @@ -// import { render, screen } from "@testing-library/react"; -// import { describe, expect, it } from "vitest"; -// import { Badge } from "./Badge"; - -// describe("Badge Component", () => { -// it("renders badge with content", () => { -// render(Success); - -// expect(screen.getByText("Success")).toBeInTheDocument(); -// }); - -// it("applies correct variant styles", () => { -// const { rerender } = render(Success); -// let badge = screen.getByText("Success"); -// expect(badge).toHaveClass("bg-green-100", "text-green-800"); - -// rerender(Error); -// badge = screen.getByText("Error"); -// expect(badge).toHaveClass("bg-red-100", "text-red-800"); - -// rerender(Info); -// badge = screen.getByText("Info"); -// expect(badge).toHaveClass("bg-slate-100", "text-slate-800"); -// }); - -// it("applies custom className", () => { -// render( -// -// Success -// , -// ); - -// const badge = screen.getByText("Success"); -// expect(badge).toHaveClass("custom-class"); -// }); - -// it("renders as span element", () => { -// render(Success); - -// const badge = screen.getByText("Success"); -// expect(badge.tagName).toBe("SPAN"); -// }); - -// it("renders children correctly", () => { -// render( -// -// Custom Content -// , -// ); - -// expect(screen.getByText("Custom")).toBeInTheDocument(); -// expect(screen.getByText("Content")).toBeInTheDocument(); -// }); - -// it("supports all badge variants", () => { -// const variants = ["success", "error", "info"] as const; - -// variants.forEach((variant) => { -// const { unmount } = render( -// -// {variant} -// , -// ); - -// expect(screen.getByTestId(`badge-${variant}`)).toBeInTheDocument(); -// unmount(); -// }); -// }); - -// it("handles long text content", () => { -// render( -// -// Very long text that should be handled properly by the component -// , -// ); - -// const badge = screen.getByText(/Very long text/); -// expect(badge).toBeInTheDocument(); -// expect(badge).toHaveClass("overflow-hidden", "text-ellipsis"); -// }); -// }); diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/Chat.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/Chat.tsx new file mode 100644 index 0000000000..0f99246088 --- /dev/null +++ b/autogpt_platform/frontend/src/components/contextual/Chat/Chat.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { Text } from "@/components/atoms/Text/Text"; +import { cn } from "@/lib/utils"; +import { useEffect, useRef } from "react"; +import { ChatContainer } from "./components/ChatContainer/ChatContainer"; +import { ChatErrorState } from "./components/ChatErrorState/ChatErrorState"; +import { ChatLoader } from "./components/ChatLoader/ChatLoader"; +import { useChat } from "./useChat"; + +export interface ChatProps { + className?: string; + urlSessionId?: string | null; + initialPrompt?: string; + onSessionNotFound?: () => void; +} + +export function Chat({ + className, + urlSessionId, + initialPrompt, + onSessionNotFound, +}: ChatProps) { + const hasHandledNotFoundRef = useRef(false); + const { + messages, + isLoading, + isCreating, + error, + isSessionNotFound, + sessionId, + createSession, + showLoader, + } = useChat({ urlSessionId }); + + useEffect( + function handleMissingSession() { + if (!onSessionNotFound) return; + if (!urlSessionId) return; + if (!isSessionNotFound || isLoading || isCreating) return; + if (hasHandledNotFoundRef.current) return; + hasHandledNotFoundRef.current = true; + onSessionNotFound(); + }, + [onSessionNotFound, urlSessionId, isSessionNotFound, isLoading, isCreating], + ); + + return ( +
+ {/* Main Content */} +
+ {/* Loading State */} + {showLoader && (isLoading || isCreating) && ( +
+
+ + + Loading your chats... + +
+
+ )} + + {/* Error State */} + {error && !isLoading && ( + + )} + + {/* Session Content */} + {sessionId && !isLoading && !error && ( + + )} +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/AIChatBubble/AIChatBubble.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/AIChatBubble/AIChatBubble.tsx new file mode 100644 index 0000000000..f5d56fcb15 --- /dev/null +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/AIChatBubble/AIChatBubble.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils"; +import { ReactNode } from "react"; + +export interface AIChatBubbleProps { + children: ReactNode; + className?: string; +} + +export function AIChatBubble({ children, className }: AIChatBubbleProps) { + return ( +
+ {children} +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/AgentCarouselMessage/AgentCarouselMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentCarouselMessage/AgentCarouselMessage.tsx similarity index 100% rename from autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/AgentCarouselMessage/AgentCarouselMessage.tsx rename to autogpt_platform/frontend/src/components/contextual/Chat/components/AgentCarouselMessage/AgentCarouselMessage.tsx diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/AgentInputsSetup/AgentInputsSetup.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/AgentInputsSetup.tsx similarity index 100% rename from autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/AgentInputsSetup/AgentInputsSetup.tsx rename to autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/AgentInputsSetup.tsx diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/AgentInputsSetup/useAgentInputsSetup.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/useAgentInputsSetup.ts similarity index 100% rename from autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/AgentInputsSetup/useAgentInputsSetup.ts rename to autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/useAgentInputsSetup.ts diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/AuthPromptWidget/AuthPromptWidget.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/AuthPromptWidget/AuthPromptWidget.tsx similarity index 99% rename from autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/AuthPromptWidget/AuthPromptWidget.tsx rename to autogpt_platform/frontend/src/components/contextual/Chat/components/AuthPromptWidget/AuthPromptWidget.tsx index 33f02e660f..b2cf92ec56 100644 --- a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/AuthPromptWidget/AuthPromptWidget.tsx +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/AuthPromptWidget/AuthPromptWidget.tsx @@ -21,7 +21,7 @@ export function AuthPromptWidget({ message, sessionId, agentInfo, - returnUrl = "/chat", + returnUrl = "/copilot/chat", className, }: AuthPromptWidgetProps) { const router = useRouter(); diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/ChatContainer.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/ChatContainer.tsx new file mode 100644 index 0000000000..b86f1c922a --- /dev/null +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/ChatContainer.tsx @@ -0,0 +1,106 @@ +import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse"; +import { Button } from "@/components/atoms/Button/Button"; +import { Text } from "@/components/atoms/Text/Text"; +import { Dialog } from "@/components/molecules/Dialog/Dialog"; +import { useBreakpoint } from "@/lib/hooks/useBreakpoint"; +import { cn } from "@/lib/utils"; +import { ChatInput } from "../ChatInput/ChatInput"; +import { MessageList } from "../MessageList/MessageList"; +import { useChatContainer } from "./useChatContainer"; + +export interface ChatContainerProps { + sessionId: string | null; + initialMessages: SessionDetailResponse["messages"]; + initialPrompt?: string; + className?: string; +} + +export function ChatContainer({ + sessionId, + initialMessages, + initialPrompt, + className, +}: ChatContainerProps) { + const { + messages, + streamingChunks, + isStreaming, + stopStreaming, + isRegionBlockedModalOpen, + sendMessageWithContext, + handleRegionModalOpenChange, + handleRegionModalClose, + } = useChatContainer({ + sessionId, + initialMessages, + initialPrompt, + }); + + const breakpoint = useBreakpoint(); + const isMobile = + breakpoint === "base" || breakpoint === "sm" || breakpoint === "md"; + + return ( +
+ + +
+ + This model is not available in your region. Please connect via VPN + and try again. + +
+ +
+
+
+
+ {/* Messages - Scrollable */} +
+
+ +
+
+ + {/* Input - Fixed at bottom */} +
+
+ +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/createStreamEventDispatcher.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/createStreamEventDispatcher.ts similarity index 55% rename from autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/createStreamEventDispatcher.ts rename to autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/createStreamEventDispatcher.ts index 844f126d49..791cf046d5 100644 --- a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/createStreamEventDispatcher.ts +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/createStreamEventDispatcher.ts @@ -1,6 +1,6 @@ import { toast } from "sonner"; import { StreamChunk } from "../../useChatStream"; -import type { HandlerDependencies } from "./useChatContainer.handlers"; +import type { HandlerDependencies } from "./handlers"; import { handleError, handleLoginNeeded, @@ -9,12 +9,30 @@ import { handleTextEnded, handleToolCallStart, handleToolResponse, -} from "./useChatContainer.handlers"; + isRegionBlockedError, +} from "./handlers"; export function createStreamEventDispatcher( deps: HandlerDependencies, ): (chunk: StreamChunk) => void { return function dispatchStreamEvent(chunk: StreamChunk): void { + if ( + chunk.type === "text_chunk" || + chunk.type === "tool_call_start" || + chunk.type === "tool_response" || + chunk.type === "login_needed" || + chunk.type === "need_login" || + chunk.type === "error" + ) { + if (!deps.hasResponseRef.current) { + console.info("[ChatStream] First response chunk:", { + type: chunk.type, + sessionId: deps.sessionId, + }); + } + deps.hasResponseRef.current = true; + } + switch (chunk.type) { case "text_chunk": handleTextChunk(chunk, deps); @@ -38,15 +56,23 @@ export function createStreamEventDispatcher( break; case "stream_end": + console.info("[ChatStream] Stream ended:", { + sessionId: deps.sessionId, + hasResponse: deps.hasResponseRef.current, + chunkCount: deps.streamingChunksRef.current.length, + }); handleStreamEnd(chunk, deps); break; case "error": + const isRegionBlocked = isRegionBlockedError(chunk); handleError(chunk, deps); // Show toast at dispatcher level to avoid circular dependencies - toast.error("Chat Error", { - description: chunk.message || chunk.content || "An error occurred", - }); + if (!isRegionBlocked) { + toast.error("Chat Error", { + description: chunk.message || chunk.content || "An error occurred", + }); + } break; case "usage": diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/useChatContainer.handlers.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/handlers.ts similarity index 66% rename from autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/useChatContainer.handlers.ts rename to autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/handlers.ts index 064b847064..96198a0386 100644 --- a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/useChatContainer.handlers.ts +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/handlers.ts @@ -7,15 +7,30 @@ import { parseToolResponse, } from "./helpers"; +function isToolCallMessage( + message: ChatMessageData, +): message is Extract { + return message.type === "tool_call"; +} + export interface HandlerDependencies { setHasTextChunks: Dispatch>; setStreamingChunks: Dispatch>; streamingChunksRef: MutableRefObject; + hasResponseRef: MutableRefObject; setMessages: Dispatch>; setIsStreamingInitiated: Dispatch>; + setIsRegionBlockedModalOpen: Dispatch>; sessionId: string; } +export function isRegionBlockedError(chunk: StreamChunk): boolean { + if (chunk.code === "MODEL_NOT_AVAILABLE_REGION") return true; + const message = chunk.message || chunk.content; + if (typeof message !== "string") return false; + return message.toLowerCase().includes("not available in your region"); +} + export function handleTextChunk(chunk: StreamChunk, deps: HandlerDependencies) { if (!chunk.content) return; deps.setHasTextChunks(true); @@ -30,16 +45,17 @@ export function handleTextEnded( _chunk: StreamChunk, deps: HandlerDependencies, ) { - console.log("[Text Ended] Saving streamed text as assistant message"); const completedText = deps.streamingChunksRef.current.join(""); if (completedText.trim()) { - const assistantMessage: ChatMessageData = { - type: "message", - role: "assistant", - content: completedText, - timestamp: new Date(), - }; - deps.setMessages((prev) => [...prev, assistantMessage]); + deps.setMessages((prev) => { + const assistantMessage: ChatMessageData = { + type: "message", + role: "assistant", + content: completedText, + timestamp: new Date(), + }; + return [...prev, assistantMessage]; + }); } deps.setStreamingChunks([]); deps.streamingChunksRef.current = []; @@ -50,30 +66,45 @@ export function handleToolCallStart( chunk: StreamChunk, deps: HandlerDependencies, ) { - const toolCallMessage: ChatMessageData = { + const toolCallMessage: Extract = { type: "tool_call", toolId: chunk.tool_id || `tool-${Date.now()}-${chunk.idx || 0}`, - toolName: chunk.tool_name || "Executing...", + toolName: chunk.tool_name || "Executing", arguments: chunk.arguments || {}, timestamp: new Date(), }; - deps.setMessages((prev) => [...prev, toolCallMessage]); - console.log("[Tool Call Start]", { - toolId: toolCallMessage.toolId, - toolName: toolCallMessage.toolName, - timestamp: new Date().toISOString(), - }); + + function updateToolCallMessages(prev: ChatMessageData[]) { + const existingIndex = prev.findIndex(function findToolCallIndex(msg) { + return isToolCallMessage(msg) && msg.toolId === toolCallMessage.toolId; + }); + if (existingIndex === -1) { + return [...prev, toolCallMessage]; + } + const nextMessages = [...prev]; + const existing = nextMessages[existingIndex]; + if (!isToolCallMessage(existing)) return prev; + const nextArguments = + toolCallMessage.arguments && + Object.keys(toolCallMessage.arguments).length > 0 + ? toolCallMessage.arguments + : existing.arguments; + nextMessages[existingIndex] = { + ...existing, + toolName: toolCallMessage.toolName || existing.toolName, + arguments: nextArguments, + timestamp: toolCallMessage.timestamp, + }; + return nextMessages; + } + + deps.setMessages(updateToolCallMessages); } export function handleToolResponse( chunk: StreamChunk, deps: HandlerDependencies, ) { - console.log("[Tool Response] Received:", { - toolId: chunk.tool_id, - toolName: chunk.tool_name, - timestamp: new Date().toISOString(), - }); let toolName = chunk.tool_name || "unknown"; if (!chunk.tool_name || chunk.tool_name === "unknown") { deps.setMessages((prev) => { @@ -127,22 +158,15 @@ export function handleToolResponse( const toolCallIndex = prev.findIndex( (msg) => msg.type === "tool_call" && msg.toolId === chunk.tool_id, ); + const hasResponse = prev.some( + (msg) => msg.type === "tool_response" && msg.toolId === chunk.tool_id, + ); + if (hasResponse) return prev; if (toolCallIndex !== -1) { const newMessages = [...prev]; - newMessages[toolCallIndex] = responseMessage; - console.log( - "[Tool Response] Replaced tool_call with matching tool_id:", - chunk.tool_id, - "at index:", - toolCallIndex, - ); + newMessages.splice(toolCallIndex + 1, 0, responseMessage); return newMessages; } - console.warn( - "[Tool Response] No tool_call found with tool_id:", - chunk.tool_id, - "appending instead", - ); return [...prev, responseMessage]; }); } @@ -167,55 +191,38 @@ export function handleStreamEnd( deps: HandlerDependencies, ) { const completedContent = deps.streamingChunksRef.current.join(""); - // Only save message if there are uncommitted chunks - // (text_ended already saved if there were tool calls) + if (!completedContent.trim() && !deps.hasResponseRef.current) { + deps.setMessages((prev) => [ + ...prev, + { + type: "message", + role: "assistant", + content: "No response received. Please try again.", + timestamp: new Date(), + }, + ]); + } if (completedContent.trim()) { - console.log( - "[Stream End] Saving remaining streamed text as assistant message", - ); const assistantMessage: ChatMessageData = { type: "message", role: "assistant", content: completedContent, timestamp: new Date(), }; - deps.setMessages((prev) => { - const updated = [...prev, assistantMessage]; - console.log("[Stream End] Final state:", { - localMessages: updated.map((m) => ({ - type: m.type, - ...(m.type === "message" && { - role: m.role, - contentLength: m.content.length, - }), - ...(m.type === "tool_call" && { - toolId: m.toolId, - toolName: m.toolName, - }), - ...(m.type === "tool_response" && { - toolId: m.toolId, - toolName: m.toolName, - success: m.success, - }), - })), - streamingChunks: deps.streamingChunksRef.current, - timestamp: new Date().toISOString(), - }); - return updated; - }); - } else { - console.log("[Stream End] No uncommitted chunks, message already saved"); + deps.setMessages((prev) => [...prev, assistantMessage]); } deps.setStreamingChunks([]); deps.streamingChunksRef.current = []; deps.setHasTextChunks(false); deps.setIsStreamingInitiated(false); - console.log("[Stream End] Stream complete, messages in local state"); } export function handleError(chunk: StreamChunk, deps: HandlerDependencies) { const errorMessage = chunk.message || chunk.content || "An error occurred"; console.error("Stream error:", errorMessage); + if (isRegionBlockedError(chunk)) { + deps.setIsRegionBlockedModalOpen(true); + } deps.setIsStreamingInitiated(false); deps.setHasTextChunks(false); deps.setStreamingChunks([]); diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/helpers.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/helpers.ts similarity index 92% rename from autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/helpers.ts rename to autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/helpers.ts index ab7dbd275d..9d51003a93 100644 --- a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/helpers.ts +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/helpers.ts @@ -1,6 +1,33 @@ +import { SessionKey, sessionStorage } from "@/services/storage/session-storage"; import type { ToolResult } from "@/types/chat"; import type { ChatMessageData } from "../ChatMessage/useChatMessage"; +export function hasSentInitialPrompt(sessionId: string): boolean { + try { + const sent = JSON.parse( + sessionStorage.get(SessionKey.CHAT_SENT_INITIAL_PROMPTS) || "{}", + ); + return sent[sessionId] === true; + } catch { + return false; + } +} + +export function markInitialPromptSent(sessionId: string): void { + try { + const sent = JSON.parse( + sessionStorage.get(SessionKey.CHAT_SENT_INITIAL_PROMPTS) || "{}", + ); + sent[sessionId] = true; + sessionStorage.set( + SessionKey.CHAT_SENT_INITIAL_PROMPTS, + JSON.stringify(sent), + ); + } catch { + // Ignore storage errors + } +} + export function removePageContext(content: string): string { // Remove "Page URL: ..." pattern at start of line (case insensitive, handles various formats) let cleaned = content.replace(/^\s*Page URL:\s*[^\n\r]*/gim, ""); @@ -207,12 +234,22 @@ export function parseToolResponse( if (responseType === "setup_requirements") { return null; } + if (responseType === "understanding_updated") { + return { + type: "tool_response", + toolId, + toolName, + result: (parsedResult || result) as ToolResult, + success: true, + timestamp: timestamp || new Date(), + }; + } } return { type: "tool_response", toolId, toolName, - result, + result: parsedResult ? (parsedResult as ToolResult) : result, success: true, timestamp: timestamp || new Date(), }; diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/useChatContainer.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.ts similarity index 65% rename from autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/useChatContainer.ts rename to autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.ts index 8e7dee7718..42dd04670d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatContainer/useChatContainer.ts +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.ts @@ -1,14 +1,17 @@ import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { useChatStream } from "../../useChatStream"; +import { usePageContext } from "../../usePageContext"; import type { ChatMessageData } from "../ChatMessage/useChatMessage"; import { createStreamEventDispatcher } from "./createStreamEventDispatcher"; import { createUserMessage, filterAuthMessages, + hasSentInitialPrompt, isToolCallArray, isValidMessage, + markInitialPromptSent, parseToolResponse, removePageContext, } from "./helpers"; @@ -16,20 +19,45 @@ import { interface Args { sessionId: string | null; initialMessages: SessionDetailResponse["messages"]; + initialPrompt?: string; } -export function useChatContainer({ sessionId, initialMessages }: Args) { +export function useChatContainer({ + sessionId, + initialMessages, + initialPrompt, +}: Args) { const [messages, setMessages] = useState([]); const [streamingChunks, setStreamingChunks] = useState([]); const [hasTextChunks, setHasTextChunks] = useState(false); const [isStreamingInitiated, setIsStreamingInitiated] = useState(false); + const [isRegionBlockedModalOpen, setIsRegionBlockedModalOpen] = + useState(false); + const hasResponseRef = useRef(false); const streamingChunksRef = useRef([]); - const { error, sendMessage: sendStreamMessage } = useChatStream(); + const previousSessionIdRef = useRef(null); + const { + error, + sendMessage: sendStreamMessage, + stopStreaming, + } = useChatStream(); const isStreaming = isStreamingInitiated || hasTextChunks; + useEffect(() => { + if (sessionId !== previousSessionIdRef.current) { + stopStreaming(previousSessionIdRef.current ?? undefined, true); + previousSessionIdRef.current = sessionId; + setMessages([]); + setStreamingChunks([]); + streamingChunksRef.current = []; + setHasTextChunks(false); + setIsStreamingInitiated(false); + hasResponseRef.current = false; + } + }, [sessionId, stopStreaming]); + const allMessages = useMemo(() => { const processedInitialMessages: ChatMessageData[] = []; - // Map to track tool calls by their ID so we can look up tool names for tool responses const toolCallMap = new Map(); for (const msg of initialMessages) { @@ -45,13 +73,9 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { ? new Date(msg.timestamp as string) : undefined; - // Remove page context from user messages when loading existing sessions if (role === "user") { content = removePageContext(content); - // Skip user messages that become empty after removing page context - if (!content.trim()) { - continue; - } + if (!content.trim()) continue; processedInitialMessages.push({ type: "message", role: "user", @@ -61,19 +85,15 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { continue; } - // Handle assistant messages first (before tool messages) to build tool call map if (role === "assistant") { - // Strip tags from content content = content .replace(/[\s\S]*?<\/thinking>/gi, "") .trim(); - // If assistant has tool calls, create tool_call messages for each if (toolCalls && isToolCallArray(toolCalls) && toolCalls.length > 0) { for (const toolCall of toolCalls) { const toolName = toolCall.function.name; const toolId = toolCall.id; - // Store tool name for later lookup toolCallMap.set(toolId, toolName); try { @@ -96,7 +116,6 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { }); } } - // Only add assistant message if there's content after stripping thinking tags if (content.trim()) { processedInitialMessages.push({ type: "message", @@ -106,7 +125,6 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { }); } } else if (content.trim()) { - // Assistant message without tool calls, but with content processedInitialMessages.push({ type: "message", role: "assistant", @@ -117,7 +135,6 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { continue; } - // Handle tool messages - look up tool name from tool call map if (role === "tool") { const toolCallId = (msg.tool_call_id as string) || ""; const toolName = toolCallMap.get(toolCallId) || "unknown"; @@ -133,7 +150,6 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { continue; } - // Handle other message types (system, etc.) if (content.trim()) { processedInitialMessages.push({ type: "message", @@ -154,9 +170,10 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { context?: { url: string; content: string }, ) { if (!sessionId) { - console.error("Cannot send message: no session ID"); + console.error("[useChatContainer] Cannot send message: no session ID"); return; } + setIsRegionBlockedModalOpen(false); if (isUserMessage) { const userMessage = createUserMessage(content); setMessages((prev) => [...filterAuthMessages(prev), userMessage]); @@ -167,14 +184,19 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { streamingChunksRef.current = []; setHasTextChunks(false); setIsStreamingInitiated(true); + hasResponseRef.current = false; + const dispatcher = createStreamEventDispatcher({ setHasTextChunks, setStreamingChunks, streamingChunksRef, + hasResponseRef, setMessages, + setIsRegionBlockedModalOpen, sessionId, setIsStreamingInitiated, }); + try { await sendStreamMessage( sessionId, @@ -184,8 +206,12 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { context, ); } catch (err) { - console.error("Failed to send message:", err); + console.error("[useChatContainer] Failed to send message:", err); setIsStreamingInitiated(false); + + // Don't show error toast for AbortError (expected during cleanup) + if (err instanceof Error && err.name === "AbortError") return; + const errorMessage = err instanceof Error ? err.message : "Failed to send message"; toast.error("Failed to send message", { @@ -196,11 +222,63 @@ export function useChatContainer({ sessionId, initialMessages }: Args) { [sessionId, sendStreamMessage], ); + const handleStopStreaming = useCallback(() => { + stopStreaming(); + setStreamingChunks([]); + streamingChunksRef.current = []; + setHasTextChunks(false); + setIsStreamingInitiated(false); + }, [stopStreaming]); + + const { capturePageContext } = usePageContext(); + + // Send initial prompt if provided (for new sessions from homepage) + useEffect( + function handleInitialPrompt() { + if (!initialPrompt || !sessionId) return; + if (initialMessages.length > 0) return; + if (hasSentInitialPrompt(sessionId)) return; + + markInitialPromptSent(sessionId); + const context = capturePageContext(); + sendMessage(initialPrompt, true, context); + }, + [ + initialPrompt, + sessionId, + initialMessages.length, + sendMessage, + capturePageContext, + ], + ); + + async function sendMessageWithContext( + content: string, + isUserMessage: boolean = true, + ) { + const context = capturePageContext(); + await sendMessage(content, isUserMessage, context); + } + + function handleRegionModalOpenChange(open: boolean) { + setIsRegionBlockedModalOpen(open); + } + + function handleRegionModalClose() { + setIsRegionBlockedModalOpen(false); + } + return { messages: allMessages, streamingChunks, isStreaming, error, + isRegionBlockedModalOpen, + setIsRegionBlockedModalOpen, + sendMessageWithContext, + handleRegionModalOpenChange, + handleRegionModalClose, sendMessage, + stopStreaming: handleStopStreaming, }; } diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup.tsx similarity index 100% rename from autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup.tsx rename to autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup.tsx diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatCredentialsSetup/useChatCredentialsSetup.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/useChatCredentialsSetup.ts similarity index 100% rename from autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatCredentialsSetup/useChatCredentialsSetup.ts rename to autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/useChatCredentialsSetup.ts diff --git a/autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatErrorState/ChatErrorState.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatErrorState/ChatErrorState.tsx similarity index 100% rename from autogpt_platform/frontend/src/app/(platform)/chat/components/Chat/components/ChatErrorState/ChatErrorState.tsx rename to autogpt_platform/frontend/src/components/contextual/Chat/components/ChatErrorState/ChatErrorState.tsx diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/ChatInput.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/ChatInput.tsx new file mode 100644 index 0000000000..8cdecf0bf4 --- /dev/null +++ b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/ChatInput.tsx @@ -0,0 +1,103 @@ +import { Button } from "@/components/atoms/Button/Button"; +import { cn } from "@/lib/utils"; +import { ArrowUpIcon, StopIcon } from "@phosphor-icons/react"; +import { useChatInput } from "./useChatInput"; + +export interface Props { + onSend: (message: string) => void; + disabled?: boolean; + isStreaming?: boolean; + onStop?: () => void; + placeholder?: string; + className?: string; +} + +export function ChatInput({ + onSend, + disabled = false, + isStreaming = false, + onStop, + placeholder = "Type your message...", + className, +}: Props) { + const inputId = "chat-input"; + const { value, setValue, handleKeyDown, handleSend, hasMultipleLines } = + useChatInput({ + onSend, + disabled: disabled || isStreaming, + maxRows: 4, + inputId, + }); + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + handleSend(); + } + + function handleChange(e: React.ChangeEvent) { + setValue(e.target.value); + } + + return ( +
+
+
+