diff --git a/.env-template b/.env-template index 13575fc3..4e00b75b 100644 --- a/.env-template +++ b/.env-template @@ -3,6 +3,14 @@ LLM_NAME=docsgpt VITE_API_STREAMING=true INTERNAL_KEY= +# Provider-specific API keys (optional - use these to enable multiple providers) +# OPENAI_API_KEY= +# ANTHROPIC_API_KEY= +# GOOGLE_API_KEY= +# GROQ_API_KEY= +# NOVITA_API_KEY= +# OPEN_ROUTER_API_KEY= + # Remote Embeddings (Optional - for using a remote embeddings API instead of local SentenceTransformer) # When set, the app will use the remote API and won't load SentenceTransformer (saves RAM) EMBEDDINGS_BASE_URL= diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 00000000..5d2bb09b --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,114 @@ +name: Publish npm libraries + +on: + workflow_dispatch: + inputs: + version: + description: > + Version bump type (patch | minor | major) or explicit semver (e.g. 1.2.3). + Applies to both docsgpt and docsgpt-react. + required: true + default: patch + +permissions: + contents: write + pull-requests: write + +jobs: + publish: + runs-on: ubuntu-latest + environment: npm-release + defaults: + run: + working-directory: extensions/react-widget + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org + + - name: Install dependencies + run: npm ci + + # ── docsgpt (HTML embedding bundle) ────────────────────────────────── + # Uses the `build` script (parcel build src/browser.tsx) and keeps + # the `targets` field so Parcel produces browser-optimised bundles. + + - name: Set package name → docsgpt + run: jq --arg n "docsgpt" '.name=$n' package.json > _tmp.json && mv _tmp.json package.json + + - name: Bump version (docsgpt) + id: version_docsgpt + run: | + VERSION="${{ github.event.inputs.version }}" + NEW_VER=$(npm version "${VERSION:-patch}" --no-git-tag-version) + echo "version=${NEW_VER#v}" >> "$GITHUB_OUTPUT" + + - name: Build docsgpt + run: npm run build + + - name: Publish docsgpt + run: npm publish --verbose + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + # ── docsgpt-react (React library bundle) ───────────────────────────── + # Uses `build:react` script (parcel build src/index.ts) and strips + # the `targets` field so Parcel treats the output as a plain library + # without browser-specific target resolution, producing a smaller bundle. + + - name: Reset package.json from source control + run: git checkout -- package.json + + - name: Set package name → docsgpt-react + run: jq --arg n "docsgpt-react" '.name=$n' package.json > _tmp.json && mv _tmp.json package.json + + - name: Remove targets field (react library build) + run: jq 'del(.targets)' package.json > _tmp.json && mv _tmp.json package.json + + - name: Bump version (docsgpt-react) to match docsgpt + run: npm version "${{ steps.version_docsgpt.outputs.version }}" --no-git-tag-version + + - name: Clean dist before react build + run: rm -rf dist + + - name: Build docsgpt-react + run: npm run build:react + + - name: Publish docsgpt-react + run: npm publish --verbose + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + # ── Commit the bumped version back to the repository ───────────────── + + - name: Reset package.json and write final version + run: | + git checkout -- package.json + jq --arg v "${{ steps.version_docsgpt.outputs.version }}" '.version=$v' \ + package.json > _tmp.json && mv _tmp.json package.json + npm install --package-lock-only + + - name: Commit version bump and create PR + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + BRANCH="chore/bump-npm-v${{ steps.version_docsgpt.outputs.version }}" + git checkout -b "$BRANCH" + git add package.json package-lock.json + git commit -m "chore: bump npm libraries to v${{ steps.version_docsgpt.outputs.version }}" + git push origin "$BRANCH" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create PR + run: | + gh pr create \ + --title "chore: bump npm libraries to v${{ steps.version_docsgpt.outputs.version }}" \ + --body "Automated version bump after npm publish." \ + --base main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/react-widget-build.yml b/.github/workflows/react-widget-build.yml new file mode 100644 index 00000000..95389d6b --- /dev/null +++ b/.github/workflows/react-widget-build.yml @@ -0,0 +1,34 @@ +name: React Widget Build + +on: + push: + paths: + - 'extensions/react-widget/**' + pull_request: + paths: + - 'extensions/react-widget/**' + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: extensions/react-widget + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: extensions/react-widget/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build diff --git a/.gitignore b/.gitignore index 9b09303d..4d8f8c84 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__/ *.py[cod] *$py.class +results.txt experiments/ experiments diff --git a/README.md b/README.md index 6e02cc9a..b83f514d 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@

-video-example-of-docs-gpt +video-example-of-docs-gpt

