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)}
-
-
window.location.reload()}
- >
- Retry
-
-
+ {thread.messages.length === 0 ? (
+
+ ) : error ? (
+
+
+
Error
+
{JSON.stringify(error)}
+
+
window.location.reload()}>
+ Retry
+
- ) : (
-
- )}
+
+ ) : (
+
+ )}
+
+
);
}
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)}
= ({
}`}
onClick={() =>
handleCopy(
- typeof message.content === "string"
- ? message.content
- : JSON.stringify(message.content),
+ typeof message.content === "string" ? message.content : JSON.stringify(message.content),
message.id!
)
}
@@ -226,7 +202,12 @@ interface ChatMessagesViewProps {
messages: Message[];
isLoading: boolean;
scrollAreaRef: React.RefObject;
- onSubmit: (inputValue: string, effort: string, model: string) => void;
+ onSubmit: (
+ inputValue: string,
+ effort: string,
+ queryModel: string,
+ reasoningModel: string
+ ) => void;
onCancel: () => void;
liveActivityEvents: ProcessedEvent[];
historicalActivities: Record;
@@ -266,10 +247,7 @@ export function ChatMessagesView({
}`}
>
{message.type === "human" ? (
-
+
) : (
{" "}
{/* AI message row structure */}
{liveActivityEvents.length > 0 ? (
) : (
diff --git a/frontend/src/components/InputForm.tsx b/frontend/src/components/InputForm.tsx
index 97aa5c67..5cb25efd 100644
--- a/frontend/src/components/InputForm.tsx
+++ b/frontend/src/components/InputForm.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { SquarePen, Brain, Send, StopCircle, Zap, Cpu } from "lucide-react";
import { Textarea } from "@/components/ui/textarea";
@@ -12,7 +12,12 @@ import {
// Updated InputFormProps
interface InputFormProps {
- onSubmit: (inputValue: string, effort: string, model: string) => void;
+ onSubmit: (
+ inputValue: string,
+ effort: string,
+ queryModel: string,
+ reasoningModel: string
+ ) => void;
onCancel: () => void;
isLoading: boolean;
hasHistory: boolean;
@@ -26,12 +31,42 @@ export const InputForm: React.FC
= ({
}) => {
const [internalInputValue, setInternalInputValue] = useState("");
const [effort, setEffort] = useState("medium");
- const [model, setModel] = useState("gemini-2.5-flash-preview-04-17");
+ const [queryModel, setQueryModel] = useState("gemini-2.0-flash"); // Recommended for queries
+ const [reasoningModel, setReasoningModel] = useState("gemini-2.5-flash");
+
+ // Load saved user preferences on component mount
+ useEffect(() => {
+ const savedReasoningModel = localStorage.getItem("preferredReasoningModel");
+ if (savedReasoningModel) {
+ setReasoningModel(savedReasoningModel);
+ }
+ }, []);
+
+ // Helper to check if model is user's saved preference
+ const isPreferredModel = (modelValue: string) => {
+ return localStorage.getItem("preferredReasoningModel") === modelValue;
+ };
+
+ // Helper to get clean display name for model
+ const getModelDisplayName = (model: string) => {
+ switch (model) {
+ case "gemini-2.0-flash":
+ return "2.0 Flash";
+ case "gemini-2.5-flash":
+ return "2.5 Flash";
+ case "gemini-2.5-pro":
+ return "2.5 Pro";
+ case "gemini-2.5-flash-lite":
+ return "2.5 Flash Lite";
+ default:
+ return model;
+ }
+ };
const handleInternalSubmit = (e?: React.FormEvent) => {
if (e) e.preventDefault();
if (!internalInputValue.trim()) return;
- onSubmit(internalInputValue, effort, model);
+ onSubmit(internalInputValue, effort, queryModel, reasoningModel);
setInternalInputValue("");
};
@@ -46,10 +81,7 @@ export const InputForm: React.FC = ({
const isSubmitDisabled = !internalInputValue.trim() || isLoading;
return (
-