From 85dd601d7fab6ea30e0b5c734840dbb2742a8cc2 Mon Sep 17 00:00:00 2001 From: ipbabble Date: Sat, 30 Aug 2025 16:53:17 -0600 Subject: [PATCH 1/2] Modernize Gemini models and add comprehensive error handling with testing Fix deprecated model configurations causing 404 errors: - Remove deprecated 1.5 and preview models (gemini-2.5-*-preview-*) - Add cost-effective gemini-2.5-flash-lite option - Update to stable model names aligned with Google's 2025 roadmap Implement graceful error recovery: - Add ModelErrorDialog with fallback model suggestions - Preserve user input during model failures - Save user model preferences with "remember choice" - Add 404 error handling to all LangGraph nodes Enhance user interface: - Clean model display names in dropdowns (hide indicators when closed) - Dual Query/Reasoning model selection with visual categorization - Professional model selection with icons and cost indicators Add regression test suite: - Backend: Model naming validation, frontend/backend consistency - Frontend: Source code validation, stable model patterns - Integration script: Automated validation prevents config drift - Support for new model variants (e.g., -lite suffix) Modernize development workflow: - Pre-commit hooks with model validation, Ruff, Prettier, ESLint - Organized tests/ directory with comprehensive documentation - Updated project structure and setup instructions --- .gitignore | 4 +- .pre-commit-config.yaml | 76 ++ Makefile | 2 +- README.md | 24 +- backend/.env.example | 2 +- backend/Makefile | 1 - backend/examples/cli_research.py | 4 +- backend/src/agent/app.py | 1 + backend/src/agent/configuration.py | 6 +- backend/src/agent/graph.py | 199 ++- backend/src/agent/prompts.py | 6 +- backend/src/agent/state.py | 5 +- backend/src/agent/tools_and_schemas.py | 1 + backend/src/agent/utils.py | 18 +- backend/test-agent.ipynb | 15 +- backend/tests/__init__.py | 1 + backend/tests/test_model_validation.py | 177 +++ frontend/.prettierignore | 7 + frontend/.prettierrc | 10 + frontend/components.json | 2 +- frontend/eslint.config.js | 27 +- frontend/package-lock.json | 1271 +++++++++++++++++- frontend/package.json | 14 +- frontend/public/vite.svg | 2 +- frontend/src/App.tsx | 185 ++- frontend/src/components/ActivityTimeline.tsx | 27 +- frontend/src/components/ChatMessagesView.tsx | 62 +- frontend/src/components/InputForm.tsx | 139 +- frontend/src/components/ModelErrorDialog.tsx | 107 ++ frontend/src/components/WelcomeScreen.tsx | 15 +- frontend/src/components/ui/badge.tsx | 31 +- frontend/src/components/ui/button.tsx | 27 +- frontend/src/components/ui/card.tsx | 39 +- frontend/src/components/ui/input.tsx | 8 +- frontend/src/components/ui/scroll-area.tsx | 20 +- frontend/src/components/ui/select.tsx | 59 +- frontend/src/components/ui/tabs.tsx | 36 +- frontend/src/components/ui/textarea.tsx | 8 +- frontend/src/global.css | 44 +- frontend/src/test/model-validation.test.tsx | 125 ++ frontend/src/test/setup.ts | 1 + frontend/tsconfig.json | 4 +- frontend/vitest.config.ts | 17 + setup-precommit.sh | 33 + tests/README.md | 46 + tests/test-model-validation.sh | 81 ++ 46 files changed, 2557 insertions(+), 432 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 backend/tests/__init__.py create mode 100644 backend/tests/test_model_validation.py create mode 100644 frontend/.prettierignore create mode 100644 frontend/.prettierrc create mode 100644 frontend/src/components/ModelErrorDialog.tsx create mode 100644 frontend/src/test/model-validation.test.tsx create mode 100644 frontend/src/test/setup.ts create mode 100644 frontend/vitest.config.ts create mode 100755 setup-precommit.sh create mode 100644 tests/README.md create mode 100755 tests/test-model-validation.sh diff --git a/.gitignore b/.gitignore index ad4a67f2..8bb1cb72 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,7 @@ Thumbs.db *.sw? # Optional backend venv (if created in root) -#.venv/ +#.venv/ # Byte-compiled / optimized / DLL files __pycache__/ @@ -199,4 +199,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -backend/.langgraph_api \ No newline at end of file +backend/.langgraph_api diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..93b95e7d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,76 @@ +# Pre-commit configuration for Gemini LangGraph Quickstart +# See https://pre-commit.com for more information + +repos: + # Model Validation (Critical - prevents 404 model errors) + - repo: local + hooks: + - id: model-validation + name: Model Configuration Validation + entry: ./tests/test-model-validation.sh + language: script + always_run: true + pass_filenames: false + description: "Prevent frontend/backend model mismatches that cause 404 errors" + + # Backend Python (Ruff - modern all-in-one tool) + - repo: local + hooks: + - id: ruff-lint + name: Ruff Linting (Python) + entry: bash -c 'cd backend && ruff check --fix .' + language: system + files: ^backend/.*\.py$ + description: "Fast Python linter (replaces flake8, isort, etc.)" + - id: ruff-format + name: Ruff Format (Python) + entry: bash -c 'cd backend && ruff format .' + language: system + files: ^backend/.*\.py$ + description: "Fast Python formatter (replaces black)" + + # Frontend TypeScript/React + - repo: local + hooks: + - id: eslint-frontend + name: ESLint (Frontend) + entry: bash -c 'cd frontend && npm run lint' + language: system + files: ^frontend/.*\.(ts|tsx|js|jsx)$ + description: "TypeScript/React linting" + - id: prettier-frontend + name: Prettier (Frontend) + entry: bash -c 'cd frontend && npm run format' + language: system + files: ^frontend/.*\.(ts|tsx|js|jsx|json|css|md)$ + description: "Frontend code formatting (auto-fix)" + - id: typescript-check + name: TypeScript Check + entry: bash -c 'cd frontend && npx tsc --noEmit' + language: system + files: ^frontend/.*\.(ts|tsx)$ + description: "TypeScript type checking" + + # General file hygiene + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-yaml + name: Check YAML + description: "Validate YAML syntax" + - id: check-json + name: Check JSON + description: "Validate JSON syntax" + - id: trailing-whitespace + name: Trim Trailing Whitespace + description: "Remove trailing whitespace" + - id: end-of-file-fixer + name: Fix End of Files + description: "Ensure files end with newline" + - id: check-merge-conflict + name: Check Merge Conflicts + description: "Check for merge conflict markers" + +# Global settings +default_install_hook_types: [pre-commit] +default_stages: [pre-commit] \ No newline at end of file diff --git a/Makefile b/Makefile index 2e5c9033..3963048f 100644 --- a/Makefile +++ b/Makefile @@ -17,4 +17,4 @@ dev-backend: # Run frontend and backend concurrently dev: @echo "Starting both frontend and backend development servers..." - @make dev-frontend & make dev-backend \ No newline at end of file + @make dev-frontend & make dev-backend diff --git a/README.md b/README.md index eef0356b..3e10a64e 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,11 @@ This project demonstrates a fullstack application using a React frontend and a L ## Project Structure -The project is divided into two main directories: +The project is divided into main directories: - `frontend/`: Contains the React application built with Vite. - `backend/`: Contains the LangGraph/FastAPI application, including the research agent logic. +- `tests/`: Contains project-wide regression tests for model validation and configuration consistency. ## Getting Started: Development and Local Testing @@ -84,6 +85,25 @@ cd backend python examples/cli_research.py "What are the latest trends in renewable energy?" ``` +## Testing + +Run comprehensive model validation tests to ensure frontend/backend model consistency: + +```bash +./tests/test-model-validation.sh +``` + +These regression tests prevent model configuration mismatches that can cause 404 errors and application crashes. See `tests/README.md` for detailed testing documentation. + +### Pre-commit Hooks + +Set up automated code quality checks that run before each commit: + +```bash +./setup-precommit.sh +``` + +This configures hooks for model validation, Python linting (Ruff), frontend formatting (Prettier), and TypeScript checking. ## Deployment @@ -117,4 +137,4 @@ Open your browser and navigate to `http://localhost:8123/app/` to see the applic ## License -This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details. +This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details. diff --git a/backend/.env.example b/backend/.env.example index fde5f6ba..5947fd93 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1 +1 @@ -# GEMINI_API_KEY= \ No newline at end of file +# GEMINI_API_KEY= diff --git a/backend/Makefile b/backend/Makefile index a1bc6d2e..692a765a 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -61,4 +61,3 @@ help: @echo 'tests - run unit tests' @echo 'test TEST_FILE= - run all tests in file' @echo 'test_watch - run unit tests in watch mode' - diff --git a/backend/examples/cli_research.py b/backend/examples/cli_research.py index a086496b..de88c779 100644 --- a/backend/examples/cli_research.py +++ b/backend/examples/cli_research.py @@ -1,5 +1,7 @@ import argparse + from langchain_core.messages import HumanMessage + from agent.graph import graph @@ -21,7 +23,7 @@ def main() -> None: ) parser.add_argument( "--reasoning-model", - default="gemini-2.5-pro-preview-05-06", + default="gemini-2.5-pro", help="Model for the final answer", ) args = parser.parse_args() diff --git a/backend/src/agent/app.py b/backend/src/agent/app.py index f20f6ed3..d8d8bc21 100644 --- a/backend/src/agent/app.py +++ b/backend/src/agent/app.py @@ -1,5 +1,6 @@ # mypy: disable - error - code = "no-untyped-def,misc" import pathlib + from fastapi import FastAPI, Response from fastapi.staticfiles import StaticFiles diff --git a/backend/src/agent/configuration.py b/backend/src/agent/configuration.py index e57122d2..2e15e51d 100644 --- a/backend/src/agent/configuration.py +++ b/backend/src/agent/configuration.py @@ -1,8 +1,8 @@ import os -from pydantic import BaseModel, Field -from typing import Any, Optional +from typing import Any from langchain_core.runnables import RunnableConfig +from pydantic import BaseModel, Field class Configuration(BaseModel): @@ -41,7 +41,7 @@ class Configuration(BaseModel): @classmethod def from_runnable_config( - cls, config: Optional[RunnableConfig] = None + cls, config: RunnableConfig | None = None ) -> "Configuration": """Create a Configuration instance from a RunnableConfig.""" configurable = ( diff --git a/backend/src/agent/graph.py b/backend/src/agent/graph.py index 0f19c3f2..ec427de1 100644 --- a/backend/src/agent/graph.py +++ b/backend/src/agent/graph.py @@ -1,29 +1,28 @@ import os -from agent.tools_and_schemas import SearchQueryList, Reflection from dotenv import load_dotenv +from google.genai import Client from langchain_core.messages import AIMessage -from langgraph.types import Send -from langgraph.graph import StateGraph -from langgraph.graph import START, END from langchain_core.runnables import RunnableConfig -from google.genai import Client +from langchain_google_genai import ChatGoogleGenerativeAI +from langgraph.graph import END, START, StateGraph +from langgraph.types import Send -from agent.state import ( - OverallState, - QueryGenerationState, - ReflectionState, - WebSearchState, -) from agent.configuration import Configuration from agent.prompts import ( + answer_instructions, get_current_date, query_writer_instructions, - web_searcher_instructions, reflection_instructions, - answer_instructions, + web_searcher_instructions, ) -from langchain_google_genai import ChatGoogleGenerativeAI +from agent.state import ( + OverallState, + QueryGenerationState, + ReflectionState, + WebSearchState, +) +from agent.tools_and_schemas import Reflection, SearchQueryList from agent.utils import ( get_citations, get_research_topic, @@ -60,9 +59,12 @@ def generate_query(state: OverallState, config: RunnableConfig) -> QueryGenerati if state.get("initial_search_query_count") is None: state["initial_search_query_count"] = configurable.number_of_initial_queries - # init Gemini 2.0 Flash + # Use query_model from frontend if provided, otherwise fallback to configuration + model_to_use = state.get("query_model", configurable.query_generator_model) + + # init Gemini model for query generation llm = ChatGoogleGenerativeAI( - model=configurable.query_generator_model, + model=model_to_use, temperature=1.0, max_retries=2, api_key=os.getenv("GEMINI_API_KEY"), @@ -77,8 +79,19 @@ def generate_query(state: OverallState, config: RunnableConfig) -> QueryGenerati number_queries=state["initial_search_query_count"], ) # Generate the search queries - result = structured_llm.invoke(formatted_prompt) - return {"search_query": result.query} + try: + result = structured_llm.invoke(formatted_prompt) + return {"search_query": result.query} + except Exception as e: + # Check if this is a model not found error + if "404" in str(e) and "models/" in str(e): + # Return an error that the frontend can catch + error_msg = ( + f"Model '{model_to_use}' not found. Please try a different model." + ) + raise ValueError(error_msg) from e + # Re-raise other errors + raise def continue_to_web_research(state: QueryGenerationState): @@ -111,29 +124,45 @@ def web_research(state: WebSearchState, config: RunnableConfig) -> OverallState: research_topic=state["search_query"], ) + # Use query_model from frontend if provided, otherwise fallback to configuration + model_to_use = state.get("query_model", configurable.query_generator_model) + # Uses the google genai client as the langchain client doesn't return grounding metadata - response = genai_client.models.generate_content( - model=configurable.query_generator_model, - contents=formatted_prompt, - config={ - "tools": [{"google_search": {}}], - "temperature": 0, - }, - ) - # resolve the urls to short urls for saving tokens and time - resolved_urls = resolve_urls( - response.candidates[0].grounding_metadata.grounding_chunks, state["id"] - ) - # Gets the citations and adds them to the generated text - citations = get_citations(response, resolved_urls) - modified_text = insert_citation_markers(response.text, citations) - sources_gathered = [item for citation in citations for item in citation["segments"]] + try: + response = genai_client.models.generate_content( + model=model_to_use, + contents=formatted_prompt, + config={ + "tools": [{"google_search": {}}], + "temperature": 0, + }, + ) + # resolve the urls to short urls for saving tokens and time + resolved_urls = resolve_urls( + response.candidates[0].grounding_metadata.grounding_chunks, state["id"] + ) + # Gets the citations and adds them to the generated text + citations = get_citations(response, resolved_urls) + modified_text = insert_citation_markers(response.text, citations) + sources_gathered = [ + item for citation in citations for item in citation["segments"] + ] - return { - "sources_gathered": sources_gathered, - "search_query": [state["search_query"]], - "web_research_result": [modified_text], - } + return { + "sources_gathered": sources_gathered, + "search_query": [state["search_query"]], + "web_research_result": [modified_text], + } + except Exception as e: + # Check if this is a model not found error + if "404" in str(e) and "models/" in str(e): + # Return an error that the frontend can catch + error_msg = ( + f"Model '{model_to_use}' not found. Please try a different model." + ) + raise ValueError(error_msg) from e + # Re-raise other errors + raise def reflection(state: OverallState, config: RunnableConfig) -> ReflectionState: @@ -163,21 +192,32 @@ def reflection(state: OverallState, config: RunnableConfig) -> ReflectionState: summaries="\n\n---\n\n".join(state["web_research_result"]), ) # init Reasoning Model - llm = ChatGoogleGenerativeAI( - model=reasoning_model, - temperature=1.0, - max_retries=2, - api_key=os.getenv("GEMINI_API_KEY"), - ) - result = llm.with_structured_output(Reflection).invoke(formatted_prompt) - - return { - "is_sufficient": result.is_sufficient, - "knowledge_gap": result.knowledge_gap, - "follow_up_queries": result.follow_up_queries, - "research_loop_count": state["research_loop_count"], - "number_of_ran_queries": len(state["search_query"]), - } + try: + llm = ChatGoogleGenerativeAI( + model=reasoning_model, + temperature=1.0, + max_retries=2, + api_key=os.getenv("GEMINI_API_KEY"), + ) + result = llm.with_structured_output(Reflection).invoke(formatted_prompt) + + return { + "is_sufficient": result.is_sufficient, + "knowledge_gap": result.knowledge_gap, + "follow_up_queries": result.follow_up_queries, + "research_loop_count": state["research_loop_count"], + "number_of_ran_queries": len(state["search_query"]), + } + except Exception as e: + # Check if this is a model not found error + if "404" in str(e) and "models/" in str(e): + # Return an error that the frontend can catch + error_msg = ( + f"Model '{reasoning_model}' not found. Please try a different model." + ) + raise ValueError(error_msg) from e + # Re-raise other errors + raise def evaluate_research( @@ -242,27 +282,38 @@ def finalize_answer(state: OverallState, config: RunnableConfig): ) # init Reasoning Model, default to Gemini 2.5 Flash - llm = ChatGoogleGenerativeAI( - model=reasoning_model, - temperature=0, - max_retries=2, - api_key=os.getenv("GEMINI_API_KEY"), - ) - result = llm.invoke(formatted_prompt) - - # Replace the short urls with the original urls and add all used urls to the sources_gathered - unique_sources = [] - for source in state["sources_gathered"]: - if source["short_url"] in result.content: - result.content = result.content.replace( - source["short_url"], source["value"] + try: + llm = ChatGoogleGenerativeAI( + model=reasoning_model, + temperature=0, + max_retries=2, + api_key=os.getenv("GEMINI_API_KEY"), + ) + result = llm.invoke(formatted_prompt) + + # Replace the short urls with the original urls and add all used urls to the sources_gathered + unique_sources = [] + for source in state["sources_gathered"]: + if source["short_url"] in result.content: + result.content = result.content.replace( + source["short_url"], source["value"] + ) + unique_sources.append(source) + + return { + "messages": [AIMessage(content=result.content)], + "sources_gathered": unique_sources, + } + except Exception as e: + # Check if this is a model not found error + if "404" in str(e) and "models/" in str(e): + # Return an error that the frontend can catch + error_msg = ( + f"Model '{reasoning_model}' not found. Please try a different model." ) - unique_sources.append(source) - - return { - "messages": [AIMessage(content=result.content)], - "sources_gathered": unique_sources, - } + raise ValueError(error_msg) from e + # Re-raise other errors + raise # Create our Agent Graph diff --git a/backend/src/agent/prompts.py b/backend/src/agent/prompts.py index 8963f6a6..071d1d01 100644 --- a/backend/src/agent/prompts.py +++ b/backend/src/agent/prompts.py @@ -16,7 +16,7 @@ def get_current_date(): - Don't generate multiple similar queries, 1 is enough. - Query should ensure that the most current information is gathered. The current date is {current_date}. -Format: +Format: - Format your response as a JSON object with ALL two of these exact keys: - "rationale": Brief explanation of why these queries are relevant - "query": A list of search queries @@ -40,7 +40,7 @@ def get_current_date(): - Query should ensure that the most current information is gathered. The current date is {current_date}. - Conduct multiple, diverse searches to gather comprehensive information. - Consolidate key findings while meticulously tracking the source(s) for each specific piece of information. -- The output should be a well-written summary or report based on your search findings. +- The output should be a well-written summary or report based on your search findings. - Only include the information found in the search results, don't make up any information. Research Topic: @@ -83,7 +83,7 @@ def get_current_date(): Instructions: - The current date is {current_date}. -- You are the final step of a multi-step research process, don't mention that you are the final step. +- You are the final step of a multi-step research process, don't mention that you are the final step. - You have access to all the information gathered from the previous steps. - You have access to the user's question. - Generate a high-quality answer to the user's question based on the provided summaries and the user's question. diff --git a/backend/src/agent/state.py b/backend/src/agent/state.py index d5ad4dcd..d6fbb8ae 100644 --- a/backend/src/agent/state.py +++ b/backend/src/agent/state.py @@ -1,5 +1,6 @@ from __future__ import annotations +import operator from dataclasses import dataclass, field from typing import TypedDict @@ -7,9 +8,6 @@ from typing_extensions import Annotated -import operator - - class OverallState(TypedDict): messages: Annotated[list, add_messages] search_query: Annotated[list, operator.add] @@ -19,6 +17,7 @@ class OverallState(TypedDict): max_research_loops: int research_loop_count: int reasoning_model: str + query_model: str class ReflectionState(TypedDict): diff --git a/backend/src/agent/tools_and_schemas.py b/backend/src/agent/tools_and_schemas.py index 5e683c34..c58dd2fa 100644 --- a/backend/src/agent/tools_and_schemas.py +++ b/backend/src/agent/tools_and_schemas.py @@ -1,4 +1,5 @@ from typing import List + from pydantic import BaseModel, Field diff --git a/backend/src/agent/utils.py b/backend/src/agent/utils.py index d02c8d91..c955796e 100644 --- a/backend/src/agent/utils.py +++ b/backend/src/agent/utils.py @@ -1,11 +1,10 @@ from typing import Any, Dict, List -from langchain_core.messages import AnyMessage, AIMessage, HumanMessage + +from langchain_core.messages import AIMessage, AnyMessage, HumanMessage def get_research_topic(messages: List[AnyMessage]) -> str: - """ - Get the research topic from the messages. - """ + """Get the research topic from the messages.""" # check if request has a history and combine the messages into a single string if len(messages) == 1: research_topic = messages[-1].content @@ -20,11 +19,10 @@ def get_research_topic(messages: List[AnyMessage]) -> str: def resolve_urls(urls_to_resolve: List[Any], id: int) -> Dict[str, str]: - """ - Create a map of the vertex ai search urls (very long) to a short url with a unique id for each url. + """Create a map of the vertex ai search urls (very long) to a short url with a unique id for each url. Ensures each original URL gets a consistent shortened form while maintaining uniqueness. """ - prefix = f"https://vertexaisearch.cloud.google.com/id/" + prefix = "https://vertexaisearch.cloud.google.com/id/" urls = [site.web.uri for site in urls_to_resolve] # Create a dictionary that maps each unique URL to its first occurrence index @@ -37,8 +35,7 @@ def resolve_urls(urls_to_resolve: List[Any], id: int) -> Dict[str, str]: def insert_citation_markers(text, citations_list): - """ - Inserts citation markers into a text string based on start and end indices. + """Inserts citation markers into a text string based on start and end indices. Args: text (str): The original text string. @@ -76,8 +73,7 @@ def insert_citation_markers(text, citations_list): def get_citations(response, resolved_urls_map): - """ - Extracts and formats citation information from a Gemini model's response. + """Extracts and formats citation information from a Gemini model's response. This function processes the grounding metadata provided in the response to construct a list of citation objects. Each citation object includes the diff --git a/backend/test-agent.ipynb b/backend/test-agent.ipynb index d100b7f1..b0349b7c 100644 --- a/backend/test-agent.ipynb +++ b/backend/test-agent.ipynb @@ -8,7 +8,13 @@ "source": [ "from agent import graph\n", "\n", - "state = graph.invoke({\"messages\": [{\"role\": \"user\", \"content\": \"Who won the euro 2024\"}], \"max_research_loops\": 3, \"initial_search_query_count\": 3})" + "state = graph.invoke(\n", + " {\n", + " \"messages\": [{\"role\": \"user\", \"content\": \"Who won the euro 2024\"}],\n", + " \"max_research_loops\": 3,\n", + " \"initial_search_query_count\": 3,\n", + " }\n", + ")" ] }, { @@ -470,7 +476,12 @@ "metadata": {}, "outputs": [], "source": [ - "state = graph.invoke({\"messages\": state[\"messages\"] + [{\"role\": \"user\", \"content\": \"How has the most titles? List the top 5\"}]})" + "state = graph.invoke(\n", + " {\n", + " \"messages\": state[\"messages\"]\n", + " + [{\"role\": \"user\", \"content\": \"How has the most titles? List the top 5\"}]\n", + " }\n", + ")" ] }, { diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 00000000..66173aec --- /dev/null +++ b/backend/tests/__init__.py @@ -0,0 +1 @@ +# Test package diff --git a/backend/tests/test_model_validation.py b/backend/tests/test_model_validation.py new file mode 100644 index 00000000..00729fca --- /dev/null +++ b/backend/tests/test_model_validation.py @@ -0,0 +1,177 @@ +""" +Regression tests to validate model configurations and prevent frontend/backend mismatches. + +These tests ensure that: +1. Backend default models are valid and available +2. Frontend model options match backend capabilities +3. Model names don't get out of sync between components +""" + +import re +from pathlib import Path + +import pytest + +# Import backend configuration +from agent.configuration import Configuration + + +def get_frontend_model_names(): + """Extract model names from frontend InputForm.tsx""" + frontend_path = ( + Path(__file__).parent.parent.parent + / "frontend" + / "src" + / "components" + / "InputForm.tsx" + ) + + if not frontend_path.exists(): + pytest.skip("Frontend InputForm.tsx not found") + + content = frontend_path.read_text() + + # Extract model names from SelectItem value attributes + model_pattern = r'value="(gemini-[^"]+)"' + models = re.findall(model_pattern, content) + + return models + + +def get_backend_default_models(): + """Get default model names from backend configuration""" + config = Configuration() + return { + "query_generator_model": config.query_generator_model, + "reflection_model": config.reflection_model, + "answer_model": config.answer_model, + } + + +class TestModelValidation: + """Test suite for model configuration validation""" + + def test_backend_models_follow_naming_convention(self): + """Ensure backend models follow Google's stable naming convention""" + config = Configuration() + models = [ + config.query_generator_model, + config.reflection_model, + config.answer_model, + ] + + for model in models: + # Should not contain "preview" (unstable) or specific dates (deprecated) + assert "preview" not in model, ( + f"Model {model} appears to be a preview/unstable version" + ) + assert not re.search(r"\d{2}-\d{2}", model), ( + f"Model {model} contains date pattern (likely deprecated)" + ) + + # Should follow gemini-X.Y-model pattern + assert re.match(r"^gemini-\d+\.\d+-(flash|pro)(-lite)?", model), ( + f"Model {model} doesn't follow expected naming pattern" + ) + + def test_frontend_models_are_valid_gemini_models(self): + """Ensure all frontend model options are valid Gemini models""" + frontend_models = get_frontend_model_names() + + # All models should be valid Gemini models following the naming convention + for frontend_model in frontend_models: + assert frontend_model.startswith("gemini-"), ( + f"Model '{frontend_model}' is not a Gemini model" + ) + assert re.match(r"^gemini-\d+\.\d+-(flash|pro)(-lite)?$", frontend_model), ( + f"Model '{frontend_model}' doesn't follow expected naming pattern" + ) + + def test_no_deprecated_preview_models_in_frontend(self): + """Ensure frontend doesn't offer deprecated preview models""" + frontend_models = get_frontend_model_names() + + for model in frontend_models: + # Flag common deprecated patterns + assert "preview-04-17" not in model, ( + f"Deprecated model {model} found in frontend" + ) + assert "preview-05-06" not in model, ( + f"Deprecated model {model} found in frontend" + ) + + # Warn about any preview models (they're unstable) + if "preview" in model: + pytest.warn( + UserWarning( + f"Preview model {model} found - these are unstable and may break" + ) + ) + + def test_model_names_are_consistent(self): + """Test that frontend offers reasonable model options""" + frontend_models = set(get_frontend_model_names()) + backend_defaults = set(get_backend_default_models().values()) + + # Frontend should include the backend default models (at minimum) + missing_defaults = backend_defaults - frontend_models + assert not missing_defaults, ( + f"Frontend missing backend default models: {missing_defaults}" + ) + + # All frontend models should follow valid patterns + for model in frontend_models: + assert re.match(r"^gemini-\d+\.\d+-(flash|pro)(-lite)?$", model), ( + f"Frontend model '{model}' doesn't follow valid naming pattern" + ) + + def test_recommended_models_are_stable(self): + """Ensure recommended/default models are stable versions""" + config = Configuration() + + # The reflection model is marked as "recommended" in frontend + reflection_model = config.reflection_model + assert "preview" not in reflection_model, ( + "Recommended reflection model should not be a preview version" + ) + assert "flash" in reflection_model, ( + "Recommended model should be a fast flash variant" + ) + + +class TestModelAvailability: + """Test suite for model availability (integration tests)""" + + @pytest.mark.integration + def test_backend_can_initialize_default_models(self): + """Test that backend can initialize with default model configurations""" + import os + + from langchain_google_genai import ChatGoogleGenerativeAI + + if not os.getenv("GEMINI_API_KEY"): + pytest.skip("GEMINI_API_KEY not set - skipping model initialization test") + + config = Configuration() + models_to_test = [ + config.query_generator_model, + config.reflection_model, + config.answer_model, + ] + + for model_name in models_to_test: + try: + # Try to initialize the model (doesn't make API calls) + llm = ChatGoogleGenerativeAI( + model=model_name, + temperature=0, + api_key=os.getenv("GEMINI_API_KEY"), + ) + assert llm.model == model_name + + except Exception as e: + pytest.fail(f"Failed to initialize model {model_name}: {e}") + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 00000000..93fc13b1 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +build/ +.next/ +coverage/ +*.min.js +*.min.css \ No newline at end of file diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 00000000..81c9a74c --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "avoid" +} diff --git a/frontend/components.json b/frontend/components.json index 45874eee..549e3b57 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 092408a9..1fe5accd 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,28 +1,25 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; export default tseslint.config( - { ignores: ['dist'] }, + { ignores: ["dist"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], }, - }, -) + } +); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 48599091..15e4661a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,6 +28,8 @@ }, "devDependencies": { "@eslint/js": "^9.22.0", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", "@types/node": "^22.15.17", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.3", @@ -36,10 +38,71 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", + "jsdom": "^26.1.0", + "prettier": "^3.6.2", "tw-animate-css": "^1.2.9", "typescript": "~5.7.2", "typescript-eslint": "^8.26.1", - "vite": "^6.3.4" + "vite": "^6.3.4", + "vitest": "^3.2.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, "node_modules/@cfworker/json-schema": { @@ -48,6 +111,121 @@ "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", "license": "MIT" }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", @@ -703,6 +881,13 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "node_modules/@langchain/core": { "version": "0.3.55", "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.55.tgz", @@ -2199,6 +2384,100 @@ "vite": "^5.2.0 || ^6" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz", + "integrity": "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -2208,6 +2487,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -2525,6 +2811,121 @@ "vite": "^4 || ^5 || ^6" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -2548,6 +2949,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2565,6 +2976,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2599,6 +3021,26 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -2660,6 +3102,16 @@ "node": ">=8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2692,6 +3144,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2748,9 +3217,19 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", "license": "Apache-2.0", "dependencies": { @@ -2837,16 +3316,51 @@ "node": ">= 8" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2869,6 +3383,13 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decode-named-character-reference": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", @@ -2882,6 +3403,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2926,6 +3457,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -2939,6 +3478,26 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", @@ -3170,6 +3729,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3186,6 +3755,16 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3428,6 +4007,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -3438,6 +4030,47 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3475,6 +4108,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inline-style-parser": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", @@ -3570,6 +4213,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3595,6 +4245,14 @@ "base64-js": "^1.5.1" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3608,6 +4266,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3937,6 +4635,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/lucide-react": { "version": "0.508.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.508.0.tgz", @@ -3946,6 +4658,27 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.18", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz", + "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", @@ -4565,6 +5298,16 @@ "node": ">=8.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4618,6 +5361,13 @@ "dev": true, "license": "MIT" }, + "node_modules/nwsapi": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", + "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "dev": true, + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4756,6 +5506,19 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4776,6 +5539,23 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4833,6 +5613,52 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -4895,6 +5721,14 @@ "react": "^19.1.0" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/react-markdown": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", @@ -5030,6 +5864,20 @@ } } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -5132,6 +5980,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5156,6 +6011,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -5203,6 +6078,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-wcswidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz", @@ -5228,6 +6110,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -5242,6 +6138,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5255,6 +6164,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.16", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", @@ -5285,6 +6214,13 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", @@ -5310,10 +6246,24 @@ "node": ">=6" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "license": "MIT", "dependencies": { "fdir": "^6.4.4", @@ -5352,6 +6302,56 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5365,6 +6365,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -5732,6 +6758,29 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite/node_modules/fdir": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", @@ -5758,6 +6807,152 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5774,6 +6969,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -5784,6 +6996,45 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9dba4f46..0db53a67 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,12 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "format": "prettier --write .", + "format:check": "prettier --check .", + "preview": "vite preview", + "test": "vitest", + "test:ui": "vitest --ui", + "test:run": "vitest run" }, "dependencies": { "@langchain/core": "^0.3.55", @@ -30,6 +35,8 @@ }, "devDependencies": { "@eslint/js": "^9.22.0", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", "@types/node": "^22.15.17", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.3", @@ -38,9 +45,12 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", + "jsdom": "^26.1.0", + "prettier": "^3.6.2", "tw-animate-css": "^1.2.9", "typescript": "~5.7.2", "typescript-eslint": "^8.26.1", - "vite": "^6.3.4" + "vite": "^6.3.4", + "vitest": "^3.2.4" } } diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg index e7b8dfb1..ee9fadaf 100644 --- a/frontend/public/vite.svg +++ b/frontend/public/vite.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d06d4021..2c4de289 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,27 +4,41 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { ProcessedEvent } from "@/components/ActivityTimeline"; import { WelcomeScreen } from "@/components/WelcomeScreen"; import { ChatMessagesView } from "@/components/ChatMessagesView"; +import { ModelErrorDialog } from "@/components/ModelErrorDialog"; import { Button } from "@/components/ui/button"; export default function App() { - const [processedEventsTimeline, setProcessedEventsTimeline] = useState< - ProcessedEvent[] - >([]); + const [processedEventsTimeline, setProcessedEventsTimeline] = useState([]); const [historicalActivities, setHistoricalActivities] = useState< Record >({}); const scrollAreaRef = useRef(null); const hasFinalizeEventOccurredRef = useRef(false); const [error, setError] = useState(null); + const [modelError, setModelError] = useState<{ + failedModel: string; + errorMessage: string; + lastSubmission: { + inputValue: string; + effort: string; + queryModel: string; + reasoningModel: string; + }; + } | null>(null); + const [lastSubmission, setLastSubmission] = useState<{ + inputValue: string; + effort: string; + queryModel: string; + reasoningModel: string; + } | null>(null); const thread = useStream<{ messages: Message[]; initial_search_query_count: number; max_research_loops: number; reasoning_model: string; + query_model: string; }>({ - apiUrl: import.meta.env.DEV - ? "http://localhost:2024" - : "http://localhost:8123", + apiUrl: import.meta.env.DEV ? "http://localhost:2024" : "http://localhost:8123", assistantId: "agent", messagesKey: "messages", onUpdateEvent: (event: any) => { @@ -37,15 +51,11 @@ export default function App() { } else if (event.web_research) { const sources = event.web_research.sources_gathered || []; const numSources = sources.length; - const uniqueLabels = [ - ...new Set(sources.map((s: any) => s.label).filter(Boolean)), - ]; + const uniqueLabels = [...new Set(sources.map((s: any) => s.label).filter(Boolean))]; const exampleLabels = uniqueLabels.slice(0, 3).join(", "); processedEvent = { title: "Web Research", - data: `Gathered ${numSources} sources. Related to: ${ - exampleLabels || "N/A" - }.`, + data: `Gathered ${numSources} sources. Related to: ${exampleLabels || "N/A"}.`, }; } else if (event.reflection) { processedEvent = { @@ -60,13 +70,23 @@ export default function App() { hasFinalizeEventOccurredRef.current = true; } if (processedEvent) { - setProcessedEventsTimeline((prevEvents) => [ - ...prevEvents, - processedEvent!, - ]); + setProcessedEventsTimeline(prevEvents => [...prevEvents, processedEvent!]); } }, onError: (error: any) => { + // Check if this is a model 404 error + if (error.message && error.message.includes("404") && error.message.includes("models/")) { + const modelMatch = error.message.match(/models\/([\w-]+)/); + if (modelMatch && lastSubmission) { + const failedModel = modelMatch[1]; + setModelError({ + failedModel, + errorMessage: error.message, + lastSubmission: lastSubmission, + }); + return; + } + } setError(error.message); }, }); @@ -83,14 +103,10 @@ export default function App() { }, [thread.messages]); useEffect(() => { - if ( - hasFinalizeEventOccurredRef.current && - !thread.isLoading && - thread.messages.length > 0 - ) { + if (hasFinalizeEventOccurredRef.current && !thread.isLoading && thread.messages.length > 0) { const lastMessage = thread.messages[thread.messages.length - 1]; if (lastMessage && lastMessage.type === "ai" && lastMessage.id) { - setHistoricalActivities((prev) => ({ + setHistoricalActivities(prev => ({ ...prev, [lastMessage.id!]: [...processedEventsTimeline], })); @@ -100,11 +116,19 @@ export default function App() { }, [thread.messages, thread.isLoading, processedEventsTimeline]); const handleSubmit = useCallback( - (submittedInputValue: string, effort: string, model: string) => { + (submittedInputValue: string, effort: string, queryModel: string, reasoningModel: string) => { if (!submittedInputValue.trim()) return; setProcessedEventsTimeline([]); hasFinalizeEventOccurredRef.current = false; + // Track this submission for error recovery + setLastSubmission({ + inputValue: submittedInputValue, + effort, + queryModel, + reasoningModel, + }); + // convert effort to, initial_search_query_count and max_research_loops // low means max 1 loop and 1 query // medium means max 3 loops and 3 queries @@ -138,7 +162,8 @@ export default function App() { messages: newMessages, initial_search_query_count: initial_search_query_count, max_research_loops: max_research_loops, - reasoning_model: model, + reasoning_model: reasoningModel, + query_model: queryModel, }); }, [thread] @@ -149,41 +174,93 @@ export default function App() { window.location.reload(); }, [thread]); + const handleModelErrorContinue = useCallback( + (fallbackModel: string, rememberChoice: boolean) => { + if (modelError) { + // Save user preference if requested + if (rememberChoice) { + localStorage.setItem("preferredReasoningModel", fallbackModel); + } + + // Seamlessly retry with fallback model + handleSubmit( + modelError.lastSubmission.inputValue, + modelError.lastSubmission.effort, + modelError.lastSubmission.queryModel, + fallbackModel + ); + setModelError(null); + } + }, + [modelError, handleSubmit] + ); + + const handleModelErrorRetry = useCallback( + (newModel: string, rememberChoice: boolean) => { + if (modelError) { + // Save user preference if requested + if (rememberChoice) { + localStorage.setItem("preferredReasoningModel", newModel); + } + + // Retry with user-selected model + handleSubmit( + modelError.lastSubmission.inputValue, + modelError.lastSubmission.effort, + modelError.lastSubmission.queryModel, + newModel + ); + setModelError(null); + } + }, + [modelError, handleSubmit] + ); + + const handleModelErrorClose = useCallback(() => { + setModelError(null); + }, []); + return (
- {thread.messages.length === 0 ? ( - - ) : error ? ( -
-
-

Error

-

{JSON.stringify(error)}

- - -
+ {thread.messages.length === 0 ? ( + + ) : error ? ( +
+
+

Error

+

{JSON.stringify(error)}

+ +
- ) : ( - - )} +
+ ) : ( + + )}
+ +
); } diff --git a/frontend/src/components/ActivityTimeline.tsx b/frontend/src/components/ActivityTimeline.tsx index b3669299..0375c54d 100644 --- a/frontend/src/components/ActivityTimeline.tsx +++ b/frontend/src/components/ActivityTimeline.tsx @@ -1,9 +1,4 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, -} from "@/components/ui/card"; +import { Card, CardContent, CardDescription, CardHeader } from "@/components/ui/card"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Loader2, @@ -28,12 +23,8 @@ interface ActivityTimelineProps { isLoading: boolean; } -export function ActivityTimeline({ - processedEvents, - isLoading, -}: ActivityTimelineProps) { - const [isTimelineCollapsed, setIsTimelineCollapsed] = - useState(false); +export function ActivityTimeline({ processedEvents, isLoading }: ActivityTimelineProps) { + const [isTimelineCollapsed, setIsTimelineCollapsed] = useState(false); const getEventIcon = (title: string, index: number) => { if (index === 0 && isLoading && processedEvents.length === 0) { return ; @@ -85,9 +76,7 @@ export function ActivityTimeline({
-

- Searching... -

+

Searching...

)} @@ -110,8 +99,8 @@ export function ActivityTimeline({ {typeof eventItem.data === "string" ? eventItem.data : Array.isArray(eventItem.data) - ? (eventItem.data as string[]).join(", ") - : JSON.stringify(eventItem.data)} + ? (eventItem.data as string[]).join(", ") + : JSON.stringify(eventItem.data)}

@@ -122,9 +111,7 @@ export function ActivityTimeline({
-

- Searching... -

+

Searching...

)} diff --git a/frontend/src/components/ChatMessagesView.tsx b/frontend/src/components/ChatMessagesView.tsx index 1a245d88..d11aa759 100644 --- a/frontend/src/components/ChatMessagesView.tsx +++ b/frontend/src/components/ChatMessagesView.tsx @@ -8,10 +8,7 @@ import { useState, ReactNode } from "react"; import ReactMarkdown from "react-markdown"; import { cn } from "@/lib/utils"; import { Badge } from "@/components/ui/badge"; -import { - ActivityTimeline, - ProcessedEvent, -} from "@/components/ActivityTimeline"; // Assuming ActivityTimeline is in the same dir or adjust path +import { ActivityTimeline, ProcessedEvent } from "@/components/ActivityTimeline"; // Assuming ActivityTimeline is in the same dir or adjust path // Markdown component props type from former ReportView type MdComponentProps = { @@ -72,10 +69,7 @@ const mdComponents = { ), blockquote: ({ className, children, ...props }: MdComponentProps) => (
{children} @@ -83,10 +77,7 @@ const mdComponents = { ), code: ({ className, children, ...props }: MdComponentProps) => ( {children} @@ -115,20 +106,14 @@ const mdComponents = { ), th: ({ className, children, ...props }: MdComponentProps) => ( {children} ), td: ({ className, children, ...props }: MdComponentProps) => ( - + {children} ), @@ -141,18 +126,13 @@ interface HumanMessageBubbleProps { } // HumanMessageBubble Component -const HumanMessageBubble: React.FC = ({ - message, - mdComponents, -}) => { +const HumanMessageBubble: React.FC = ({ message, mdComponents }) => { return (
- {typeof message.content === "string" - ? message.content - : JSON.stringify(message.content)} + {typeof message.content === "string" ? message.content : JSON.stringify(message.content)}
); @@ -197,9 +177,7 @@ const AiMessageBubble: React.FC = ({ )} - {typeof message.content === "string" - ? message.content - : JSON.stringify(message.content)} + {typeof message.content === "string" ? message.content : JSON.stringify(message.content)}