Key Features: diff --git a/application/agents/agent_creator.py b/application/agents/agent_creator.py index e15165bf..d623269f 100644 --- a/application/agents/agent_creator.py +++ b/application/agents/agent_creator.py @@ -1,7 +1,8 @@ import logging +from application.agents.agentic_agent import AgenticAgent from application.agents.classic_agent import ClassicAgent -from application.agents.react_agent import ReActAgent +from application.agents.research_agent import ResearchAgent from application.agents.workflow_agent import WorkflowAgent logger = logging.getLogger(__name__) @@ -10,7 +11,9 @@ logger = logging.getLogger(__name__) class AgentCreator: agents = { "classic": ClassicAgent, - "react": ReActAgent, + "react": ClassicAgent, # backwards compat: react falls back to classic + "agentic": AgenticAgent, + "research": ResearchAgent, "workflow": WorkflowAgent, } diff --git a/application/agents/agentic_agent.py b/application/agents/agentic_agent.py new file mode 100644 index 00000000..fe08277a --- /dev/null +++ b/application/agents/agentic_agent.py @@ -0,0 +1,63 @@ +import logging +from typing import Dict, Generator, Optional + +from application.agents.base import BaseAgent +from application.agents.tools.internal_search import ( + INTERNAL_TOOL_ID, + add_internal_search_tool, +) +from application.logging import LogContext + +logger = logging.getLogger(__name__) + + +class AgenticAgent(BaseAgent): + """Agent where the LLM controls retrieval via tools. + + Unlike ClassicAgent which pre-fetches docs into the prompt, + AgenticAgent gives the LLM an internal_search tool so it can + decide when, what, and whether to search. + """ + + def __init__( + self, + retriever_config: Optional[Dict] = None, + *args, + **kwargs, + ): + super().__init__(*args, **kwargs) + self.retriever_config = retriever_config or {} + + def _gen_inner( + self, query: str, log_context: LogContext + ) -> Generator[Dict, None, None]: + tools_dict = self.tool_executor.get_tools() + add_internal_search_tool(tools_dict, self.retriever_config) + self._prepare_tools(tools_dict) + + # 4. Build messages (prompt has NO pre-fetched docs) + messages = self._build_messages(self.prompt, query) + + # 5. Call LLM — the handler manages the tool loop + llm_response = self._llm_gen(messages, log_context) + + yield from self._handle_response( + llm_response, tools_dict, messages, log_context + ) + + # 6. Collect sources from internal search tool results + self._collect_internal_sources() + + yield {"sources": self.retrieved_docs} + yield {"tool_calls": self._get_truncated_tool_calls()} + + log_context.stacks.append( + {"component": "agent", "data": {"tool_calls": self.tool_calls.copy()}} + ) + + def _collect_internal_sources(self): + """Collect retrieved docs from the cached InternalSearchTool instance.""" + cache_key = f"internal_search:{INTERNAL_TOOL_ID}:{self.user or ''}" + tool = self.tool_executor._loaded_tools.get(cache_key) + if tool and hasattr(tool, "retrieved_docs") and tool.retrieved_docs: + self.retrieved_docs = tool.retrieved_docs diff --git a/application/agents/base.py b/application/agents/base.py index ee55a449..15735c8c 100644 --- a/application/agents/base.py +++ b/application/agents/base.py @@ -3,20 +3,15 @@ import uuid from abc import ABC, abstractmethod from typing import Dict, Generator, List, Optional -from bson.objectid import ObjectId - -from application.agents.tools.tool_action_parser import ToolActionParser -from application.agents.tools.tool_manager import ToolManager +from application.agents.tool_executor import ToolExecutor from application.core.json_schema_utils import ( JsonSchemaValidationError, normalize_json_schema_payload, ) -from application.core.mongo_db import MongoDB from application.core.settings import settings from application.llm.handlers.handler_creator import LLMHandlerCreator from application.llm.llm_creator import LLMCreator from application.logging import build_stack_data, log_activity, LogContext -from application.security.encryption import decrypt_credentials logger = logging.getLogger(__name__) @@ -41,6 +36,10 @@ class BaseAgent(ABC): limited_request_mode: Optional[bool] = False, request_limit: Optional[int] = settings.DEFAULT_AGENT_LIMITS["request_limit"], compressed_summary: Optional[str] = None, + llm=None, + llm_handler=None, + tool_executor: Optional[ToolExecutor] = None, + backup_models: Optional[List[str]] = None, ): self.endpoint = endpoint self.llm_name = llm_name @@ -51,22 +50,42 @@ class BaseAgent(ABC): self.prompt = prompt self.decoded_token = decoded_token or {} self.user: str = self.decoded_token.get("sub") - self.tool_config: Dict = {} self.tools: List[Dict] = [] - self.tool_calls: List[Dict] = [] self.chat_history: List[Dict] = chat_history if chat_history is not None else [] - self.llm = LLMCreator.create_llm( - llm_name, - api_key=api_key, - user_api_key=user_api_key, - decoded_token=decoded_token, - model_id=model_id, - agent_id=agent_id, - ) + + # Dependency injection for LLM — fall back to creating if not provided + if llm is not None: + self.llm = llm + else: + self.llm = LLMCreator.create_llm( + llm_name, + api_key=api_key, + user_api_key=user_api_key, + decoded_token=decoded_token, + model_id=model_id, + agent_id=agent_id, + backup_models=backup_models, + ) + self.retrieved_docs = retrieved_docs or [] - self.llm_handler = LLMHandlerCreator.create_handler( - llm_name if llm_name else "default" - ) + + if llm_handler is not None: + self.llm_handler = llm_handler + else: + self.llm_handler = LLMHandlerCreator.create_handler( + llm_name if llm_name else "default" + ) + + # Tool executor — injected or created + if tool_executor is not None: + self.tool_executor = tool_executor + else: + self.tool_executor = ToolExecutor( + user_api_key=user_api_key, + user=self.user, + decoded_token=decoded_token, + ) + self.attachments = attachments or [] self.json_schema = None if json_schema is not None: @@ -94,333 +113,72 @@ class BaseAgent(ABC): ) -> Generator[Dict, None, None]: pass + # ---- Tool delegation (thin wrappers around ToolExecutor) ---- + + @property + def tool_calls(self) -> List[Dict]: + return self.tool_executor.tool_calls + + @tool_calls.setter + def tool_calls(self, value: List[Dict]): + self.tool_executor.tool_calls = value + def _get_tools(self, api_key: str = None) -> Dict[str, Dict]: - mongo = MongoDB.get_client() - db = mongo[settings.MONGO_DB_NAME] - agents_collection = db["agents"] - tools_collection = db["user_tools"] - - agent_data = agents_collection.find_one({"key": api_key or self.user_api_key}) - tool_ids = agent_data.get("tools", []) if agent_data else [] - - tools = ( - tools_collection.find( - {"_id": {"$in": [ObjectId(tool_id) for tool_id in tool_ids]}} - ) - if tool_ids - else [] - ) - tools = list(tools) - tools_by_id = {str(tool["_id"]): tool for tool in tools} if tools else {} - - return tools_by_id + return self.tool_executor._get_tools_by_api_key(api_key or self.user_api_key) def _get_user_tools(self, user="local"): - mongo = MongoDB.get_client() - db = mongo[settings.MONGO_DB_NAME] - user_tools_collection = db["user_tools"] - user_tools = user_tools_collection.find({"user": user, "status": True}) - user_tools = list(user_tools) - - return {str(i): tool for i, tool in enumerate(user_tools)} + return self.tool_executor._get_user_tools(user) def _build_tool_parameters(self, action): - params = {"type": "object", "properties": {}, "required": []} - for param_type in ["query_params", "headers", "body", "parameters"]: - if param_type in action and action[param_type].get("properties"): - for k, v in action[param_type]["properties"].items(): - if v.get("filled_by_llm", True): - params["properties"][k] = { - key: value - for key, value in v.items() - if key not in ("filled_by_llm", "value", "required") - } - if v.get("required", False): - params["required"].append(k) - return params + return self.tool_executor._build_tool_parameters(action) def _prepare_tools(self, tools_dict): - self.tools = [ - { - "type": "function", - "function": { - "name": f"{action['name']}_{tool_id}", - "description": action["description"], - "parameters": self._build_tool_parameters(action), - }, - } - for tool_id, tool in tools_dict.items() - if ( - (tool["name"] == "api_tool" and "actions" in tool.get("config", {})) - or (tool["name"] != "api_tool" and "actions" in tool) - ) - for action in ( - tool["config"]["actions"].values() - if tool["name"] == "api_tool" - else tool["actions"] - ) - if action.get("active", True) - ] + self.tools = self.tool_executor.prepare_tools_for_llm(tools_dict) def _execute_tool_action(self, tools_dict, call): - parser = ToolActionParser(self.llm.__class__.__name__) - tool_id, action_name, call_args = parser.parse_args(call) - - call_id = getattr(call, "id", None) or str(uuid.uuid4()) - - # Check if parsing failed - - if tool_id is None or action_name is None: - error_message = f"Error: Failed to parse LLM tool call. Tool name: {getattr(call, 'name', 'unknown')}" - logger.error(error_message) - - tool_call_data = { - "tool_name": "unknown", - "call_id": call_id, - "action_name": getattr(call, "name", "unknown"), - "arguments": call_args or {}, - "result": f"Failed to parse tool call. Invalid tool name format: {getattr(call, 'name', 'unknown')}", - } - yield {"type": "tool_call", "data": {**tool_call_data, "status": "error"}} - self.tool_calls.append(tool_call_data) - return "Failed to parse tool call.", call_id - # Check if tool_id exists in available tools - - if tool_id not in tools_dict: - error_message = f"Error: Tool ID '{tool_id}' extracted from LLM call not found in available tools_dict. Available IDs: {list(tools_dict.keys())}" - logger.error(error_message) - - # Return error result - - tool_call_data = { - "tool_name": "unknown", - "call_id": call_id, - "action_name": f"{action_name}_{tool_id}", - "arguments": call_args, - "result": f"Tool with ID {tool_id} not found. Available tools: {list(tools_dict.keys())}", - } - yield {"type": "tool_call", "data": {**tool_call_data, "status": "error"}} - self.tool_calls.append(tool_call_data) - return f"Tool with ID {tool_id} not found.", call_id - tool_call_data = { - "tool_name": tools_dict[tool_id]["name"], - "call_id": call_id, - "action_name": f"{action_name}_{tool_id}", - "arguments": call_args, - } - yield {"type": "tool_call", "data": {**tool_call_data, "status": "pending"}} - - tool_data = tools_dict[tool_id] - action_data = ( - tool_data["config"]["actions"][action_name] - if tool_data["name"] == "api_tool" - else next( - action - for action in tool_data["actions"] - if action["name"] == action_name - ) + return self.tool_executor.execute( + tools_dict, call, self.llm.__class__.__name__ ) - query_params, headers, body, parameters = {}, {}, {}, {} - param_types = { - "query_params": query_params, - "headers": headers, - "body": body, - "parameters": parameters, - } - - for param_type, target_dict in param_types.items(): - if param_type in action_data and action_data[param_type].get("properties"): - for param, details in action_data[param_type]["properties"].items(): - if ( - param not in call_args - and "value" in details - and details["value"] - ): - target_dict[param] = details["value"] - for param, value in call_args.items(): - for param_type, target_dict in param_types.items(): - if param_type in action_data and param in action_data[param_type].get( - "properties", {} - ): - target_dict[param] = value - tm = ToolManager(config={}) - - # Prepare tool_config and add tool_id for memory tools - - if tool_data["name"] == "api_tool": - action_config = tool_data["config"]["actions"][action_name] - tool_config = { - "url": action_config["url"], - "method": action_config["method"], - "headers": headers, - "query_params": query_params, - } - if "body_content_type" in action_config: - tool_config["body_content_type"] = action_config.get( - "body_content_type", "application/json" - ) - tool_config["body_encoding_rules"] = action_config.get( - "body_encoding_rules", {} - ) - else: - tool_config = tool_data["config"].copy() if tool_data["config"] else {} - if tool_config.get("encrypted_credentials") and self.user: - decrypted = decrypt_credentials( - tool_config["encrypted_credentials"], self.user - ) - tool_config.update(decrypted) - tool_config["auth_credentials"] = decrypted - tool_config.pop("encrypted_credentials", None) - tool_config["tool_id"] = str(tool_data.get("_id", tool_id)) - if hasattr(self, "conversation_id") and self.conversation_id: - tool_config["conversation_id"] = self.conversation_id - if tool_data["name"] == "mcp_tool": - tool_config["query_mode"] = True - tool = tm.load_tool( - tool_data["name"], - tool_config=tool_config, - user_id=self.user, - ) - resolved_arguments = ( - {"query_params": query_params, "headers": headers, "body": body} - if tool_data["name"] == "api_tool" - else parameters - ) - if tool_data["name"] == "api_tool": - logger.debug( - f"Executing api: {action_name} with query_params: {query_params}, headers: {headers}, body: {body}" - ) - result = tool.execute_action(action_name, **body) - else: - logger.debug(f"Executing tool: {action_name} with args: {call_args}") - result = tool.execute_action(action_name, **parameters) - - get_artifact_id = ( - getattr(tool, "get_artifact_id", None) - if tool_data["name"] != "api_tool" - else None - ) - - artifact_id = None - if callable(get_artifact_id): - try: - artifact_id = get_artifact_id(action_name, **parameters) - except Exception: - logger.exception( - "Failed to extract artifact_id from tool %s for action %s", - tool_data["name"], - action_name, - ) - - artifact_id = str(artifact_id).strip() if artifact_id is not None else "" - if artifact_id: - tool_call_data["artifact_id"] = artifact_id - result_full = str(result) - tool_call_data["resolved_arguments"] = resolved_arguments - tool_call_data["result_full"] = result_full - tool_call_data["result"] = ( - f"{result_full[:50]}..." if len(result_full) > 50 else result_full - ) - - stream_tool_call_data = { - key: value - for key, value in tool_call_data.items() - if key not in {"result_full", "resolved_arguments"} - } - yield {"type": "tool_call", "data": {**stream_tool_call_data, "status": "completed"}} - self.tool_calls.append(tool_call_data) - - return result, call_id - def _get_truncated_tool_calls(self): - return [ - { - "tool_name": tool_call.get("tool_name"), - "call_id": tool_call.get("call_id"), - "action_name": tool_call.get("action_name"), - "arguments": tool_call.get("arguments"), - "artifact_id": tool_call.get("artifact_id"), - "result": ( - f"{str(tool_call['result'])[:50]}..." - if len(str(tool_call["result"])) > 50 - else tool_call["result"] - ), - "status": "completed", - } - for tool_call in self.tool_calls - ] + return self.tool_executor.get_truncated_tool_calls() + + # ---- Context / token management ---- def _calculate_current_context_tokens(self, messages: List[Dict]) -> int: - """ - Calculate total tokens in current context (messages). - - Args: - messages: List of message dicts - - Returns: - Total token count - """ from application.api.answer.services.compression.token_counter import ( TokenCounter, ) - return TokenCounter.count_message_tokens(messages) def _check_context_limit(self, messages: List[Dict]) -> bool: - """ - Check if we're approaching context limit (80%). - - Args: - messages: Current message list - - Returns: - True if at or above 80% of context limit - """ from application.core.model_utils import get_token_limit - from application.core.settings import settings try: - # Calculate current tokens current_tokens = self._calculate_current_context_tokens(messages) self.current_token_count = current_tokens - - # Get context limit for model context_limit = get_token_limit(self.model_id) - - # Calculate threshold (80%) threshold = int(context_limit * settings.COMPRESSION_THRESHOLD_PERCENTAGE) - # Check if we've reached the limit if current_tokens >= threshold: logger.warning( f"Context limit approaching: {current_tokens}/{context_limit} tokens " f"({(current_tokens/context_limit)*100:.1f}%)" ) return True - return False - except Exception as e: logger.error(f"Error checking context limit: {str(e)}", exc_info=True) return False def _validate_context_size(self, messages: List[Dict]) -> None: - """ - Pre-flight validation before calling LLM. Logs warnings but never raises errors. - - Args: - messages: Messages to be sent to LLM - """ from application.core.model_utils import get_token_limit current_tokens = self._calculate_current_context_tokens(messages) self.current_token_count = current_tokens context_limit = get_token_limit(self.model_id) - percentage = (current_tokens / context_limit) * 100 - # Log based on usage level if current_tokens >= context_limit: logger.warning( f"Context at limit: {current_tokens:,}/{context_limit:,} tokens " @@ -435,44 +193,32 @@ class BaseAgent(ABC): ) def _truncate_text_middle(self, text: str, max_tokens: int) -> str: - """ - Truncate text by removing content from the middle, preserving start and end. - - Args: - text: Text to truncate - max_tokens: Maximum tokens allowed - - Returns: - Truncated text with middle removed if needed - """ from application.utils import num_tokens_from_string current_tokens = num_tokens_from_string(text) if current_tokens <= max_tokens: return text - # Estimate chars per token (roughly 4 chars per token for English) chars_per_token = len(text) / current_tokens if current_tokens > 0 else 4 - target_chars = int(max_tokens * chars_per_token * 0.95) # 5% safety margin + target_chars = int(max_tokens * chars_per_token * 0.95) if target_chars <= 0: return "" - # Split: keep 40% from start, 40% from end, remove middle start_chars = int(target_chars * 0.4) end_chars = int(target_chars * 0.4) truncation_marker = "\n\n[... content truncated to fit context limit ...]\n\n" - truncated = text[:start_chars] + truncation_marker + text[-end_chars:] logger.info( f"Truncated text from {current_tokens:,} to ~{max_tokens:,} tokens " f"(removed middle section)" ) - return truncated + # ---- Message building ---- + def _build_messages( self, system_prompt: str, @@ -482,7 +228,6 @@ class BaseAgent(ABC): from application.core.model_utils import get_token_limit from application.utils import num_tokens_from_string - # Append compression summary to system prompt if present if self.compressed_summary: compression_context = ( "\n\n---\n\n" @@ -496,23 +241,18 @@ class BaseAgent(ABC): context_limit = get_token_limit(self.model_id) system_tokens = num_tokens_from_string(system_prompt) - # Reserve 10% for response/tools safety_buffer = int(context_limit * 0.1) available_after_system = context_limit - system_tokens - safety_buffer - # Max tokens for query: 80% of available space (leave room for history) max_query_tokens = int(available_after_system * 0.8) query_tokens = num_tokens_from_string(query) - # Truncate query from middle if it exceeds 80% of available context if query_tokens > max_query_tokens: query = self._truncate_text_middle(query, max_query_tokens) query_tokens = num_tokens_from_string(query) - # Calculate remaining budget for chat history available_for_history = max(available_after_system - query_tokens, 0) - # Truncate chat history to fit within available budget working_history = self._truncate_history_to_fit( self.chat_history, available_for_history, @@ -557,16 +297,6 @@ class BaseAgent(ABC): history: List[Dict], max_tokens: int, ) -> List[Dict]: - """ - Truncate chat history to fit within token budget, keeping most recent messages. - - Args: - history: Full chat history - max_tokens: Maximum tokens allowed for history - - Returns: - Truncated history (most recent messages that fit) - """ from application.utils import num_tokens_from_string if not history or max_tokens <= 0: @@ -575,7 +305,6 @@ class BaseAgent(ABC): truncated = [] current_tokens = 0 - # Iterate from newest to oldest for message in reversed(history): message_tokens = 0 @@ -595,7 +324,7 @@ class BaseAgent(ABC): if current_tokens + message_tokens <= max_tokens: current_tokens += message_tokens - truncated.insert(0, message) # Maintain chronological order + truncated.insert(0, message) else: break @@ -607,13 +336,13 @@ class BaseAgent(ABC): return truncated + # ---- LLM generation ---- + def _llm_gen(self, messages: List[Dict], log_context: Optional[LogContext] = None): - # Pre-flight context validation - fail fast if over limit self._validate_context_size(messages) gen_kwargs = {"model": self.model_id, "messages": messages} if self.attachments: - # Usage accounting only; stripped before provider invocation. gen_kwargs["_usage_attachments"] = self.attachments if ( diff --git a/application/agents/classic_agent.py b/application/agents/classic_agent.py index af8e585f..d6dced9e 100644 --- a/application/agents/classic_agent.py +++ b/application/agents/classic_agent.py @@ -15,11 +15,7 @@ class ClassicAgent(BaseAgent): ) -> Generator[Dict, None, None]: """Core generator function for ClassicAgent execution flow""" - tools_dict = ( - self._get_user_tools(self.user) - if not self.user_api_key - else self._get_tools(self.user_api_key) - ) + tools_dict = self.tool_executor.get_tools() self._prepare_tools(tools_dict) messages = self._build_messages(self.prompt, query) diff --git a/application/agents/react_agent.py b/application/agents/react_agent.py deleted file mode 100644 index 92be75f6..00000000 --- a/application/agents/react_agent.py +++ /dev/null @@ -1,238 +0,0 @@ -import logging -import os -from typing import Any, Dict, Generator, List - -from application.agents.base import BaseAgent -from application.logging import build_stack_data, LogContext - -logger = logging.getLogger(__name__) - -MAX_ITERATIONS_REASONING = 10 - -current_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -) -with open( - os.path.join(current_dir, "application/prompts", "react_planning_prompt.txt"), "r" -) as f: - PLANNING_PROMPT_TEMPLATE = f.read() -with open( - os.path.join(current_dir, "application/prompts", "react_final_prompt.txt"), "r" -) as f: - FINAL_PROMPT_TEMPLATE = f.read() - - -class ReActAgent(BaseAgent): - """ - Research and Action (ReAct) Agent - Advanced reasoning agent with iterative planning. - - Implements a think-act-observe loop for complex problem-solving: - 1. Creates a strategic plan based on the query - 2. Executes tools and gathers observations - 3. Iteratively refines approach until satisfied - 4. Synthesizes final answer from all observations - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.plan: str = "" - self.observations: List[str] = [] - - def _gen_inner( - self, query: str, log_context: LogContext - ) -> Generator[Dict, None, None]: - """Execute ReAct reasoning loop with planning, action, and observation cycles""" - - self._reset_state() - - tools_dict = ( - self._get_tools(self.user_api_key) - if self.user_api_key - else self._get_user_tools(self.user) - ) - self._prepare_tools(tools_dict) - - for iteration in range(1, MAX_ITERATIONS_REASONING + 1): - yield {"thought": f"Reasoning... (iteration {iteration})\n\n"} - - yield from self._planning_phase(query, log_context) - - if not self.plan: - logger.warning( - f"ReActAgent: No plan generated in iteration {iteration}" - ) - break - self.observations.append(f"Plan (iteration {iteration}): {self.plan}") - - satisfied = yield from self._execution_phase(query, tools_dict, log_context) - - if satisfied: - logger.info("ReActAgent: Goal satisfied, stopping reasoning loop") - break - yield from self._synthesis_phase(query, log_context) - - def _reset_state(self): - """Reset agent state for new query""" - self.plan = "" - self.observations = [] - - def _planning_phase( - self, query: str, log_context: LogContext - ) -> Generator[Dict, None, None]: - """Generate strategic plan for query""" - logger.info("ReActAgent: Creating plan...") - - plan_prompt = self._build_planning_prompt(query) - messages = [{"role": "user", "content": plan_prompt}] - - plan_stream = self.llm.gen_stream( - model=self.model_id, - messages=messages, - tools=self.tools if self.tools else None, - ) - - if log_context: - log_context.stacks.append( - {"component": "planning_llm", "data": build_stack_data(self.llm)} - ) - plan_parts = [] - for chunk in plan_stream: - content = self._extract_content(chunk) - if content: - plan_parts.append(content) - yield {"thought": content} - self.plan = "".join(plan_parts) - - def _execution_phase( - self, query: str, tools_dict: Dict, log_context: LogContext - ) -> Generator[bool, None, None]: - """Execute plan with tool calls and observations""" - execution_prompt = self._build_execution_prompt(query) - messages = self._build_messages(execution_prompt, query) - - llm_response = self._llm_gen(messages, log_context) - initial_content = self._extract_content(llm_response) - - if initial_content: - self.observations.append(f"Initial response: {initial_content}") - processed_response = self._llm_handler( - llm_response, tools_dict, messages, log_context - ) - - for tool_call in self.tool_calls: - observation = ( - f"Executed: {tool_call.get('tool_name', 'Unknown')} " - f"with args {tool_call.get('arguments', {})}. " - f"Result: {str(tool_call.get('result', ''))[:200]}" - ) - self.observations.append(observation) - final_content = self._extract_content(processed_response) - if final_content: - self.observations.append(f"Response after tools: {final_content}") - if log_context: - log_context.stacks.append( - { - "component": "agent_tool_calls", - "data": {"tool_calls": self.tool_calls.copy()}, - } - ) - yield {"sources": self.retrieved_docs} - yield {"tool_calls": self._get_truncated_tool_calls()} - - return "SATISFIED" in (final_content or "") - - def _synthesis_phase( - self, query: str, log_context: LogContext - ) -> Generator[Dict, None, None]: - """Synthesize final answer from all observations""" - logger.info("ReActAgent: Generating final answer...") - - final_prompt = self._build_final_answer_prompt(query) - messages = [{"role": "user", "content": final_prompt}] - - final_stream = self.llm.gen_stream( - model=self.model_id, messages=messages, tools=None - ) - - if log_context: - log_context.stacks.append( - {"component": "final_answer_llm", "data": build_stack_data(self.llm)} - ) - for chunk in final_stream: - content = self._extract_content(chunk) - if content: - yield {"answer": content} - - def _build_planning_prompt(self, query: str) -> str: - """Build planning phase prompt""" - prompt = PLANNING_PROMPT_TEMPLATE.replace("{query}", query) - prompt = prompt.replace("{prompt}", self.prompt or "") - prompt = prompt.replace("{summaries}", "") - prompt = prompt.replace("{observations}", "\n".join(self.observations)) - return prompt - - def _build_execution_prompt(self, query: str) -> str: - """Build execution phase prompt with plan and observations""" - observations_str = "\n".join(self.observations) - - if len(observations_str) > 20000: - observations_str = observations_str[:20000] + "\n...[truncated]" - return ( - f"{self.prompt or ''}\n\n" - f"Follow this plan:\n{self.plan}\n\n" - f"Observations:\n{observations_str}\n\n" - f"If sufficient data exists to answer '{query}', respond with 'SATISFIED'. " - f"Otherwise, continue executing the plan." - ) - - def _build_final_answer_prompt(self, query: str) -> str: - """Build final synthesis prompt""" - observations_str = "\n".join(self.observations) - - if len(observations_str) > 10000: - observations_str = observations_str[:10000] + "\n...[truncated]" - logger.warning("ReActAgent: Observations truncated for final answer") - return FINAL_PROMPT_TEMPLATE.format(query=query, observations=observations_str) - - def _extract_content(self, response: Any) -> str: - """Extract text content from various LLM response formats""" - if not response: - return "" - collected = [] - - if isinstance(response, str): - return response - if hasattr(response, "message") and hasattr(response.message, "content"): - if response.message.content: - return response.message.content - if hasattr(response, "choices") and response.choices: - if hasattr(response.choices[0], "message"): - content = response.choices[0].message.content - if content: - return content - if hasattr(response, "content") and isinstance(response.content, list): - if response.content and hasattr(response.content[0], "text"): - return response.content[0].text - try: - for chunk in response: - content_piece = "" - - if hasattr(chunk, "choices") and chunk.choices: - if hasattr(chunk.choices[0], "delta"): - delta_content = chunk.choices[0].delta.content - if delta_content: - content_piece = delta_content - elif hasattr(chunk, "type") and chunk.type == "content_block_delta": - if hasattr(chunk, "delta") and hasattr(chunk.delta, "text"): - content_piece = chunk.delta.text - elif isinstance(chunk, str): - content_piece = chunk - if content_piece: - collected.append(content_piece) - except (TypeError, AttributeError): - logger.debug( - f"Response not iterable or unexpected format: {type(response)}" - ) - except Exception as e: - logger.error(f"Error extracting content: {e}") - return "".join(collected) \ No newline at end of file diff --git a/application/agents/research_agent.py b/application/agents/research_agent.py new file mode 100644 index 00000000..280fa2cd --- /dev/null +++ b/application/agents/research_agent.py @@ -0,0 +1,692 @@ +import json +import logging +import os +import time +from typing import Dict, Generator, List, Optional + +from application.agents.base import BaseAgent +from application.agents.tool_executor import ToolExecutor +from application.agents.tools.internal_search import ( + INTERNAL_TOOL_ID, + add_internal_search_tool, +) +from application.agents.tools.think import THINK_TOOL_ENTRY, THINK_TOOL_ID +from application.logging import LogContext + +logger = logging.getLogger(__name__) + +# Defaults (can be overridden via constructor) +DEFAULT_MAX_STEPS = 6 +DEFAULT_MAX_SUB_ITERATIONS = 5 +DEFAULT_TIMEOUT_SECONDS = 300 # 5 minutes +DEFAULT_TOKEN_BUDGET = 100_000 +DEFAULT_PARALLEL_WORKERS = 3 + +# Adaptive depth caps per complexity level +COMPLEXITY_CAPS = { + "simple": 2, + "moderate": 4, + "complex": 6, +} + +_PROMPTS_DIR = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "prompts", + "research", +) + + +def _load_prompt(name: str) -> str: + with open(os.path.join(_PROMPTS_DIR, name), "r") as f: + return f.read() + + +CLARIFICATION_PROMPT = _load_prompt("clarification.txt") +PLANNING_PROMPT = _load_prompt("planning.txt") +STEP_PROMPT = _load_prompt("step.txt") +SYNTHESIS_PROMPT = _load_prompt("synthesis.txt") + + +# --------------------------------------------------------------------------- +# CitationManager +# --------------------------------------------------------------------------- + + +class CitationManager: + """Tracks and deduplicates citations across research steps.""" + + def __init__(self): + self.citations: Dict[int, Dict] = {} + self._counter = 0 + + def add(self, doc: Dict) -> int: + """Register a source, return its citation number. Deduplicates by source.""" + source = doc.get("source", "") + title = doc.get("title", "") + for num, existing in self.citations.items(): + if existing.get("source") == source and existing.get("title") == title: + return num + self._counter += 1 + self.citations[self._counter] = doc + return self._counter + + def add_docs(self, docs: List[Dict]) -> str: + """Register multiple docs, return formatted citation mapping text.""" + mapping_lines = [] + for doc in docs: + num = self.add(doc) + title = doc.get("title", "Untitled") + mapping_lines.append(f"[{num}] {title}") + return "\n".join(mapping_lines) + + def format_references(self) -> str: + """Generate [N] -> source mapping for report footer.""" + if not self.citations: + return "No sources found." + lines = [] + for num, doc in sorted(self.citations.items()): + title = doc.get("title", "Untitled") + source = doc.get("source", "Unknown") + filename = doc.get("filename", "") + display = filename or title + lines.append(f"[{num}] {display} — {source}") + return "\n".join(lines) + + def get_all_docs(self) -> List[Dict]: + return list(self.citations.values()) + + +# --------------------------------------------------------------------------- +# ResearchAgent +# --------------------------------------------------------------------------- + + +class ResearchAgent(BaseAgent): + """Multi-step research agent with parallel execution and budget controls. + + Orchestrates: Plan -> Research (per step, optionally parallel) -> Synthesize. + """ + + def __init__( + self, + retriever_config: Optional[Dict] = None, + max_steps: int = DEFAULT_MAX_STEPS, + max_sub_iterations: int = DEFAULT_MAX_SUB_ITERATIONS, + timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS, + token_budget: int = DEFAULT_TOKEN_BUDGET, + parallel_workers: int = DEFAULT_PARALLEL_WORKERS, + *args, + **kwargs, + ): + super().__init__(*args, **kwargs) + self.retriever_config = retriever_config or {} + self.max_steps = max_steps + self.max_sub_iterations = max_sub_iterations + self.timeout_seconds = timeout_seconds + self.token_budget = token_budget + self.parallel_workers = parallel_workers + self.citations = CitationManager() + self._start_time: float = 0 + self._tokens_used: int = 0 + self._last_token_snapshot: int = 0 + + # ------------------------------------------------------------------ + # Budget & timeout helpers + # ------------------------------------------------------------------ + + def _is_timed_out(self) -> bool: + return (time.monotonic() - self._start_time) >= self.timeout_seconds + + def _elapsed(self) -> float: + return round(time.monotonic() - self._start_time, 1) + + def _track_tokens(self, count: int): + self._tokens_used += count + + def _budget_remaining(self) -> int: + return max(self.token_budget - self._tokens_used, 0) + + def _is_over_budget(self) -> bool: + return self._tokens_used >= self.token_budget + + def _snapshot_llm_tokens(self) -> int: + """Read current token usage from LLM and return delta since last snapshot.""" + current = self.llm.token_usage.get("prompt_tokens", 0) + self.llm.token_usage.get("generated_tokens", 0) + delta = current - self._last_token_snapshot + self._last_token_snapshot = current + return delta + + # ------------------------------------------------------------------ + # Main orchestration + # ------------------------------------------------------------------ + + def _gen_inner( + self, query: str, log_context: LogContext + ) -> Generator[Dict, None, None]: + self._start_time = time.monotonic() + tools_dict = self._setup_tools() + + # Phase 0: Clarification (skip if user is responding to a prior clarification) + if not self._is_follow_up(): + clarification = self._clarification_phase(query) + if clarification: + yield {"metadata": {"is_clarification": True}} + yield {"answer": clarification} + yield {"sources": []} + yield {"tool_calls": []} + log_context.stacks.append( + {"component": "agent", "data": {"clarification": True}} + ) + return + + # Phase 1: Planning (with adaptive depth) + yield {"type": "research_progress", "data": {"status": "planning"}} + plan, complexity = self._planning_phase(query) + + if not plan: + logger.warning("ResearchAgent: Planning produced no steps, falling back") + plan = [{"query": query, "rationale": "Direct investigation"}] + complexity = "simple" + + yield { + "type": "research_plan", + "data": {"steps": plan, "complexity": complexity}, + } + + # Phase 2: Research each step (yields progress events in real-time) + intermediate_reports = [] + for i, step in enumerate(plan): + step_num = i + 1 + step_query = step.get("query", query) + + if self._is_timed_out(): + logger.warning( + f"ResearchAgent: Timeout at step {step_num}/{len(plan)} " + f"({self._elapsed()}s)" + ) + break + if self._is_over_budget(): + logger.warning( + f"ResearchAgent: Token budget exhausted at step {step_num}/{len(plan)}" + ) + break + + yield { + "type": "research_progress", + "data": { + "step": step_num, + "total": len(plan), + "query": step_query, + "status": "researching", + }, + } + + report = self._research_step(step_query, tools_dict) + intermediate_reports.append({"step": step, "content": report}) + + yield { + "type": "research_progress", + "data": { + "step": step_num, + "total": len(plan), + "query": step_query, + "status": "complete", + }, + } + + # Phase 3: Synthesis (streaming) + if self._is_timed_out(): + logger.warning( + f"ResearchAgent: Timeout ({self._elapsed()}s) before synthesis, " + f"synthesizing with {len(intermediate_reports)} reports" + ) + yield { + "type": "research_progress", + "data": { + "status": "synthesizing", + "elapsed_seconds": self._elapsed(), + "tokens_used": self._tokens_used, + }, + } + yield from self._synthesis_phase( + query, plan, intermediate_reports, tools_dict, log_context + ) + + # Sources and tool calls + self.retrieved_docs = self.citations.get_all_docs() + yield {"sources": self.retrieved_docs} + yield {"tool_calls": self._get_truncated_tool_calls()} + + logger.info( + f"ResearchAgent completed: {len(intermediate_reports)}/{len(plan)} steps, " + f"{self._elapsed()}s, ~{self._tokens_used} tokens" + ) + log_context.stacks.append( + {"component": "agent", "data": {"tool_calls": self.tool_calls.copy()}} + ) + + # ------------------------------------------------------------------ + # Tool setup + # ------------------------------------------------------------------ + + def _setup_tools(self) -> Dict: + """Build tools_dict with user tools + internal search + think.""" + tools_dict = self.tool_executor.get_tools() + + add_internal_search_tool(tools_dict, self.retriever_config) + + think_entry = dict(THINK_TOOL_ENTRY) + think_entry["config"] = {} + tools_dict[THINK_TOOL_ID] = think_entry + + self._prepare_tools(tools_dict) + return tools_dict + + # ------------------------------------------------------------------ + # Phase 0: Clarification + # ------------------------------------------------------------------ + + def _is_follow_up(self) -> bool: + """Check if the user is responding to a prior clarification. + + Uses the metadata flag stored in the conversation DB — no string matching. + Only skip clarification when the last query was explicitly flagged + as a clarification by this agent. + """ + if not self.chat_history: + return False + last = self.chat_history[-1] + meta = last.get("metadata", {}) + return bool(meta.get("is_clarification")) + + def _clarification_phase(self, question: str) -> Optional[str]: + """Ask the LLM whether the question needs clarification. + + Returns formatted clarification text if needed, or None to proceed. + Uses response_format to force valid JSON output. + """ + messages = [ + {"role": "system", "content": CLARIFICATION_PROMPT}, + {"role": "user", "content": question}, + ] + + try: + response = self.llm.gen( + model=self.model_id, + messages=messages, + tools=None, + response_format={"type": "json_object"}, + ) + text = self._extract_text(response) + self._track_tokens(self._snapshot_llm_tokens()) + logger.info(f"ResearchAgent clarification response: {text[:300]}") + + data = self._parse_clarification_json(text) + if not data or not data.get("needs_clarification"): + return None + + questions = data.get("questions", []) + if not questions: + return None + + # Format as a friendly response + lines = [ + "Before I begin researching, I'd like to clarify a few things:\n" + ] + for i, q in enumerate(questions[:3], 1): + lines.append(f"{i}. {q}") + lines.append( + "\nPlease provide these details and I'll start the research." + ) + return "\n".join(lines) + + except Exception as e: + logger.error(f"Clarification phase failed: {e}", exc_info=True) + return None # proceed with research on failure + + def _parse_clarification_json(self, text: str) -> Optional[Dict]: + """Parse clarification JSON from LLM response.""" + try: + return json.loads(text) + except json.JSONDecodeError: + pass + + # Try extracting from code fences + for marker in ["```json", "```"]: + if marker in text: + start = text.index(marker) + len(marker) + end = text.index("```", start) if "```" in text[start:] else len(text) + try: + return json.loads(text[start:end].strip()) + except (json.JSONDecodeError, ValueError): + pass + + # Try finding JSON object + for i, ch in enumerate(text): + if ch == "{": + for j in range(len(text) - 1, i, -1): + if text[j] == "}": + try: + return json.loads(text[i : j + 1]) + except json.JSONDecodeError: + continue + break + + return None + + # ------------------------------------------------------------------ + # Phase 1: Planning (with adaptive depth) + # ------------------------------------------------------------------ + + def _planning_phase(self, question: str) -> tuple[List[Dict], str]: + """Decompose the question into research steps via LLM. + + Returns (steps, complexity) where complexity is simple/moderate/complex. + """ + messages = [ + {"role": "system", "content": PLANNING_PROMPT}, + {"role": "user", "content": question}, + ] + + try: + response = self.llm.gen( + model=self.model_id, + messages=messages, + tools=None, + response_format={"type": "json_object"}, + ) + text = self._extract_text(response) + self._track_tokens(self._snapshot_llm_tokens()) + logger.info(f"ResearchAgent planning LLM response: {text[:500]}") + + plan_data = self._parse_plan_json(text) + if isinstance(plan_data, dict): + complexity = plan_data.get("complexity", "moderate") + steps = plan_data.get("steps", []) + else: + complexity = "moderate" + steps = plan_data + + # Adaptive depth: cap steps based on assessed complexity + cap = COMPLEXITY_CAPS.get(complexity, self.max_steps) + cap = min(cap, self.max_steps) + steps = steps[:cap] + + logger.info( + f"ResearchAgent plan: complexity={complexity}, " + f"steps={len(steps)} (cap={cap})" + ) + return steps, complexity + + except Exception as e: + logger.error(f"Planning phase failed: {e}", exc_info=True) + return ( + [{"query": question, "rationale": "Direct investigation (planning failed)"}], + "simple", + ) + + def _parse_plan_json(self, text: str): + """Extract JSON plan from LLM response. Returns dict or list.""" + # Try direct parse + try: + data = json.loads(text) + if isinstance(data, dict) and "steps" in data: + return data + if isinstance(data, list): + return data + except json.JSONDecodeError: + pass + + # Try extracting from markdown code fences + for marker in ["```json", "```"]: + if marker in text: + start = text.index(marker) + len(marker) + end = text.index("```", start) if "```" in text[start:] else len(text) + try: + data = json.loads(text[start:end].strip()) + if isinstance(data, dict) and "steps" in data: + return data + if isinstance(data, list): + return data + except (json.JSONDecodeError, ValueError): + pass + + # Try finding JSON object in text + for i, ch in enumerate(text): + if ch == "{": + for j in range(len(text) - 1, i, -1): + if text[j] == "}": + try: + data = json.loads(text[i : j + 1]) + if isinstance(data, dict) and "steps" in data: + return data + except json.JSONDecodeError: + continue + break + + logger.warning(f"Could not parse plan JSON from: {text[:200]}") + return [] + + # ------------------------------------------------------------------ + # Phase 2: Research step (core loop) + # ------------------------------------------------------------------ + + def _research_step(self, step_query: str, tools_dict: Dict) -> str: + """Run a focused research loop for one sub-question (sequential path).""" + report = self._research_step_with_executor( + step_query, tools_dict, self.tool_executor + ) + self._collect_step_sources() + return report + + def _research_step_with_executor( + self, step_query: str, tools_dict: Dict, executor: ToolExecutor + ) -> str: + """Core research loop. Works with any ToolExecutor instance.""" + system_prompt = STEP_PROMPT.replace("{step_query}", step_query) + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": step_query}, + ] + + last_search_empty = False + + for iteration in range(self.max_sub_iterations): + # Check timeout and budget + if self._is_timed_out(): + logger.info( + f"Research step '{step_query[:50]}' timed out at iteration {iteration}" + ) + break + if self._is_over_budget(): + logger.info( + f"Research step '{step_query[:50]}' hit token budget at iteration {iteration}" + ) + break + + try: + response = self.llm.gen( + model=self.model_id, + messages=messages, + tools=self.tools if self.tools else None, + ) + self._track_tokens(self._snapshot_llm_tokens()) + except Exception as e: + logger.error( + f"Research step LLM call failed (iteration {iteration}): {e}", + exc_info=True, + ) + break + + parsed = self.llm_handler.parse_response(response) + + if not parsed.requires_tool_call: + return parsed.content or "No findings for this step." + + # Execute tool calls + messages, last_search_empty = self._execute_step_tools_with_refinement( + parsed.tool_calls, tools_dict, messages, executor, last_search_empty + ) + + # Max iterations / timeout / budget — ask for summary + messages.append( + { + "role": "user", + "content": "Please summarize your findings so far based on the information gathered.", + } + ) + try: + response = self.llm.gen( + model=self.model_id, messages=messages, tools=None + ) + self._track_tokens(self._snapshot_llm_tokens()) + text = self._extract_text(response) + return text or "Research step completed." + except Exception: + return "Research step completed." + + def _execute_step_tools_with_refinement( + self, + tool_calls, + tools_dict: Dict, + messages: List[Dict], + executor: ToolExecutor, + last_search_empty: bool, + ) -> tuple[List[Dict], bool]: + """Execute tool calls with query refinement on empty results. + + Returns (updated_messages, was_last_search_empty). + """ + search_returned_empty = False + + for call in tool_calls: + gen = executor.execute( + tools_dict, call, self.llm.__class__.__name__ + ) + result = None + call_id = None + while True: + try: + event = next(gen) + # Log tool_call status events instead of discarding them + if isinstance(event, dict) and event.get("type") == "tool_call": + logger.debug( + "Tool %s status: %s", + event.get("data", {}).get("action_name", ""), + event.get("data", {}).get("status", ""), + ) + except StopIteration as e: + result, call_id = e.value + break + + # Detect empty search results for refinement + is_search = "search" in (call.name or "").lower() + result_str = str(result) if result else "" + if is_search and "No documents found" in result_str: + search_returned_empty = True + if last_search_empty: + # Two consecutive empty searches — inject refinement hint + result_str += ( + "\n\nHint: Previous search also returned no results. " + "Try a very different query with different keywords, " + "or broaden your search terms." + ) + result = result_str + + function_call_content = { + "function_call": { + "name": call.name, + "args": call.arguments, + "call_id": call_id, + } + } + messages.append( + {"role": "assistant", "content": [function_call_content]} + ) + tool_message = self.llm_handler.create_tool_message(call, result) + messages.append(tool_message) + + return messages, search_returned_empty + + def _collect_step_sources(self): + """Collect sources from InternalSearchTool and register with CitationManager.""" + cache_key = f"internal_search:{INTERNAL_TOOL_ID}:{self.user or ''}" + tool = self.tool_executor._loaded_tools.get(cache_key) + if tool and hasattr(tool, "retrieved_docs"): + for doc in tool.retrieved_docs: + self.citations.add(doc) + + # ------------------------------------------------------------------ + # Phase 3: Synthesis + # ------------------------------------------------------------------ + + def _synthesis_phase( + self, + question: str, + plan: List[Dict], + intermediate_reports: List[Dict], + tools_dict: Dict, + log_context: LogContext, + ) -> Generator[Dict, None, None]: + """Compile all findings into a final cited report (streaming).""" + plan_lines = [] + for i, step in enumerate(plan, 1): + plan_lines.append( + f"{i}. {step.get('query', 'Unknown')} — {step.get('rationale', '')}" + ) + plan_summary = "\n".join(plan_lines) + + findings_parts = [] + for i, report in enumerate(intermediate_reports, 1): + step_query = report["step"].get("query", "Unknown") + content = report["content"] + findings_parts.append( + f"--- Step {i}: {step_query} ---\n{content}" + ) + findings = "\n\n".join(findings_parts) + + references = self.citations.format_references() + + synthesis_prompt = SYNTHESIS_PROMPT.replace("{question}", question) + synthesis_prompt = synthesis_prompt.replace("{plan_summary}", plan_summary) + synthesis_prompt = synthesis_prompt.replace("{findings}", findings) + synthesis_prompt = synthesis_prompt.replace("{references}", references) + + messages = [ + {"role": "system", "content": synthesis_prompt}, + {"role": "user", "content": f"Please write the research report for: {question}"}, + ] + + llm_response = self.llm.gen_stream( + model=self.model_id, messages=messages, tools=None + ) + + if log_context: + from application.logging import build_stack_data + + log_context.stacks.append( + {"component": "synthesis_llm", "data": build_stack_data(self.llm)} + ) + + yield from self._handle_response( + llm_response, tools_dict, messages, log_context + ) + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def _extract_text(self, response) -> str: + """Extract text content from a non-streaming LLM response.""" + if isinstance(response, str): + return response + if hasattr(response, "message") and hasattr(response.message, "content"): + return response.message.content or "" + if hasattr(response, "choices") and response.choices: + choice = response.choices[0] + if hasattr(choice, "message") and hasattr(choice.message, "content"): + return choice.message.content or "" + if hasattr(response, "content") and isinstance(response.content, list): + if response.content and hasattr(response.content[0], "text"): + return response.content[0].text or "" + return str(response) if response else "" diff --git a/application/agents/tool_executor.py b/application/agents/tool_executor.py new file mode 100644 index 00000000..69739076 --- /dev/null +++ b/application/agents/tool_executor.py @@ -0,0 +1,313 @@ +import logging +import uuid +from typing import Dict, List, Optional + +from bson.objectid import ObjectId + +from application.agents.tools.tool_action_parser import ToolActionParser +from application.agents.tools.tool_manager import ToolManager +from application.core.mongo_db import MongoDB +from application.core.settings import settings +from application.security.encryption import decrypt_credentials + +logger = logging.getLogger(__name__) + + +class ToolExecutor: + """Handles tool discovery, preparation, and execution. + + Extracted from BaseAgent to separate concerns and enable tool caching. + """ + + def __init__( + self, + user_api_key: Optional[str] = None, + user: Optional[str] = None, + decoded_token: Optional[Dict] = None, + ): + self.user_api_key = user_api_key + self.user = user + self.decoded_token = decoded_token + self.tool_calls: List[Dict] = [] + self._loaded_tools: Dict[str, object] = {} + self.conversation_id: Optional[str] = None + + def get_tools(self) -> Dict[str, Dict]: + """Load tool configs from DB based on user context.""" + if self.user_api_key: + return self._get_tools_by_api_key(self.user_api_key) + return self._get_user_tools(self.user or "local") + + def _get_tools_by_api_key(self, api_key: str) -> Dict[str, Dict]: + mongo = MongoDB.get_client() + db = mongo[settings.MONGO_DB_NAME] + agents_collection = db["agents"] + tools_collection = db["user_tools"] + + agent_data = agents_collection.find_one({"key": api_key}) + tool_ids = agent_data.get("tools", []) if agent_data else [] + + tools = ( + tools_collection.find( + {"_id": {"$in": [ObjectId(tool_id) for tool_id in tool_ids]}} + ) + if tool_ids + else [] + ) + tools = list(tools) + return {str(tool["_id"]): tool for tool in tools} if tools else {} + + def _get_user_tools(self, user: str = "local") -> Dict[str, Dict]: + mongo = MongoDB.get_client() + db = mongo[settings.MONGO_DB_NAME] + user_tools_collection = db["user_tools"] + user_tools = user_tools_collection.find({"user": user, "status": True}) + user_tools = list(user_tools) + return {str(i): tool for i, tool in enumerate(user_tools)} + + def prepare_tools_for_llm(self, tools_dict: Dict) -> List[Dict]: + """Convert tool configs to LLM function schemas.""" + return [ + { + "type": "function", + "function": { + "name": f"{action['name']}_{tool_id}", + "description": action["description"], + "parameters": self._build_tool_parameters(action), + }, + } + for tool_id, tool in tools_dict.items() + if ( + (tool["name"] == "api_tool" and "actions" in tool.get("config", {})) + or (tool["name"] != "api_tool" and "actions" in tool) + ) + for action in ( + tool["config"]["actions"].values() + if tool["name"] == "api_tool" + else tool["actions"] + ) + if action.get("active", True) + ] + + def _build_tool_parameters(self, action: Dict) -> Dict: + params = {"type": "object", "properties": {}, "required": []} + for param_type in ["query_params", "headers", "body", "parameters"]: + if param_type in action and action[param_type].get("properties"): + for k, v in action[param_type]["properties"].items(): + if v.get("filled_by_llm", True): + params["properties"][k] = { + key: value + for key, value in v.items() + if key not in ("filled_by_llm", "value", "required") + } + if v.get("required", False): + params["required"].append(k) + return params + + def execute(self, tools_dict: Dict, call, llm_class_name: str): + """Execute a tool call. Yields status events, returns (result, call_id).""" + parser = ToolActionParser(llm_class_name) + tool_id, action_name, call_args = parser.parse_args(call) + + call_id = getattr(call, "id", None) or str(uuid.uuid4()) + + if tool_id is None or action_name is None: + error_message = f"Error: Failed to parse LLM tool call. Tool name: {getattr(call, 'name', 'unknown')}" + logger.error(error_message) + + tool_call_data = { + "tool_name": "unknown", + "call_id": call_id, + "action_name": getattr(call, "name", "unknown"), + "arguments": call_args or {}, + "result": f"Failed to parse tool call. Invalid tool name format: {getattr(call, 'name', 'unknown')}", + } + yield {"type": "tool_call", "data": {**tool_call_data, "status": "error"}} + self.tool_calls.append(tool_call_data) + return "Failed to parse tool call.", call_id + + if tool_id not in tools_dict: + error_message = f"Error: Tool ID '{tool_id}' extracted from LLM call not found in available tools_dict. Available IDs: {list(tools_dict.keys())}" + logger.error(error_message) + + tool_call_data = { + "tool_name": "unknown", + "call_id": call_id, + "action_name": f"{action_name}_{tool_id}", + "arguments": call_args, + "result": f"Tool with ID {tool_id} not found. Available tools: {list(tools_dict.keys())}", + } + yield {"type": "tool_call", "data": {**tool_call_data, "status": "error"}} + self.tool_calls.append(tool_call_data) + return f"Tool with ID {tool_id} not found.", call_id + + tool_call_data = { + "tool_name": tools_dict[tool_id]["name"], + "call_id": call_id, + "action_name": f"{action_name}_{tool_id}", + "arguments": call_args, + } + yield {"type": "tool_call", "data": {**tool_call_data, "status": "pending"}} + + tool_data = tools_dict[tool_id] + action_data = ( + tool_data["config"]["actions"][action_name] + if tool_data["name"] == "api_tool" + else next( + action + for action in tool_data["actions"] + if action["name"] == action_name + ) + ) + + query_params, headers, body, parameters = {}, {}, {}, {} + param_types = { + "query_params": query_params, + "headers": headers, + "body": body, + "parameters": parameters, + } + + for param_type, target_dict in param_types.items(): + if param_type in action_data and action_data[param_type].get("properties"): + for param, details in action_data[param_type]["properties"].items(): + if ( + param not in call_args + and "value" in details + and details["value"] + ): + target_dict[param] = details["value"] + for param, value in call_args.items(): + for param_type, target_dict in param_types.items(): + if param_type in action_data and param in action_data[param_type].get( + "properties", {} + ): + target_dict[param] = value + + # Load tool (with caching) + tool = self._get_or_load_tool( + tool_data, tool_id, action_name, + headers=headers, query_params=query_params, + ) + + resolved_arguments = ( + {"query_params": query_params, "headers": headers, "body": body} + if tool_data["name"] == "api_tool" + else parameters + ) + if tool_data["name"] == "api_tool": + logger.debug( + f"Executing api: {action_name} with query_params: {query_params}, headers: {headers}, body: {body}" + ) + result = tool.execute_action(action_name, **body) + else: + logger.debug(f"Executing tool: {action_name} with args: {call_args}") + result = tool.execute_action(action_name, **parameters) + + get_artifact_id = ( + getattr(tool, "get_artifact_id", None) + if tool_data["name"] != "api_tool" + else None + ) + + artifact_id = None + if callable(get_artifact_id): + try: + artifact_id = get_artifact_id(action_name, **parameters) + except Exception: + logger.exception( + "Failed to extract artifact_id from tool %s for action %s", + tool_data["name"], + action_name, + ) + + artifact_id = str(artifact_id).strip() if artifact_id is not None else "" + if artifact_id: + tool_call_data["artifact_id"] = artifact_id + result_full = str(result) + tool_call_data["resolved_arguments"] = resolved_arguments + tool_call_data["result_full"] = result_full + tool_call_data["result"] = ( + f"{result_full[:50]}..." if len(result_full) > 50 else result_full + ) + + stream_tool_call_data = { + key: value + for key, value in tool_call_data.items() + if key not in {"result_full", "resolved_arguments"} + } + yield {"type": "tool_call", "data": {**stream_tool_call_data, "status": "completed"}} + self.tool_calls.append(tool_call_data) + + return result, call_id + + def _get_or_load_tool( + self, tool_data: Dict, tool_id: str, action_name: str, + headers: Optional[Dict] = None, query_params: Optional[Dict] = None, + ): + """Load a tool, using cache when possible.""" + cache_key = f"{tool_data['name']}:{tool_id}:{self.user or ''}" + if cache_key in self._loaded_tools: + return self._loaded_tools[cache_key] + + tm = ToolManager(config={}) + + if tool_data["name"] == "api_tool": + action_config = tool_data["config"]["actions"][action_name] + tool_config = { + "url": action_config["url"], + "method": action_config["method"], + "headers": headers or {}, + "query_params": query_params or {}, + } + if "body_content_type" in action_config: + tool_config["body_content_type"] = action_config.get( + "body_content_type", "application/json" + ) + tool_config["body_encoding_rules"] = action_config.get( + "body_encoding_rules", {} + ) + else: + tool_config = tool_data["config"].copy() if tool_data["config"] else {} + if tool_config.get("encrypted_credentials") and self.user: + decrypted = decrypt_credentials( + tool_config["encrypted_credentials"], self.user + ) + tool_config.update(decrypted) + tool_config["auth_credentials"] = decrypted + tool_config.pop("encrypted_credentials", None) + tool_config["tool_id"] = str(tool_data.get("_id", tool_id)) + if self.conversation_id: + tool_config["conversation_id"] = self.conversation_id + if tool_data["name"] == "mcp_tool": + tool_config["query_mode"] = True + + tool = tm.load_tool( + tool_data["name"], + tool_config=tool_config, + user_id=self.user, + ) + + # Don't cache api_tool since config varies by action + if tool_data["name"] != "api_tool": + self._loaded_tools[cache_key] = tool + + return tool + + def get_truncated_tool_calls(self) -> List[Dict]: + return [ + { + "tool_name": tool_call.get("tool_name"), + "call_id": tool_call.get("call_id"), + "action_name": tool_call.get("action_name"), + "arguments": tool_call.get("arguments"), + "artifact_id": tool_call.get("artifact_id"), + "result": ( + f"{str(tool_call['result'])[:50]}..." + if len(str(tool_call["result"])) > 50 + else tool_call["result"] + ), + "status": "completed", + } + for tool_call in self.tool_calls + ] diff --git a/application/agents/tools/internal_search.py b/application/agents/tools/internal_search.py new file mode 100644 index 00000000..2cd7915b --- /dev/null +++ b/application/agents/tools/internal_search.py @@ -0,0 +1,436 @@ +import json +import logging +from typing import Dict, List, Optional + +from application.agents.tools.base import Tool +from application.core.settings import settings +from application.retriever.retriever_creator import RetrieverCreator + +logger = logging.getLogger(__name__) + + +class InternalSearchTool(Tool): + """Wraps the ClassicRAG retriever as an LLM-callable tool. + + Instead of pre-fetching docs into the prompt, the LLM decides + when and what to search. Supports multiple searches per session. + + Optional capabilities (enabled when sources have directory_structure): + - path_filter on search: restrict results to a specific file/folder + - list_files action: browse the file/folder structure + """ + + def __init__(self, config: Dict): + self.config = config + self.retrieved_docs: List[Dict] = [] + self._retriever = None + self._directory_structure: Optional[Dict] = None + self._dir_structure_loaded = False + + def _get_retriever(self): + if self._retriever is None: + self._retriever = RetrieverCreator.create_retriever( + self.config.get("retriever_name", "classic"), + source=self.config.get("source", {}), + chat_history=[], + prompt="", + chunks=int(self.config.get("chunks", 2)), + doc_token_limit=int(self.config.get("doc_token_limit", 50000)), + model_id=self.config.get("model_id", "docsgpt-local"), + user_api_key=self.config.get("user_api_key"), + agent_id=self.config.get("agent_id"), + llm_name=self.config.get("llm_name", settings.LLM_PROVIDER), + api_key=self.config.get("api_key", settings.API_KEY), + decoded_token=self.config.get("decoded_token"), + ) + return self._retriever + + def _get_directory_structure(self) -> Optional[Dict]: + """Load directory structure from MongoDB for the configured sources.""" + if self._dir_structure_loaded: + return self._directory_structure + + self._dir_structure_loaded = True + source = self.config.get("source", {}) + active_docs = source.get("active_docs", []) + if not active_docs: + return None + + try: + from bson.objectid import ObjectId + from application.core.mongo_db import MongoDB + + mongo = MongoDB.get_client() + db = mongo[settings.MONGO_DB_NAME] + sources_collection = db["sources"] + + if isinstance(active_docs, str): + active_docs = [active_docs] + + merged_structure = {} + for doc_id in active_docs: + try: + source_doc = sources_collection.find_one( + {"_id": ObjectId(doc_id)} + ) + if not source_doc: + continue + dir_str = source_doc.get("directory_structure") + if dir_str: + if isinstance(dir_str, str): + dir_str = json.loads(dir_str) + source_name = source_doc.get("name", doc_id) + if len(active_docs) > 1: + merged_structure[source_name] = dir_str + else: + merged_structure = dir_str + except Exception as e: + logger.debug(f"Could not load dir structure for {doc_id}: {e}") + + self._directory_structure = merged_structure if merged_structure else None + except Exception as e: + logger.debug(f"Failed to load directory structures: {e}") + + return self._directory_structure + + def execute_action(self, action_name: str, **kwargs): + if action_name == "search": + return self._execute_search(**kwargs) + elif action_name == "list_files": + return self._execute_list_files(**kwargs) + return f"Unknown action: {action_name}" + + def _execute_search(self, **kwargs) -> str: + query = kwargs.get("query", "") + path_filter = kwargs.get("path_filter", "") + + if not query: + return "Error: 'query' parameter is required." + + try: + retriever = self._get_retriever() + docs = retriever.search(query) + except Exception as e: + logger.error(f"Internal search failed: {e}", exc_info=True) + return "Search failed: an internal error occurred." + + if not docs: + return "No documents found matching your query." + + # Apply path filter if specified + if path_filter: + path_lower = path_filter.lower() + docs = [ + d + for d in docs + if path_lower in d.get("source", "").lower() + or path_lower in d.get("filename", "").lower() + or path_lower in d.get("title", "").lower() + ] + if not docs: + return f"No documents found matching query '{query}' in path '{path_filter}'." + + # Accumulate for source tracking + for doc in docs: + if doc not in self.retrieved_docs: + self.retrieved_docs.append(doc) + + # Format results for the LLM + formatted = [] + for i, doc in enumerate(docs, 1): + title = doc.get("title", "Untitled") + text = doc.get("text", "") + source = doc.get("source", "Unknown") + filename = doc.get("filename", "") + header = filename or title + formatted.append(f"[{i}] {header} (source: {source})\n{text}") + + return "\n\n---\n\n".join(formatted) + + def _execute_list_files(self, **kwargs) -> str: + path = kwargs.get("path", "") + dir_structure = self._get_directory_structure() + + if not dir_structure: + return "No file structure available for the current sources." + + # Navigate to the requested path + current = dir_structure + if path: + for part in path.strip("/").split("/"): + if not part: + continue + if isinstance(current, dict) and part in current: + current = current[part] + else: + return f"Path '{path}' not found in the file structure." + + # Format the structure for the LLM + return self._format_structure(current, path or "/") + + def _format_structure(self, node: Dict, current_path: str) -> str: + if not isinstance(node, dict): + return f"'{current_path}' is a file, not a directory." + + lines = [f"File structure at '{current_path}':\n"] + folders = [] + files = [] + + for name, value in sorted(node.items()): + if isinstance(value, dict): + # Check if it's a file metadata dict or a folder + if "type" in value or "size_bytes" in value or "token_count" in value: + # It's a file with metadata + size = value.get("token_count", "") + ftype = value.get("type", "") + info_parts = [] + if ftype: + info_parts.append(ftype) + if size: + info_parts.append(f"{size} tokens") + info = f" ({', '.join(info_parts)})" if info_parts else "" + files.append(f" {name}{info}") + else: + # It's a folder + count = self._count_files(value) + folders.append(f" {name}/ ({count} items)") + else: + files.append(f" {name}") + + if folders: + lines.append("Folders:") + lines.extend(folders) + if files: + lines.append("Files:") + lines.extend(files) + if not folders and not files: + lines.append(" (empty)") + + return "\n".join(lines) + + def _count_files(self, node: Dict) -> int: + count = 0 + for value in node.values(): + if isinstance(value, dict): + if "type" in value or "size_bytes" in value or "token_count" in value: + count += 1 + else: + count += self._count_files(value) + else: + count += 1 + return count + + def get_actions_metadata(self): + actions = [ + { + "name": "search", + "description": ( + "Search the user's uploaded documents and knowledge base. " + "Use this to find relevant information before answering questions. " + "You can call this multiple times with different queries." + ), + "parameters": { + "properties": { + "query": { + "type": "string", + "description": "The search query. Be specific and focused.", + "filled_by_llm": True, + "required": True, + }, + } + }, + } + ] + + # Add path_filter and list_files only if directory structure exists + has_structure = self.config.get("has_directory_structure", False) + if has_structure: + actions[0]["parameters"]["properties"]["path_filter"] = { + "type": "string", + "description": ( + "Optional: filter results to a specific file or folder path. " + "Use list_files first to see available paths." + ), + "filled_by_llm": True, + "required": False, + } + actions.append( + { + "name": "list_files", + "description": ( + "Browse the file and folder structure of the knowledge base. " + "Use this to see what files are available before searching. " + "Optionally provide a path to browse a specific folder." + ), + "parameters": { + "properties": { + "path": { + "type": "string", + "description": "Optional: folder path to browse. Leave empty for root.", + "filled_by_llm": True, + "required": False, + } + } + }, + } + ) + + return actions + + def get_config_requirements(self): + return {} + + +# Constants for building synthetic tools_dict entries +INTERNAL_TOOL_ID = "internal" + + +def build_internal_tool_entry(has_directory_structure: bool = False) -> Dict: + """Build the tools_dict entry for InternalSearchTool. + + Dynamically includes list_files and path_filter based on + whether the sources have directory structure. + """ + search_params = { + "properties": { + "query": { + "type": "string", + "description": "The search query. Be specific and focused.", + "filled_by_llm": True, + "required": True, + } + } + } + + actions = [ + { + "name": "search", + "description": ( + "Search the user's uploaded documents and knowledge base. " + "Use this to find relevant information before answering questions. " + "You can call this multiple times with different queries." + ), + "active": True, + "parameters": search_params, + } + ] + + if has_directory_structure: + search_params["properties"]["path_filter"] = { + "type": "string", + "description": ( + "Optional: filter results to a specific file or folder path. " + "Use list_files first to see available paths." + ), + "filled_by_llm": True, + "required": False, + } + actions.append( + { + "name": "list_files", + "description": ( + "Browse the file and folder structure of the knowledge base. " + "Use this to see what files are available before searching. " + "Optionally provide a path to browse a specific folder." + ), + "active": True, + "parameters": { + "properties": { + "path": { + "type": "string", + "description": "Optional: folder path to browse. Leave empty for root.", + "filled_by_llm": True, + "required": False, + } + } + }, + } + ) + + return {"name": "internal_search", "actions": actions} + + +# Keep backward compat +INTERNAL_TOOL_ENTRY = build_internal_tool_entry(has_directory_structure=False) + + +def sources_have_directory_structure(source: Dict) -> bool: + """Check if any of the active sources have directory_structure in MongoDB.""" + active_docs = source.get("active_docs", []) + if not active_docs: + return False + + try: + from bson.objectid import ObjectId + from application.core.mongo_db import MongoDB + + mongo = MongoDB.get_client() + db = mongo[settings.MONGO_DB_NAME] + sources_collection = db["sources"] + + if isinstance(active_docs, str): + active_docs = [active_docs] + + for doc_id in active_docs: + try: + source_doc = sources_collection.find_one( + {"_id": ObjectId(doc_id)}, + {"directory_structure": 1}, + ) + if source_doc and source_doc.get("directory_structure"): + return True + except Exception: + continue + except Exception as e: + logger.debug(f"Could not check directory structure: {e}") + + return False + + +def add_internal_search_tool(tools_dict: Dict, retriever_config: Dict) -> None: + """Add the internal search tool to tools_dict if sources are configured. + + Shared by AgenticAgent and ResearchAgent to avoid duplicate setup logic. + Mutates tools_dict in place. + """ + source = retriever_config.get("source", {}) + has_sources = bool(source.get("active_docs")) + if not retriever_config or not has_sources: + return + + has_dir = sources_have_directory_structure(source) + internal_entry = build_internal_tool_entry(has_directory_structure=has_dir) + internal_entry["config"] = build_internal_tool_config( + **retriever_config, + has_directory_structure=has_dir, + ) + tools_dict[INTERNAL_TOOL_ID] = internal_entry + + +def build_internal_tool_config( + source: Dict, + retriever_name: str = "classic", + chunks: int = 2, + doc_token_limit: int = 50000, + model_id: str = "docsgpt-local", + user_api_key: Optional[str] = None, + agent_id: Optional[str] = None, + llm_name: str = None, + api_key: str = None, + decoded_token: Optional[Dict] = None, + has_directory_structure: bool = False, +) -> Dict: + """Build the config dict for InternalSearchTool.""" + return { + "source": source, + "retriever_name": retriever_name, + "chunks": chunks, + "doc_token_limit": doc_token_limit, + "model_id": model_id, + "user_api_key": user_api_key, + "agent_id": agent_id, + "llm_name": llm_name or settings.LLM_PROVIDER, + "api_key": api_key or settings.API_KEY, + "decoded_token": decoded_token, + "has_directory_structure": has_directory_structure, + } diff --git a/application/agents/tools/think.py b/application/agents/tools/think.py new file mode 100644 index 00000000..7c1fc2b6 --- /dev/null +++ b/application/agents/tools/think.py @@ -0,0 +1,68 @@ +from application.agents.tools.base import Tool + + +THINK_TOOL_ID = "think" + +THINK_TOOL_ENTRY = { + "name": "think", + "actions": [ + { + "name": "reason", + "description": ( + "Use this tool to think through your reasoning step by step " + "before deciding on your next action. Always reason before " + "searching or answering." + ), + "active": True, + "parameters": { + "properties": { + "reasoning": { + "type": "string", + "description": "Your step-by-step reasoning and analysis", + "filled_by_llm": True, + "required": True, + } + } + }, + } + ], +} + + +class ThinkTool(Tool): + """Pseudo-tool that captures chain-of-thought reasoning. + + Returns a short acknowledgment so the LLM can continue. + The reasoning content is captured in tool_call data for transparency. + """ + + def __init__(self, config=None): + pass + + def execute_action(self, action_name: str, **kwargs): + return "Continue." + + def get_actions_metadata(self): + return [ + { + "name": "reason", + "description": ( + "Use this tool to think through your reasoning step by step " + "before deciding on your next action. Always reason before " + "searching or answering." + ), + "parameters": { + "properties": { + "reasoning": { + "type": "string", + "description": "Your step-by-step reasoning and analysis", + "filled_by_llm": True, + "required": True, + } + } + }, + } + ] + + def get_config_requirements(self): + return {} diff --git a/application/agents/workflows/node_agent.py b/application/agents/workflows/node_agent.py index d3c9d607..67437e45 100644 --- a/application/agents/workflows/node_agent.py +++ b/application/agents/workflows/node_agent.py @@ -2,9 +2,10 @@ from typing import Any, Dict, List, Optional, Type +from application.agents.agentic_agent import AgenticAgent from application.agents.base import BaseAgent from application.agents.classic_agent import ClassicAgent -from application.agents.react_agent import ReActAgent +from application.agents.research_agent import ResearchAgent from application.agents.workflows.schemas import AgentType @@ -36,7 +37,8 @@ class ToolFilterMixin: return filtered_tools -class WorkflowNodeClassicAgent(ToolFilterMixin, ClassicAgent): +class _WorkflowNodeMixin: + """Common __init__ for all workflow node agents.""" def __init__( self, @@ -57,32 +59,25 @@ class WorkflowNodeClassicAgent(ToolFilterMixin, ClassicAgent): self._allowed_tool_ids = tool_ids or [] -class WorkflowNodeReActAgent(ToolFilterMixin, ReActAgent): +class WorkflowNodeClassicAgent(ToolFilterMixin, _WorkflowNodeMixin, ClassicAgent): + pass - def __init__( - self, - endpoint: str, - llm_name: str, - model_id: str, - api_key: str, - tool_ids: Optional[List[str]] = None, - **kwargs, - ): - super().__init__( - endpoint=endpoint, - llm_name=llm_name, - model_id=model_id, - api_key=api_key, - **kwargs, - ) - self._allowed_tool_ids = tool_ids or [] + +class WorkflowNodeAgenticAgent(ToolFilterMixin, _WorkflowNodeMixin, AgenticAgent): + pass + + +class WorkflowNodeResearchAgent(ToolFilterMixin, _WorkflowNodeMixin, ResearchAgent): + pass class WorkflowNodeAgentFactory: _agents: Dict[AgentType, Type[BaseAgent]] = { AgentType.CLASSIC: WorkflowNodeClassicAgent, - AgentType.REACT: WorkflowNodeReActAgent, + AgentType.REACT: WorkflowNodeClassicAgent, # backwards compat + AgentType.AGENTIC: WorkflowNodeAgenticAgent, + AgentType.RESEARCH: WorkflowNodeResearchAgent, } @classmethod diff --git a/application/agents/workflows/schemas.py b/application/agents/workflows/schemas.py index 5355b88e..2a5bc79e 100644 --- a/application/agents/workflows/schemas.py +++ b/application/agents/workflows/schemas.py @@ -18,6 +18,8 @@ class NodeType(str, Enum): class AgentType(str, Enum): CLASSIC = "classic" REACT = "react" + AGENTIC = "agentic" + RESEARCH = "research" class ExecutionStatus(str, Enum): diff --git a/application/agents/workflows/workflow_engine.py b/application/agents/workflows/workflow_engine.py index 5444458a..00d471da 100644 --- a/application/agents/workflows/workflow_engine.py +++ b/application/agents/workflows/workflow_engine.py @@ -7,6 +7,7 @@ from application.agents.workflows.cel_evaluator import CelEvaluationError, evalu from application.agents.workflows.node_agent import WorkflowNodeAgentFactory from application.agents.workflows.schemas import ( AgentNodeConfig, + AgentType, ConditionNodeConfig, ExecutionStatus, NodeExecutionLog, @@ -223,18 +224,32 @@ class WorkflowEngine: f'Model "{node_model_id}" does not support structured output for node "{node.title}"' ) - node_agent = WorkflowNodeAgentFactory.create( - agent_type=node_config.agent_type, - endpoint=self.agent.endpoint, - llm_name=node_llm_name, - model_id=node_model_id, - api_key=node_api_key, - tool_ids=node_config.tools, - prompt=node_config.system_prompt, - chat_history=self.agent.chat_history, - decoded_token=self.agent.decoded_token, - json_schema=node_json_schema, - ) + factory_kwargs = { + "agent_type": node_config.agent_type, + "endpoint": self.agent.endpoint, + "llm_name": node_llm_name, + "model_id": node_model_id, + "api_key": node_api_key, + "tool_ids": node_config.tools, + "prompt": node_config.system_prompt, + "chat_history": self.agent.chat_history, + "decoded_token": self.agent.decoded_token, + "json_schema": node_json_schema, + } + + # Agentic/research agents need retriever_config for on-demand search + if node_config.agent_type in (AgentType.AGENTIC, AgentType.RESEARCH): + factory_kwargs["retriever_config"] = { + "source": {"active_docs": node_config.sources} if node_config.sources else {}, + "retriever_name": node_config.retriever or "classic", + "chunks": int(node_config.chunks) if node_config.chunks else 2, + "model_id": node_model_id, + "llm_name": node_llm_name, + "api_key": node_api_key, + "decoded_token": self.agent.decoded_token, + } + + node_agent = WorkflowNodeAgentFactory.create(**factory_kwargs) full_response_parts: List[str] = [] structured_response_parts: List[str] = [] diff --git a/application/api/answer/routes/answer.py b/application/api/answer/routes/answer.py index b90ffa15..f3111605 100644 --- a/application/api/answer/routes/answer.py +++ b/application/api/answer/routes/answer.py @@ -74,21 +74,10 @@ class AnswerResource(Resource, BaseAnswerResource): decoded_token = getattr(request, "decoded_token", None) processor = StreamProcessor(data, decoded_token) try: - processor.initialize() + agent = processor.build_agent(data.get("question", "")) if not processor.decoded_token: return make_response({"error": "Unauthorized"}, 401) - docs_together, docs_list = processor.pre_fetch_docs( - data.get("question", "") - ) - tools_data = processor.pre_fetch_tools() - - agent = processor.create_agent( - docs_together=docs_together, - docs=docs_list, - tools_data=tools_data, - ) - if error := self.check_usage(processor.agent_config): return error diff --git a/application/api/answer/routes/base.py b/application/api/answer/routes/base.py index 2f9c9950..29d3d66c 100644 --- a/application/api/answer/routes/base.py +++ b/application/api/answer/routes/base.py @@ -206,9 +206,12 @@ class BaseAnswerResource: is_structured = False schema_info = None structured_chunks = [] + query_metadata = {} for line in agent.gen(query=question): - if "answer" in line: + if "metadata" in line: + query_metadata.update(line["metadata"]) + elif "answer" in line: response_full += str(line["answer"]) if line.get("structured"): is_structured = True @@ -295,6 +298,7 @@ class BaseAnswerResource: is_shared_usage=is_shared_usage, shared_token=shared_token, attachment_ids=attachment_ids, + metadata=query_metadata if query_metadata else None, ) # Persist compression metadata/summary if it exists and wasn't saved mid-execution compression_meta = getattr(agent, "compression_metadata", None) @@ -384,6 +388,7 @@ class BaseAnswerResource: is_shared_usage=is_shared_usage, shared_token=shared_token, attachment_ids=attachment_ids, + metadata=query_metadata if query_metadata else None, ) compression_meta = getattr(agent, "compression_metadata", None) compression_saved = getattr(agent, "compression_saved", False) diff --git a/application/api/answer/routes/stream.py b/application/api/answer/routes/stream.py index d1a71b25..c2cc0ec6 100644 --- a/application/api/answer/routes/stream.py +++ b/application/api/answer/routes/stream.py @@ -80,7 +80,7 @@ class StreamResource(Resource, BaseAnswerResource): decoded_token = getattr(request, "decoded_token", None) processor = StreamProcessor(data, decoded_token) try: - processor.initialize() + agent = processor.build_agent(data["question"]) if not processor.decoded_token: return Response( self.error_stream_generate("Unauthorized"), @@ -88,13 +88,6 @@ class StreamResource(Resource, BaseAnswerResource): mimetype="text/event-stream", ) - docs_together, docs_list = processor.pre_fetch_docs(data["question"]) - tools_data = processor.pre_fetch_tools() - - agent = processor.create_agent( - docs_together=docs_together, docs=docs_list, tools_data=tools_data - ) - if error := self.check_usage(processor.agent_config): return error return Response( diff --git a/application/api/answer/services/conversation_service.py b/application/api/answer/services/conversation_service.py index 09baf3d1..00fc52cc 100644 --- a/application/api/answer/services/conversation_service.py +++ b/application/api/answer/services/conversation_service.py @@ -60,6 +60,7 @@ class ConversationService: is_shared_usage: bool = False, shared_token: Optional[str] = None, attachment_ids: Optional[List[str]] = None, + metadata: Optional[Dict[str, Any]] = None, ) -> str: """Save or update a conversation in the database""" if decoded_token is None: @@ -93,6 +94,11 @@ class ConversationService: f"queries.{index}.timestamp": current_time, f"queries.{index}.attachments": attachment_ids, f"queries.{index}.model_id": model_id, + **( + {f"queries.{index}.metadata": metadata} + if metadata + else {} + ), } }, ) @@ -124,6 +130,7 @@ class ConversationService: "timestamp": current_time, "attachments": attachment_ids, "model_id": model_id, + **({"metadata": metadata} if metadata else {}), } } }, @@ -156,22 +163,24 @@ class ConversationService: if not completion or not completion.strip(): completion = question[:50] if question else "New Conversation" + query_doc = { + "prompt": question, + "response": response, + "thought": thought, + "sources": sources, + "tool_calls": tool_calls, + "timestamp": current_time, + "attachments": attachment_ids, + "model_id": model_id, + } + if metadata: + query_doc["metadata"] = metadata + conversation_data = { "user": user_id, "date": current_time, "name": completion, - "queries": [ - { - "prompt": question, - "response": response, - "thought": thought, - "sources": sources, - "tool_calls": tool_calls, - "timestamp": current_time, - "attachments": attachment_ids, - "model_id": model_id, - } - ], + "queries": [query_doc], } if api_key: diff --git a/application/api/answer/services/stream_processor.py b/application/api/answer/services/stream_processor.py index 0cf47094..28a2e8d4 100644 --- a/application/api/answer/services/stream_processor.py +++ b/application/api/answer/services/stream_processor.py @@ -38,13 +38,23 @@ def get_prompt(prompt_id: str, prompts_collection=None) -> str: current_dir = Path(__file__).resolve().parents[3] prompts_dir = current_dir / "prompts" - preset_mapping = { + # Maps for classic agent types + CLASSIC_PRESETS = { "default": "chat_combine_default.txt", "creative": "chat_combine_creative.txt", "strict": "chat_combine_strict.txt", "reduce": "chat_reduce_prompt.txt", } + # Agentic counterparts — same styles, but with search tool instructions + AGENTIC_PRESETS = { + "default": "agentic/default.txt", + "creative": "agentic/creative.txt", + "strict": "agentic/strict.txt", + } + + preset_mapping = {**CLASSIC_PRESETS, **{f"agentic_{k}": v for k, v in AGENTIC_PRESETS.items()}} + if prompt_id in preset_mapping: file_path = os.path.join(prompts_dir, preset_mapping[prompt_id]) try: @@ -91,6 +101,7 @@ class StreamProcessor: self.is_shared_usage = False self.shared_token = None self.agent_id = self.data.get("agent_id") + self.agent_key = None self.model_id: Optional[str] = None self.conversation_service = ConversationService() self.compression_orchestrator = CompressionOrchestrator( @@ -111,6 +122,29 @@ class StreamProcessor: self._load_conversation_history() self._process_attachments() + def build_agent(self, question: str): + """One call to go from request data to a ready-to-run agent. + + Combines initialize(), pre_fetch_docs(), pre_fetch_tools(), and + create_agent() into a single convenience method. + """ + self.initialize() + + agent_type = self.agent_config.get("agent_type", "classic") + + # Agentic/research agents skip pre-fetch — the LLM searches on-demand via tools + if agent_type in ("agentic", "research"): + tools_data = self.pre_fetch_tools() + return self.create_agent(tools_data=tools_data) + + docs_together, docs_list = self.pre_fetch_docs(question) + tools_data = self.pre_fetch_tools() + return self.create_agent( + docs_together=docs_together, + docs=docs_list, + tools_data=tools_data, + ) + def _load_conversation_history(self): """Load conversation history either from DB or request""" if self.conversation_id and self.initial_user_id: @@ -124,9 +158,17 @@ class StreamProcessor: if settings.ENABLE_CONVERSATION_COMPRESSION: self._handle_compression(conversation) else: - # Original behavior - load all history + # Original behavior - load all history (include metadata if present) self.history = [ - {"prompt": query["prompt"], "response": query["response"]} + { + "prompt": query["prompt"], + "response": query["response"], + **( + {"metadata": query["metadata"]} + if "metadata" in query + else {} + ), + } for query in conversation.get("queries", []) ] else: @@ -135,14 +177,8 @@ class StreamProcessor: ) def _handle_compression(self, conversation: Dict[str, Any]): - """ - Handle conversation compression logic using orchestrator. - - Args: - conversation: Full conversation document - """ + """Handle conversation compression logic using orchestrator.""" try: - # Use orchestrator to handle all compression logic result = self.compression_orchestrator.compress_if_needed( conversation_id=self.conversation_id, user_id=self.initial_user_id, @@ -153,12 +189,15 @@ class StreamProcessor: if not result.success: logger.error(f"Compression failed: {result.error}, using full history") self.history = [ - {"prompt": query["prompt"], "response": query["response"]} + { + "prompt": query["prompt"], + "response": query["response"], + **({"metadata": query["metadata"]} if "metadata" in query else {}), + } for query in conversation.get("queries", []) ] return - # Set compressed summary if compression was performed if result.compression_performed and result.compressed_summary: self.compressed_summary = result.compressed_summary self.compressed_summary_tokens = TokenCounter.count_message_tokens( @@ -169,17 +208,27 @@ class StreamProcessor: f"+ {len(result.recent_queries)} recent messages" ) - # Build history from recent queries self.history = result.as_history() + # Preserve metadata from recent queries (as_history only has prompt/response) + recent = result.recent_queries if result.recent_queries else conversation.get("queries", []) + for i, entry in enumerate(self.history): + # Match by index from the end of recent queries + offset = len(recent) - len(self.history) + qi = offset + i + if 0 <= qi < len(recent) and "metadata" in recent[qi]: + entry["metadata"] = recent[qi]["metadata"] except Exception as e: logger.error( f"Error handling compression, falling back to standard history: {str(e)}", exc_info=True, ) - # Fallback to original behavior self.history = [ - {"prompt": query["prompt"], "response": query["response"]} + { + "prompt": query["prompt"], + "response": query["response"], + **({"metadata": query["metadata"]} if "metadata" in query else {}), + } for query in conversation.get("queries", []) ] @@ -191,9 +240,6 @@ class StreamProcessor: ) def _get_attachments_content(self, attachment_ids, user_id): - """ - Retrieve content from attachment documents based on their IDs. - """ if not attachment_ids: return [] attachments = [] @@ -202,7 +248,6 @@ class StreamProcessor: attachment_doc = self.attachments_collection.find_one( {"_id": ObjectId(attachment_id), "user": user_id} ) - if attachment_doc: attachments.append(attachment_doc) except Exception as e: @@ -232,7 +277,6 @@ class StreamProcessor: ) self.model_id = requested_model else: - # Check if agent has a default model configured agent_default_model = self.agent_config.get("default_model_id", "") if agent_default_model and validate_model_id(agent_default_model): self.model_id = agent_default_model @@ -285,7 +329,6 @@ class StreamProcessor: data["source"] = "default" else: data["source"] = None - # Handle multiple sources sources = data.get("sources", []) if sources and isinstance(sources, list): @@ -311,7 +354,6 @@ class StreamProcessor: else: data["sources"] = [] - # Preserve model configuration from agent data["default_model_id"] = data.get("default_model_id", "") return data @@ -376,7 +418,10 @@ class StreamProcessor: return None def _configure_agent(self): - """Configure the agent based on request data""" + """Configure the agent based on request data. + + Unified flow: resolve the effective API key, then extract config once. + """ agent_id = self._resolve_agent_id() self.agent_key, self.is_shared_usage, self.shared_token = self._get_agent_key( @@ -384,55 +429,37 @@ class StreamProcessor: ) self.agent_id = str(agent_id) if agent_id else None - api_key = self.data.get("api_key") - if api_key: - data_key = self._get_data_from_api_key(api_key) + # Determine the effective API key (explicit > agent-derived) + effective_key = self.data.get("api_key") or self.agent_key + + if effective_key: + data_key = self._get_data_from_api_key(effective_key) if data_key.get("_id"): self.agent_id = str(data_key.get("_id")) + self.agent_config.update( { "prompt_id": data_key.get("prompt_id", "default"), "agent_type": data_key.get("agent_type", settings.AGENT_NAME), - "user_api_key": api_key, + "user_api_key": effective_key, "json_schema": data_key.get("json_schema"), "default_model_id": data_key.get("default_model_id", ""), + "models": data_key.get("models", []), } ) - self.initial_user_id = data_key.get("user") - self.decoded_token = {"sub": data_key.get("user")} - if data_key.get("source"): - self.source = {"active_docs": data_key["source"]} - if data_key.get("workflow"): - self.agent_config["workflow"] = data_key["workflow"] - self.agent_config["workflow_owner"] = data_key.get("user") - if data_key.get("retriever"): - self.retriever_config["retriever_name"] = data_key["retriever"] - if data_key.get("chunks") is not None: - try: - self.retriever_config["chunks"] = int(data_key["chunks"]) - except (ValueError, TypeError): - logger.warning( - f"Invalid chunks value: {data_key['chunks']}, using default value 2" - ) - self.retriever_config["chunks"] = 2 - elif self.agent_key: - data_key = self._get_data_from_api_key(self.agent_key) - if data_key.get("_id"): - self.agent_id = str(data_key.get("_id")) - self.agent_config.update( - { - "prompt_id": data_key.get("prompt_id", "default"), - "agent_type": data_key.get("agent_type", settings.AGENT_NAME), - "user_api_key": self.agent_key, - "json_schema": data_key.get("json_schema"), - "default_model_id": data_key.get("default_model_id", ""), - } - ) - self.decoded_token = ( - self.decoded_token - if self.is_shared_usage - else {"sub": data_key.get("user")} - ) + + # Set identity context + if self.data.get("api_key"): + # External API key: use the key owner's identity + self.initial_user_id = data_key.get("user") + self.decoded_token = {"sub": data_key.get("user")} + elif self.is_shared_usage: + # Shared agent: keep the caller's identity + pass + else: + # Owner using their own agent + self.decoded_token = {"sub": data_key.get("user")} + if data_key.get("source"): self.source = {"active_docs": data_key["source"]} if data_key.get("workflow"): @@ -449,6 +476,7 @@ class StreamProcessor: ) self.retriever_config["chunks"] = 2 else: + # No API key — default/workflow configuration agent_type = settings.AGENT_NAME if self.data.get("workflow") and isinstance( self.data.get("workflow"), dict @@ -531,12 +559,7 @@ class StreamProcessor: return None, None def pre_fetch_tools(self) -> Optional[Dict[str, Any]]: - """Pre-fetch tool data for template rendering before agent creation - - Can be controlled via: - 1. Global setting: ENABLE_TOOL_PREFETCH in .env - 2. Per-request: disable_tool_prefetch in request data - """ + """Pre-fetch tool data for template rendering before agent creation""" if not settings.ENABLE_TOOL_PREFETCH: logger.info( "Tool pre-fetching disabled globally via ENABLE_TOOL_PREFETCH setting" @@ -755,11 +778,21 @@ class StreamProcessor: tools_data: Optional[Dict[str, Any]] = None, ): """Create and return the configured agent with rendered prompt""" + agent_type = self.agent_config["agent_type"] + + # For agentic agents, swap standard presets for their agentic + # counterparts (which include search tool instructions instead of + # {summaries}). Custom / user-provided prompts pass through as-is. raw_prompt = self._get_prompt_content() if raw_prompt is None: - raw_prompt = get_prompt( - self.agent_config["prompt_id"], self.prompts_collection - ) + prompt_id = self.agent_config.get("prompt_id", "default") + agentic_presets = {"default", "creative", "strict"} + if agent_type in ("agentic", "research") and prompt_id in agentic_presets: + raw_prompt = get_prompt( + f"agentic_{prompt_id}", self.prompts_collection + ) + else: + raw_prompt = get_prompt(prompt_id, self.prompts_collection) self._prompt_content = raw_prompt rendered_prompt = self.prompt_renderer.render_prompt( @@ -779,7 +812,35 @@ class StreamProcessor: ) system_api_key = get_api_key_for_provider(provider or settings.LLM_PROVIDER) - agent_type = self.agent_config["agent_type"] + # Create LLM and handler (dependency injection) + from application.llm.llm_creator import LLMCreator + from application.llm.handlers.handler_creator import LLMHandlerCreator + from application.agents.tool_executor import ToolExecutor + + # Compute backup models: agent's configured models minus the active one + agent_models = self.agent_config.get("models", []) + backup_models = [m for m in agent_models if m != self.model_id] + + llm = LLMCreator.create_llm( + provider or settings.LLM_PROVIDER, + api_key=system_api_key, + user_api_key=self.agent_config["user_api_key"], + decoded_token=self.decoded_token, + model_id=self.model_id, + agent_id=self.agent_id, + backup_models=backup_models, + ) + llm_handler = LLMHandlerCreator.create_handler( + provider if provider else "default" + ) + + user = self.decoded_token.get("sub") if self.decoded_token else None + tool_executor = ToolExecutor( + user_api_key=self.agent_config["user_api_key"], + user=user, + decoded_token=self.decoded_token, + ) + tool_executor.conversation_id = self.conversation_id # Base agent kwargs agent_kwargs = { @@ -796,10 +857,31 @@ class StreamProcessor: "attachments": self.attachments, "json_schema": self.agent_config.get("json_schema"), "compressed_summary": self.compressed_summary, + "llm": llm, + "llm_handler": llm_handler, + "tool_executor": tool_executor, } - # Workflow-specific kwargs for workflow agents - if agent_type == "workflow": + # Type-specific kwargs + if agent_type in ("agentic", "research"): + agent_kwargs["retriever_config"] = { + "source": self.source, + "retriever_name": self.retriever_config.get( + "retriever_name", "classic" + ), + "chunks": self.retriever_config.get("chunks", 2), + "doc_token_limit": self.retriever_config.get( + "doc_token_limit", 50000 + ), + "model_id": self.model_id, + "user_api_key": self.agent_config["user_api_key"], + "agent_id": self.agent_id, + "llm_name": provider or settings.LLM_PROVIDER, + "api_key": system_api_key, + "decoded_token": self.decoded_token, + } + + elif agent_type == "workflow": workflow_config = self.agent_config.get("workflow") if isinstance(workflow_config, str): agent_kwargs["workflow_id"] = workflow_config diff --git a/application/api/user/agents/routes.py b/application/api/user/agents/routes.py index 02b185c2..c95f225d 100644 --- a/application/api/user/agents/routes.py +++ b/application/api/user/agents/routes.py @@ -104,6 +104,8 @@ AGENT_TYPE_SCHEMAS = { } AGENT_TYPE_SCHEMAS["react"] = AGENT_TYPE_SCHEMAS["classic"] +AGENT_TYPE_SCHEMAS["agentic"] = AGENT_TYPE_SCHEMAS["classic"] +AGENT_TYPE_SCHEMAS["research"] = AGENT_TYPE_SCHEMAS["classic"] AGENT_TYPE_SCHEMAS["openai"] = AGENT_TYPE_SCHEMAS["classic"] diff --git a/application/api/user/base.py b/application/api/user/base.py index d4f70aa6..afc22268 100644 --- a/application/api/user/base.py +++ b/application/api/user/base.py @@ -145,14 +145,22 @@ def resolve_tool_details(tool_ids): Returns: List of tool details with id, name, and display_name """ + valid_ids = [] + for tid in tool_ids: + try: + valid_ids.append(ObjectId(tid)) + except Exception: + continue tools = user_tools_collection.find( - {"_id": {"$in": [ObjectId(tid) for tid in tool_ids]}} - ) + {"_id": {"$in": valid_ids}} + ) if valid_ids else [] return [ { "id": str(tool["_id"]), "name": tool.get("name", ""), - "display_name": tool.get("displayName", tool.get("name", "")), + "display_name": tool.get("customName") + or tool.get("displayName") + or tool.get("name", ""), } for tool in tools ] diff --git a/application/app.py b/application/app.py index bb9b3b1b..aed069e0 100644 --- a/application/app.py +++ b/application/app.py @@ -72,6 +72,11 @@ def home(): return "Welcome to DocsGPT Backend!" +@app.route("/api/health") +def health(): + return jsonify({"status": "ok"}) + + @app.route("/api/config") def get_config(): response = { diff --git a/application/core/model_configs.py b/application/core/model_configs.py index 841be925..f8e70759 100644 --- a/application/core/model_configs.py +++ b/application/core/model_configs.py @@ -27,6 +27,8 @@ ANTHROPIC_ATTACHMENTS = IMAGE_ATTACHMENTS OPENROUTER_ATTACHMENTS = IMAGE_ATTACHMENTS +NOVITA_ATTACHMENTS = IMAGE_ATTACHMENTS + OPENAI_MODELS = [ AvailableModel( @@ -193,6 +195,46 @@ OPENROUTER_MODELS = [ ), ] +NOVITA_MODELS = [ + AvailableModel( + id="moonshotai/kimi-k2.5", + provider=ModelProvider.NOVITA, + display_name="Kimi K2.5", + description="MoE model with function calling, structured output, reasoning, and vision", + capabilities=ModelCapabilities( + supports_tools=True, + supports_structured_output=True, + supported_attachment_types=NOVITA_ATTACHMENTS, + context_window=262144, + ), + ), + AvailableModel( + id="zai-org/glm-5", + provider=ModelProvider.NOVITA, + display_name="GLM-5", + description="MoE model with function calling, structured output, and reasoning", + capabilities=ModelCapabilities( + supports_tools=True, + supports_structured_output=True, + supported_attachment_types=[], + context_window=202800, + ), + ), + AvailableModel( + id="minimax/minimax-m2.5", + provider=ModelProvider.NOVITA, + display_name="MiniMax M2.5", + description="MoE model with function calling, structured output, and reasoning", + capabilities=ModelCapabilities( + supports_tools=True, + supports_structured_output=True, + supported_attachment_types=[], + context_window=204800, + ), + ), +] + + AZURE_OPENAI_MODELS = [ AvailableModel( id="azure-gpt-4", diff --git a/application/core/model_settings.py b/application/core/model_settings.py index 475045d1..044c426b 100644 --- a/application/core/model_settings.py +++ b/application/core/model_settings.py @@ -114,6 +114,10 @@ class ModelRegistry: settings.LLM_PROVIDER == "openrouter" and settings.API_KEY ): self._add_openrouter_models(settings) + if settings.NOVITA_API_KEY or ( + settings.LLM_PROVIDER == "novita" and settings.API_KEY + ): + self._add_novita_models(settings) if settings.HUGGINGFACE_API_KEY or ( settings.LLM_PROVIDER == "huggingface" and settings.API_KEY ): @@ -245,6 +249,21 @@ class ModelRegistry: for model in OPENROUTER_MODELS: self.models[model.id] = model + def _add_novita_models(self, settings): + from application.core.model_configs import NOVITA_MODELS + + if settings.NOVITA_API_KEY: + for model in NOVITA_MODELS: + self.models[model.id] = model + return + if settings.LLM_PROVIDER == "novita" and settings.LLM_NAME: + for model in NOVITA_MODELS: + if model.id == settings.LLM_NAME: + self.models[model.id] = model + return + for model in NOVITA_MODELS: + self.models[model.id] = model + def _add_docsgpt_models(self, settings): model_id = "docsgpt-local" model = AvailableModel( diff --git a/application/core/model_utils.py b/application/core/model_utils.py index 94dc8973..95a6a0d7 100644 --- a/application/core/model_utils.py +++ b/application/core/model_utils.py @@ -10,6 +10,7 @@ def get_api_key_for_provider(provider: str) -> Optional[str]: provider_key_map = { "openai": settings.OPENAI_API_KEY, "openrouter": settings.OPEN_ROUTER_API_KEY, + "novita": settings.NOVITA_API_KEY, "anthropic": settings.ANTHROPIC_API_KEY, "google": settings.GOOGLE_API_KEY, "groq": settings.GROQ_API_KEY, diff --git a/application/core/settings.py b/application/core/settings.py index 6d260f77..9dbc1584 100644 --- a/application/core/settings.py +++ b/application/core/settings.py @@ -5,9 +5,7 @@ from typing import Optional from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict -current_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -) +current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) class Settings(BaseSettings): @@ -15,15 +13,11 @@ class Settings(BaseSettings): AUTH_TYPE: Optional[str] = None # simple_jwt, session_jwt, or None LLM_PROVIDER: str = "docsgpt" - LLM_NAME: Optional[str] = ( - None # if LLM_PROVIDER is openai, LLM_NAME can be gpt-4 or gpt-3.5-turbo - ) + LLM_NAME: Optional[str] = None # if LLM_PROVIDER is openai, LLM_NAME can be gpt-4 or gpt-3.5-turbo EMBEDDINGS_NAME: str = "huggingface_sentence-transformers/all-mpnet-base-v2" EMBEDDINGS_BASE_URL: Optional[str] = None # Remote embeddings API URL (OpenAI-compatible) - EMBEDDINGS_KEY: Optional[str] = ( - None # api key for embeddings (if using openai, just copy API_KEY) - ) - + EMBEDDINGS_KEY: Optional[str] = None # api key for embeddings (if using openai, just copy API_KEY) + CELERY_BROKER_URL: str = "redis://localhost:6379/0" CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1" MONGO_URI: str = "mongodb://localhost:27017/docsgpt" @@ -45,9 +39,7 @@ class Settings(BaseSettings): PARSE_IMAGE_REMOTE: bool = False DOCLING_OCR_ENABLED: bool = False # Enable OCR for docling parsers (PDF, images) DOCLING_OCR_ATTACHMENTS_ENABLED: bool = False # Enable OCR for docling when parsing attachments - VECTOR_STORE: str = ( - "faiss" # "faiss" or "elasticsearch" or "qdrant" or "milvus" or "lancedb" or "pgvector" - ) + VECTOR_STORE: str = "faiss" # "faiss" or "elasticsearch" or "qdrant" or "milvus" or "lancedb" or "pgvector" RETRIEVERS_ENABLED: list = ["classic_rag"] AGENT_NAME: str = "classic" FALLBACK_LLM_PROVIDER: Optional[str] = None # provider for fallback llm @@ -55,12 +47,8 @@ class Settings(BaseSettings): FALLBACK_LLM_API_KEY: Optional[str] = None # api key for fallback llm # Google Drive integration - GOOGLE_CLIENT_ID: Optional[str] = ( - None # Replace with your actual Google OAuth client ID - ) - GOOGLE_CLIENT_SECRET: Optional[str] = ( - None # Replace with your actual Google OAuth client secret - ) + GOOGLE_CLIENT_ID: Optional[str] = None # Replace with your actual Google OAuth client ID + GOOGLE_CLIENT_SECRET: Optional[str] = None # Replace with your actual Google OAuth client secret CONNECTOR_REDIRECT_BASE_URI: Optional[str] = ( "http://127.0.0.1:7091/api/connectors/callback" ##add redirect url as it is to your provider's console(gcp) ) @@ -72,7 +60,7 @@ class Settings(BaseSettings): MICROSOFT_AUTHORITY: Optional[str] = None # e.g., "https://login.microsoftonline.com/{tenant_id}" # GitHub source - GITHUB_ACCESS_TOKEN: Optional[str] = None # PAT token with read repo access + GITHUB_ACCESS_TOKEN: Optional[str] = None # PAT token with read repo access # LLM Cache CACHE_REDIS_URL: str = "redis://localhost:6379/2" @@ -90,16 +78,13 @@ class Settings(BaseSettings): GROQ_API_KEY: Optional[str] = None HUGGINGFACE_API_KEY: Optional[str] = None OPEN_ROUTER_API_KEY: Optional[str] = None + NOVITA_API_KEY: Optional[str] = None OPENAI_API_BASE: Optional[str] = None # azure openai api base url OPENAI_API_VERSION: Optional[str] = None # azure openai api version AZURE_DEPLOYMENT_NAME: Optional[str] = None # azure deployment name for answering - AZURE_EMBEDDINGS_DEPLOYMENT_NAME: Optional[str] = ( - None # azure deployment name for embeddings - ) - OPENAI_BASE_URL: Optional[str] = ( - None # openai base url for open ai compatable models - ) + AZURE_EMBEDDINGS_DEPLOYMENT_NAME: Optional[str] = None # azure deployment name for embeddings + OPENAI_BASE_URL: Optional[str] = None # openai base url for open ai compatable models # elasticsearch ELASTIC_CLOUD_ID: Optional[str] = None # cloud id for elasticsearch @@ -141,9 +126,7 @@ class Settings(BaseSettings): # LanceDB vectorstore config LANCEDB_PATH: str = "./data/lancedb" # Path where LanceDB stores its local data - LANCEDB_TABLE_NAME: Optional[str] = ( - "docsgpts" # Name of the table to use for storing vectors - ) + LANCEDB_TABLE_NAME: Optional[str] = "docsgpts" # Name of the table to use for storing vectors FLASK_DEBUG_MODE: bool = False STORAGE_TYPE: str = "local" # local or s3 @@ -180,6 +163,7 @@ class Settings(BaseSettings): "GOOGLE_API_KEY", "GROQ_API_KEY", "HUGGINGFACE_API_KEY", + "NOVITA_API_KEY", "EMBEDDINGS_KEY", "FALLBACK_LLM_API_KEY", "QDRANT_API_KEY", diff --git a/application/llm/base.py b/application/llm/base.py index 9a0f249e..c8e30b4f 100644 --- a/application/llm/base.py +++ b/application/llm/base.py @@ -16,22 +16,61 @@ class BaseLLM(ABC): agent_id=None, model_id=None, base_url=None, + backup_models=None, ): self.decoded_token = decoded_token self.agent_id = str(agent_id) if agent_id else None self.model_id = model_id self.base_url = base_url self.token_usage = {"prompt_tokens": 0, "generated_tokens": 0} + self._backup_models = backup_models or [] self._fallback_llm = None - self._fallback_sequence_index = 0 @property def fallback_llm(self): - """Lazy-loaded fallback LLM from FALLBACK_* settings.""" - if self._fallback_llm is None and settings.FALLBACK_LLM_PROVIDER: - try: - from application.llm.llm_creator import LLMCreator + """Lazy-loaded fallback LLM: tries per-agent backup models first, + then the global FALLBACK_* settings.""" + if self._fallback_llm is not None: + return self._fallback_llm + from application.llm.llm_creator import LLMCreator + from application.core.model_utils import ( + get_provider_from_model_id, + get_api_key_for_provider, + ) + + # Try per-agent backup models first + for backup_model_id in self._backup_models: + try: + provider = get_provider_from_model_id(backup_model_id) + if not provider: + logger.warning( + f"Could not resolve provider for backup model: {backup_model_id}" + ) + continue + api_key = get_api_key_for_provider(provider) + self._fallback_llm = LLMCreator.create_llm( + provider, + api_key=api_key, + user_api_key=getattr(self, "user_api_key", None), + decoded_token=self.decoded_token, + model_id=backup_model_id, + agent_id=self.agent_id, + ) + logger.info( + f"Fallback LLM initialized from agent backup model: " + f"{provider}/{backup_model_id}" + ) + return self._fallback_llm + except Exception as e: + logger.warning( + f"Failed to initialize backup model {backup_model_id}: {str(e)}" + ) + continue + + # Fall back to global FALLBACK_* settings + if settings.FALLBACK_LLM_PROVIDER: + try: self._fallback_llm = LLMCreator.create_llm( settings.FALLBACK_LLM_PROVIDER, api_key=settings.FALLBACK_LLM_API_KEY or settings.API_KEY, @@ -41,12 +80,14 @@ class BaseLLM(ABC): agent_id=self.agent_id, ) logger.info( - f"Fallback LLM initialized: {settings.FALLBACK_LLM_PROVIDER}/{settings.FALLBACK_LLM_NAME}" + f"Fallback LLM initialized from global settings: " + f"{settings.FALLBACK_LLM_PROVIDER}/{settings.FALLBACK_LLM_NAME}" ) except Exception as e: logger.error( f"Failed to initialize fallback LLM: {str(e)}", exc_info=True ) + return self._fallback_llm @staticmethod @@ -74,20 +115,60 @@ class BaseLLM(ABC): method = decorator(method) return method(self, *args, **kwargs) + is_stream = "stream" in method_name + + if is_stream: + return self._stream_with_fallback( + decorated_method, method_name, *args, **kwargs + ) + try: return decorated_method() except Exception as e: if not self.fallback_llm: logger.error(f"Primary LLM failed and no fallback configured: {str(e)}") raise + fallback = self.fallback_llm logger.warning( - f"Primary LLM failed. Falling back to {settings.FALLBACK_LLM_PROVIDER}/{settings.FALLBACK_LLM_NAME}. Error: {str(e)}" + f"Primary LLM failed. Falling back to " + f"{fallback.model_id}. Error: {str(e)}" ) fallback_method = getattr( - self.fallback_llm, method_name.replace("_raw_", "") + fallback, method_name.replace("_raw_", "") ) - return fallback_method(*args, **kwargs) + fallback_kwargs = {**kwargs, "model": fallback.model_id} + return fallback_method(*args, **fallback_kwargs) + + def _stream_with_fallback( + self, decorated_method, method_name, *args, **kwargs + ): + """ + Wrapper generator that catches mid-stream errors and falls back. + + Unlike non-streaming calls where exceptions are raised immediately, + streaming generators raise exceptions during iteration. This wrapper + ensures that if the primary LLM fails at any point during streaming + (creation or mid-stream), we fall back to the backup model. + """ + try: + yield from decorated_method() + except Exception as e: + if not self.fallback_llm: + logger.error( + f"Primary LLM failed and no fallback configured: {str(e)}" + ) + raise + fallback = self.fallback_llm + logger.warning( + f"Primary LLM failed mid-stream. Falling back to " + f"{fallback.model_id}. Error: {str(e)}" + ) + fallback_method = getattr( + fallback, method_name.replace("_raw_", "") + ) + fallback_kwargs = {**kwargs, "model": fallback.model_id} + yield from fallback_method(*args, **fallback_kwargs) def gen(self, model, messages, stream=False, tools=None, *args, **kwargs): decorators = [gen_token_usage, gen_cache] diff --git a/application/llm/handlers/handler_creator.py b/application/llm/handlers/handler_creator.py index 1560eea1..78409ded 100644 --- a/application/llm/handlers/handler_creator.py +++ b/application/llm/handlers/handler_creator.py @@ -7,6 +7,7 @@ class LLMHandlerCreator: handlers = { "openai": OpenAILLMHandler, "google": GoogleLLMHandler, + "novita": OpenAILLMHandler, # Novita uses OpenAI-compatible API "default": OpenAILLMHandler, } diff --git a/application/llm/llm_creator.py b/application/llm/llm_creator.py index 9f444359..d01fa2d8 100644 --- a/application/llm/llm_creator.py +++ b/application/llm/llm_creator.py @@ -38,6 +38,7 @@ class LLMCreator: decoded_token, model_id=None, agent_id=None, + backup_models=None, *args, **kwargs, ): @@ -59,6 +60,7 @@ class LLMCreator: model_id=model_id, agent_id=agent_id, base_url=base_url, + backup_models=backup_models, *args, **kwargs, ) diff --git a/application/llm/novita.py b/application/llm/novita.py index b741c4f3..6a5de742 100644 --- a/application/llm/novita.py +++ b/application/llm/novita.py @@ -1,13 +1,13 @@ from application.core.settings import settings from application.llm.openai import OpenAILLM -NOVITA_BASE_URL = "https://api.novita.ai/v3/openai" +NOVITA_BASE_URL = "https://api.novita.ai/openai" class NovitaLLM(OpenAILLM): def __init__(self, api_key=None, user_api_key=None, base_url=None, *args, **kwargs): super().__init__( - api_key=api_key or settings.API_KEY, + api_key=api_key or settings.NOVITA_API_KEY or settings.API_KEY, user_api_key=user_api_key, base_url=base_url or NOVITA_BASE_URL, *args, diff --git a/application/llm/openai.py b/application/llm/openai.py index d3ece394..8bfc3cef 100644 --- a/application/llm/openai.py +++ b/application/llm/openai.py @@ -101,9 +101,13 @@ class OpenAILLM(BaseLLM): for item in content: if "function_call" in item: # Function calls need their own message - cleaned_args = self._remove_null_values( - item["function_call"]["args"] - ) + args = item["function_call"]["args"] + if isinstance(args, str): + try: + args = json.loads(args) + except (json.JSONDecodeError, TypeError): + pass + cleaned_args = self._remove_null_values(args) tool_call = { "id": item["function_call"]["call_id"], "type": "function", diff --git a/application/prompts/agentic/creative.txt b/application/prompts/agentic/creative.txt new file mode 100644 index 00000000..a1360b76 --- /dev/null +++ b/application/prompts/agentic/creative.txt @@ -0,0 +1,16 @@ +You are a helpful AI assistant, DocsGPT. You are proactive and helpful. Try to use tools, if they are available to you, +be proactive and fill in missing information. +Users can Upload documents for your context as attachments or sources via UI using the Conversation input box. +If appropriate, your answers can include code examples, formatted as follows: +```(language) +(code) +``` +Users are also able to see charts and diagrams if you use them with valid mermaid syntax in your responses. +Try to respond with mermaid charts if visualization helps with users queries. +You effectively utilize chat history, ensuring relevant and tailored responses. +You have access to a search tool that searches the user's uploaded documents and knowledge base. +Use the search_internal tool to find relevant information before answering questions. +You may search multiple times with different queries if needed. +Do not guess when documents are available — search first, then answer based on what you find. +If no relevant documents are found, use your general knowledge and tool capabilities. +Allow yourself to be very creative and use your imagination. diff --git a/application/prompts/agentic/default.txt b/application/prompts/agentic/default.txt new file mode 100644 index 00000000..a2118414 --- /dev/null +++ b/application/prompts/agentic/default.txt @@ -0,0 +1,15 @@ +You are a helpful AI assistant, DocsGPT. You are proactive and helpful. Try to use tools, if they are available to you, +be proactive and fill in missing information. +Users can Upload documents for your context as attachments or sources via UI using the Conversation input box. +If appropriate, your answers can include code examples, formatted as follows: +```(language) +(code) +``` +Users are also able to see charts and diagrams if you use them with valid mermaid syntax in your responses. +Try to respond with mermaid charts if visualization helps with users queries. +You effectively utilize chat history, ensuring relevant and tailored responses. +You have access to a search tool that searches the user's uploaded documents and knowledge base. +Use the search_internal tool to find relevant information before answering questions. +You may search multiple times with different queries if needed. +Do not guess when documents are available — search first, then answer based on what you find. +If no relevant documents are found, use your general knowledge and tool capabilities. diff --git a/application/prompts/agentic/strict.txt b/application/prompts/agentic/strict.txt new file mode 100644 index 00000000..dd327918 --- /dev/null +++ b/application/prompts/agentic/strict.txt @@ -0,0 +1,16 @@ +You are a helpful AI assistant, DocsGPT. You are proactive and helpful. Try to use tools, if they are available to you, +be proactive and fill in missing information. +Users can Upload documents for your context as attachments or sources via UI using the Conversation input box. +If appropriate, your answers can include code examples, formatted as follows: +```(language) +(code) +``` +Users are also able to see charts and diagrams if you use them with valid mermaid syntax in your responses. +Try to respond with mermaid charts if visualization helps with users queries. +You effectively utilize chat history, ensuring relevant and tailored responses. +You have access to a search tool that searches the user's uploaded documents and knowledge base. +Use the search_internal tool to find relevant information before answering questions. +You may search multiple times with different queries if needed. +You MUST search before answering any factual question. Do not guess or use general knowledge when documents are available. +If you dont have enough information from the search results or tools, answer "I don't know" or "I don't have enough information". +Never make up information or provide false information! diff --git a/application/prompts/react_final_prompt.txt b/application/prompts/react_final_prompt.txt deleted file mode 100644 index 50916127..00000000 --- a/application/prompts/react_final_prompt.txt +++ /dev/null @@ -1,3 +0,0 @@ -Query: {query} -Observations: {observations} -Now, using the insights from the observations, formulate a well-structured and precise final answer. \ No newline at end of file diff --git a/application/prompts/react_planning_prompt.txt b/application/prompts/react_planning_prompt.txt deleted file mode 100644 index c3571845..00000000 --- a/application/prompts/react_planning_prompt.txt +++ /dev/null @@ -1,13 +0,0 @@ -You are an AI assistant and talk like you're thinking out loud. Given the following query, outline a concise thought process that includes key steps and considerations necessary for effective analysis and response. Avoid pointwise formatting. The goal is to break down the query into manageable components without excessive detail, focusing on clarity and logical progression. - -Include the following elements in your thought and execution process: -1. Identify the main objective of the query. -2. Determine any relevant context or background information needed to understand the query. -3. List potential approaches or methods to address the query. -4. Highlight any critical factors or constraints that may influence the outcome. -5. Plan with available tools to help you with the analysis but dont execute them. Tools will be executed by another AI. - -Query: {query} -Summaries: {summaries} -Prompt: {prompt} -Observations(potentially previous tool calls): {observations} diff --git a/application/prompts/research/clarification.txt b/application/prompts/research/clarification.txt new file mode 100644 index 00000000..499d46e3 --- /dev/null +++ b/application/prompts/research/clarification.txt @@ -0,0 +1,23 @@ +You are a research assistant evaluating whether a user's question is clear enough to begin in-depth research. + +Decide whether the question can be researched as-is, or whether you need to ask clarifying questions first. + +Proceed WITHOUT clarification if: +- The question has a clear topic and intent +- There is enough context to form a research plan +- The question is broad but can be broken into sub-topics +- Documents/sources are available to search + +Ask for clarification ONLY if: +- The question is critically ambiguous (e.g., "review the thing" with no context about what "thing" means) +- Key information is missing that would make research impossible (not just imperfect) +- The user seems to reference something specific but hasn't provided it + +Err on the side of proceeding. Most questions are clear enough to start researching. + +You MUST respond with ONLY a valid JSON object (no markdown, no code fences): +{ + "needs_clarification": true or false, + "questions": ["question 1", "question 2"] (only if needs_clarification is true, max 3 questions), + "reason": "brief explanation of why clarification is or isn't needed" +} diff --git a/application/prompts/research/planning.txt b/application/prompts/research/planning.txt new file mode 100644 index 00000000..74c84a94 --- /dev/null +++ b/application/prompts/research/planning.txt @@ -0,0 +1,23 @@ +You are a research planner. Your job is to analyze the user's question and create a focused research plan. + +IMPORTANT: Every step must be a concrete research action — something you can search for and find information about. Never generate steps that ask the user for more information or request documents. Work with what you have. + +Assess the question's complexity: +- "simple": Can be answered with 1-2 focused searches (e.g., "What is our refund policy?") +- "moderate": Needs 3-4 searches across different topics (e.g., "Compare our pricing across product lines") +- "complex": Requires 5-6 searches with synthesis (e.g., "Audit our compliance docs against regulation X") + +Break the question into sub-questions appropriate to its complexity. Don't over-decompose simple questions. + +Consider: +- What distinct aspects of the question need separate investigation? +- What order should they be investigated in? +- Are there any dependencies between sub-questions? + +You MUST respond with ONLY a valid JSON object in this exact format (no markdown, no code fences): +{ + "complexity": "simple|moderate|complex", + "steps": [ + {"query": "specific sub-question to investigate", "rationale": "why this step is needed"} + ] +} diff --git a/application/prompts/research/step.txt b/application/prompts/research/step.txt new file mode 100644 index 00000000..c0bf48a0 --- /dev/null +++ b/application/prompts/research/step.txt @@ -0,0 +1,13 @@ +You are a research agent investigating a specific topic. Your goal is to find comprehensive, accurate information. + +Your current research task: {step_query} + +Instructions: +1. Use the available tools to search for and gather relevant information. +2. If you have a search_internal tool, use it to search the knowledge base. You may search multiple times with different queries. +3. If you have a reason_think tool, use it to analyze what you've found before drawing conclusions. +4. After gathering sufficient information, provide a detailed summary of your findings. +5. Cite specific documents and passages you found. Reference sources by their titles or filenames. +6. If you cannot find relevant information through tools, use your general knowledge but clearly indicate this. + +Be thorough — prefer completeness over brevity. Include all relevant details you find. diff --git a/application/prompts/research/synthesis.txt b/application/prompts/research/synthesis.txt new file mode 100644 index 00000000..30ea3958 --- /dev/null +++ b/application/prompts/research/synthesis.txt @@ -0,0 +1,22 @@ +You are compiling a comprehensive research report based on multiple research steps. + +Original question: {question} + +Research plan: +{plan_summary} + +Intermediate findings: +{findings} + +Write a well-structured, thorough report that: +1. Directly addresses the user's original question +2. Synthesizes findings from all research steps into a coherent narrative +3. Uses inline citations [N] for every factual claim, where N maps to the source list below +4. Highlights key insights and connections across different research steps +5. Notes any gaps or areas where information was insufficient +6. Includes a "References" section at the end listing all cited sources + +Available sources for citation: +{references} + +Format the report with clear headings and sections. Be comprehensive but well-organized. diff --git a/application/seed/config/premade_agents.yaml b/application/seed/config/premade_agents.yaml index cd895bec..df404156 100644 --- a/application/seed/config/premade_agents.yaml +++ b/application/seed/config/premade_agents.yaml @@ -18,7 +18,7 @@ agents: - name: "Researcher" description: "A specialized research agent that performs deep dives into subjects." image: "https://d3dg1063dc54p9.cloudfront.net/imgs/agents/agent-researcher.svg" - agent_type: "react" + agent_type: "classic" prompt: name: "Researcher-Agent" content: | diff --git a/application/templates/template_engine.py b/application/templates/template_engine.py index cd779a83..37a55dd6 100644 --- a/application/templates/template_engine.py +++ b/application/templates/template_engine.py @@ -3,12 +3,12 @@ from typing import Any, Dict, List, Optional, Set from jinja2 import ( ChainableUndefined, - Environment, nodes, select_autoescape, TemplateSyntaxError, ) -from jinja2.exceptions import UndefinedError +from jinja2.exceptions import SecurityError, UndefinedError +from jinja2.sandbox import SandboxedEnvironment logger = logging.getLogger(__name__) @@ -23,7 +23,7 @@ class TemplateEngine: """Jinja2-based template engine for dynamic prompt rendering""" def __init__(self): - self._env = Environment( + self._env = SandboxedEnvironment( undefined=ChainableUndefined, trim_blocks=True, lstrip_blocks=True, @@ -57,6 +57,10 @@ class TemplateEngine: error_msg = f"Undefined variable in template: {e.message}" logger.error(error_msg) raise TemplateRenderError(error_msg) from e + except SecurityError as e: + error_msg = f"Template security violation: {str(e)}" + logger.error(error_msg) + raise TemplateRenderError(error_msg) from e except Exception as e: error_msg = f"Template rendering failed: {str(e)}" logger.error(error_msg) diff --git a/docs/content/Agents/basics.mdx b/docs/content/Agents/basics.mdx index d701d998..3ad407df 100644 --- a/docs/content/Agents/basics.mdx +++ b/docs/content/Agents/basics.mdx @@ -44,36 +44,40 @@ The main set of instructions or system [prompt](/Guides/Customising-prompts) tha ## Understanding Agent Types -DocsGPT allows for different "types" of agents, each with a distinct way of processing information and generating responses. The code for these agent types can be found in the `application/agents/` directory. +DocsGPT supports several agent types, each with a distinct way of processing information. The code for these can be found in the `application/agents/` directory. -### 1. Classic Agent (`classic_agent.py`) +### 1. Classic Agent -**How it works:** The Classic Agent follows a traditional Retrieval Augmented Generation (RAG) approach. - 1. **Retrieve:** When a query is made, it first searches the selected Source documents for relevant information. - 2. **Augment:** This retrieved data is then added to the context, along with the main Prompt and the user's query. - 3. **Generate:** The LLM generates a response based on this augmented context. It can also utilize any configured tools if the LLM decides they are necessary. +The Classic Agent follows a traditional Retrieval Augmented Generation (RAG) approach: it retrieves relevant document chunks, augments the prompt context with them, and generates a response. It can also use configured tools if the LLM decides they are necessary. -**Best for:** - * Direct question-answering over a specific set of documents. - * Tasks where the primary goal is to extract and synthesize information from the provided sources. - * Simpler tool integrations where the decision to use a tool is straightforward. +**Best for:** Direct question-answering over a specific set of documents and straightforward tool use. -### 2. ReAct Agent (`react_agent.py`) +### 2. Agentic Agent -**How it works:** The ReAct Agent employs a more sophisticated "Reason and Act" framework. This involves a multi-step process: - 1. **Plan (Thought):** Based on the query, its prompt, and available tools/sources, the LLM first generates a plan or a sequence of thoughts on how to approach the problem. You might see this output as a "thought" process during generation. - 2. **Act:** The agent then executes actions based on this plan. This might involve querying its sources, using a tool, or performing internal reasoning. - 3. **Observe:** It gathers observations from the results of its actions (e.g., data from a tool, snippets from documents). - 4. **Repeat (if necessary):** Steps 2 and 3 can be repeated as the agent refines its approach or gathers more information. - 5. **Conclude:** Finally, it generates the final answer based on the initial query and all accumulated observations. +Unlike Classic which pre-fetches documents into the prompt, the Agentic Agent gives the LLM an `internal_search` tool so it can decide **when, what, and whether** to search. This means the LLM controls its own retrieval — it can search multiple times, refine queries, or skip retrieval entirely if the question doesn't need it. -**Best for:** - * More complex tasks that require multi-step reasoning or problem-solving. - * Scenarios where the agent needs to dynamically decide which tools to use and in what order, based on intermediate results. - * Interactive tasks where the agent needs to "think" through a problem. +**Best for:** Tasks where the agent needs to dynamically decide how to gather information, use multiple tools in sequence, or combine retrieval with external tool calls. + +### 3. Research Agent + +A multi-phase agent designed for in-depth research tasks: +1. **Clarification** — Determines if the question needs clarification before proceeding. +2. **Planning** — Decomposes the question into research steps with adaptive depth based on complexity. +3. **Research** — Executes each step, calling tools and refining queries as needed. +4. **Synthesis** — Compiles findings into a final cited report. + +Includes budget controls for max steps, timeout, and token limits to keep research bounded. + +**Best for:** Complex questions that require multi-step investigation, gathering information from multiple sources, and producing structured reports with citations. + +### 4. Workflow Agent + +Executes predefined workflows composed of connected nodes (AI Agent, Set State, Condition). See the [Workflow Nodes](/Agents/nodes) page for details on building workflows. + +**Best for:** Structured, multi-step processes with branching logic and shared state between steps. -Developers looking to introduce new agent architectures can explore the `application/agents/` directory. `classic_agent.py` and `react_agent.py` serve as excellent starting points, demonstrating how to inherit from `BaseAgent` and structure agent logic. +The legacy "ReAct" agent type is still accepted for backwards compatibility but maps to the Classic Agent internally. New agents should use Classic, Agentic, or Research instead. ## Navigating and Managing Agents in DocsGPT diff --git a/docs/content/Deploying/Amazon-Lightsail.mdx b/docs/content/Deploying/Amazon-Lightsail.mdx index 24aef74b..a83e6959 100644 --- a/docs/content/Deploying/Amazon-Lightsail.mdx +++ b/docs/content/Deploying/Amazon-Lightsail.mdx @@ -70,9 +70,9 @@ Inside the DocsGPT folder create a `.env` file and copy the contents of `.env_sa Make sure your `.env` file looks like this: ``` -OPENAI_API_KEY=(Your OpenAI API key) +API_KEY= +LLM_NAME=docsgpt VITE_API_STREAMING=true -SELF_HOSTED_MODEL=false ``` To save the file, press CTRL+X, then Y, and then ENTER. diff --git a/docs/content/Deploying/DocsGPT-Settings.mdx b/docs/content/Deploying/DocsGPT-Settings.mdx index b51f3d6d..42c7963a 100644 --- a/docs/content/Deploying/DocsGPT-Settings.mdx +++ b/docs/content/Deploying/DocsGPT-Settings.mdx @@ -104,7 +104,7 @@ DocsGPT can transcribe audio in two places: - Voice input in the chat. - Audio file ingestion. Uploaded `.wav`, `.mp3`, `.m4a`, `.ogg`, and `.webm` files are transcribed first and then passed through the normal parser, chunking, embedding, and indexing pipeline. -For an end-to-end walkthrough, see the [Speech and Audio Guide](/Guides/speech-and-audio). +The settings below control speech-to-text behaviour for both voice input and audio file ingestion. | Setting | Purpose | Typical values | | --- | --- | --- | @@ -214,6 +214,31 @@ If you have configured `AUTH_TYPE=simple_jwt`, the DocsGPT frontend will prompt }} /> +## S3 Storage Backend + +By default DocsGPT stores files locally. Set `STORAGE_TYPE=s3` to use Amazon S3 instead. + +| Setting | Description | Default | +| --- | --- | --- | +| `STORAGE_TYPE` | `local` or `s3` | `local` | +| `S3_BUCKET_NAME` | S3 bucket name | `docsgpt-test-bucket` | +| `SAGEMAKER_ACCESS_KEY` | AWS access key ID | — | +| `SAGEMAKER_SECRET_KEY` | AWS secret access key | — | +| `SAGEMAKER_REGION` | AWS region | — | +| `URL_STRATEGY` | `backend` (proxy through API) or `s3` (direct S3 URLs) | `backend` | + +The S3 credentials use `SAGEMAKER_*` variable names because they are shared with the SageMaker integration. + +```env +STORAGE_TYPE=s3 +S3_BUCKET_NAME=your-bucket-name +SAGEMAKER_ACCESS_KEY=your-aws-access-key-id +SAGEMAKER_SECRET_KEY=your-aws-secret-access-key +SAGEMAKER_REGION=us-east-1 +``` + +Your IAM user needs these permissions on the bucket: `s3:PutObject`, `s3:GetObject`, `s3:DeleteObject`, `s3:ListBucket`, `s3:HeadObject`. + ## Exploring More Settings These are just the basic settings to get you started. The `settings.py` file contains many more advanced options that you can explore to further customize DocsGPT, such as: diff --git a/docs/content/Deploying/Railway.mdx b/docs/content/Deploying/Railway.mdx index 89928ab2..d6e74a24 100644 --- a/docs/content/Deploying/Railway.mdx +++ b/docs/content/Deploying/Railway.mdx @@ -86,13 +86,9 @@ Make sure your `.env` file looks like this: ``` - -OPENAI_API_KEY=(Your OpenAI API key) - +API_KEY= +LLM_NAME=docsgpt VITE_API_STREAMING=true - -SELF_HOSTED_MODEL=false - ``` diff --git a/docs/content/Extensions/api-key-guide.mdx b/docs/content/Extensions/api-key-guide.mdx index f1b83633..41520395 100644 --- a/docs/content/Extensions/api-key-guide.mdx +++ b/docs/content/Extensions/api-key-guide.mdx @@ -11,18 +11,18 @@ DocsGPT API keys are essential for developers and users who wish to integrate th After uploading your document, you can obtain an API key either through the graphical user interface or via an API call: -- **Graphical User Interface:** Navigate to the Settings section of the DocsGPT web app, find the API Keys option, and press 'Create New' to generate your key. -- **API Call:** Alternatively, you can use the `/api/create_api_key` endpoint to create a new API key. For detailed instructions, visit [DocsGPT API Documentation](https://gptcloud.arc53.com/). +- **Graphical User Interface:** Navigate to the Settings section of the DocsGPT web app, find the Agents option, and press 'Create New' to generate a new agent (which includes an API key). +- **API Call:** Alternatively, you can use the `/api/create_agent` endpoint to create a new agent. An API key is automatically generated for each agent. For detailed instructions, visit [DocsGPT API Documentation](https://gptcloud.arc53.com/). ## Understanding Key Variables -Upon creating your API key, you will encounter several key variables. Each serves a specific purpose: +Upon creating your agent, you will encounter several key variables. Each serves a specific purpose: -- **Name:** Assign a name to your API key for easy identification. -- **Source:** Indicates the source document(s) linked to your API key, which DocsGPT will use to generate responses. -- **ID:** A unique identifier for your API key. You can view this by making a call to `/api/get_api_keys`. -- **Key:** The API key itself, which will be used in your application to authenticate API requests. +- **Name:** Assign a name to your agent for easy identification. +- **Source:** Indicates the source document(s) linked to your agent, which DocsGPT will use to generate responses. +- **ID:** A unique identifier for your agent. You can view this by making a call to `/api/get_agents`. +- **Key:** The API key for the agent, which will be used in your application to authenticate API requests. -With your API key ready, you can now integrate DocsGPT into your application, such as the DocsGPT Widget or any other software, via `/api/answer` or `/stream` endpoints. The source document is preset with the API key, allowing you to bypass fields like `selectDocs` and `active_docs` during implementation. +With your API key ready, you can now integrate DocsGPT into your application, such as the DocsGPT Widget or any other software, via `/api/answer` or `/stream` endpoints. The source document is preset with the agent, allowing you to bypass fields like `selectDocs` and `active_docs` during implementation. Congratulations on taking the first step towards enhancing your applications with DocsGPT! diff --git a/docs/content/Guides/Architecture.mdx b/docs/content/Guides/Architecture.mdx index 247abda6..d981dc63 100644 --- a/docs/content/Guides/Architecture.mdx +++ b/docs/content/Guides/Architecture.mdx @@ -64,7 +64,7 @@ flowchart LR * **Technology:** Supports multiple vector databases. * **Responsibility:** Vector Stores are used to store and retrieve vector embeddings of document chunks. This enables semantic search and retrieval of relevant document snippets in response to user queries. * **Key Features:** - * Supports vector databases including FAISS, Elasticsearch, Qdrant, Milvus, and LanceDB. + * Supports vector databases including FAISS, Elasticsearch, Qdrant, Milvus, MongoDB Atlas Vector Search, and pgvector. * Provides storage and indexing of high-dimensional vector embeddings. * Enables editing and updating of vector indexes including specific chunks. diff --git a/docs/content/Guides/How-to-train-on-other-documentation.mdx b/docs/content/Guides/How-to-train-on-other-documentation.mdx index 4e11d6fa..4b233c08 100644 --- a/docs/content/Guides/How-to-train-on-other-documentation.mdx +++ b/docs/content/Guides/How-to-train-on-other-documentation.mdx @@ -16,7 +16,7 @@ Training on other documentation sources can greatly enhance the versatility and Make sure you have the document on which you want to train on ready with you on the device which you are using .You can also use links to the documentation to train on. - Note: The document should be either of the given file formats .pdf, .txt, .rst, .docx, .md, .zip and limited to 25mb.You can also train using the link of the documentation. + Note: Supported file formats include .pdf, .txt, .rst, .docx, .md, .mdx, .csv, .epub, .html, .json, .xlsx, .pptx, .png, .jpg, .jpeg, and audio files (.wav, .mp3, .m4a, .ogg, .webm). You can also train using the link of the documentation. diff --git a/docs/content/Guides/How-to-use-different-LLM.mdx b/docs/content/Guides/How-to-use-different-LLM.mdx index 217bc690..f92b3ce1 100644 --- a/docs/content/Guides/How-to-use-different-LLM.mdx +++ b/docs/content/Guides/How-to-use-different-LLM.mdx @@ -35,8 +35,34 @@ Choose the LLM of your choice. For open source version please edit `LLM_PROVIDER`, `LLM_NAME` and others in the .env file. Refer to [⚙️ App Configuration](/Deploying/DocsGPT-Settings) for more information. ### Step 2 Visit [☁️ Cloud Providers](/Models/cloud-providers) for the updated list of online models. Make sure you have the right API_KEY and correct LLM_PROVIDER. -For self-hosted please visit [🖥️ Local Inference](/Models/local-inference). +For self-hosted please visit [🖥️ Local Inference](/Models/local-inference). +## Fallback LLM + +DocsGPT can automatically switch to a fallback LLM when the primary model fails, including mid-stream. This works with both streaming and non-streaming requests. + +**Fallback order:** +1. Per-agent backup models (other models configured on the same agent) +2. Global fallback (`FALLBACK_LLM_*` env vars below) +3. Error returned if all fail + +| Setting | Description | Default | +| --- | --- | --- | +| `FALLBACK_LLM_PROVIDER` | Provider name (e.g., `openai`, `anthropic`, `google`) | — | +| `FALLBACK_LLM_NAME` | Model name (e.g., `gpt-4o`, `claude-sonnet-4-20250514`) | — | +| `FALLBACK_LLM_API_KEY` | API key for the fallback provider | Falls back to `API_KEY` | + +All three (`FALLBACK_LLM_PROVIDER`, `FALLBACK_LLM_NAME`, and an API key) must resolve for the global fallback to activate. + +```env +FALLBACK_LLM_PROVIDER=anthropic +FALLBACK_LLM_NAME=claude-sonnet-4-20250514 +FALLBACK_LLM_API_KEY=sk-ant-your-anthropic-key +``` + + +For maximum resilience, use a fallback provider from a different cloud than your primary. Each agent can also have multiple models configured — the other models are tried first before the global fallback. + diff --git a/docs/content/Guides/Integrations/_meta.js b/docs/content/Guides/Integrations/_meta.js index 8eb263d6..6630e25b 100644 --- a/docs/content/Guides/Integrations/_meta.js +++ b/docs/content/Guides/Integrations/_meta.js @@ -2,5 +2,13 @@ export default { "google-drive-connector": { "title": "🔗 Google Drive", "href": "/Guides/Integrations/google-drive-connector" + }, + "sharepoint-connector": { + "title": "🔗 SharePoint / OneDrive", + "href": "/Guides/Integrations/sharepoint-connector" + }, + "mcp-tool-integration": { + "title": "🔗 MCP Tools", + "href": "/Guides/Integrations/mcp-tool-integration" } } diff --git a/docs/content/Guides/Integrations/mcp-tool-integration.mdx b/docs/content/Guides/Integrations/mcp-tool-integration.mdx new file mode 100644 index 00000000..d33e1b94 --- /dev/null +++ b/docs/content/Guides/Integrations/mcp-tool-integration.mdx @@ -0,0 +1,66 @@ +--- +title: MCP Tool Integration +description: Connect external tools to DocsGPT agents using the Model Context Protocol (MCP) standard. +--- + +import { Callout } from 'nextra/components' +import { Steps } from 'nextra/components' + +# MCP Tool Integration + +The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) integration lets you connect external tool servers to DocsGPT. Your agents can then discover and call tools provided by those servers during conversations — for example, querying a CRM, running code, or accessing a database. + +## Setup + + + +### Step 1: Configure Environment Variables (Optional) + +Only needed if your MCP servers use OAuth authentication: + +```env +MCP_OAUTH_REDIRECT_URI=https://yourdomain.com/api/mcp_server/callback +``` + +If not set, falls back to `API_URL/api/mcp_server/callback`. + +### Step 2: Add an MCP Server + +Go to **Settings** > **Tools** > **Add Tool** > **MCP Server**. Enter the server URL, select an auth type, and click **Test Connection** to verify, then **Save**. + +### Step 3: Enable for Your Agent + +In your agent configuration, enable the MCP tools you want the agent to use. + + + +## Authentication Types + +| Auth Type | Config Fields | +|-----------|---------------| +| **None** | — | +| **Bearer** | `bearer_token` | +| **API Key** | `api_key`, `api_key_header` (default: `X-API-Key`) | +| **Basic** | `username`, `password` | +| **OAuth** | `oauth_scopes` (optional) | + + +For OAuth in production, `MCP_OAUTH_REDIRECT_URI` must be a publicly accessible URL pointing to your DocsGPT backend. + + +## API Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/mcp_server/test` | POST | Test a connection without saving | +| `/api/mcp_server/save` | POST | Save or update a server configuration | +| `/api/mcp_server/callback` | GET | OAuth callback handler | +| `/api/mcp_server/oauth_status/` | GET | Poll OAuth flow status | +| `/api/mcp_server/auth_status` | GET | Batch check auth status for all MCP tools | + +## Troubleshooting + +- **Connection refused** — Verify the URL and that the server is reachable from your backend. +- **403 Forbidden** — Check credentials and permissions. +- **Timed out** — Default is 30s; increase timeout in tool config (max 300s). +- **OAuth "needs_auth" persists** — Verify `MCP_OAUTH_REDIRECT_URI` is correct and Redis is running. diff --git a/docs/content/Guides/Integrations/sharepoint-connector.mdx b/docs/content/Guides/Integrations/sharepoint-connector.mdx new file mode 100644 index 00000000..cf8b27bf --- /dev/null +++ b/docs/content/Guides/Integrations/sharepoint-connector.mdx @@ -0,0 +1,63 @@ +--- +title: SharePoint / OneDrive Connector +description: Connect your Microsoft SharePoint or OneDrive as an external knowledge base to upload and process files directly. +--- + +import { Callout } from 'nextra/components' +import { Steps } from 'nextra/components' + +# SharePoint / OneDrive Connector + +Connect your SharePoint or OneDrive account to upload and process files directly as an external knowledge base. Supports Office files, PDFs, text files, CSVs, images, and more. Authentication is handled via Microsoft Entra ID (Azure AD) with automatic token refresh. + +## Setup + + + +### Step 1: Create an App Registration in Azure + +1. Go to the [Azure Portal](https://portal.azure.com/) > **Microsoft Entra ID** > **App registrations** > **New registration** +2. Set **Redirect URI** (Web) to: + - Local: `http://localhost:7091/api/connectors/callback?provider=share_point` + - Production: `https://yourdomain.com/api/connectors/callback?provider=share_point` + +### Step 2: Configure API Permissions + +In your App Registration, go to **API permissions** > **Add a permission** > **Microsoft Graph** > **Delegated permissions** and add: `Files.Read`, `Files.Read.All`, `Sites.Read.All`. Grant admin consent if possible. + +### Step 3: Create a Client Secret + +Go to **Certificates & secrets** > **New client secret**. Copy the secret value immediately (it won't be shown again). + +### Step 4: Configure Environment Variables + +Add to your `.env` file: + +```env +MICROSOFT_CLIENT_ID=your-azure-ad-client-id +MICROSOFT_CLIENT_SECRET=your-azure-ad-client-secret +MICROSOFT_TENANT_ID=your-azure-ad-tenant-id +``` + +| Variable | Description | Required | Default | +|----------|-------------|----------|---------| +| `MICROSOFT_CLIENT_ID` | Application (client) ID from App Registration overview | Yes | — | +| `MICROSOFT_CLIENT_SECRET` | Client secret value | Yes | — | +| `MICROSOFT_TENANT_ID` | Directory (tenant) ID | No | `common` | +| `MICROSOFT_AUTHORITY` | Login endpoint override | No | Auto-constructed | + + +`MICROSOFT_TENANT_ID=common` (the default) allows any Microsoft account to authenticate. Set this to your specific tenant ID in production. + + +### Step 5: Restart and Use + +Restart your application, then go to the upload section in DocsGPT and select **SharePoint / OneDrive** as the source. You'll be redirected to Microsoft to sign in, then can browse and select files to process. + + + +## Troubleshooting + +- **Option not appearing** — Verify `MICROSOFT_CLIENT_ID` and `MICROSOFT_CLIENT_SECRET` are set, then restart. +- **Authentication failed** — Check that the redirect URI matches exactly, including `?provider=share_point`. +- **Permission denied** — Ensure admin consent is granted and the user has access to the target files. diff --git a/docs/content/Guides/My-AI-answers-questions-using-external-knowledge.mdx b/docs/content/Guides/My-AI-answers-questions-using-external-knowledge.mdx index 318bf41b..96d9ee6b 100644 --- a/docs/content/Guides/My-AI-answers-questions-using-external-knowledge.mdx +++ b/docs/content/Guides/My-AI-answers-questions-using-external-knowledge.mdx @@ -7,20 +7,10 @@ description: If your AI uses external knowledge and is not explicit enough, it is ok, because we try to make DocsGPT friendly. -But if you want to adjust it, here is a simple way:- - -- Got to `application/prompts/chat_combine_prompt.txt` - -- And change it to +But if you want to adjust it, prompts are now managed through the UI and API using a template-based system. See the [Customising Prompts](/Guides/Customising-prompts) guide for details. +To make the AI stricter about staying on-topic, edit your active prompt template (via **Sidebar → Settings → Active Prompt**) to include instructions like: ``` - -You are a DocsGPT, friendly and helpful AI assistant by Arc53 that provides help with documents. You give thorough answers with code examples, if possible. -Write an answer for the question below based on the provided context. If the context provides insufficient information, reply "I cannot answer". -You have access to chat history and can use it to help answer the question. ----------------- -{summaries} - ``` diff --git a/docs/content/Guides/_meta.js b/docs/content/Guides/_meta.js index a5c91e04..25c49b20 100644 --- a/docs/content/Guides/_meta.js +++ b/docs/content/Guides/_meta.js @@ -29,7 +29,7 @@ export default { "title": "OCR", "href": "/Guides/ocr" }, - "Integrations": { +"Integrations": { "title": "🔗 Integrations" } } diff --git a/docs/content/quickstart.mdx b/docs/content/quickstart.mdx index 394276a8..60654a73 100644 --- a/docs/content/quickstart.mdx +++ b/docs/content/quickstart.mdx @@ -70,7 +70,7 @@ The easiest way to launch DocsGPT is using the provided `setup.sh` script. This To stop DocsGPT, simply open a new terminal in the `DocsGPT` directory and run: ```bash - docker compose -f deployment/docker-compose.yaml down + docker compose -f deployment/docker-compose-hub.yaml down ``` (or the specific `docker compose` command shown at the end of the `setup.sh` execution, which may include optional compose files depending on your choices). diff --git a/extensions/react-widget/custom.d.ts b/extensions/react-widget/custom.d.ts index 4914b1ec..e69de29b 100644 --- a/extensions/react-widget/custom.d.ts +++ b/extensions/react-widget/custom.d.ts @@ -1,9 +0,0 @@ -declare module "*.svg" { - import * as React from "react"; - - const ReactComponent: React.FunctionComponent< - React.SVGProps & { title?: string } - >; - - export default ReactComponent; - } \ No newline at end of file diff --git a/extensions/react-widget/eslint.config.mjs b/extensions/react-widget/eslint.config.mjs new file mode 100644 index 00000000..987d463c --- /dev/null +++ b/extensions/react-widget/eslint.config.mjs @@ -0,0 +1,72 @@ +import js from '@eslint/js' +import tsParser from '@typescript-eslint/parser' +import tsPlugin from '@typescript-eslint/eslint-plugin' +import react from 'eslint-plugin-react' +import unusedImports from 'eslint-plugin-unused-imports' +import prettier from 'eslint-plugin-prettier' +import globals from 'globals' + +export default [ + { + ignores: [ + 'node_modules/', + 'dist/', + 'prettier.config.cjs', + 'custom.d.ts', + 'package-lock.json', + 'package.json', + ], + }, + { + files: ['**/*.{js,jsx,ts,tsx}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + parser: tsParser, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + ...globals.browser, + ...globals.es2021, + ...globals.node, + }, + }, + plugins: { + '@typescript-eslint': tsPlugin, + react, + 'unused-imports': unusedImports, + prettier, + }, + rules: { + ...js.configs.recommended.rules, + ...tsPlugin.configs.recommended.rules, + ...react.configs.recommended.rules, + ...prettier.configs.recommended.rules, + 'react/prop-types': 'off', + 'unused-imports/no-unused-imports': 'error', + 'react/react-in-jsx-scope': 'off', + 'no-undef': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { varsIgnorePattern: '^_', argsIgnorePattern: '^_' }, + ], + '@typescript-eslint/no-unused-expressions': 'warn', + 'prettier/prettier': [ + 'error', + { + endOfLine: 'auto', + }, + ], + }, + settings: { + react: { + version: 'detect', + }, + }, + }, +] + diff --git a/extensions/react-widget/package-lock.json b/extensions/react-widget/package-lock.json index 5275956b..59afc9f1 100644 --- a/extensions/react-widget/package-lock.json +++ b/extensions/react-widget/package-lock.json @@ -1,12 +1,12 @@ { "name": "docsgpt", - "version": "0.5.1", + "version": "0.6.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "docsgpt", - "version": "0.5.1", + "version": "0.6.3", "license": "Apache-2.0", "dependencies": { "@babel/plugin-transform-flow-strip-types": "^7.23.3", @@ -19,7 +19,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "dompurify": "^3.1.5", - "flow-bin": "^0.305.0", + "flow-bin": "^0.306.0", "markdown-it": "^14.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -29,14 +29,24 @@ "@babel/core": "^7.24.0", "@babel/preset-env": "^7.24.0", "@babel/preset-react": "^7.23.3", + "@eslint/js": "^9.39.4", "@parcel/packager-ts": "^2.16.4", "@parcel/transformer-typescript-types": "^2.16.4", "@types/dompurify": "^3.0.5", "@types/markdown-it": "^14.1.2", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.57.2", + "@typescript-eslint/parser": "^8.57.2", "babel-loader": "^10.1.1", + "eslint": "^9.39.4", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-unused-imports": "^4.4.1", + "globals": "^15.15.0", "parcel": "^2.16.4", + "prettier": "^3.8.1", "process": "^0.11.10", "svgo": "^3.3.3", "typescript": "^5.3.3" @@ -917,6 +927,16 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.6.tgz", @@ -1812,6 +1832,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", @@ -1833,22 +1862,293 @@ "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", - "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", "dependencies": { - "@emotion/memoize": "^0.8.1" + "@emotion/memoize": "^0.9.0" } }, "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" }, "node_modules/@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", @@ -3888,6 +4188,19 @@ "@parcel/core": "^2.16.4" } }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@radix-ui/react-icons": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", @@ -4324,13 +4637,6 @@ "tslib": "^2.8.0" } }, - "node_modules/@swc/helpers/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "devOptional": true, - "license": "0BSD" - }, "node_modules/@swc/types": { "version": "0.1.25", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", @@ -4349,6 +4655,20 @@ "@types/trusted-types": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -4400,9 +4720,10 @@ } }, "node_modules/@types/stylis": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", - "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz", + "integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==", + "license": "MIT" }, "node_modules/@types/trusted-types": { "version": "2.0.7", @@ -4410,6 +4731,292 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "devOptional": true }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -4430,6 +5037,170 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/babel-loader": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.1.1.tgz", @@ -4495,6 +5266,16 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/base-x": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", @@ -4523,6 +5304,19 @@ "dev": true, "license": "ISC" }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -4567,6 +5361,56 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4703,6 +5547,13 @@ "node": ">= 10" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -4747,6 +5598,21 @@ } } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -4846,16 +5712,72 @@ "license": "CC0-1.0" }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -4866,6 +5788,49 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -4877,6 +5842,19 @@ "node": ">=0.10" } }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -4982,6 +5960,21 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.313", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", @@ -5008,6 +6001,184 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", + "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -5017,6 +6188,381 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz", + "integrity": "sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^10.0.0 || ^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5026,6 +6572,65 @@ "node": ">=0.10.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -5054,10 +6659,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/flow-bin": { - "version": "0.305.0", - "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.305.0.tgz", - "integrity": "sha512-Y5eILvtOF3pYdy2eratXYHpcluoruPMUtvwyCaBupefVQDrJgKRGjLrDFq3/n8jHX/ITijk2oQb2ll2VxK3uyg==", + "version": "0.306.0", + "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.306.0.tgz", + "integrity": "sha512-NaAyPsFWZSY59NLCL+6lUXPS5KCO4td9h7XO0kV9VGzRr19sypImQYc0DD1Skm+SHt0/mOIYKI21KtmhQ4KHBg==", "license": "MIT", "bin": { "flow": "cli.js" @@ -5066,6 +6692,22 @@ "node": ">=0.10.0" } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5075,6 +6717,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5083,6 +6766,31 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-port": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", @@ -5092,12 +6800,105 @@ "node": ">=6" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": ">=4" + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-flag": { @@ -5109,6 +6910,64 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -5121,6 +6980,16 @@ "node": ">= 0.4" } }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -5137,19 +7006,167 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5163,6 +7180,42 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -5174,6 +7227,32 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -5182,6 +7261,200 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5210,12 +7483,33 @@ "node": ">=4" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -5227,6 +7521,46 @@ "node": ">=6" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -5564,6 +7898,13 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5609,6 +7950,16 @@ "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -5634,10 +7985,39 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/msgpackr": { "version": "1.11.9", @@ -5681,15 +8061,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -5697,6 +8078,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -5715,6 +8103,25 @@ "node": "^16 || ^18 || >= 20" } }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/node-gyp-build-optional-packages": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", @@ -5760,11 +8167,155 @@ "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ordered-binary": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.1.tgz", "integrity": "sha512-5VyHfHY3cd0iza71JepYG50My+YUbrFtGoUz2ooEydPyPM7Aai/JW098juLr+RG6+rDJuzNNTsEQu2DZa1A41A==" }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5922,6 +8473,16 @@ "node": ">=8" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -5943,20 +8504,32 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -5971,10 +8544,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -5985,6 +8559,45 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -5994,6 +8607,28 @@ "node": ">= 0.6.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -6026,6 +8661,13 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.16.0.tgz", @@ -6036,6 +8678,29 @@ "node": ">=0.10.0" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -6070,6 +8735,27 @@ "@babel/runtime": "^7.8.4" } }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regexpu-core": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", @@ -6134,6 +8820,26 @@ "node": ">=4" } }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6153,6 +8859,41 @@ } ] }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/sax": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", @@ -6179,11 +8920,159 @@ "semver": "bin/semver.js" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -6195,27 +9084,154 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/styled-components": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.11.tgz", - "integrity": "sha512-Ui0jXPzbp1phYij90h12ksljKGqF8ncGx+pjrNPsSPhbUUjWT2tD1FwGo2LF6USCnbrsIhNngDfodhxbegfEOA==", + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@emotion/is-prop-valid": "1.2.2", - "@emotion/unitless": "0.8.1", - "@types/stylis": "4.2.5", + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-components": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.3.12.tgz", + "integrity": "sha512-hFR6xsVkVYbsdcUlzPYFvFfoc6o2KlV0VvgRIQwSYMtdThM7SCxnjX9efh/cWce2kTq16I/Kl3xM98xiLptsXA==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.4.0", + "@emotion/unitless": "0.10.0", + "@types/stylis": "4.2.7", "css-to-react-native": "3.2.0", - "csstype": "3.1.3", - "postcss": "8.4.38", + "csstype": "3.2.3", + "postcss": "8.4.49", "shallowequal": "1.1.0", - "stylis": "4.3.2", - "tslib": "2.6.2" + "stylis": "4.3.6", + "tslib": "2.8.1" }, "engines": { "node": ">= 16" @@ -6227,12 +9243,18 @@ "peerDependencies": { "react": ">= 16.8.0", "react-dom": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, "node_modules/stylis": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", - "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" }, "node_modules/supports-color": { "version": "7.2.0", @@ -6290,6 +9312,22 @@ "url": "https://opencollective.com/svgo" } }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/term-size": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", @@ -6303,6 +9341,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6314,10 +9369,37 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } }, "node_modules/type-fest": { "version": "0.20.2", @@ -6332,6 +9414,84 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -6351,6 +9511,25 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -6421,6 +9600,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/utility-types": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", @@ -6434,6 +9623,121 @@ "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==" }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/extensions/react-widget/package.json b/extensions/react-widget/package.json index 3d329799..dde9335d 100644 --- a/extensions/react-widget/package.json +++ b/extensions/react-widget/package.json @@ -1,6 +1,6 @@ { "name": "docsgpt", - "version": "0.5.1", + "version": "0.6.3", "private": false, "description": "DocsGPT 🦖 is an innovative open-source tool designed to simplify the retrieval of information from project documentation using advanced GPT models 🤖.", "source": "./src/index.html", @@ -35,7 +35,9 @@ "serve": "parcel serve -p 3000", "dev": "parcel -p 3000", "test": "jest", - "lint": "eslint", + "lint": "eslint ./src --ext .jsx,.js,.ts,.tsx", + "lint-fix": "eslint ./src --ext .jsx,.js,.ts,.tsx --fix", + "format": "prettier ./src --write", "check": "tsc --noEmit", "ci": "yarn build && yarn test && yarn lint && yarn check" }, @@ -50,7 +52,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "dompurify": "^3.1.5", - "flow-bin": "^0.305.0", + "flow-bin": "^0.306.0", "markdown-it": "^14.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -60,14 +62,24 @@ "@babel/core": "^7.24.0", "@babel/preset-env": "^7.24.0", "@babel/preset-react": "^7.23.3", + "@eslint/js": "^9.39.4", "@parcel/packager-ts": "^2.16.4", "@parcel/transformer-typescript-types": "^2.16.4", "@types/dompurify": "^3.0.5", "@types/markdown-it": "^14.1.2", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.57.2", + "@typescript-eslint/parser": "^8.57.2", "babel-loader": "^10.1.1", + "eslint": "^9.39.4", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-unused-imports": "^4.4.1", + "globals": "^15.15.0", "parcel": "^2.16.4", + "prettier": "^3.8.1", "process": "^0.11.10", "svgo": "^3.3.3", "typescript": "^5.3.3" diff --git a/extensions/react-widget/prettier.config.cjs b/extensions/react-widget/prettier.config.cjs new file mode 100644 index 00000000..61f01195 --- /dev/null +++ b/extensions/react-widget/prettier.config.cjs @@ -0,0 +1,8 @@ +module.exports = { + trailingComma: 'all', + tabWidth: 2, + semi: true, + singleQuote: true, + printWidth: 80, +}; + diff --git a/extensions/react-widget/src/App.tsx b/extensions/react-widget/src/App.tsx index 4bb24bae..45567031 100644 --- a/extensions/react-widget/src/App.tsx +++ b/extensions/react-widget/src/App.tsx @@ -1,11 +1,11 @@ -import React from "react" -import {DocsGPTWidget} from "./components/DocsGPTWidget" -import {SearchBar} from "./components/SearchBar" +import React from 'react'; +import { DocsGPTWidget } from './components/DocsGPTWidget'; +import { SearchBar } from './components/SearchBar'; export const App = () => { return (
- - + +
- ) -} \ No newline at end of file + ); +}; diff --git a/extensions/react-widget/src/assets/dislike.svg b/extensions/react-widget/src/assets/dislike.svg deleted file mode 100644 index ec1d24c2..00000000 --- a/extensions/react-widget/src/assets/dislike.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/extensions/react-widget/src/assets/like.svg b/extensions/react-widget/src/assets/like.svg deleted file mode 100644 index c49604ed..00000000 --- a/extensions/react-widget/src/assets/like.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/extensions/react-widget/src/browser.tsx b/extensions/react-widget/src/browser.tsx index 8bee7748..d28afe8f 100644 --- a/extensions/react-widget/src/browser.tsx +++ b/extensions/react-widget/src/browser.tsx @@ -1,10 +1,10 @@ //exports browser ready methods -import { createRoot } from "react-dom/client"; +import { createRoot } from 'react-dom/client'; import { DocsGPTWidget } from './components/DocsGPTWidget'; import { SearchBar } from './components/SearchBar'; -import React from "react"; +import React from 'react'; if (typeof window !== 'undefined') { const renderWidget = (elementId: string, props = {}) => { const root = createRoot(document.getElementById(elementId) as HTMLElement); @@ -14,9 +14,11 @@ if (typeof window !== 'undefined') { const root = createRoot(document.getElementById(elementId) as HTMLElement); root.render(); }; - (window as any).renderDocsGPTWidget = renderWidget; + (window as unknown as Record).renderDocsGPTWidget = + renderWidget; - (window as any).renderSearchBar = renderSearchBar; + (window as unknown as Record).renderSearchBar = + renderSearchBar; } export { DocsGPTWidget, SearchBar }; diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index e778e5a8..ad7075ce 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -1,47 +1,104 @@ -"use client"; -import React, { useRef, useState, useEffect } from 'react' +'use client'; +import React, { useRef, useState } from 'react'; import DOMPurify from 'dompurify'; import styled, { keyframes, css } from 'styled-components'; -import { PaperPlaneIcon, RocketIcon, ExclamationTriangleIcon, Cross2Icon } from '@radix-ui/react-icons'; -import { FEEDBACK, MESSAGE_TYPE, Query, Status, WidgetCoreProps, WidgetProps } from '../types/index'; +import { + PaperPlaneIcon, + RocketIcon, + ExclamationTriangleIcon, + Cross2Icon, +} from '@radix-ui/react-icons'; +import { + FEEDBACK, + MESSAGE_TYPE, + Query, + Status, + WidgetCoreProps, + WidgetProps, +} from '../types/index'; import { fetchAnswerStreaming, sendFeedback } from '../requests/streamingApi'; import { ThemeProvider } from 'styled-components'; -import Like from '../assets/like.svg'; -import Dislike from '../assets/dislike.svg'; import MarkdownIt from 'markdown-it'; +const LikeIcon = (props: React.SVGProps) => ( + + + + +); + +const DislikeIcon = (props: React.SVGProps) => ( + + + + +); + const themes = { dark: { bg: '#222327', text: '#fff', primary: { - text: "#FAFAFA", - bg: '#222327' + text: '#FAFAFA', + bg: '#222327', }, secondary: { - text: "#A1A1AA", - bg: "#38383b" - } + text: '#A1A1AA', + bg: '#38383b', + }, }, light: { bg: '#fff', text: '#000', primary: { - text: "#222327", - bg: "#fff" + text: '#222327', + bg: '#fff', }, secondary: { - text: "#A1A1AA", - bg: "#F6F6F6" - } - } + text: '#A1A1AA', + bg: '#F6F6F6', + }, + }, }; const sizesConfig = { small: { size: 'small', width: '320px', height: '400px' }, medium: { size: 'medium', width: '400px', height: '80vh' }, large: { size: 'large', width: '666px', height: '75vh' }, - getCustom: (custom: { width: string; height: string; maxWidth?: string; maxHeight?: string }) => ({ + getCustom: (custom: { + width: string; + height: string; + maxWidth?: string; + maxHeight?: string; + }) => ({ size: 'custom', width: custom.width, height: custom.height, @@ -59,7 +116,7 @@ const createBox = keyframes` 100% { transform: scale(1); } -` +`; const closeBox = keyframes` 0% { transform: scale(1); @@ -70,7 +127,7 @@ const closeBox = keyframes` 100% { transform: scale(0.6); } -` +`; const openContainer = keyframes` 0% { @@ -78,49 +135,49 @@ const openContainer = keyframes` height: 100px; } 100% { - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; + width: ${(props) => props.theme.dimensions!.width}; + height: ${(props) => props.theme.dimensions!.height}; border-radius: 12px; - }` + }`; const closeContainer = keyframes` 0% { - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; + width: ${(props) => props.theme.dimensions!.width}; + height: ${(props) => props.theme.dimensions!.height}; border-radius: 12px; } 100% { width: 200px; height: 100px; } -` +`; const fadeIn = keyframes` from { opacity: 0; - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; + width: ${(props) => props.theme.dimensions!.width}; + height: ${(props) => props.theme.dimensions!.height}; transform: scale(0.9); } to { opacity: 1; transform: scale(1); - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; + width: ${(props) => props.theme.dimensions!.width}; + height: ${(props) => props.theme.dimensions!.height}; } -` +`; const fadeOut = keyframes` from { opacity: 1; - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; + width: ${(props) => props.theme.dimensions!.width}; + height: ${(props) => props.theme.dimensions!.height}; } to { opacity: 0; transform: scale(0.9); - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height}; + width: ${(props) => props.theme.dimensions!.width}; + height: ${(props) => props.theme.dimensions!.height}; } -` +`; const scaleAnimation = keyframes` from { transform: scale(1.2); @@ -128,7 +185,7 @@ const scaleAnimation = keyframes` to { transform: scale(1); } -` +`; const Overlay = styled.div` position: fixed; top: 0; @@ -138,134 +195,152 @@ const Overlay = styled.div` background-color: rgba(0, 0, 0, 0.5); z-index: 999; transition: opacity 0.5s; -` - +`; const WidgetContainer = styled.div<{ $modal?: boolean }>` - all: initial; - position: fixed; - right: ${props => props.$modal ? '50%' : '10px'}; - bottom: ${props => props.$modal ? '50%' : '10px'}; - z-index: 1001; - transform-origin:100% 100%; - display: block; - &.modal{ - transform : translate(50%,50%); - } - &.open { - animation: css ${createBox} 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; - } - &.close { - animation: css ${closeBox} 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; - } - align-items: center; - text-align: left; + all: initial; + position: fixed; + right: ${(props) => (props.$modal ? '50%' : '10px')}; + bottom: ${(props) => (props.$modal ? '50%' : '10px')}; + z-index: 1001; + transform-origin: 100% 100%; + display: block; + &.modal { + transform: translate(50%, 50%); + } + &.open { + animation: css ${createBox} 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; + } + &.close { + animation: css ${closeBox} 250ms cubic-bezier(0.25, 0.1, 0.25, 1) forwards; + } + align-items: center; + text-align: left; `; const StyledContainer = styled.div<{ $isOpen: boolean }>` - all: initial; - max-height: ${(props) => props.theme.dimensions.maxHeight}; - max-width: ${(props) => props.theme.dimensions.maxWidth}; - width: ${(props) => props.theme.dimensions.width}; - height: ${(props) => props.theme.dimensions.height} ; - position: relative; - flex-direction: column; - justify-content: space-between; - bottom: 0; - left: 0; - background-color: ${(props) => props.theme.primary.bg}; - font-family: sans-serif; - display: flex; - border-radius: 12px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.1); - padding: 26px 26px 0px 26px; - animation: ${({ $isOpen, theme }) => - theme.dimensions.size === 'large' + all: initial; + max-height: ${(props) => props.theme.dimensions!.maxHeight}; + max-width: ${(props) => props.theme.dimensions!.maxWidth}; + width: ${(props) => props.theme.dimensions!.width}; + height: ${(props) => props.theme.dimensions!.height}; + position: relative; + flex-direction: column; + justify-content: space-between; + bottom: 0; + left: 0; + background-color: ${(props) => props.theme.primary.bg}; + font-family: sans-serif; + display: flex; + border-radius: 12px; + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.05), + 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 26px 26px 0px 26px; + animation: ${({ $isOpen, theme }) => + theme.dimensions!.size === 'large' ? $isOpen - ? css`${fadeIn} 150ms ease-in forwards` - : css` ${fadeOut} 150ms ease-in forwards` + ? css` + ${fadeIn} 150ms ease-in forwards + ` + : css` + ${fadeOut} 150ms ease-in forwards + ` : $isOpen - ? css`${openContainer} 150ms ease-in forwards` - : css`${closeContainer} 250ms ease-in forwards`}; - @media only screen and (max-width: 768px) { - max-height: 100vh; - max-width: 80vw; - overflow: auto; - } + ? css` + ${openContainer} 150ms ease-in forwards + ` + : css` + ${closeContainer} 250ms ease-in forwards + `}; + @media only screen and (max-width: 768px) { + max-height: 100vh; + max-width: 80vw; + overflow: auto; + } `; -const FloatingButton = styled.div<{ $bgcolor: string, $hidden: boolean, $isAnimatingButton: boolean }>` - position: fixed; - display: ${props => props.$hidden ? "none" : "flex"}; - z-index: 500; - justify-content: center; - gap: 8px; - padding: 14px; - align-items: center; - bottom: 16px; - color: white; - font-family: sans-serif; - right: 16px; - font-weight: 500; - border-radius: 9999px; - background: ${props => props.$bgcolor}; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - cursor: pointer; - animation: ${props => props.$isAnimatingButton ? css`${scaleAnimation} 200ms forwards` : 'none'}; - &:hover { - transform: scale(1.1); - transition: transform 0.2s ease-in-out; - } - &:not(:hover) { - transition: transform 0.2s ease-in-out; - } +const FloatingButton = styled.div<{ + $bgcolor: string; + $hidden: boolean; + $isAnimatingButton: boolean; +}>` + position: fixed; + display: ${(props) => (props.$hidden ? 'none' : 'flex')}; + z-index: 500; + justify-content: center; + gap: 8px; + padding: 14px; + align-items: center; + bottom: 16px; + color: white; + font-family: sans-serif; + right: 16px; + font-weight: 500; + border-radius: 9999px; + background: ${(props) => props.$bgcolor}; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + cursor: pointer; + animation: ${(props) => + props.$isAnimatingButton + ? css` + ${scaleAnimation} 200ms forwards + ` + : 'none'}; + &:hover { + transform: scale(1.1); + transition: transform 0.2s ease-in-out; + } + &:not(:hover) { + transition: transform 0.2s ease-in-out; + } `; const CancelButton = styled.button` - cursor: pointer; - position: absolute; - top: 0; - right: 0; - margin: 8px; - width: 30px; - padding: 0; - background-color: transparent; - border: none; - outline: none; - color: inherit; - transition: opacity 0.3s ease; - opacity: 0.6; - &:hover { - opacity: 1; - } - .white-filter { - filter: invert(100%); - } + cursor: pointer; + position: absolute; + top: 0; + right: 0; + margin: 8px; + width: 30px; + padding: 0; + background-color: transparent; + border: none; + outline: none; + color: inherit; + transition: opacity 0.3s ease; + opacity: 0.6; + &:hover { + opacity: 1; + } + .white-filter { + filter: invert(100%); + } `; const Header = styled.div` - display: flex; - align-items: flex-start; + display: flex; + align-items: flex-start; `; const ContentWrapper = styled.div` - display: flex; - flex-direction: column; - gap:2px; - margin-left: 8px; + display: flex; + flex-direction: column; + gap: 2px; + margin-left: 8px; `; const Title = styled.h3` - font-size: 14px; - font-weight: normal; - color: ${props => props.theme.primary.text}; - margin: 0; + font-size: 14px; + font-weight: normal; + color: ${(props) => props.theme.primary.text}; + margin: 0; `; const Description = styled.p` - font-size: 13.75px; - color: ${props => props.theme.secondary.text}; - margin: 0 ; - padding: 0 ; + font-size: 13.75px; + color: ${(props) => props.theme.secondary.text}; + margin: 0; + padding: 0; `; const Conversation = styled.div` @@ -274,7 +349,7 @@ const Conversation = styled.div` text-align: left; overflow-y: auto; scrollbar-width: thin; - scrollbar-color: ${props => props.theme.secondary.bg} transparent; /* thumb color track color */ + scrollbar-color: ${(props) => props.theme.secondary.bg} transparent; /* thumb color track color */ `; const Feedback = styled.div` background-color: transparent; @@ -285,153 +360,149 @@ const Feedback = styled.div` clear: both; `; const MessageBubble = styled.div<{ $type: MESSAGE_TYPE }>` - display: block; - font-size: 16px; - position: relative; - width: 100%;; - float: right; - margin: 0px; - &:hover ${Feedback} * { - visibility: visible ; - - + display: block; + font-size: 16px; + position: relative; + width: 100%; + float: right; + margin: 0px; + &:hover ${Feedback} * { + visibility: visible; } `; const Message = styled.div<{ $type: MESSAGE_TYPE }>` - background: ${props => props.$type === 'QUESTION' ? - 'linear-gradient(to bottom right, #8860DB, #6D42C5)' : - props.theme.secondary.bg}; - color: ${props => props.$type === 'ANSWER' ? props.theme.primary.text : '#fff'}; - border: none; - float: ${props => props.$type === 'QUESTION' ? 'right' : 'left'}; - max-width: ${props => props.$type === 'ANSWER' ? '90%' : '80%'}; - overflow: auto; - margin: 4px; - display: block; - line-height: 1.5; - padding: 12px; - border-radius: 6px; - overflow-wrap: break-word; + background: ${(props) => + props.$type === 'QUESTION' + ? 'linear-gradient(to bottom right, #8860DB, #6D42C5)' + : props.theme.secondary.bg}; + color: ${(props) => + props.$type === 'ANSWER' ? props.theme.primary.text : '#fff'}; + border: none; + float: ${(props) => (props.$type === 'QUESTION' ? 'right' : 'left')}; + max-width: ${(props) => (props.$type === 'ANSWER' ? '90%' : '80%')}; + overflow: auto; + margin: 4px; + display: block; + line-height: 1.5; + padding: 12px; + border-radius: 6px; + overflow-wrap: break-word; `; const Markdown = styled.div` - pre { - padding: 8px; - width: 90%; - font-size: 12px; - border-radius: 6px; - overflow-x: auto; - background-color: #1B1C1F; - color: #fff ; - } + pre { + padding: 8px; + width: 90%; + font-size: 12px; + border-radius: 6px; + overflow-x: auto; + background-color: #1b1c1f; + color: #fff; + } - h1 { - font-size: clamp(14px,40vw,16px); - } + h1 { + font-size: clamp(14px, 40vw, 16px); + } - h2 { - font-size: 14px; - } + h2 { + font-size: 14px; + } - h3 { - font-size: 14px; - } + h3 { + font-size: 14px; + } - p { - - margin: 0px; - } + p { + margin: 0px; + } - code:not(pre code) { - border-radius: 6px; - padding: 1px 3px; - font-size: 12px; - display: inline-block; - background-color: #646464; - color: #fff ; - } + code:not(pre code) { + border-radius: 6px; + padding: 1px 3px; + font-size: 12px; + display: inline-block; + background-color: #646464; + color: #fff; + } - code { - white-space: pre-wrap ; - overflow-wrap: break-word; - word-break: break-all; - } + code { + white-space: pre-wrap; + overflow-wrap: break-word; + word-break: break-all; + } - ul{ - padding:0px; - margin: 1rem 0; - list-style-position: outside; - list-style-type: disc; - padding-left: 1rem; - white-space: normal; - } - - ol{ - padding:0px; - margin: 1rem 0; - list-style-position: outside; - list-style-type: decimal; - padding-left: 1rem; - white-space: normal; - } - - li{ - line-height: 1.625; - } - .dgpt-table-container { - margin: 20px 0; - width:100%; - overflow-x: scroll !important; - border: 1px solid #a2a2ab; - border-radius: 6px; - -webkit-overflow-scrolling: touch; - -ms-overflow-style: scrollbar; - scrollbar-width: thin; - scrollbar-color: #a2a2ab #38383b; - } - + ul { + padding: 0px; + margin: 1rem 0; + list-style-position: outside; + list-style-type: disc; + padding-left: 1rem; + white-space: normal; + } - table, .dgpt-table { - width: 100%; - border-collapse: collapse; - text-align: left; - min-width:600px; - - } - thead, .dgpt-thead { - font-size: 12px; - text-transform: uppercase; - - } - - - th, .dgpt-th, td, .dgpt-td { - padding: 10px; - border-bottom: 1px solid #a2a2ab; - font-size:14px; - - } - th{ - font-weight: normal !important; - } - td{ - font-weight: bold; - } - - - -` + ol { + padding: 0px; + margin: 1rem 0; + list-style-position: outside; + list-style-type: decimal; + padding-left: 1rem; + white-space: normal; + } + + li { + line-height: 1.625; + } + .dgpt-table-container { + margin: 20px 0; + width: 100%; + overflow-x: scroll !important; + border: 1px solid #a2a2ab; + border-radius: 6px; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: scrollbar; + scrollbar-width: thin; + scrollbar-color: #a2a2ab #38383b; + } + + table, + .dgpt-table { + width: 100%; + border-collapse: collapse; + text-align: left; + min-width: 600px; + } + thead, + .dgpt-thead { + font-size: 12px; + text-transform: uppercase; + } + + th, + .dgpt-th, + td, + .dgpt-td { + padding: 10px; + border-bottom: 1px solid #a2a2ab; + font-size: 14px; + } + th { + font-weight: normal !important; + } + td { + font-weight: bold; + } +`; const ErrorAlert = styled.div` color: #b91c1c; - border:0.1px solid #b91c1c; + border: 0.1px solid #b91c1c; display: flex; - padding:4px; - margin:11.2px; + padding: 4px; + margin: 11.2px; opacity: 90%; max-width: 70%; font-weight: 400; border-radius: 6px; justify-content: space-evenly; -` +`; //dot loading animation const dotBounce = keyframes` 0%, 80%, 100% { @@ -447,13 +518,14 @@ const DotAnimation = styled.div` animation: ${dotBounce} 1s infinite ease-in-out; `; // delay classes as styled components -const Delay = styled(DotAnimation) <{ $delay: number }>` - animation-delay: ${props => props.$delay + 'ms'}; +const Delay = styled(DotAnimation)<{ $delay: number }>` + animation-delay: ${(props) => props.$delay + 'ms'}; `; const PromptContainer = styled.form` background-color: transparent; - min-height: ${props => props.theme.dimensions.size == 'large' ? '40px' : '23px'}; - max-height:150px; + min-height: ${(props) => + props.theme.dimensions!.size == 'large' ? '40px' : '23px'}; + max-height: 150px; display: flex; align-items: end; justify-content: space-evenly; @@ -462,12 +534,16 @@ const StyledTextarea = styled.textarea` box-sizing: border-box; width: 100%; border: 1px solid #686877; - padding: ${props => props.theme.dimensions.size === 'large' ? '18px 12px 14px 12px' : '8px 12px 4px 12px'}; + padding: ${(props) => + props.theme.dimensions!.size === 'large' + ? '18px 12px 14px 12px' + : '8px 12px 4px 12px'}; background-color: transparent; font-size: 16px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; border-radius: 6px; - color: ${props => props.theme.text}; + color: ${(props) => props.theme.text}; outline: none; resize: none; transition: height 0.1s ease; @@ -475,7 +551,8 @@ const StyledTextarea = styled.textarea` white-space: pre-wrap; line-height: 1.4; text-align: left; - min-height: ${props => props.theme.dimensions.size === 'large' ? '60px' : '40px'}; + min-height: ${(props) => + props.theme.dimensions!.size === 'large' ? '60px' : '40px'}; max-height: 140px; overflow-y: auto; scrollbar-width: thin; @@ -493,35 +570,37 @@ const StyledTextarea = styled.textarea` } &::placeholder { text-align: left; - } `; const StyledButton = styled.button` display: flex; justify-content: center; align-items: center; - background-image: linear-gradient(to bottom right, #5AF0EC, #E80D9D); + background-image: linear-gradient(to bottom right, #5af0ec, #e80d9d); background-color: rgba(0, 0, 0, 0.3); border-radius: 6px; - min-width: ${props => props.theme.dimensions.size === 'large' ? '60px' : '40px'}; - height: ${props => props.theme.dimensions.size === 'large' ? '60px' : '40px'}; - margin-left:8px; + min-width: ${(props) => + props.theme.dimensions!.size === 'large' ? '60px' : '40px'}; + height: ${(props) => + props.theme.dimensions!.size === 'large' ? '60px' : '40px'}; + margin-left: 8px; padding: 0px; - + border: none; cursor: pointer; outline: none; - &:hover{ + &:hover { opacity: 90%; } &:disabled { background-image: linear-gradient(to bottom right, #2d938f, #b31877); - }`; + } +`; const HeroContainer = styled.div` position: relative; width: 90%; max-width: 500px; - background-image: linear-gradient(to bottom right, #5AF0EC, #ff1bf4); + background-image: linear-gradient(to bottom right, #5af0ec, #ff1bf4); border-radius: 10px; margin: 16px auto; padding: 2px; @@ -532,48 +611,47 @@ const HeroWrapper = styled.div` justify-content: flex-start; gap: 8px; align-items: middle; - background-color: ${props => props.theme.primary.bg}; - border-radius: 10px; + background-color: ${(props) => props.theme.primary.bg}; + border-radius: 10px; font-weight: normal; padding: 12px; -` +`; const HeroTitle = styled.h3` - color: ${props => props.theme.text}; + color: ${(props) => props.theme.text}; font-size: 16px; - margin:0px ; + margin: 0px; padding: 0px; `; const HeroDescription = styled.p` - color: ${props => props.theme.text}; + color: ${(props) => props.theme.text}; font-size: 12px; line-height: 1.5; margin: 0px; padding: 0px; `; const Hyperlink = styled.a` - color: #9971EC; + color: #9971ec; text-decoration: none; `; const Tagline = styled.div` text-align: center; display: block; - color: ${props => props.theme.secondary.text}; - padding: 12px ; + color: ${(props) => props.theme.secondary.text}; + padding: 12px; font-size: 12px; `; - const SourcesList = styled.div` display: flex; - margin:12px 0px; + margin: 12px 0px; flex-wrap: wrap; gap: 8px; `; const SourceLink = styled.a` - color: ${props => props.theme.primary.text}; + color: ${(props) => props.theme.primary.text}; text-decoration: none; - background: ${props => props.theme.secondary.bg}; + background: ${(props) => props.theme.secondary.bg}; padding: 4px 12px; border-radius: 85px; font-size: 14px; @@ -585,14 +663,14 @@ const SourceLink = styled.a` overflow: hidden; text-overflow: ellipsis; line-height: 1.5; - + &:hover { opacity: 0.8; } `; const ExtraButton = styled.button` - color: #9971EC; + color: #9971ec; background: transparent; border-radius: 85px; padding: 4px 12px; @@ -601,23 +679,27 @@ const ExtraButton = styled.button` cursor: pointer; transition: opacity 0.2s ease; text-align: center; - height:auto; + height: auto; &:hover { opacity: 0.8; } `; -const SourcesComponent = ({ sources }: { sources: Array<{ source: string; title: string }> }) => { +const SourcesComponent = ({ + sources, +}: { + sources: Array<{ source: string; title: string }>; +}) => { const [showAll, setShowAll] = React.useState(false); const visibleSources = showAll ? sources : sources.slice(0, 3); const extraCount = sources.length - 3; - + return ( {visibleSources.map((source, idx) => ( - @@ -626,18 +708,30 @@ const SourcesComponent = ({ sources }: { sources: Array<{ source: string; title: ))} {sources.length > 3 && ( setShowAll(!showAll)}> - {showAll ? "Show less" : `+ ${extraCount} more`} + {showAll ? 'Show less' : `+ ${extraCount} more`} )} ); }; -const Hero = ({ title, description, theme }: { title: string, description: string, theme: string }) => { +const Hero = ({ + title, + description, + theme, +}: { + title: string; + description: string; + theme: string; +}) => { return ( - + {title} {description} @@ -645,26 +739,26 @@ const Hero = ({ title, description, theme }: { title: string, description: strin ); }; export const DocsGPTWidget = (props: WidgetProps) => { - const { buttonIcon = 'https://d3dg1063dc54p9.cloudfront.net/widget/chat.svg', buttonText = 'Ask a question', buttonBg = 'linear-gradient(to bottom right, #5AF0EC, #E80D9D)', defaultOpen = false, ...coreProps - } = props + } = props; const [open, setOpen] = React.useState(defaultOpen); const [isAnimatingButton, setIsAnimatingButton] = React.useState(false); - const [isFloatingButtonVisible, setIsFloatingButtonVisible] = React.useState(true); + const [isFloatingButtonVisible, setIsFloatingButtonVisible] = + React.useState(true); React.useEffect(() => { if (isFloatingButtonVisible) setTimeout(() => setIsAnimatingButton(true), 250); return () => { - setIsAnimatingButton(false) - } - }, [isFloatingButtonVisible]) + setIsAnimatingButton(false); + }; + }, [isFloatingButtonVisible]); const handleClose = () => { setIsFloatingButtonVisible(true); @@ -673,24 +767,29 @@ export const DocsGPTWidget = (props: WidgetProps) => { const handleOpen = () => { setOpen(true); setIsFloatingButtonVisible(false); - } + }; return ( <> - + {buttonText} - ) -} + ); +}; export const WidgetCore = ({ apiHost = 'https://gptcloud.arc53.com', - apiKey = "527686a3-e867-4b4d-9fec-f5f45fdb613a", + apiKey = '527686a3-e867-4b4d-9fec-f5f45fdb613a', avatar = 'https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png', title = 'Get AI assistance', - description = 'DocsGPT\'s AI Chatbot is here to help', + description = "DocsGPT's AI Chatbot is here to help", heroTitle = 'Welcome to DocsGPT !', heroDescription = 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.', size = 'small', @@ -699,15 +798,17 @@ export const WidgetCore = ({ isOpen = false, showSources = true, handleClose, - prefilledQuery = '' + prefilledQuery = '', }: WidgetCoreProps) => { - const [prompt, setPrompt] = React.useState(""); + const [prompt, setPrompt] = React.useState(''); const [mounted, setMounted] = React.useState(false); const [status, setStatus] = React.useState('idle'); const [queries, setQueries] = React.useState([]); - const [conversationId, setConversationId] = React.useState(null); + const [conversationId, setConversationId] = React.useState( + null, + ); const [eventInterrupt, setEventInterrupt] = React.useState(false); //click or scroll by user while autoScrolling - const [hasScrolledToLast, setHasScrolledToLast] = useState(true); + const [, setHasScrolledToLast] = useState(true); const isBubbleHovered = useRef(false); const conversationRef = useRef(null); @@ -715,23 +816,22 @@ export const WidgetCore = ({ const promptRef = React.useRef(null); const md = new MarkdownIt(); //Custom markdown for the table - md.renderer.rules.table_open = () => '
'; + md.renderer.rules.table_open = () => + '
'; md.renderer.rules.table_close = () => '
'; md.renderer.rules.thead_open = () => ''; md.renderer.rules.tr_open = () => ''; md.renderer.rules.td_open = () => ''; md.renderer.rules.th_open = () => ''; - - React.useEffect(() => { if (isOpen) { setMounted(true); // Mount the component - appendQuery(prefilledQuery) + appendQuery(prefilledQuery); } else { // Wait for animations before unmounting const timeout = setTimeout(() => { - setMounted(false) + setMounted(false); }, 250); return () => clearTimeout(timeout); } @@ -739,12 +839,16 @@ export const WidgetCore = ({ const handleUserInterrupt = () => { if (!eventInterrupt && status === 'loading') setEventInterrupt(true); - } + }; const scrollIntoView = () => { if (!conversationRef?.current || eventInterrupt) return; - if (status === 'idle' || !queries.length || !queries[queries.length - 1].response) { + if ( + status === 'idle' || + !queries.length || + !queries[queries.length - 1].response + ) { conversationRef.current.scrollTo({ behavior: 'smooth', top: conversationRef.current.scrollHeight, @@ -763,8 +867,8 @@ export const WidgetCore = ({ }; React.useEffect(() => { - !eventInterrupt && scrollIntoView(); - + if (!eventInterrupt) scrollIntoView(); + conversationRef.current?.addEventListener('scroll', checkScroll); return () => { conversationRef.current?.removeEventListener('scroll', checkScroll); @@ -774,44 +878,52 @@ export const WidgetCore = ({ async function handleFeedback(feedback: FEEDBACK, index: number) { let query = queries[index]; if (!query.response || !conversationId) { - console.log("Cannot submit feedback: missing response or conversation ID"); + console.log( + 'Cannot submit feedback: missing response or conversation ID', + ); return; } // If clicking the same feedback button that's already active, remove the feedback by sending null if (query.feedback === feedback) { try { - const response = await sendFeedback({ - question: query.prompt, - answer: query.response, - feedback: null, - apikey: apiKey, - conversation_id: conversationId, - question_index: index, - }, apiHost); + const response = await sendFeedback( + { + question: query.prompt, + answer: query.response, + feedback: null, + apikey: apiKey, + conversation_id: conversationId, + question_index: index, + }, + apiHost, + ); if (response.status === 200) { const updatedQuery = { ...query }; delete updatedQuery.feedback; setQueries((prev: Query[]) => - prev.map((q, i) => (i === index ? updatedQuery : q)) + prev.map((q, i) => (i === index ? updatedQuery : q)), ); } } catch (err) { - console.error("Failed to submit feedback:", err); + console.error('Failed to submit feedback:', err); } return; } try { - const response = await sendFeedback({ - question: query.prompt, - answer: query.response, - feedback: feedback, - apikey: apiKey, - conversation_id: conversationId, - question_index: index, - }, apiHost); + const response = await sendFeedback( + { + question: query.prompt, + answer: query.response, + feedback: feedback, + apikey: apiKey, + conversation_id: conversationId, + question_index: index, + }, + apiHost, + ); if (response.status === 200) { setQueries((prev: Query[]) => { @@ -824,124 +936,121 @@ export const WidgetCore = ({ }); } } catch (err) { - console.error("Failed to submit feedback:", err); + console.error('Failed to submit feedback:', err); } } async function stream(question: string) { - setStatus('loading') + setStatus('loading'); try { - await fetchAnswerStreaming( - { - question: question, - apiKey: apiKey, - apiHost: apiHost, - history: queries, - conversationId: conversationId, - onEvent: (event: MessageEvent) => { - const data = JSON.parse(event.data); - // check if the 'end' event has been received - if (data.type === 'end') { - setStatus('idle'); - } - else if (data.type === 'id') { - setConversationId(data.id) - } - else if (data.type === 'error') { - const updatedQueries = [...queries]; - updatedQueries[updatedQueries.length - 1].error = data.error; - setQueries(updatedQueries); - setStatus('idle') - } - else if (data.type === 'source' && showSources) { - const updatedQueries = [...queries]; - updatedQueries[updatedQueries.length - 1].sources = data.source; - setQueries(updatedQueries); - } - else { - const result = data.answer ? data.answer : ''; //Fallback to an empty string if data.answer is undefined - const streamingResponse = queries[queries.length - 1].response ? queries[queries.length - 1].response : ''; - const updatedQueries = [...queries]; - updatedQueries[updatedQueries.length - 1].response = streamingResponse + result; - setQueries(updatedQueries); - } + await fetchAnswerStreaming({ + question: question, + apiKey: apiKey, + apiHost: apiHost, + history: queries, + conversationId: conversationId, + onEvent: (event: MessageEvent) => { + const data = JSON.parse(event.data); + // check if the 'end' event has been received + if (data.type === 'end') { + setStatus('idle'); + } else if (data.type === 'id') { + setConversationId(data.id); + } else if (data.type === 'error') { + const updatedQueries = [...queries]; + updatedQueries[updatedQueries.length - 1].error = data.error; + setQueries(updatedQueries); + setStatus('idle'); + } else if (data.type === 'source' && showSources) { + const updatedQueries = [...queries]; + updatedQueries[updatedQueries.length - 1].sources = data.source; + setQueries(updatedQueries); + } else { + const result = data.answer ? data.answer : ''; //Fallback to an empty string if data.answer is undefined + const streamingResponse = queries[queries.length - 1].response + ? queries[queries.length - 1].response + : ''; + const updatedQueries = [...queries]; + updatedQueries[updatedQueries.length - 1].response = + streamingResponse + result; + setQueries(updatedQueries); } - } - ); - } catch (error) { + }, + }); + } catch { const updatedQueries = [...queries]; - updatedQueries[updatedQueries.length - 1].error = 'Something went wrong !' + updatedQueries[updatedQueries.length - 1].error = + 'Something went wrong !'; setQueries(updatedQueries); - setStatus('idle') + setStatus('idle'); //setEventInterrupt(false) } - } - const appendQuery = async (userQuery: string) => { - if (!userQuery) - return; + if (!userQuery) return; setEventInterrupt(false); queries.push({ prompt: userQuery }); setPrompt(''); await stream(userQuery); - } + }; // submit handler const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!prompt.trim()) return; if (promptRef.current) { - promptRef.current.style.height = "auto"; + promptRef.current.style.height = 'auto'; } await appendQuery(prompt); - } - const handlePromptKeyDown = async (e: React.KeyboardEvent) => { + }; + const handlePromptKeyDown = async ( + e: React.KeyboardEvent, + ) => { if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); // Prevent sending empty messages - if (promptRef.current && promptRef.current.value.trim() === "") return; + if (promptRef.current && promptRef.current.value.trim() === '') return; //Rest the input to it's original size after submitting - if(promptRef.current){ - promptRef.current.value = ""; - promptRef.current.style.height = "auto"; + if (promptRef.current) { + promptRef.current.value = ''; + promptRef.current.style.height = 'auto'; } await appendQuery(prompt); - } -} + } + }; // Auto-resize the input textarea while typing, clamping to base or max height - const handleUserInput = (e: React.KeyboardEvent) =>{ - const el = promptRef.current; - if (!el) return; - const baseHeight = size === 'large' ? 60 : 40; - const maxHeight = 140; - el.style.height = 'auto'; - const next = Math.min(el.scrollHeight, maxHeight); - el.style.height = Math.max(baseHeight, next) + 'px'; + const handleUserInput = () => { + const el = promptRef.current; + if (!el) return; + const baseHeight = size === 'large' ? 60 : 40; + const maxHeight = 140; + el.style.height = 'auto'; + const next = Math.min(el.scrollHeight, maxHeight); + el.style.height = Math.max(baseHeight, next) + 'px'; + }; - } - // Update prompt state, auto resize textarea to content, and maintain scroll on new lines - const handlePromptChange = (event: React.ChangeEvent) => { - const value = event.target.value; - setPrompt(value); - const el = event.currentTarget; - const baseHeight = size === 'large' ? 60 : 40; - const maxHeight = 140; - el.style.height = 'auto'; - const next = Math.min(el.scrollHeight, maxHeight); - el.style.height = Math.max(baseHeight, next) + 'px'; - if(value.includes("\n")){ - el.scrollTop = el.scrollHeight; - - } - - - } - const handleImageError = (event: React.SyntheticEvent) => { - event.currentTarget.src = "https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"; + const handlePromptChange = ( + event: React.ChangeEvent, + ) => { + const value = event.target.value; + setPrompt(value); + const el = event.currentTarget; + const baseHeight = size === 'large' ? 60 : 40; + const maxHeight = 140; + el.style.height = 'auto'; + const next = Math.min(el.scrollHeight, maxHeight); + el.style.height = Math.max(baseHeight, next) + 'px'; + if (value.includes('\n')) { + el.scrollTop = el.scrollHeight; + } + }; + const handleImageError = ( + event: React.SyntheticEvent, + ) => { + event.currentTarget.src = + 'https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png'; }; const dimensions = @@ -950,123 +1059,200 @@ export const WidgetCore = ({ : sizesConfig[size]; if (!mounted) return null; - - return ( - {isOpen && size === 'large' && - - } - {( - + {isOpen && size === 'large' && } + { +
- +
- docs-gpt + docs-gpt {title} {description}
- - { - queries.length > 0 ? queries?.map((query, index) => { + > + {queries.length > 0 ? ( + queries?.map((query, index) => { return ( - { - query.prompt && - + {query.prompt && ( + + $type="QUESTION" + ref={ + !(query.response || query.error) && + index === queries.length - 1 + ? endMessageRef + : null + } + > {query.prompt} - } - { - query.response ? { isBubbleHovered.current = true }} $type='ANSWER'> - {showSources && query.sources && query.sources.length > 0 && query.sources.some(source => source.source !== 'local') && ( - source.source !== 'local')} /> - )} + )} + {query.response ? ( + { + isBubbleHovered.current = true; + }} + $type="ANSWER" + > + {showSources && + query.sources && + query.sources.length > 0 && + query.sources.some( + (source) => source.source !== 'local', + ) && ( + source.source !== 'local', + )} + /> + )} - {collectFeedback && + {collectFeedback && ( - } + + )} - :
- { - query.error ? - - -
-
Network Error
- {query.error} -
-
- : - - . - . - . - - - } -
- } -
) + ) : ( +
+ {query.error ? ( + + +
+
Network Error
+ + {query.error} + +
+
+ ) : ( + + + . + . + . + + + )} +
+ )} + + ); }) - : - } + ) : ( + + )}
- + - + disabled={prompt.trim().length == 0 || status !== 'idle'} + > + Powered by  - DocsGPT + + DocsGPT +
- ) }
- ) -} \ No newline at end of file + ); +}; diff --git a/extensions/react-widget/src/components/SearchBar.tsx b/extensions/react-widget/src/components/SearchBar.tsx index c890e054..a6a8ae18 100644 --- a/extensions/react-widget/src/components/SearchBar.tsx +++ b/extensions/react-widget/src/components/SearchBar.tsx @@ -7,235 +7,239 @@ import { Result } from '@/types'; import { getOS, processMarkdownString } from '../utils/helper'; import DOMPurify from 'dompurify'; import { - CodeIcon, - TextAlignLeftIcon, - HeadingIcon, - ReaderIcon, - ListBulletIcon, - QuoteIcon + CodeIcon, + TextAlignLeftIcon, + HeadingIcon, + ReaderIcon, + ListBulletIcon, + QuoteIcon, } from '@radix-ui/react-icons'; const themes = { - dark: { - name: 'dark', - bg: '#202124', - text: '#EDEDED', - primary: { - text: "#FAFAFA", - bg: '#111111' - }, - secondary: { - text: "#A1A1AA", - bg: "#38383b" - } + dark: { + name: 'dark', + bg: '#202124', + text: '#EDEDED', + primary: { + text: '#FAFAFA', + bg: '#111111', }, - light: { - name: 'light', - bg: '#EAEAEA', - text: '#171717', - primary: { - text: "#222327", - bg: "#fff" - }, - secondary: { - text: "#A1A1AA", - bg: "#F6F6F6" - } - } -} + secondary: { + text: '#A1A1AA', + bg: '#38383b', + }, + }, + light: { + name: 'light', + bg: '#EAEAEA', + text: '#171717', + primary: { + text: '#222327', + bg: '#fff', + }, + secondary: { + text: '#A1A1AA', + bg: '#F6F6F6', + }, + }, +}; const GlobalStyle = createGlobalStyle` .highlight { - color: ${props => props.theme.name === 'dark' ? '#4B9EFF' : '#0066CC'}; + color: ${(props) => (props.theme.name === 'dark' ? '#4B9EFF' : '#0066CC')}; font-weight: 500; } `; const loadGeistFont = () => { - const link = document.createElement('link'); - link.href = 'https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap'; - link.rel = 'stylesheet'; - document.head.appendChild(link); + const link = document.createElement('link'); + link.href = + 'https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap'; + link.rel = 'stylesheet'; + document.head.appendChild(link); }; const Main = styled.div` - all: initial; - font-family: 'Geist', sans-serif; -` + all: initial; + font-family: 'Geist', sans-serif; +`; const SearchButton = styled.button<{ $inputWidth: string }>` - padding: 6px 6px; - font-family: inherit; - width: ${({ $inputWidth }) => $inputWidth}; - border-radius: 8px; - display: inline; - color: ${props => props.theme.secondary.text}; - outline: none; - border: none; - background-color: ${props => props.theme.secondary.bg}; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - transition: background-color 128ms linear; - text-align: left; - cursor: pointer; -` - -const Container = styled.div` - position: relative; - display: inline-block; -` -const SearchOverlay = styled.div` - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: #0000001A; - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - z-index: 99; + padding: 6px 6px; + font-family: inherit; + width: ${({ $inputWidth }) => $inputWidth}; + border-radius: 8px; + display: inline; + color: ${(props) => props.theme.secondary.text}; + outline: none; + border: none; + background-color: ${(props) => props.theme.secondary.bg}; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + transition: background-color 128ms linear; + text-align: left; + cursor: pointer; `; +const Container = styled.div` + position: relative; + display: inline-block; +`; +const SearchOverlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #0000001a; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + z-index: 99; +`; const SearchResults = styled.div` - position: fixed; - display: flex; - flex-direction: column; - background-color: ${props => props.theme.name === 'dark' ? - 'rgba(0, 0, 0, 0.15)' : - 'rgba(255, 255, 255, 0.4)'}; - border: 1px solid rgba(255, 255, 255, 0.18); - border-radius: 15px; - padding: 8px 0px 8px 0px; - width: 792px; - max-width: 90vw; - height: 396px; - z-index: 100; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - color: ${props => props.theme.primary.text}; - - box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); - backdrop-filter: blur(82px); - -webkit-backdrop-filter: blur(82px); - border-radius: 10px; - - box-sizing: border-box; + position: fixed; + display: flex; + flex-direction: column; + background-color: ${(props) => + props.theme.name === 'dark' + ? 'rgba(0, 0, 0, 0.15)' + : 'rgba(255, 255, 255, 0.4)'}; + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 15px; + padding: 8px 0px 8px 0px; + width: 792px; + max-width: 90vw; + height: 396px; + z-index: 100; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + color: ${(props) => props.theme.primary.text}; - @media only screen and (max-width: 768px) { - height: 80vh; - width: 90vw; - } + box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); + backdrop-filter: blur(82px); + -webkit-backdrop-filter: blur(82px); + border-radius: 10px; + + box-sizing: border-box; + + @media only screen and (max-width: 768px) { + height: 80vh; + width: 90vw; + } `; const SearchResultsScroll = styled.div` - flex: 1; - overflow-y: auto; - overflow-x: hidden; - scrollbar-gutter: stable; - scrollbar-width: thin; - scrollbar-color: #383838 transparent; - padding: 0 16px; + flex: 1; + overflow-y: auto; + overflow-x: hidden; + scrollbar-gutter: stable; + scrollbar-width: thin; + scrollbar-color: #383838 transparent; + padding: 0 16px; `; const IconTitleWrapper = styled.div` - display: flex; - align-items: center; - gap: 8px; + display: flex; + align-items: center; + gap: 8px; - .element-icon{ - margin: 4px; - } + .element-icon { + margin: 4px; + } `; const Title = styled.h3` - font-size: 15px; - font-weight: 400; - color: ${props => props.theme.primary.text}; - margin: 0; - overflow-wrap: break-word; - white-space: normal; - overflow: hidden; - text-overflow: ellipsis; + font-size: 15px; + font-weight: 400; + color: ${(props) => props.theme.primary.text}; + margin: 0; + overflow-wrap: break-word; + white-space: normal; + overflow: hidden; + text-overflow: ellipsis; `; const ContentWrapper = styled.div` - display: flex; - flex-direction: column; - gap: 12px; + display: flex; + flex-direction: column; + gap: 12px; `; - - const ResultWrapper = styled.div` - display: flex; - align-items: flex-start; - width: 100%; - box-sizing: border-box; - padding: 8px 16px; - cursor: pointer; - background-color: transparent; - font-family: 'Geist', sans-serif; - border-radius: 8px; + display: flex; + align-items: flex-start; + width: 100%; + box-sizing: border-box; + padding: 8px 16px; + cursor: pointer; + background-color: transparent; + font-family: 'Geist', sans-serif; + border-radius: 8px; - word-wrap: break-word; - overflow-wrap: break-word; - word-break: break-word; - white-space: normal; - overflow: hidden; - text-overflow: ellipsis; + word-wrap: break-word; + overflow-wrap: break-word; + word-break: break-word; + white-space: normal; + overflow: hidden; + text-overflow: ellipsis; - &:hover { - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - } + &:hover { + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + } `; const Content = styled.div` - display: flex; - margin-left: 8px; - flex-direction: column; - gap: 8px; - padding: 4px 0px 0px 12px; - font-size: 15px; - color: ${props => props.theme.primary.text}; - line-height: 1.6; - border-left: 2px solid ${props => props.theme.primary.text}CC; - overflow: hidden; - + display: flex; + margin-left: 8px; + flex-direction: column; + gap: 8px; + padding: 4px 0px 0px 12px; + font-size: 15px; + color: ${(props) => props.theme.primary.text}; + line-height: 1.6; + border-left: 2px solid ${(props) => props.theme.primary.text}CC; + overflow: hidden; `; const ContentSegment = styled.div` - display: flex; - align-items: flex-start; - gap: 8px; - padding-right: 16px; - overflow-wrap: break-word; - white-space: normal; - overflow: hidden; - text-overflow: ellipsis; -` + display: flex; + align-items: flex-start; + gap: 8px; + padding-right: 16px; + overflow-wrap: break-word; + white-space: normal; + overflow: hidden; + text-overflow: ellipsis; +`; const Toolkit = styled.kbd` - position: absolute; - right: 4px; - top: 50%; - transform: translateY(-50%); - background-color: ${(props) => props.theme.primary.bg}; - color: ${(props) => props.theme.secondary.text}; - font-weight: 600; - font-size: 10px; - padding: 3px 6px; - border: 1px solid ${(props) => props.theme.secondary.text}; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; - z-index: 1; - pointer-events: none; -` + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + background-color: ${(props) => props.theme.primary.bg}; + color: ${(props) => props.theme.secondary.text}; + font-weight: 600; + font-size: 10px; + padding: 3px 6px; + border: 1px solid ${(props) => props.theme.secondary.text}; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + pointer-events: none; +`; const Loader = styled.div` margin: 2rem auto; - border: 4px solid ${props => props.theme.name === 'dark' ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)'}; - border-top: 4px solid ${props => props.theme.name === 'dark' ? '#FFFFFF' : props.theme.primary.bg}; + border: 4px solid + ${(props) => + props.theme.name === 'dark' + ? 'rgba(255, 255, 255, 0.2)' + : 'rgba(0, 0, 0, 0.1)'}; + border-top: 4px solid + ${(props) => + props.theme.name === 'dark' ? '#FFFFFF' : props.theme.primary.bg}; border-radius: 50%; width: 12px; height: 12px; @@ -255,320 +259,353 @@ const NoResults = styled.div` margin-top: 2rem; text-align: center; font-size: 14px; - color: ${props => props.theme.name === 'dark' ? '#E0E0E0' : '#505050'}; + color: ${(props) => (props.theme.name === 'dark' ? '#E0E0E0' : '#505050')}; font-weight: 500; `; const AskAIButton = styled.button` - display: flex; - align-items: center; - justify-content: flex-start; - gap: 12px; - width: calc(100% - 32px); - margin: 0 16px 16px 16px; - box-sizing: border-box; - height: 50px; - padding: 8px 24px; - border: none; - border-radius: 8px; - color: ${props => props.theme.text}; - cursor: pointer; - font-size: 16px; - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - background-color: ${props => props.theme.name === 'dark' ? - 'rgba(255, 255, 255, 0.05)' : - 'rgba(0, 0, 0, 0.03)'}; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 12px; + width: calc(100% - 32px); + margin: 0 16px 16px 16px; + box-sizing: border-box; + height: 50px; + padding: 8px 24px; + border: none; + border-radius: 8px; + color: ${(props) => props.theme.text}; + cursor: pointer; + font-size: 16px; + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + background-color: ${(props) => + props.theme.name === 'dark' + ? 'rgba(255, 255, 255, 0.05)' + : 'rgba(0, 0, 0, 0.03)'}; - &:hover { - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - background-color: ${props => props.theme.name === 'dark' ? - 'rgba(255, 255, 255, 0.1)' : - 'rgba(0, 0, 0, 0.06)'}; - } + &:hover { + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + background-color: ${(props) => + props.theme.name === 'dark' + ? 'rgba(255, 255, 255, 0.1)' + : 'rgba(0, 0, 0, 0.06)'}; + } `; const SearchHeader = styled.div` - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 12px; - padding-bottom: 12px; - border-bottom: 1px solid ${props => props.theme.name === 'dark' ? '#FFFFFF24' : 'rgba(0, 0, 0, 0.14)'}; + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px solid + ${(props) => + props.theme.name === 'dark' ? '#FFFFFF24' : 'rgba(0, 0, 0, 0.14)'}; `; - - const TextField = styled.input` - width: calc(100% - 32px); - margin: 0 16px; - padding: 12px 16px; - border: none; - background-color: transparent; - color: ${props => props.theme.text}; - font-size: 20px; - font-weight: 400; - outline: none; - - &:focus { - border-color: none; - } + width: calc(100% - 32px); + margin: 0 16px; + padding: 12px 16px; + border: none; + background-color: transparent; + color: ${(props) => props.theme.text}; + font-size: 20px; + font-weight: 400; + outline: none; - &::placeholder { - color: ${props => props.theme.name === 'dark' ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.5)'} !important; - opacity: 100%; /* Force opacity to ensure placeholder is visible */ - font-weight: 500; - } -` + &:focus { + border-color: none; + } - - -const EscapeInstruction = styled.kbd` - display: flex; - align-items: center; - justify-content: center; - margin: 12px 16px 0; - padding: 4px 8px; - border-radius: 4px; - background-color: transparent; - border: 1px solid ${props => props.theme.name === 'dark' ? - 'rgba(237, 237, 237, 0.6)' : - 'rgba(23, 23, 23, 0.6)'}; - color: ${props => props.theme.name === 'dark' ? '#EDEDED' : '#171717'}; - font-size: 12px; - font-family: 'Geist', sans-serif; - white-space: nowrap; - cursor: pointer; - width: fit-content; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; + &::placeholder { + color: ${(props) => + props.theme.name === 'dark' + ? 'rgba(255, 255, 255, 0.6)' + : 'rgba(0, 0, 0, 0.5)'} !important; + opacity: 100%; /* Force opacity to ensure placeholder is visible */ + font-weight: 500; + } `; +const EscapeInstruction = styled.kbd` + display: flex; + align-items: center; + justify-content: center; + margin: 12px 16px 0; + padding: 4px 8px; + border-radius: 4px; + background-color: transparent; + border: 1px solid + ${(props) => + props.theme.name === 'dark' + ? 'rgba(237, 237, 237, 0.6)' + : 'rgba(23, 23, 23, 0.6)'}; + color: ${(props) => (props.theme.name === 'dark' ? '#EDEDED' : '#171717')}; + font-size: 12px; + font-family: 'Geist', sans-serif; + white-space: nowrap; + cursor: pointer; + width: fit-content; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +`; export const SearchBar = ({ - apiKey = "74039c6d-bff7-44ce-ae55-2973cbf13837", - apiHost = "https://gptcloud.arc53.com", - theme = "dark", - placeholder = "Search or Ask AI...", - width = "256px", - buttonText = "Search here" + apiKey = '74039c6d-bff7-44ce-ae55-2973cbf13837', + apiHost = 'https://gptcloud.arc53.com', + theme = 'dark', + placeholder = 'Search or Ask AI...', + width = '256px', + buttonText = 'Search here', }: SearchBarProps) => { - const [input, setInput] = React.useState(""); - const [loading, setLoading] = React.useState(false); - const [isWidgetOpen, setIsWidgetOpen] = React.useState(false); - const inputRef = React.useRef(null); - const containerRef = React.useRef(null); - const [isResultVisible, setIsResultVisible] = React.useState(false); - const [results, setResults] = React.useState([]); - const debounceTimeout = React.useRef | null>(null); - const abortControllerRef = React.useRef(null); - const browserOS = getOS(); - const isTouch = 'ontouchstart' in window; + const [input, setInput] = React.useState(''); + const [loading, setLoading] = React.useState(false); + const [isWidgetOpen, setIsWidgetOpen] = React.useState(false); + const inputRef = React.useRef(null); + const containerRef = React.useRef(null); + const [isResultVisible, setIsResultVisible] = React.useState(false); + const [results, setResults] = React.useState([]); + const debounceTimeout = React.useRef | null>( + null, + ); + const abortControllerRef = React.useRef(null); + const browserOS = getOS(); + const isTouch = 'ontouchstart' in window; - const getKeyboardInstruction = () => { - if (isResultVisible) return "Enter"; - return browserOS === 'mac' ? '⌘ + K' : 'Ctrl + K'; + const getKeyboardInstruction = () => { + if (isResultVisible) return 'Enter'; + return browserOS === 'mac' ? '⌘ + K' : 'Ctrl + K'; + }; + + React.useEffect(() => { + loadGeistFont(); + const handleClickOutside = (event: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + setIsResultVisible(false); + } }; - React.useEffect(() => { - loadGeistFont() - const handleClickOutside = (event: MouseEvent) => { - if (containerRef.current && !containerRef.current.contains(event.target as Node)) { - setIsResultVisible(false); - } - }; + const handleKeyDown = (event: KeyboardEvent) => { + if ( + ((browserOS === 'win' || browserOS === 'linux') && + event.ctrlKey && + event.key === 'k') || + (browserOS === 'mac' && event.metaKey && event.key === 'k') + ) { + event.preventDefault(); + inputRef.current?.focus(); + setIsResultVisible(true); + } else if (event.key === 'Escape') { + setIsResultVisible(false); + } + }; - const handleKeyDown = (event: KeyboardEvent) => { - if ( - ((browserOS === 'win' || browserOS === 'linux') && event.ctrlKey && event.key === 'k') || - (browserOS === 'mac' && event.metaKey && event.key === 'k') - ) { - event.preventDefault(); - inputRef.current?.focus(); - setIsResultVisible(true); - } else if (event.key === 'Escape') { - setIsResultVisible(false); - } - }; + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleKeyDown); + }; + }, []); - - document.addEventListener('mousedown', handleClickOutside); - document.addEventListener('keydown', handleKeyDown); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - document.removeEventListener('keydown', handleKeyDown); - }; - }, []); - - React.useEffect(() => { + React.useEffect(() => { if (!input) { - setResults([]); - setLoading(false); - return; + setResults([]); + setLoading(false); + return; } setLoading(true); if (debounceTimeout.current) { - clearTimeout(debounceTimeout.current); + clearTimeout(debounceTimeout.current); } if (abortControllerRef.current) { - abortControllerRef.current.abort(); + abortControllerRef.current.abort(); } const abortController = new AbortController(); abortControllerRef.current = abortController; debounceTimeout.current = setTimeout(() => { - getSearchResults(input, apiKey, apiHost, abortController.signal) - .then((data) => setResults(data)) - .catch((err) => !abortController.signal.aborted && console.log(err)) - .finally(() => setLoading(false)); + getSearchResults(input, apiKey, apiHost, abortController.signal) + .then((data) => setResults(data)) + .catch((err) => !abortController.signal.aborted && console.log(err)) + .finally(() => setLoading(false)); }, 500); return () => { - abortController.abort(); - clearTimeout(debounceTimeout.current ?? undefined); + abortController.abort(); + clearTimeout(debounceTimeout.current ?? undefined); }; -}, [input]) + }, [input]); - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - event.preventDefault(); - openWidget(); - } - }; + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + openWidget(); + } + }; - const openWidget = () => { - setIsWidgetOpen(true); - setIsResultVisible(false); - }; + const openWidget = () => { + setIsWidgetOpen(true); + setIsResultVisible(false); + }; - const handleClose = () => { - setIsWidgetOpen(false); - setIsResultVisible(true); - }; + const handleClose = () => { + setIsWidgetOpen(false); + setIsResultVisible(true); + }; - return ( - -
- - - setIsResultVisible(true)} - $inputWidth={width} - > - {buttonText} - - { - isResultVisible && ( - <> - setIsResultVisible(false)} /> - - - setInput(e.target.value)} - onKeyDown={(e) => handleKeyDown(e)} - placeholder={placeholder} - autoFocus - /> - setIsResultVisible(false)}> - Esc - - - - DocsGPT - Ask the AI - - - {!loading ? ( - results.length > 0 ? ( - results.map((res, key) => { - const containsSource = res.source !== 'local'; - const processedResults = processMarkdownString(res.text, input); - if (processedResults) - return ( - { - if (!containsSource) return; - window.open(res.source, '_blank', 'noopener, noreferrer'); - }} - > -
- - - - {res.title} - - - {processedResults.map((element, index) => ( - - - {element.tag === 'code' && } - {(element.tag === 'bulletList' || element.tag === 'numberedList') && } - {element.tag === 'text' && } - {element.tag === 'heading' && } - {element.tag === 'blockquote' && } - -
- - ))} - - -
- - ); - return null; - }) - ) : ( - No results found - ) - ) : ( - - )} - - - - ) - } - { - isTouch ? - - { - setIsWidgetOpen(true) - }} - title={"Tap to Ask the AI"}> - Tap - - : - - {getKeyboardInstruction()} - - } - - -
-
- ) -} + return ( + +
+ + + setIsResultVisible(true)} + $inputWidth={width} + > + {buttonText} + + {isResultVisible && ( + <> + setIsResultVisible(false)} /> + + + setInput(e.target.value)} + onKeyDown={(e) => handleKeyDown(e)} + placeholder={placeholder} + autoFocus + /> + setIsResultVisible(false)}> + Esc + + + + DocsGPT + Ask the AI + + + {!loading ? ( + results.length > 0 ? ( + results.map((res, key) => { + const containsSource = res.source !== 'local'; + const processedResults = processMarkdownString( + res.text, + input, + ); + if (processedResults) + return ( + { + if (!containsSource) return; + window.open( + res.source, + '_blank', + 'noopener, noreferrer', + ); + }} + > +
+ + + + {res.title} + + + {processedResults.map((element, index) => ( + + + {element.tag === 'code' && ( + + )} + {(element.tag === 'bulletList' || + element.tag === 'numberedList') && ( + + )} + {element.tag === 'text' && ( + + )} + {element.tag === 'heading' && ( + + )} + {element.tag === 'blockquote' && ( + + )} + +
+ + ))} + + +
+ + ); + return null; + }) + ) : ( + No results found + ) + ) : ( + + )} + + + + )} + {isTouch ? ( + { + setIsWidgetOpen(true); + }} + title={'Tap to Ask the AI'} + > + Tap + + ) : ( + + {getKeyboardInstruction()} + + )} + + +
+
+ ); +}; diff --git a/extensions/react-widget/src/index.ts b/extensions/react-widget/src/index.ts index 5f2e30e8..ecdec8ee 100644 --- a/extensions/react-widget/src/index.ts +++ b/extensions/react-widget/src/index.ts @@ -1,3 +1,3 @@ //exports methods for React -export {SearchBar} from "./components/SearchBar" -export { DocsGPTWidget } from "./components/DocsGPTWidget"; +export { SearchBar } from './components/SearchBar'; +export { DocsGPTWidget } from './components/DocsGPTWidget'; diff --git a/extensions/react-widget/src/main.tsx b/extensions/react-widget/src/main.tsx index f238c51e..0bd57ba2 100644 --- a/extensions/react-widget/src/main.tsx +++ b/extensions/react-widget/src/main.tsx @@ -1,8 +1,7 @@ - //development -import { createRoot } from "react-dom/client"; -import { App } from "./App"; -import React from "react"; -const container = document.getElementById("app") as HTMLElement; -const root = createRoot(container) +import { createRoot } from 'react-dom/client'; +import { App } from './App'; +import React from 'react'; +const container = document.getElementById('app') as HTMLElement; +const root = createRoot(container); root.render(); diff --git a/extensions/react-widget/src/requests/searchAPI.ts b/extensions/react-widget/src/requests/searchAPI.ts index 7411a810..3fc43ed6 100644 --- a/extensions/react-widget/src/requests/searchAPI.ts +++ b/extensions/react-widget/src/requests/searchAPI.ts @@ -1,20 +1,24 @@ -import { Result } from "@/types"; - -async function getSearchResults(question: string, apiKey: string, apiHost: string, signal: AbortSignal): Promise { +import { Result } from '@/types'; +async function getSearchResults( + question: string, + apiKey: string, + apiHost: string, + signal: AbortSignal, +): Promise { const payload = { question, - api_key: apiKey + api_key: apiKey, }; try { const response = await fetch(`${apiHost}/api/search`, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: JSON.stringify(payload), - signal: signal + signal: signal, }); if (!response.ok) { @@ -23,15 +27,12 @@ async function getSearchResults(question: string, apiKey: string, apiHost: strin const data: Result[] = await response.json(); return data; - } catch (error) { - if (!(error instanceof DOMException && error.name == "AbortError")) { - console.error("Failed to fetch documents:", error); + if (!(error instanceof DOMException && error.name == 'AbortError')) { + console.error('Failed to fetch documents:', error); } throw error; } } -export { - getSearchResults -} \ No newline at end of file +export { getSearchResults }; diff --git a/extensions/react-widget/src/requests/streamingApi.ts b/extensions/react-widget/src/requests/streamingApi.ts index f55f9d0c..e27eaa3f 100644 --- a/extensions/react-widget/src/requests/streamingApi.ts +++ b/extensions/react-widget/src/requests/streamingApi.ts @@ -1,5 +1,3 @@ -import { FEEDBACK } from "@/types"; - interface HistoryItem { prompt: string; response?: string; @@ -30,7 +28,9 @@ export function fetchAnswerStreaming({ history = [], conversationId = null, apiHost = '', - onEvent = () => { console.log("Event triggered, but no handler provided."); } + onEvent = () => { + console.log('Event triggered, but no handler provided.'); + }, }: FetchAnswerStreamingProps): Promise { return new Promise((resolve, reject) => { const body = { @@ -38,7 +38,7 @@ export function fetchAnswerStreaming({ history: JSON.stringify(history), conversation_id: conversationId, model: 'default', - api_key: apiKey + api_key: apiKey, }; fetch(apiHost + '/stream', { method: 'POST', @@ -52,7 +52,7 @@ export function fetchAnswerStreaming({ const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); - let counter = 0; + let counter = 0; // eslint-disable-line @typescript-eslint/no-unused-vars const processStream = ({ done, value, @@ -95,12 +95,14 @@ export function fetchAnswerStreaming({ }); } - -export const sendFeedback = (payload: FeedbackPayload, apiHost: string): Promise => { +export const sendFeedback = ( + payload: FeedbackPayload, + apiHost: string, +): Promise => { return fetch(`${apiHost}/api/feedback`, { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', }, body: JSON.stringify({ question: payload.question, @@ -108,7 +110,7 @@ export const sendFeedback = (payload: FeedbackPayload, apiHost: string): Promis feedback: payload.feedback, api_key: payload.apikey, conversation_id: payload.conversation_id, - question_index: payload.question_index + question_index: payload.question_index, }), }); }; diff --git a/extensions/react-widget/src/types/index.ts b/extensions/react-widget/src/types/index.ts index 5c6dada3..9fb60fff 100644 --- a/extensions/react-widget/src/types/index.ts +++ b/extensions/react-widget/src/types/index.ts @@ -1,3 +1,30 @@ +import 'styled-components'; + +declare module 'styled-components' { + export interface DefaultTheme { + bg: string; + text: string; + primary: { + text: string; + bg: string; + }; + secondary: { + text: string; + bg: string; + }; + /** Present only in SearchBar theme */ + name?: string; + /** Present only in DocsGPTWidget theme (always provided when these styled components render) */ + dimensions?: { + size: string; + width: string; + height: string; + maxWidth?: string; + maxHeight?: string; + }; + } +} + export type MESSAGE_TYPE = 'QUESTION' | 'ANSWER' | 'ERROR'; export type Status = 'idle' | 'loading' | 'failed'; @@ -11,7 +38,7 @@ export interface Query { response?: string; feedback?: FEEDBACK; error?: string; - sources?: { title: string; text: string, source:string }[]; + sources?: { title: string; text: string; source: string }[]; conversationId?: string | null; title?: string | null; } @@ -24,30 +51,34 @@ export interface WidgetProps { description?: string; heroTitle?: string; heroDescription?: string; - size?: 'small' | 'medium' | 'large' | { - custom: { - width: string; - height: string; - maxWidth?: string; - maxHeight?: string; - }; - }; - theme?:THEME, - buttonIcon?:string; - buttonText?:string; - buttonBg?:string; - collectFeedback?:boolean; + size?: + | 'small' + | 'medium' + | 'large' + | { + custom: { + width: string; + height: string; + maxWidth?: string; + maxHeight?: string; + }; + }; + theme?: THEME; + buttonIcon?: string; + buttonText?: string; + buttonBg?: string; + collectFeedback?: boolean; showSources?: boolean; defaultOpen?: boolean; } -export interface WidgetCoreProps extends WidgetProps { - widgetRef?:React.RefObject | null; - handleClose?:React.MouseEventHandler | undefined; - isOpen:boolean; +export interface WidgetCoreProps extends WidgetProps { + widgetRef?: React.RefObject | null; + handleClose?: React.MouseEventHandler | undefined; + isOpen: boolean; prefilledQuery?: string; } -export interface SearchBarProps { +export interface SearchBarProps { apiHost?: string; apiKey?: string; theme?: THEME; @@ -57,7 +88,7 @@ export interface SearchBarProps { } export interface Result { - text:string; - title:string; - source:string; + text: string; + title: string; + source: string; } diff --git a/extensions/react-widget/src/utils/helper.ts b/extensions/react-widget/src/utils/helper.ts index 9f92fdcb..75c8c2a0 100644 --- a/extensions/react-widget/src/utils/helper.ts +++ b/extensions/react-widget/src/utils/helper.ts @@ -30,11 +30,15 @@ interface ParsedElement { tag: string; } -export const processMarkdownString = (markdown: string, keyword?: string): ParsedElement[] => { +export const processMarkdownString = ( + markdown: string, + keyword?: string, +): ParsedElement[] => { const lines = markdown.trim().split('\n'); const keywordLower = keyword?.toLowerCase(); - const escapeRegExp = (str: string) => str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + const escapeRegExp = (str: string) => + str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); const escapedKeyword = keyword ? escapeRegExp(keyword) : ''; const keywordRegex = keyword ? new RegExp(`(${escapedKeyword})`, 'gi') : null; @@ -56,7 +60,7 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse const codeContent = codeBlockContent.join('\n'); const parsedElement: ParsedElement = { content: codeContent, - tag: 'code' + tag: 'code', }; if (!firstLine) { @@ -64,7 +68,10 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse } if (keywordLower && codeContent.toLowerCase().includes(keywordLower)) { - parsedElement.content = parsedElement.content.replace(keywordRegex!, '$1'); + parsedElement.content = parsedElement.content.replace( + keywordRegex!, + '$1', + ); matchingLines.push(parsedElement); } } @@ -89,30 +96,30 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse content = headingMatch[2]; parsedElement = { content: content, - tag: 'heading' + tag: 'heading', }; } else if (bulletMatch) { content = bulletMatch[1]; parsedElement = { content: content, - tag: 'bulletList' + tag: 'bulletList', }; } else if (numberedMatch) { content = numberedMatch[1]; parsedElement = { content: content, - tag: 'numberedList' + tag: 'numberedList', }; } else if (blockquoteMatch) { content = blockquoteMatch[1]; parsedElement = { content: content, - tag: 'blockquote' + tag: 'blockquote', }; } else { parsedElement = { content: content, - tag: 'text' + tag: 'text', }; } @@ -120,8 +127,14 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse firstLine = parsedElement; } - if (keywordLower && parsedElement.content.toLowerCase().includes(keywordLower)) { - parsedElement.content = parsedElement.content.replace(keywordRegex!, '$1'); + if ( + keywordLower && + parsedElement.content.toLowerCase().includes(keywordLower) + ) { + parsedElement.content = parsedElement.content.replace( + keywordRegex!, + '$1', + ); matchingLines.push(parsedElement); } } @@ -130,7 +143,7 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse const codeContent = codeBlockContent.join('\n'); const parsedElement: ParsedElement = { content: codeContent, - tag: 'code' + tag: 'code', }; if (!firstLine) { @@ -138,7 +151,10 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse } if (keywordLower && codeContent.toLowerCase().includes(keywordLower)) { - parsedElement.content = parsedElement.content.replace(keywordRegex!, '$1'); + parsedElement.content = parsedElement.content.replace( + keywordRegex!, + '$1', + ); matchingLines.push(parsedElement); } } diff --git a/extensions/react-widget/tsconfig.json b/extensions/react-widget/tsconfig.json index 88e86216..20540869 100644 --- a/extensions/react-widget/tsconfig.json +++ b/extensions/react-widget/tsconfig.json @@ -1,9 +1,5 @@ { "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["src/*", "@/*"] - }, "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], @@ -23,8 +19,11 @@ "noUnusedLocals": false, "noUnusedParameters": false, "noFallthroughCasesInSwitch": true, - /* The "typeRoots" configuration specifies the locations where - TypeScript looks for type definitions (.d.ts files) to + "paths": { + "@/*": ["./src/*"] + }, + /* The "typeRoots" configuration specifies the locations where + TypeScript looks for type definitions (.d.ts files) to include in the compilation process.*/ "typeRoots": ["./dist/index.d.ts", "node_modules/@types"] }, diff --git a/frontend/src/agents/NewAgent.tsx b/frontend/src/agents/NewAgent.tsx index ce2e8f78..7038e1c7 100644 --- a/frontend/src/agents/NewAgent.tsx +++ b/frontend/src/agents/NewAgent.tsx @@ -28,6 +28,7 @@ import { import PromptsModal from '../preferences/PromptsModal'; import Prompts from '../settings/Prompts'; import { UserToolType } from '../settings/types'; +import { getToolDisplayName } from '../utils/toolUtils'; import AgentPreview from './AgentPreview'; import { Agent, ToolSummary } from './types'; import WorkflowBuilder from './workflow/WorkflowBuilder'; @@ -137,7 +138,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { const chunks = ['0', '2', '4', '6', '8', '10']; const agentTypes = [ { label: t('agents.form.agentTypes.classic'), value: 'classic' }, - { label: t('agents.form.agentTypes.react'), value: 'react' }, + { label: 'Agentic', value: 'agentic' }, + { label: 'Research', value: 'research' }, ]; const isPublishable = () => { @@ -428,8 +430,9 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { const data = await response.json(); const tools: OptionType[] = data.tools.map((tool: UserToolType) => ({ id: tool.id, - label: tool.customName ? tool.customName : tool.displayName, + label: getToolDisplayName(tool), icon: `/toolIcons/tool_${tool.name}.svg`, + name: tool.name, })); setUserTools(tools); }; @@ -946,7 +949,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { > {selectedTools.length > 0 ? selectedTools - .map((tool) => tool.display_name || tool.name) + .map((tool) => getToolDisplayName(tool)) .filter(Boolean) .join(', ') : t('agents.form.placeholders.selectTools')} @@ -963,7 +966,10 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { .filter((tool) => newSelectedIds.has(tool.id)) .map((tool) => ({ id: String(tool.id), - name: tool.label, + name: + typeof tool.name === 'string' + ? tool.name + : tool.label, display_name: tool.label, })), ) diff --git a/frontend/src/agents/SharedAgentCard.tsx b/frontend/src/agents/SharedAgentCard.tsx index 2812d5a4..98c86cb7 100644 --- a/frontend/src/agents/SharedAgentCard.tsx +++ b/frontend/src/agents/SharedAgentCard.tsx @@ -1,4 +1,5 @@ import AgentImage from '../components/AgentImage'; +import { getToolDisplayName } from '../utils/toolUtils'; import { Agent } from './types'; export default function SharedAgentCard({ agent }: { agent: Agent }) { @@ -64,10 +65,10 @@ export default function SharedAgentCard({ agent }: { agent: Agent }) { > {`${tool.name}{' '} - {tool.name} + {getToolDisplayName(tool)} ))} diff --git a/frontend/src/agents/WorkflowBuilder.tsx b/frontend/src/agents/WorkflowBuilder.tsx index fd2d15e8..548ee906 100644 --- a/frontend/src/agents/WorkflowBuilder.tsx +++ b/frontend/src/agents/WorkflowBuilder.tsx @@ -40,9 +40,12 @@ import { } from '@/components/ui/select'; import { Sheet, SheetContent } from '@/components/ui/sheet'; +import { useSelector } from 'react-redux'; + import modelService from '../api/services/modelService'; import userService from '../api/services/userService'; import ArrowLeft from '../assets/arrow-left.svg'; +import { selectToken } from '../preferences/preferenceSlice'; import { WorkflowNode } from './types/workflow'; import { AgentNode, @@ -55,7 +58,7 @@ import WorkflowPreview from './workflow/WorkflowPreview'; import type { Model } from '../models/types'; interface AgentNodeConfig { - agent_type: 'classic' | 'react'; + agent_type: 'classic' | 'agentic' | 'research'; llm_name?: string; model_id?: string; system_prompt: string; @@ -77,6 +80,7 @@ interface UserTool { function WorkflowBuilderInner() { const navigate = useNavigate(); + const token = useSelector(selectToken); const { agentId } = useParams<{ agentId?: string }>(); const [searchParams] = useSearchParams(); const folderId = searchParams.get('folder_id'); @@ -304,7 +308,7 @@ function WorkflowBuilderInner() { setAvailableModels(modelService.transformModels(modelsData.models)); } - const toolsResponse = await userService.getUserTools(null); + const toolsResponse = await userService.getUserTools(token); if (toolsResponse.ok) { const toolsData = await toolsResponse.json(); setAvailableTools(toolsData.tools); @@ -881,7 +885,12 @@ function WorkflowBuilderInner() { Classic - ReAct + + Agentic + + + Research + diff --git a/frontend/src/agents/workflow/WorkflowBuilder.tsx b/frontend/src/agents/workflow/WorkflowBuilder.tsx index d57bcf17..00681017 100644 --- a/frontend/src/agents/workflow/WorkflowBuilder.tsx +++ b/frontend/src/agents/workflow/WorkflowBuilder.tsx @@ -54,7 +54,11 @@ import { FileUpload } from '../../components/FileUpload'; import AgentDetailsModal from '../../modals/AgentDetailsModal'; import ConfirmationModal from '../../modals/ConfirmationModal'; import { ActiveState } from '../../models/misc'; -import { selectToken } from '../../preferences/preferenceSlice'; +import { + selectSourceDocs, + selectToken, +} from '../../preferences/preferenceSlice'; +import { getToolDisplayName } from '../../utils/toolUtils'; import { Agent } from '../types'; import { ConditionCase, WorkflowNode } from '../types/workflow'; import MobileBlocker from './components/MobileBlocker'; @@ -74,7 +78,7 @@ import type { Model } from '../../models/types'; const PRIMARY_ACTION_SPINNER_DELAY_MS = 180; interface AgentNodeConfig { - agent_type: 'classic' | 'react'; + agent_type: 'classic' | 'agentic' | 'research'; llm_name?: string; model_id?: string; system_prompt: string; @@ -92,6 +96,7 @@ interface UserTool { id: string; name: string; displayName: string; + customName?: string; } function validateJsonSchemaConfig(schema: unknown): string | null { @@ -298,6 +303,7 @@ function createWorkflowPayload( function WorkflowBuilderInner() { const navigate = useNavigate(); const token = useSelector(selectToken); + const sourceDocs = useSelector(selectSourceDocs); const { agentId } = useParams<{ agentId?: string }>(); const [searchParams] = useSearchParams(); const folderId = searchParams.get('folder_id'); @@ -339,6 +345,14 @@ function WorkflowBuilderInner() { const [availableModels, setAvailableModels] = useState([]); const [defaultAgentModelId, setDefaultAgentModelId] = useState(''); const [availableTools, setAvailableTools] = useState([]); + const sourceOptions = useMemo( + () => + (sourceDocs ?? []).map((doc) => ({ + value: doc.id ?? 'default', + label: doc.name, + })), + [sourceDocs], + ); const [agentJsonSchemaDrafts, setAgentJsonSchemaDrafts] = useState< Record >({}); @@ -385,31 +399,39 @@ function WorkflowBuilderInner() { [], ); - const onConnect = useCallback((params: Connection) => { - setEdges((eds) => { - const exists = eds.some( - (e) => - e.source === params.source && - e.sourceHandle === params.sourceHandle && - e.target === params.target && - e.targetHandle === params.targetHandle, - ); - if (exists) return eds; - - const filtered = eds.filter( - (e) => - !( + const onConnect = useCallback( + (params: Connection) => { + setEdges((eds) => { + const exists = eds.some( + (e) => e.source === params.source && - e.sourceHandle === (params.sourceHandle ?? null) - ) && - !( + e.sourceHandle === params.sourceHandle && e.target === params.target && - e.targetHandle === (params.targetHandle ?? null) - ), - ); - return addEdge(params, filtered); - }); - }, []); + e.targetHandle === params.targetHandle, + ); + if (exists) return eds; + + const targetNode = nodes.find((n) => n.id === params.target); + const isEndNode = targetNode?.type === 'end'; + + const filtered = eds.filter( + (e) => + !( + e.source === params.source && + e.sourceHandle === (params.sourceHandle ?? null) + ) && + // End nodes accept multiple incoming edges + (isEndNode || + !( + e.target === params.target && + e.targetHandle === (params.targetHandle ?? null) + )), + ); + return addEdge(params, filtered); + }); + }, + [nodes], + ); const onEdgeClick = useCallback((_event: React.MouseEvent, edge: Edge) => { setEdges((eds) => eds.filter((e) => e.id !== edge.id)); @@ -699,7 +721,7 @@ function WorkflowBuilderInner() { setDefaultAgentModelId(preferredDefaultModel); } - const toolsResponse = await userService.getUserTools(null); + const toolsResponse = await userService.getUserTools(token); if (toolsResponse.ok) { const toolsData = await toolsResponse.json(); setAvailableTools(toolsData.tools); @@ -1269,8 +1291,8 @@ function WorkflowBuilderInner() { const handlePrimaryAction = useCallback(() => { if (isPrimaryActionDisabled) return; - void persistWorkflow(!canManageAgent); - }, [isPrimaryActionDisabled, persistWorkflow, canManageAgent]); + void persistWorkflow(false); + }, [isPrimaryActionDisabled, persistWorkflow]); const agentForDetails = useMemo( () => ({ @@ -1746,8 +1768,11 @@ function WorkflowBuilderInner() { Classic - - ReAct + + Agentic + + + Research @@ -1887,7 +1912,7 @@ function WorkflowBuilderInner() { ({ value: tool.id, - label: tool.displayName, + label: getToolDisplayName(tool), }))} selected={ selectedNode.data.config?.tools || [] @@ -1905,6 +1930,28 @@ function WorkflowBuilderInner() { emptyText="No tools available" /> +
+ + + handleUpdateNodeData({ + config: { + ...(selectedNode.data.config || {}), + sources: newSources, + }, + }) + } + placeholder="Select sources..." + searchPlaceholder="Search sources..." + emptyText="No sources available" + /> +