diff --git a/.gitignore b/.gitignore index e648d2f2..668056ae 100644 --- a/.gitignore +++ b/.gitignore @@ -46,5 +46,4 @@ ptest/ /node_modules/ .claude/ -values-dev.yaml -values-prod.yaml \ No newline at end of file +values-*.yaml diff --git a/Makefile b/Makefile index b67b5d17..89666b74 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ IMAGE_NAME ?= alphatrion IMAGE_REPO := $(IMAGE_REGISTRY)/$(IMAGE_NAME) GIT_TAG ?= $(shell git describe --tags --dirty --always) IMG ?= $(IMAGE_REPO):$(GIT_TAG) -PLATFORMS ?= linux/arm64,linux/amd64 +PLATFORMS ?= linux/amd64 POETRY := poetry RUFF := .venv/bin/ruff @@ -51,6 +51,7 @@ test-integration: lint docker-compose -f ./docker-compose.yaml up -d; \ trap "docker-compose -f ./docker-compose.yaml down" EXIT; \ until docker exec postgres pg_isready -U alphatr1on; do sleep 1; done; \ + until docker exec clickhouse clickhouse-client --query "SELECT 1"; do sleep 1; done; \ until curl -sf http://localhost:11434/api/tags | grep "smollm:135m" > /dev/null; do sleep 1; done; \ $(PYTEST) tests/integration --timeout=30; \ ' diff --git a/alphatrion/__init__.py b/alphatrion/__init__.py index c0abaf18..182ea7cc 100644 --- a/alphatrion/__init__.py +++ b/alphatrion/__init__.py @@ -1,4 +1,4 @@ -from alphatrion.log.log import log_artifact, log_metrics, log_params, log_result +from alphatrion.log.log import log_artifact, log_dataset, log_metrics, log_params from alphatrion.runtime.runtime import init __all__ = [ @@ -6,5 +6,5 @@ "log_artifact", "log_params", "log_metrics", - "log_result", + "log_dataset", ] diff --git a/alphatrion/artifact/artifact.py b/alphatrion/artifact/artifact.py index 62ca1301..86a8e4aa 100644 --- a/alphatrion/artifact/artifact.py +++ b/alphatrion/artifact/artifact.py @@ -9,8 +9,7 @@ class Artifact: - def __init__(self, team_id: str, insecure: bool = False): - self._team_id = team_id + def __init__(self, insecure: bool = False): self._url = get_registry_url() self._client = oras.client.OrasClient( hostname=self._url.strip("/"), auth_backend="token", insecure=insecure @@ -50,7 +49,7 @@ def push( if version is None: version = utiltime.now_2_hash() - path = f"{self._team_id}/{repo_name}:{version}" + path = f"{repo_name}:{version}" target = f"{self._url}/{path}" try: @@ -61,7 +60,7 @@ def push( return path def list_versions(self, repo_name: str) -> list[str]: - target = f"{self._url}/{self._team_id}/{repo_name}" + target = f"{self._url}/{repo_name}" try: tags = self._client.get_tags(target) return tags @@ -91,7 +90,7 @@ def pull( (defaults to ORAS temp directory) :return: list of absolute file paths that were downloaded """ - path = f"{self._team_id}/{repo_name}:{version}" + path = f"{repo_name}:{version}" target = f"{self._url}/{path}" if output_dir: @@ -115,7 +114,7 @@ def pull( os.chdir(original_dir) def delete(self, repo_name: str, versions: str | list[str]): - target = f"{self._url}/{self._team_id}/{repo_name}" + target = f"{self._url}/{repo_name}" try: self._client.delete_tags(target, tags=versions) diff --git a/alphatrion/envs.py b/alphatrion/envs.py index ab24d640..2048cfcb 100644 --- a/alphatrion/envs.py +++ b/alphatrion/envs.py @@ -21,4 +21,3 @@ # Runtime related envs ROOT_PATH = "ALPHATRION_ROOT_PATH" -AUTO_CLEANUP = "ALPHATRION_AUTO_CLEANUP" diff --git a/alphatrion/experiment/base.py b/alphatrion/experiment/base.py index f3a879db..da54e53d 100644 --- a/alphatrion/experiment/base.py +++ b/alphatrion/experiment/base.py @@ -217,6 +217,19 @@ def _start( # to avoid confusion. if exp_obj and exp_obj.status != Status.COMPLETED: self._id = exp_obj.uuid + usage = exp_obj.usage + + # reset to running status, also need to reset the tokens. + if usage and "total_tokens" in usage: + # delete the tokens in the usage + usage.delete("total_tokens") + usage.delete("input_tokens") + usage.delete("output_tokens") + self._runtime._metadb.update_experiment( + experiment_id=self._id, + status=Status.RUNNING, + usage=usage, + ) elif exp_obj and exp_obj.status == Status.COMPLETED: raise RuntimeError( f"Experiment with name '{name}' already exists and is completed. \ @@ -366,6 +379,8 @@ def is_done(self) -> bool: # or it could lead to experiment not being marked as completed. # TODO: Should we distinguish done and cancel? def done(self): + if self.is_done(): + return self._cancel() self._cleanup() diff --git a/alphatrion/log/log.py b/alphatrion/log/log.py index aaa38d9b..daf221f7 100644 --- a/alphatrion/log/log.py +++ b/alphatrion/log/log.py @@ -1,19 +1,18 @@ import asyncio +import json import os +import tempfile from collections.abc import Callable from typing import Any from alphatrion.runtime.contextvars import current_exp_id, current_run_id from alphatrion.runtime.runtime import global_runtime from alphatrion.snapshot.snapshot import ( - ExecutionKind, - build_run_execution, checkpoint_path, - snapshot_path, ) +from alphatrion.storage import runtime as storage_runtime BEST_RESULT_PATH = "best_result_path" -EXECUTION_RESULT = "execution_result" async def log_artifact( @@ -45,7 +44,7 @@ async def log_artifact( if runtime is None: raise RuntimeError("Runtime is not initialized. Please call init() first.") - if not runtime.artifact_storage_enabled(): + if not storage_runtime.artifact_storage_enabled(): raise RuntimeError( "Artifact storage is not enabled in the runtime." "Set ENABLE_ARTIFACT_STORAGE=true in the environment variables." @@ -59,7 +58,7 @@ async def log_artifact( loop = asyncio.get_running_loop() return await loop.run_in_executor( - None, runtime._artifact.push, repo_name, paths, version + None, runtime._artifact.push, f"{runtime.team_id}/{repo_name}", paths, version ) @@ -146,53 +145,61 @@ async def log_metrics(metrics: dict[str, float]) -> bool: return is_best_metric -# log_result is used to log the result of a run/experiment, -# including both input and output, e.g. you want to save the code snippet. -# It will be stored in the object storage as a JSON file if object storage -# is enabled or locally otherwise. -async def log_result( - output: dict[str, Any], - input: dict[str, Any] | None = None, - phase: str = "success", - kind: ExecutionKind = ExecutionKind.RUN, -): - result = None - - if kind == ExecutionKind.RUN: - result = build_run_execution(output=output, input=input, phase=phase) - else: - raise NotImplementedError( - f"Logging record of kind {result.kind} is not implemented yet." - ) +# log_records is used to log a list of records, which is similar to log_metrics +# but for tracing the execution of the code. +# async def log_records(): - # Can I get the file size to store in the database? - path = snapshot_path() - if os.path.exists(path) is False: - os.makedirs(path, exist_ok=True) - - # Will eventually be cleanup on Experiment done() if AUTO_CLEANUP is enabled. - # Considering the record file is small, we just save it locally first. - # If this changes in the future, we should delete them after uploading. - with open(os.path.join(path, "result.json"), "w") as f: - f.write(result.model_dump_json()) +async def log_dataset( + name: str, + data_or_path: dict[str, Any] | str | list[str], +): + """ + Log dataset to the database and artifact registry. - file_size = os.path.getsize(os.path.join(path, "result.json")) + :param name: the name of the dataset. + :param data_or_path: the data to be logged, currently support dict only, + will support more types in the future. + """ runtime = global_runtime() - # If not enabled, only save to local disk. - if runtime.artifact_storage_enabled(): + if isinstance(data_or_path, dict): + with tempfile.TemporaryDirectory() as tmpdir: + os.chdir(tmpdir) + with open(name, "w") as f: + f.write(json.dumps(data_or_path)) + file_size = os.path.getsize(name) + + path = await log_artifact( + paths=name, + repo_name="dataset", + ) + + runtime.metadb.create_dataset( + name=name, + team_id=runtime.team_id, + user_id=runtime.user_id, + path=path, + experiment_id=current_exp_id.get(), + run_id=current_run_id.get(), + meta={"size": file_size}, + ) + return + elif isinstance(data_or_path, (str, list)): path = await log_artifact( - paths=os.path.join(path, "result.json"), - repo_name="execution", + paths=data_or_path, + repo_name="dataset", ) - runtime.metadb.update_run( + runtime.metadb.create_dataset( + name=name, + team_id=runtime.team_id, + user_id=runtime.user_id, + path=path, + experiment_id=current_exp_id.get(), run_id=current_run_id.get(), - meta={ - EXECUTION_RESULT: { - "path": path, - "size": file_size, - "file_name": "result.json", - } - }, ) + return + + raise NotImplementedError( + f"Logging dataset of type {type(data_or_path)} is not implemented yet." + ) diff --git a/alphatrion/run/run.py b/alphatrion/run/run.py index 82f897c0..6fa430a3 100644 --- a/alphatrion/run/run.py +++ b/alphatrion/run/run.py @@ -1,5 +1,6 @@ import asyncio import uuid +from datetime import UTC, datetime from alphatrion.runtime.contextvars import current_run_id from alphatrion.runtime.runtime import global_runtime @@ -50,9 +51,13 @@ def done(self): if self.cancelled(): return + run = self._runtime._metadb.get_run(run_id=self.id) + duration = ( + datetime.now(UTC) - run.created_at.replace(tzinfo=UTC) + ).total_seconds() + self._runtime.metadb.update_run( - run_id=self._id, - status=Status.COMPLETED, + run_id=self._id, status=Status.COMPLETED, duration=duration ) self._result = self._task.result() @@ -60,9 +65,14 @@ def cancel(self): # TODO: we should wait for the task to be actually cancelled # and catch the CancelledError exception in the task function. self._task.cancel() + + run = self._runtime._metadb.get_run(run_id=self.id) + duration = ( + datetime.now(UTC) - run.created_at.replace(tzinfo=UTC) + ).total_seconds() + self._runtime.metadb.update_run( - run_id=self._id, - status=Status.CANCELLED, + run_id=self._id, status=Status.CANCELLED, duration=duration ) def cancelled(self) -> bool: diff --git a/alphatrion/runtime/runtime.py b/alphatrion/runtime/runtime.py index dcf719cf..8720c755 100644 --- a/alphatrion/runtime/runtime.py +++ b/alphatrion/runtime/runtime.py @@ -3,7 +3,6 @@ import uuid from alphatrion import envs -from alphatrion.artifact.artifact import Artifact from alphatrion.storage import runtime as storage_runtime from alphatrion.storage.sqlstore import SQLStore @@ -58,6 +57,7 @@ def __init__( storage_runtime.init() self._metadb = storage_runtime.storage_runtime().metadb self._tracestore = storage_runtime.storage_runtime().tracestore + self._artifact = storage_runtime.storage_runtime().artifact self._user_id = user_id self._team_id = team_id @@ -74,18 +74,9 @@ def __init__( self._team_id = teams[0].uuid self._root_path = os.getenv(envs.ROOT_PATH, os.path.expanduser("~/.alphatrion")) - - artifact_insecure = os.getenv(envs.ARTIFACT_INSECURE, "false").lower() == "true" - - if self.artifact_storage_enabled(): - self._artifact = Artifact(team_id=self._team_id, insecure=artifact_insecure) - if not os.path.exists(self._root_path): os.makedirs(self._root_path, exist_ok=True) - def artifact_storage_enabled(self) -> bool: - return os.getenv(envs.ENABLE_ARTIFACT_STORAGE, "true").lower() == "true" - @property def metadb(self) -> SQLStore: return self._metadb @@ -94,6 +85,10 @@ def metadb(self) -> SQLStore: def tracestore(self): return self._tracestore + @property + def artifact(self): + return self._artifact + @property def user_id(self) -> uuid.UUID: return self._user_id diff --git a/alphatrion/server/graphql/resolvers.py b/alphatrion/server/graphql/resolvers.py index 59704cee..3c96749b 100644 --- a/alphatrion/server/graphql/resolvers.py +++ b/alphatrion/server/graphql/resolvers.py @@ -4,27 +4,41 @@ import httpx import strawberry +from fastapi import logger from alphatrion import envs from alphatrion.artifact import artifact +from alphatrion.server.graphql.types import ArtifactFile +from alphatrion.server.repo.gcs_repo import GCSRepoService, detect_language +from alphatrion.server.repo.local_repo import LocalRepoService from alphatrion.storage import runtime -from alphatrion.storage.sql_models import Status +from alphatrion.storage.sql_models import ( + FINISHED_STATUS, + Status, +) from .types import ( AddUserToTeamInput, ArtifactContent, ArtifactRepository, ArtifactTag, + ContentSnapshot, + ContentSnapshotSummary, CreateTeamInput, CreateUserInput, DailyTokenUsage, + Dataset, Experiment, + ExperimentFitnessSummary, GraphQLExperimentType, GraphQLExperimentTypeEnum, GraphQLStatusEnum, Label, Metric, RemoveUserFromTeamInput, + RepoFileContent, + RepoFileEntry, + RepoFileTree, Run, Span, Team, @@ -137,6 +151,7 @@ def list_experiments( duration=e.duration, status=GraphQLStatusEnum[Status(e.status).name], kind=GraphQLExperimentTypeEnum[GraphQLExperimentType(e.kind).name], + cost=e.cost, created_at=e.created_at, updated_at=e.updated_at, ) @@ -159,6 +174,7 @@ def get_experiment(id: strawberry.ID) -> Experiment | None: duration=exp.duration, status=GraphQLStatusEnum[Status(exp.status).name], kind=GraphQLExperimentTypeEnum[GraphQLExperimentType(exp.kind).name], + cost=exp.cost, created_at=exp.created_at, updated_at=exp.updated_at, ) @@ -174,7 +190,7 @@ def list_runs( ) -> list[Run]: metadb = runtime.storage_runtime().metadb runs = metadb.list_runs_by_exp_id( - exp_id=uuid.UUID(experiment_id), + experiment_id=uuid.UUID(experiment_id), page=page, page_size=page_size, order_by=order_by, @@ -187,7 +203,9 @@ def list_runs( user_id=r.user_id, experiment_id=r.experiment_id, meta=r.meta, + duration=r.duration, status=GraphQLStatusEnum[Status(r.status).name], + cost=r.cost, created_at=r.created_at, ) for r in runs @@ -204,7 +222,9 @@ def get_run(id: strawberry.ID) -> Run | None: user_id=run.user_id, experiment_id=run.experiment_id, meta=run.meta, + duration=run.duration, status=GraphQLStatusEnum[Status(run.status).name], + cost=run.cost, created_at=run.created_at, ) return None @@ -292,6 +312,7 @@ def list_exps_by_timeframe( duration=e.duration, status=GraphQLStatusEnum[Status(e.status).name], kind=GraphQLExperimentTypeEnum[GraphQLExperimentType(e.kind).name], + cost=e.cost, created_at=e.created_at, updated_at=e.updated_at, ) @@ -323,47 +344,103 @@ async def list_artifact_tags( ) -> list[ArtifactTag]: """List tags for a repository.""" - arf = artifact.Artifact(team_id=team_id, insecure=True) - return [ArtifactTag(name=tag) for tag in arf.list_versions(repo_name)] + arf = runtime.storage_runtime().artifact + return [ + ArtifactTag(name=tag) for tag in arf.list_versions(f"{team_id}/{repo_name}") + ] + + @staticmethod + async def list_artifact_files( + team_id: str, tag: str, repo_name: str + ) -> list[ArtifactFile]: + """List files in an artifact without loading content.""" + + try: + arf = runtime.storage_runtime().artifact + file_paths = arf.pull(repo_name=f"{team_id}/{repo_name}", version=tag) + + if not file_paths: + return [] + + files = [] + for file_path in file_paths: + filename = os.path.basename(file_path) + file_size = os.path.getsize(file_path) + + # Determine content type based on file extension + if filename.endswith(".json"): + content_type = "application/json" + elif ( + filename.endswith(".txt") + or filename.endswith(".log") + or filename.endswith((".py", ".js", ".ts", ".tsx", ".jsx")) + ): + content_type = "text/plain" + else: + content_type = "text/plain" + + files.append( + ArtifactFile( + filename=filename, size=file_size, content_type=content_type + ) + ) + + return files + except Exception as e: + raise RuntimeError(f"Failed to list artifact files: {e}") from e @staticmethod async def get_artifact_content( - team_id: str, tag: str, repo_name: str | None = None + team_id: str, + tag: str, + repo_name: str | None = None, + filename: str | None = None, ) -> ArtifactContent: """Get artifact content from registry.""" try: # Initialize artifact client - arf = artifact.Artifact(team_id=team_id, insecure=True) + arf = runtime.storage_runtime().artifact # Pull the artifact - ORAS will manage temp directory # Returns absolute paths to files in ORAS temp directory - # Note: One potential issue is if we download too many large files, - # it may fill up disk space. For now we assume artifacts are - # reasonably sized and/or users will manage their registry storage. - file_paths = arf.pull(repo_name=repo_name, version=tag) + file_paths = arf.pull(repo_name=f"{team_id}/{repo_name}", version=tag) if not file_paths: raise RuntimeError("No files found in artifact") - # Read first file content (file_paths now contains absolute paths) - file_path = file_paths[0] + # Find the requested file or use first file + file_path = None + if filename: + for path in file_paths: + if os.path.basename(path) == filename: + file_path = path + break + if not file_path: + raise RuntimeError(f"File '{filename}' not found in artifact") + else: + file_path = file_paths[0] + + # Read file content with open(file_path, encoding="utf-8") as f: content = f.read() # Get filename from path - filename = os.path.basename(file_path) + actual_filename = os.path.basename(file_path) # Determine content type based on file extension - # TODO: for multiple files, this is not right. - if filename.endswith(".json"): + if actual_filename.endswith(".json"): content_type = "application/json" - elif filename.endswith(".txt") or filename.endswith(".log"): + elif ( + actual_filename.endswith(".txt") + or actual_filename.endswith(".log") + or actual_filename.endswith((".py", ".js", ".ts", ".tsx", ".jsx")) + ): content_type = "text/plain" else: content_type = "text/plain" return ArtifactContent( - filename=filename, content=content, content_type=content_type + filename=actual_filename, content=content, content_type=content_type ) except Exception as e: raise RuntimeError(f"Failed to get artifact content: {e}") from e @@ -377,30 +454,22 @@ def aggregate_run_tokens(run_id: strawberry.ID) -> dict[str, int]: return {"total_tokens": 0, "input_tokens": 0, "output_tokens": 0} try: - trace_store = runtime.storage_runtime().tracestore - spans = trace_store.get_llm_spans_by_run_id(run_id) - # Don't close - it's a shared singleton connection - - total_tokens = 0 - input_tokens = 0 - output_tokens = 0 - - for span in spans: - span_attrs = span.get("SpanAttributes", {}) - - # Aggregate tokens from LLM spans - if "llm.usage.total_tokens" in span_attrs: - total_tokens += int(span_attrs["llm.usage.total_tokens"]) - if "gen_ai.usage.input_tokens" in span_attrs: - input_tokens += int(span_attrs["gen_ai.usage.input_tokens"]) - if "gen_ai.usage.output_tokens" in span_attrs: - output_tokens += int(span_attrs["gen_ai.usage.output_tokens"]) - - return { - "total_tokens": total_tokens, - "input_tokens": input_tokens, - "output_tokens": output_tokens, - } + run = runtime.storage_runtime().metadb.get_run(run_id=run_id) + if run.status in FINISHED_STATUS: + if run.usage and "total_tokens" in run.usage: + return { + "total_tokens": run.usage.get("total_tokens", 0), + "input_tokens": run.usage.get("input_tokens", 0), + "output_tokens": run.usage.get("output_tokens", 0), + } + else: + usage = GraphQLResolvers.get_run_usage(run_id) + runtime.storage_runtime().metadb.update_run( + run_id=run_id, usage=usage + ) + return usage + else: + return GraphQLResolvers.get_run_usage(run_id) except Exception as e: import logging @@ -409,6 +478,33 @@ def aggregate_run_tokens(run_id: strawberry.ID) -> dict[str, int]: ) return {"total_tokens": 0, "input_tokens": 0, "output_tokens": 0} + @staticmethod + def get_run_usage(run_id: strawberry.ID) -> dict[str, int]: + trace_store = runtime.storage_runtime().tracestore + spans = trace_store.get_llm_spans_by_run_id(run_id) + # Don't close - it's a shared singleton connection + + total_tokens = 0 + input_tokens = 0 + output_tokens = 0 + + for span in spans: + span_attrs = span.get("SpanAttributes", {}) + + # Aggregate tokens from LLM spans + if "llm.usage.total_tokens" in span_attrs: + total_tokens += int(span_attrs["llm.usage.total_tokens"]) + if "gen_ai.usage.input_tokens" in span_attrs: + input_tokens += int(span_attrs["gen_ai.usage.input_tokens"]) + if "gen_ai.usage.output_tokens" in span_attrs: + output_tokens += int(span_attrs["gen_ai.usage.output_tokens"]) + + return { + "total_tokens": total_tokens, + "input_tokens": input_tokens, + "output_tokens": output_tokens, + } + @staticmethod def aggregate_experiment_tokens(experiment_id: strawberry.ID) -> dict[str, int]: """Aggregate token usage from all spans in an experiment.""" @@ -417,31 +513,24 @@ def aggregate_experiment_tokens(experiment_id: strawberry.ID) -> dict[str, int]: return {"total_tokens": 0, "input_tokens": 0, "output_tokens": 0} try: - trace_store = runtime.storage_runtime().tracestore - # Get all LLM spans for this experiment in a single query - spans = trace_store.get_llm_spans_by_exp_id(experiment_id) - # Don't close - it's a shared singleton connection - - total_tokens = 0 - input_tokens = 0 - output_tokens = 0 - - for span in spans: - span_attrs = span.get("SpanAttributes", {}) - - # Aggregate tokens from LLM spans - if "llm.usage.total_tokens" in span_attrs: - total_tokens += int(span_attrs["llm.usage.total_tokens"]) - if "gen_ai.usage.input_tokens" in span_attrs: - input_tokens += int(span_attrs["gen_ai.usage.input_tokens"]) - if "gen_ai.usage.output_tokens" in span_attrs: - output_tokens += int(span_attrs["gen_ai.usage.output_tokens"]) - - return { - "total_tokens": total_tokens, - "input_tokens": input_tokens, - "output_tokens": output_tokens, - } + exp = runtime.storage_runtime().metadb.get_experiment( + experiment_id=experiment_id + ) + if exp.status in FINISHED_STATUS: + if exp.usage and "total_tokens" in exp.usage: + return { + "total_tokens": exp.usage.get("total_tokens", 0), + "input_tokens": exp.usage.get("input_tokens", 0), + "output_tokens": exp.usage.get("output_tokens", 0), + } + else: + usage = GraphQLResolvers.get_experiment_usage(experiment_id) + runtime.storage_runtime().metadb.update_experiment( + experiment_id=experiment_id, usage=usage + ) + return usage + else: + return GraphQLResolvers.get_experiment_usage(experiment_id) except Exception as e: import logging @@ -450,6 +539,34 @@ def aggregate_experiment_tokens(experiment_id: strawberry.ID) -> dict[str, int]: ) return {"total_tokens": 0, "input_tokens": 0, "output_tokens": 0} + @staticmethod + def get_experiment_usage(experiment_id: strawberry.ID): + trace_store = runtime.storage_runtime().tracestore + # Get all LLM spans for this experiment in a single query + spans = trace_store.get_llm_spans_by_exp_id(experiment_id) + # Don't close - it's a shared singleton connection + + total_tokens = 0 + input_tokens = 0 + output_tokens = 0 + + for span in spans: + span_attrs = span.get("SpanAttributes", {}) + + # Aggregate tokens from LLM spans + if "llm.usage.total_tokens" in span_attrs: + total_tokens += int(span_attrs["llm.usage.total_tokens"]) + if "gen_ai.usage.input_tokens" in span_attrs: + input_tokens += int(span_attrs["gen_ai.usage.input_tokens"]) + if "gen_ai.usage.output_tokens" in span_attrs: + output_tokens += int(span_attrs["gen_ai.usage.output_tokens"]) + + return { + "total_tokens": total_tokens, + "input_tokens": input_tokens, + "output_tokens": output_tokens, + } + @staticmethod def list_spans(run_id: strawberry.ID) -> list[Span]: """List all spans for a specific run.""" @@ -564,6 +681,371 @@ def get_daily_token_usage( print(f"Failed to fetch daily token usage: {e}") return [] + @staticmethod + def list_content_snapshots( + experiment_id: strawberry.ID, page: int = 0, page_size: int = 1000 + ) -> list[ContentSnapshot]: + metadb = runtime.storage_runtime().metadb + snapshots = metadb.list_content_snapshots_by_experiment_id( + experiment_id=uuid.UUID(experiment_id), page=page, page_size=page_size + ) + return [ + ContentSnapshot( + id=s.uuid, + team_id=s.team_id, + experiment_id=s.experiment_id, + run_id=s.run_id, + content_uid=s.content_uid, + content_text=s.content_text, + parent_uid=s.parent_uid, + co_parent_uids=s.co_parent_uids, + fitness=s.fitness, + evaluation=s.evaluation, + metainfo=s.metainfo, + language=s.language, + created_at=s.created_at, + ) + for s in snapshots + ] + + @staticmethod + def list_content_snapshots_summary( + experiment_id: strawberry.ID, page: int = 0, page_size: int = 1000000 + ) -> list[ContentSnapshotSummary]: + """Returns lightweight content snapshots without content_text for charts.""" + metadb = runtime.storage_runtime().metadb + snapshots = metadb.list_content_snapshots_summary_by_experiment_id( + experiment_id=uuid.UUID(experiment_id), page=page, page_size=page_size + ) + return [ + ContentSnapshotSummary( + id=s["uuid"], + team_id=s["team_id"], + experiment_id=s["experiment_id"], + run_id=s["run_id"], + content_uid=s["content_uid"], + parent_uid=s["parent_uid"], + co_parent_uids=s["co_parent_uids"], + fitness=s["fitness"], + language=s["language"], + metainfo=s["metainfo"], + created_at=s["created_at"], + ) + for s in snapshots + ] + + @staticmethod + def batch_experiment_fitness( + experiment_ids: list[str], + ) -> list[ExperimentFitnessSummary]: + """Batch-fetch fitness values for multiple experiments in one query.""" + metadb = runtime.storage_runtime().metadb + uuids = [uuid.UUID(eid) for eid in experiment_ids] + grouped = metadb.list_fitness_by_experiment_ids(uuids) + return [ + ExperimentFitnessSummary( + experiment_id=eid, + fitness_values=[ + s["fitness"] + for s in grouped.get(uuid.UUID(eid), []) + if s["fitness"] is not None + ], + ) + for eid in experiment_ids + ] + + @staticmethod + def get_content_snapshot(id: strawberry.ID) -> ContentSnapshot | None: + metadb = runtime.storage_runtime().metadb + snapshot = metadb.get_content_snapshot(snapshot_id=uuid.UUID(str(id))) + if snapshot: + return ContentSnapshot( + id=snapshot.uuid, + team_id=snapshot.team_id, + experiment_id=snapshot.experiment_id, + run_id=snapshot.run_id, + content_uid=snapshot.content_uid, + content_text=snapshot.content_text, + parent_uid=snapshot.parent_uid, + co_parent_uids=snapshot.co_parent_uids, + fitness=snapshot.fitness, + evaluation=snapshot.evaluation, + metainfo=snapshot.metainfo, + language=snapshot.language, + created_at=snapshot.created_at, + ) + return None + + @staticmethod + def get_content_lineage( + experiment_id: strawberry.ID, content_uid: str + ) -> list[ContentSnapshot]: + metadb = runtime.storage_runtime().metadb + lineage = metadb.get_content_lineage( + experiment_id=uuid.UUID(experiment_id), content_uid=content_uid + ) + return [ + ContentSnapshot( + id=s.uuid, + team_id=s.team_id, + experiment_id=s.experiment_id, + run_id=s.run_id, + content_uid=s.content_uid, + content_text=s.content_text, + parent_uid=s.parent_uid, + co_parent_uids=s.co_parent_uids, + fitness=s.fitness, + evaluation=s.evaluation, + metainfo=s.metainfo, + language=s.language, + created_at=s.created_at, + ) + for s in lineage + ] + + @staticmethod + def _get_experiment_repo_name(experiment_id: strawberry.ID) -> str | None: + """Get the experiment name to use for GCS repo lookup.""" + metadb = runtime.storage_runtime().metadb + exp = metadb.get_experiment(experiment_id=uuid.UUID(experiment_id)) + if exp: + return exp.name + return None + + @staticmethod + def get_repo_file_tree(experiment_id: strawberry.ID) -> RepoFileTree: + """Get the file tree structure for an experiment's repository.""" + try: + # Get experiment name to use for GCS path + experiment_name = GraphQLResolvers._get_experiment_repo_name(experiment_id) + if not experiment_name: + return RepoFileTree(exists=False, error="Experiment not found") + + repo_service = GCSRepoService.get_instance() + + # Check if repo exists using experiment name + if not repo_service.repo_exists(experiment_name): + return RepoFileTree(exists=False) + + # Get file tree + tree_dict = repo_service.get_file_tree(experiment_name) + if tree_dict is None: + return RepoFileTree(exists=False) + + # Convert dict to RepoFileEntry + def dict_to_entry(d: dict) -> RepoFileEntry: + children = None + if d.get("children") is not None: + children = [dict_to_entry(c) for c in d["children"]] + return RepoFileEntry( + name=d["name"], + path=d["path"], + is_dir=d["is_dir"], + children=children, + ) + + root = dict_to_entry(tree_dict) + return RepoFileTree(exists=True, root=root) + + except Exception as e: + logger.error( + f"Error getting repo file tree for experiment {experiment_id}: {e}" + ) + return RepoFileTree(exists=False, error=str(e)) + + @staticmethod + def get_repo_file_content( + experiment_id: strawberry.ID, file_path: str + ) -> RepoFileContent: + """Get the content of a specific file from an experiment's repository.""" + try: + # Get experiment name to use for GCS path + experiment_name = GraphQLResolvers._get_experiment_repo_name(experiment_id) + if not experiment_name: + return RepoFileContent(path=file_path, error="Experiment not found") + + repo_service = GCSRepoService.get_instance() + + # Get file content using experiment name + content = repo_service.get_file_content(experiment_name, file_path) + if content is None: + return RepoFileContent( + path=file_path, + error="File not found or could not be read", + ) + + # Detect language from file path + language = detect_language(file_path) + + return RepoFileContent( + path=file_path, + content=content, + language=language, + ) + + except Exception as e: + logger.error( + f"Error getting repo file content for experiment {experiment_id}, " + f"file {file_path}: {e}" + ) + return RepoFileContent(path=file_path, error=str(e)) + + @staticmethod + def get_local_repo_file_tree(path: str) -> RepoFileTree: + """Get the file tree structure for a local directory.""" + try: + repo_service = LocalRepoService.get_instance() + + # Check if path exists + if not repo_service.path_exists(path): + return RepoFileTree( + exists=False, error="Path not found or not a directory" + ) + + # Get file tree + tree_dict = repo_service.get_file_tree(path) + if tree_dict is None: + return RepoFileTree(exists=False, error="Could not read directory") + + # Convert dict to RepoFileEntry + def dict_to_entry(d: dict) -> RepoFileEntry: + children = None + if d.get("children") is not None: + children = [dict_to_entry(c) for c in d["children"]] + return RepoFileEntry( + name=d["name"], + path=d["path"], + is_dir=d["is_dir"], + children=children, + ) + + root = dict_to_entry(tree_dict) + return RepoFileTree(exists=True, root=root) + + except Exception as e: + logger.error(f"Error getting local repo file tree for path {path}: {e}") + return RepoFileTree(exists=False, error=str(e)) + + @staticmethod + def get_local_repo_file_content(base_path: str, file_path: str) -> RepoFileContent: + """Get the content of a specific file from a local directory.""" + try: + repo_service = LocalRepoService.get_instance() + + # Get file content + content = repo_service.get_file_content(base_path, file_path) + if content is None: + return RepoFileContent( + path=file_path, + error="File not found or could not be read", + ) + + # Detect language from file path + language = detect_language(file_path) + + return RepoFileContent( + path=file_path, + content=content, + language=language, + ) + + except Exception as e: + logger.error( + f"Error getting local repo file content for path {base_path}, " + f"file {file_path}: {e}" + ) + return RepoFileContent(path=file_path, error=str(e)) + + @staticmethod + def list_metric_keys(experiment_id: strawberry.ID) -> list[str]: + metadb = runtime.storage_runtime().metadb + return metadb.list_metric_keys_by_exp_id(exp_id=uuid.UUID(experiment_id)) + + @staticmethod + def list_metrics_by_key( + experiment_id: strawberry.ID, key: str, max_points: int | None = None + ) -> list[Metric]: + """Get metrics for a specific experiment filtered by key.""" + metadb = runtime.storage_runtime().metadb + # Get all metrics for the experiment + metrics = metadb.list_metrics_by_exp_id(exp_id=uuid.UUID(experiment_id)) + # Filter by key + filtered = [m for m in metrics if m.key == key] + # Limit to max_points if specified + if max_points is not None and len(filtered) > max_points: + # Take evenly spaced samples + step = len(filtered) / max_points + indices = [int(i * step) for i in range(max_points)] + filtered = [filtered[i] for i in indices] + return [ + Metric( + id=m.uuid, + key=m.key, + value=m.value, + team_id=m.team_id, + experiment_id=m.experiment_id, + run_id=m.run_id, + created_at=m.created_at, + ) + for m in filtered + ] + + def list_datasets( + team_id: strawberry.ID, + experiment_id: strawberry.ID | None = None, + run_id: strawberry.ID | None = None, + page: int = 0, + page_size: int = 20, + order_by: str = "created_at", + order_desc: bool = True, + ) -> list[Dataset]: + metadb = runtime.storage_runtime().metadb + datasets = metadb.list_datasets( + team_id=team_id, + experiment_id=experiment_id, + run_id=run_id, + page=page, + page_size=page_size, + order_by=order_by, + order_desc=order_desc, + ) + return [ + Dataset( + id=d.uuid, + name=d.name, + description=d.description, + path=d.path, + meta=d.meta, + team_id=d.team_id, + experiment_id=d.experiment_id, + run_id=d.run_id, + user_id=d.user_id, + created_at=d.created_at, + updated_at=d.updated_at, + ) + for d in datasets + ] + + @staticmethod + def get_dataset(id: strawberry.ID) -> Dataset | None: + metadb = runtime.storage_runtime().metadb + dataset = metadb.get_dataset(dataset_id=uuid.UUID(id)) + if dataset: + return Dataset( + id=dataset.uuid, + name=dataset.name, + description=dataset.description, + path=dataset.path, + meta=dataset.meta, + team_id=dataset.team_id, + experiment_id=dataset.experiment_id, + run_id=dataset.run_id, + user_id=dataset.user_id, + created_at=dataset.created_at, + updated_at=dataset.updated_at, + ) + return None + class GraphQLMutations: @staticmethod @@ -673,3 +1155,43 @@ def remove_user_from_team(input: RemoveUserFromTeamInput) -> bool: # Remove user from team (deletes TeamMember entry) return metadb.remove_user_from_team(user_id=user_id, team_id=team_id) + + @staticmethod + # TODO: We should have the team_id in the header for authz, and verify the + # team_id matches the experiment's team_id before allowing deletion. + def delete_experiment(experiment_id: strawberry.ID) -> bool: + metadb = runtime.storage_runtime().metadb + # Soft delete experiment by setting is_del flag + return metadb.delete_experiment(experiment_id=experiment_id) + + @staticmethod + # TODO: We should have the team_id in the header for authz, and verify the + # team_id matches the experiment's team_id before allowing deletion. + def delete_experiments(experiment_ids: list[strawberry.ID]) -> int: + metadb = runtime.storage_runtime().metadb + # Convert strawberry IDs to UUIDs + uuids = [uuid.UUID(exp_id) for exp_id in experiment_ids] + # Soft delete experiments by setting is_del flag + return metadb.delete_experiments(experiment_ids=uuids) + + @staticmethod + def delete_dataset(dataset_id: strawberry.ID) -> bool: + metadb = runtime.storage_runtime().metadb + artifact = runtime.storage_runtime().artifact + dataset = metadb.get_dataset(dataset_id=dataset_id) + + # delete the artifact file as well + if dataset: + try: + repo_name, version = dataset.path.split(":", 1) + artifact.delete(repo_name=repo_name, versions=version) + except Exception as e: + print(f"Failed to delete artifact for dataset {dataset_id}: {e}") + + return metadb.delete_dataset(dataset_id=dataset_id) + + @staticmethod + def delete_datasets(dataset_ids: list[strawberry.ID]) -> bool: + for id in dataset_ids: + GraphQLMutations.delete_dataset(dataset_id=id) + return True diff --git a/alphatrion/server/graphql/schema.py b/alphatrion/server/graphql/schema.py index d1265c15..b3af566f 100644 --- a/alphatrion/server/graphql/schema.py +++ b/alphatrion/server/graphql/schema.py @@ -4,13 +4,21 @@ from alphatrion.server.graphql.types import ( AddUserToTeamInput, ArtifactContent, + ArtifactFile, ArtifactRepository, ArtifactTag, + ContentSnapshot, + ContentSnapshotSummary, CreateTeamInput, CreateUserInput, DailyTokenUsage, + Dataset, Experiment, + ExperimentFitnessSummary, + Metric, RemoveUserFromTeamInput, + RepoFileContent, + RepoFileTree, Run, Span, Team, @@ -94,14 +102,130 @@ async def artifact_tags( ) -> list[ArtifactTag]: return await GraphQLResolvers.list_artifact_tags(str(team_id), repo_name) + @strawberry.field + async def artifact_files( + self, + team_id: strawberry.ID, + tag: str, + repo_name: str, + ) -> list[ArtifactFile]: + return await GraphQLResolvers.list_artifact_files(str(team_id), tag, repo_name) + @strawberry.field async def artifact_content( self, team_id: strawberry.ID, tag: str, repo_name: str, + filename: str | None = None, ) -> ArtifactContent: - return await GraphQLResolvers.get_artifact_content(str(team_id), tag, repo_name) + return await GraphQLResolvers.get_artifact_content( + str(team_id), tag, repo_name, filename + ) + + @strawberry.field + def content_snapshots( + self, experiment_id: strawberry.ID, page: int = 0, page_size: int = 200 + ) -> list[ContentSnapshot]: + return GraphQLResolvers.list_content_snapshots( + experiment_id=str(experiment_id), page=page, page_size=page_size + ) + + @strawberry.field + def content_snapshots_summary( + self, experiment_id: strawberry.ID, page: int = 0, page_size: int = 2000 + ) -> list[ContentSnapshotSummary]: + """Lightweight content snapshots without content_text for charts.""" + return GraphQLResolvers.list_content_snapshots_summary( + experiment_id=str(experiment_id), page=page, page_size=page_size + ) + + content_snapshot: ContentSnapshot | None = strawberry.field( + resolver=GraphQLResolvers.get_content_snapshot + ) + + @strawberry.field + def batch_experiment_fitness( + self, experiment_ids: list[str] + ) -> list[ExperimentFitnessSummary]: + """Batch-fetch fitness values for multiple experiments in one query.""" + return GraphQLResolvers.batch_experiment_fitness(experiment_ids=experiment_ids) + + @strawberry.field + def content_lineage( + self, experiment_id: strawberry.ID, content_uid: str + ) -> list[ContentSnapshot]: + return GraphQLResolvers.get_content_lineage( + experiment_id=str(experiment_id), content_uid=content_uid + ) + + # WILL BE DEPRECATED --- IGNORE --- + + @strawberry.field + def repo_file_tree(self, experiment_id: strawberry.ID) -> RepoFileTree: + """Get the file tree structure for an experiment's repository.""" + return GraphQLResolvers.get_repo_file_tree(experiment_id=str(experiment_id)) + + @strawberry.field + def repo_file_content( + self, experiment_id: strawberry.ID, file_path: str + ) -> RepoFileContent: + """Get the content of a specific file from an experiment's repository.""" + return GraphQLResolvers.get_repo_file_content( + experiment_id=str(experiment_id), file_path=file_path + ) + + @strawberry.field + def local_repo_file_tree(self, path: str) -> RepoFileTree: + """Get the file tree structure for a local directory.""" + return GraphQLResolvers.get_local_repo_file_tree(path=path) + + @strawberry.field + def local_repo_file_content( + self, base_path: str, file_path: str + ) -> RepoFileContent: + """Get the content of a specific file from a local directory.""" + return GraphQLResolvers.get_local_repo_file_content( + base_path=base_path, file_path=file_path + ) + + @strawberry.field + def metric_keys(self, experiment_id: strawberry.ID) -> list[str]: + """Get available metric keys for an experiment.""" + return GraphQLResolvers.list_metric_keys(experiment_id=str(experiment_id)) + + @strawberry.field + def metrics_by_key( + self, experiment_id: strawberry.ID, key: str, max_points: int | None = None + ) -> list["Metric"]: + """Get metrics for a specific experiment filtered by key.""" + return GraphQLResolvers.list_metrics_by_key( + experiment_id=str(experiment_id), key=key, max_points=max_points + ) + + # Dataset queries + @strawberry.field + def datasets( + self, + team_id: strawberry.ID, + experiment_id: strawberry.ID | None = None, + run_id: strawberry.ID | None = None, + page: int = 0, + page_size: int = 20, + order_by: str = "created_at", + order_desc: bool = True, + ) -> list[Dataset]: + return GraphQLResolvers.list_datasets( + team_id=team_id, + experiment_id=experiment_id, + run_id=run_id, + page=page, + page_size=page_size, + order_by=order_by, + order_desc=order_desc, + ) + + dataset: Dataset | None = strawberry.field(resolver=GraphQLResolvers.get_dataset) @strawberry.type @@ -126,5 +250,21 @@ def add_user_to_team(self, input: AddUserToTeamInput) -> bool: def remove_user_from_team(self, input: RemoveUserFromTeamInput) -> bool: return GraphQLMutations.remove_user_from_team(input=input) + @strawberry.mutation + def delete_experiment(self, experiment_id: strawberry.ID) -> bool: + return GraphQLMutations.delete_experiment(experiment_id=experiment_id) + + @strawberry.mutation + def delete_experiments(self, experiment_ids: list[strawberry.ID]) -> int: + return GraphQLMutations.delete_experiments(experiment_ids=experiment_ids) + + @strawberry.mutation + def delete_dataset(self, dataset_id: strawberry.ID) -> bool: + return GraphQLMutations.delete_dataset(dataset_id=dataset_id) + + @strawberry.mutation + def delete_datasets(self, dataset_ids: list[strawberry.ID]) -> bool: + return GraphQLMutations.delete_datasets(dataset_ids=dataset_ids) + schema = strawberry.Schema(query=Query, mutation=Mutation) diff --git a/alphatrion/server/graphql/types.py b/alphatrion/server/graphql/types.py index 41083cf8..59e1b0b6 100644 --- a/alphatrion/server/graphql/types.py +++ b/alphatrion/server/graphql/types.py @@ -119,11 +119,10 @@ class Experiment: params: JSON | None duration: float status: GraphQLStatusEnum + cost: JSON | None created_at: datetime updated_at: datetime - _token_cache: strawberry.Private[dict[str, int] | None] = None - @strawberry.field def labels(self) -> list[Label]: from .resolvers import GraphQLResolvers @@ -155,11 +154,11 @@ class Run: user_id: strawberry.ID experiment_id: strawberry.ID meta: JSON | None + duration: float status: GraphQLStatusEnum + cost: JSON | None created_at: datetime - _token_cache: strawberry.Private[dict[str, int] | None] = None - @strawberry.field def metrics(self) -> list["Metric"]: """Get metrics for this run.""" @@ -198,6 +197,21 @@ class Metric: created_at: datetime +@strawberry.type +class Dataset: + id: strawberry.ID + name: str + description: str | None + path: str + meta: JSON | None + team_id: strawberry.ID + experiment_id: strawberry.ID | None + run_id: strawberry.ID | None + user_id: strawberry.ID + created_at: datetime + updated_at: datetime + + # Input types for mutations @strawberry.input class CreateUserInput: @@ -245,6 +259,13 @@ class ArtifactTag: name: str +@strawberry.type +class ArtifactFile: + filename: str + size: int + content_type: str + + @strawberry.type class ArtifactContent: filename: str @@ -297,3 +318,79 @@ class DailyTokenUsage: total_tokens: int input_tokens: int output_tokens: int + + +# WILL BE DEPRECATED SOON + + +@strawberry.type +class ContentSnapshot: + id: strawberry.ID + team_id: strawberry.ID + experiment_id: strawberry.ID + run_id: strawberry.ID | None + content_uid: str + content_text: str + parent_uid: str | None + co_parent_uids: JSON | None + fitness: JSON | None + evaluation: JSON | None + metainfo: JSON | None + language: str | None + created_at: datetime + + +@strawberry.type +class ContentSnapshotSummary: + """Lightweight ContentSnapshot without content_text for listing/charting.""" + + id: strawberry.ID + team_id: strawberry.ID + experiment_id: strawberry.ID + run_id: strawberry.ID | None + content_uid: str + parent_uid: str | None + co_parent_uids: JSON | None + fitness: JSON | None + language: str | None + metainfo: JSON | None + created_at: datetime + + +@strawberry.type +class ExperimentFitnessSummary: + """Batch fitness data for an experiment — just experiment_id and list of + fitness values. + """ + + experiment_id: strawberry.ID + fitness_values: list[JSON] + + +@strawberry.type +class RepoFileEntry: + """Represents a file or directory entry in a repository.""" + + name: str + path: str + is_dir: bool + children: list["RepoFileEntry"] | None = None + + +@strawberry.type +class RepoFileTree: + """File tree structure for a trial's repository.""" + + exists: bool + root: RepoFileEntry | None = None + error: str | None = None + + +@strawberry.type +class RepoFileContent: + """Content of a file from a trial's repository.""" + + path: str + content: str | None = None + language: str | None = None + error: str | None = None diff --git a/alphatrion/server/repo/__init__.py b/alphatrion/server/repo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/alphatrion/server/repo/gcs_repo.py b/alphatrion/server/repo/gcs_repo.py new file mode 100644 index 00000000..016b0274 --- /dev/null +++ b/alphatrion/server/repo/gcs_repo.py @@ -0,0 +1,280 @@ +import io +import logging +import os +import zipfile +from functools import lru_cache + +from google.cloud import storage +from google.cloud.exceptions import NotFound + +logger = logging.getLogger(__name__) + + +class FileEntry: + """Represents a file or directory entry in the repository.""" + + def __init__(self, name: str, path: str, is_dir: bool): + self.name = name + self.path = path + self.is_dir = is_dir + self.children: list[FileEntry] | None = [] if is_dir else None + + def to_dict(self) -> dict: + result = { + "name": self.name, + "path": self.path, + "is_dir": self.is_dir, + } + if self.children is not None: + result["children"] = [child.to_dict() for child in self.children] + return result + + +class GCSRepoService: + """Service for accessing repository files stored in GCS as zip archives.""" + + _instance: "GCSRepoService | None" = None + + def __init__(self, bucket_name: str | None = None): + default_bucket = "hi-artifacts" + self.bucket_name = bucket_name or default_bucket + self._client: storage.Client | None = None + self._bucket: storage.Bucket | None = None + self._zip_cache: dict[str, zipfile.ZipFile] = {} + self._zip_bytes_cache: dict[str, bytes] = {} + + @classmethod + def get_instance(cls) -> "GCSRepoService": + """Get singleton instance of GCSRepoService.""" + if cls._instance is None: + cls._instance = cls() + return cls._instance + + @property + def client(self) -> storage.Client: + if self._client is None: + self._client = storage.Client() + return self._client + + @property + def bucket(self) -> storage.Bucket: + if self._bucket is None: + self._bucket = self.client.bucket(self.bucket_name) + return self._bucket + + def _get_blob_path(self, trial_id: str) -> str: + """Get the GCS blob path for a trial's repo zip.""" + return f"repos/{trial_id}.zip" + + def repo_exists(self, trial_id: str) -> bool: + """Check if a repository zip exists for the given trial.""" + try: + blob = self.bucket.blob(self._get_blob_path(trial_id)) + return blob.exists() + except Exception as e: + logger.error(f"Error checking repo existence for trial {trial_id}: {e}") + return False + + def _get_zip_file(self, trial_id: str) -> zipfile.ZipFile | None: + """Get or download the zip file for a trial.""" + if trial_id in self._zip_cache: + return self._zip_cache[trial_id] + + try: + blob = self.bucket.blob(self._get_blob_path(trial_id)) + zip_bytes = blob.download_as_bytes() + self._zip_bytes_cache[trial_id] = zip_bytes + zip_file = zipfile.ZipFile(io.BytesIO(zip_bytes), "r") + self._zip_cache[trial_id] = zip_file + return zip_file + except NotFound: + logger.warning(f"Repo zip not found for trial {trial_id}") + return None + except Exception as e: + logger.error(f"Error downloading repo zip for trial {trial_id}: {e}") + return None + + def get_file_tree(self, trial_id: str) -> dict | None: + """ + Get the file tree structure for a trial's repository. + + Returns a nested dict structure representing the file tree, + or None if the repository doesn't exist. + """ + zip_file = self._get_zip_file(trial_id) + if zip_file is None: + return None + + # Build a tree structure from the zip file contents + root = FileEntry(name="", path="", is_dir=True) + dir_map: dict[str, FileEntry] = {"": root} + + # Get all file paths and sort them to process directories first + file_paths = sorted(zip_file.namelist()) + + for file_path in file_paths: + # Skip empty paths + if not file_path: + continue + + # Normalize path (remove trailing slash for directories) + normalized_path = file_path.rstrip("/") + is_directory = file_path.endswith("/") + + # Split the path into parts + parts = normalized_path.split("/") + file_name = parts[-1] + + # Get or create parent directories + parent_path = "" + for i, part in enumerate(parts[:-1]): + current_path = "/".join(parts[: i + 1]) + if current_path not in dir_map: + parent = dir_map.get(parent_path, root) + new_dir = FileEntry(name=part, path=current_path, is_dir=True) + if parent.children is not None: + parent.children.append(new_dir) + dir_map[current_path] = new_dir + parent_path = current_path + + # Add the file/directory entry + if normalized_path not in dir_map: + parent = dir_map.get(parent_path, root) + entry = FileEntry( + name=file_name, path=normalized_path, is_dir=is_directory + ) + if parent.children is not None: + parent.children.append(entry) + if is_directory: + dir_map[normalized_path] = entry + + # Sort children alphabetically (directories first) + def sort_children(entry: FileEntry) -> None: + if entry.children is not None: + entry.children.sort(key=lambda x: (not x.is_dir, x.name.lower())) + for child in entry.children: + sort_children(child) + + sort_children(root) + + return root.to_dict() + + def get_file_content(self, trial_id: str, file_path: str) -> str | None: + """ + Get the content of a specific file from a trial's repository. + + Returns the file content as a string, or None if not found. + """ + zip_file = self._get_zip_file(trial_id) + if zip_file is None: + return None + + try: + # Try both with and without trailing slash for the path + try: + content = zip_file.read(file_path) + except KeyError: + # Try without leading slash if present + if file_path.startswith("/"): + content = zip_file.read(file_path[1:]) + else: + raise + + # Try to decode as UTF-8, fall back to latin-1 + try: + return content.decode("utf-8") + except UnicodeDecodeError: + return content.decode("latin-1") + except KeyError: + logger.warning(f"File {file_path} not found in repo for trial {trial_id}") + return None + except Exception as e: + logger.error( + f"Error reading file {file_path} from repo for trial {trial_id}: {e}" + ) + return None + + def clear_cache(self, trial_id: str | None = None) -> None: + """Clear the zip file cache for a specific trial or all trials.""" + if trial_id: + if trial_id in self._zip_cache: + self._zip_cache[trial_id].close() + del self._zip_cache[trial_id] + if trial_id in self._zip_bytes_cache: + del self._zip_bytes_cache[trial_id] + else: + for zf in self._zip_cache.values(): + zf.close() + self._zip_cache.clear() + self._zip_bytes_cache.clear() + + +# Helper function to detect language from file extension +@lru_cache(maxsize=128) +def detect_language(file_path: str) -> str | None: + """Detect the programming language based on file extension.""" + extension_map = { + ".py": "python", + ".js": "javascript", + ".jsx": "jsx", + ".ts": "typescript", + ".tsx": "tsx", + ".java": "java", + ".c": "c", + ".cpp": "cpp", + ".cc": "cpp", + ".h": "c", + ".hpp": "cpp", + ".go": "go", + ".rs": "rust", + ".rb": "ruby", + ".php": "php", + ".swift": "swift", + ".kt": "kotlin", + ".scala": "scala", + ".cs": "csharp", + ".sh": "bash", + ".bash": "bash", + ".zsh": "bash", + ".sql": "sql", + ".html": "html", + ".htm": "html", + ".css": "css", + ".scss": "scss", + ".sass": "sass", + ".less": "less", + ".json": "json", + ".yaml": "yaml", + ".yml": "yaml", + ".toml": "toml", + ".xml": "xml", + ".md": "markdown", + ".markdown": "markdown", + ".rst": "restructuredtext", + ".r": "r", + ".R": "r", + ".lua": "lua", + ".pl": "perl", + ".pm": "perl", + ".ex": "elixir", + ".exs": "elixir", + ".erl": "erlang", + ".hrl": "erlang", + ".hs": "haskell", + ".clj": "clojure", + ".vue": "vue", + ".svelte": "svelte", + ".dockerfile": "dockerfile", + ".tf": "hcl", + ".proto": "protobuf", + ".graphql": "graphql", + ".gql": "graphql", + } + + _, ext = os.path.splitext(file_path.lower()) + + # Special case for Dockerfile without extension + if file_path.lower().endswith("dockerfile"): + return "dockerfile" + + return extension_map.get(ext) diff --git a/alphatrion/server/repo/local_repo.py b/alphatrion/server/repo/local_repo.py new file mode 100644 index 00000000..83e06fd0 --- /dev/null +++ b/alphatrion/server/repo/local_repo.py @@ -0,0 +1,171 @@ +import logging +from pathlib import Path + +from alphatrion.server.repo.gcs_repo import FileEntry, detect_language + +logger = logging.getLogger(__name__) + +# Directories and files to skip when building the tree +SKIP_PATTERNS = { + "__pycache__", + ".git", + ".svn", + ".hg", + "node_modules", + ".venv", + "venv", + ".env", + ".idea", + ".vscode", + ".DS_Store", + "*.pyc", + "*.pyo", + "*.so", + "*.dylib", + "*.egg-info", + "dist", + "build", + ".pytest_cache", + ".mypy_cache", + ".ruff_cache", + ".tox", + "coverage", + ".coverage", + "htmlcov", +} + + +def should_skip(name: str) -> bool: + """Check if a file or directory should be skipped.""" + if name in SKIP_PATTERNS: + return True + # Check wildcard patterns + for pattern in SKIP_PATTERNS: + if pattern.startswith("*") and name.endswith(pattern[1:]): + return True + return False + + +class LocalRepoService: + """Service for accessing local filesystem repositories.""" + + _instance: "LocalRepoService | None" = None + + @classmethod + def get_instance(cls) -> "LocalRepoService": + """Get singleton instance of LocalRepoService.""" + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def path_exists(self, path: str) -> bool: + """Check if a path exists and is a directory.""" + try: + p = Path(path) + return p.exists() and p.is_dir() + except Exception as e: + logger.error(f"Error checking path existence for {path}: {e}") + return False + + def get_file_tree(self, base_path: str, max_depth: int = 10) -> dict | None: + """ + Get the file tree structure for a local directory. + + Returns a nested dict structure representing the file tree, + or None if the path doesn't exist. + """ + try: + base = Path(base_path).resolve() + if not base.exists() or not base.is_dir(): + return None + + root = FileEntry(name="", path="", is_dir=True) + self._build_tree(base, root, base, depth=0, max_depth=max_depth) + + # Sort children alphabetically (directories first) + self._sort_children(root) + + return root.to_dict() + except Exception as e: + logger.error(f"Error building file tree for {base_path}: {e}") + return None + + def _build_tree( + self, + current_path: Path, + parent: FileEntry, + base_path: Path, + depth: int, + max_depth: int, + ) -> None: + """Recursively build the file tree.""" + if depth >= max_depth: + return + + try: + entries = sorted(current_path.iterdir(), key=lambda x: x.name.lower()) + except PermissionError: + logger.warning(f"Permission denied: {current_path}") + return + except Exception as e: + logger.warning(f"Error reading directory {current_path}: {e}") + return + + for entry in entries: + if should_skip(entry.name): + continue + + # Calculate relative path from base + try: + rel_path = str(entry.relative_to(base_path)) + except ValueError: + rel_path = entry.name + + file_entry = FileEntry( + name=entry.name, path=rel_path, is_dir=entry.is_dir() + ) + + if parent.children is not None: + parent.children.append(file_entry) + + if entry.is_dir(): + self._build_tree(entry, file_entry, base_path, depth + 1, max_depth) + + def _sort_children(self, entry: FileEntry) -> None: + """Sort children alphabetically (directories first).""" + if entry.children is not None: + entry.children.sort(key=lambda x: (not x.is_dir, x.name.lower())) + for child in entry.children: + self._sort_children(child) + + def get_file_content(self, base_path: str, file_path: str) -> str | None: + """ + Get the content of a specific file. + + Returns the file content as a string, or None if not found. + """ + try: + full_path = Path(base_path) / file_path + full_path = full_path.resolve() + + # Security check: ensure the file is within the base path + base = Path(base_path).resolve() + if not str(full_path).startswith(str(base)): + logger.warning(f"Path traversal attempt: {file_path}") + return None + + if not full_path.exists() or not full_path.is_file(): + return None + + # Read file with encoding detection + try: + return full_path.read_text(encoding="utf-8") + except UnicodeDecodeError: + return full_path.read_text(encoding="latin-1") + except Exception as e: + logger.error(f"Error reading file {file_path} from {base_path}: {e}") + return None + + def get_file_language(self, file_path: str) -> str | None: + """Detect the programming language based on file extension.""" + return detect_language(file_path) diff --git a/alphatrion/snapshot/__init__.py b/alphatrion/snapshot/__init__.py index 60a4d35f..e69de29b 100644 --- a/alphatrion/snapshot/__init__.py +++ b/alphatrion/snapshot/__init__.py @@ -1,15 +0,0 @@ -from alphatrion.snapshot.snapshot import ( - ExecutionKind, - ExecutionResult, - Metadata, - Spec, - Status, -) - -__all__ = [ - "ExecutionKind", - "ExecutionResult", - "Metadata", - "Spec", - "Status", -] diff --git a/alphatrion/snapshot/snapshot.py b/alphatrion/snapshot/snapshot.py index 6e683f8b..9d2b5bea 100644 --- a/alphatrion/snapshot/snapshot.py +++ b/alphatrion/snapshot/snapshot.py @@ -1,8 +1,4 @@ -import enum from pathlib import Path -from typing import Any - -from pydantic import BaseModel from alphatrion.runtime.contextvars import current_exp_id, current_run_id from alphatrion.runtime.runtime import global_runtime @@ -31,67 +27,6 @@ """ -class ExecutionKind(enum.Enum): - RUN = "run" - # CUSTOM_EXECUTION_DEFINITION = "crd" - - -# time information is recorded in the metadata database, -# we can not get the endtime in the run. -class Metadata(BaseModel): - id: str - - -class Spec(BaseModel): - parameters: dict[str, Any] - - -class Status(BaseModel): - input: dict[str, Any] | None = None - output: dict[str, Any] - phase: str - - -class ExecutionResult(BaseModel): - schema_version: str - kind: ExecutionKind - metadata: Metadata - spec: Spec - status: Status - - -def build_run_execution( - output: dict[str, Any], input: dict[str, Any] | None = None, phase: str = "success" -) -> ExecutionResult: - run_id = current_run_id.get() - run_obj = global_runtime().metadb.get_run(run_id=run_id) - if run_obj is None: - raise RuntimeError(f"Run {run_id} not found in the database.") - - exp_obj = global_runtime().metadb.get_experiment( - experiment_id=run_obj.experiment_id - ) - if exp_obj is None: - raise RuntimeError( - f"Experiment {run_obj.experiment_id} not found in the database." - ) - - result = ExecutionResult( - schema_version="1.0", - kind=ExecutionKind.RUN, - metadata=Metadata( - id=str(run_id), - ), - spec=Spec(parameters=exp_obj.params or {}), - status=Status( - input=input or {}, - output=output, - phase=phase, - ), - ) - return result - - def snapshot_path() -> str: runtime = global_runtime() return ( diff --git a/alphatrion/storage/runtime.py b/alphatrion/storage/runtime.py index 9607b34a..298bb3d9 100644 --- a/alphatrion/storage/runtime.py +++ b/alphatrion/storage/runtime.py @@ -6,6 +6,7 @@ from traceloop.sdk import Traceloop from alphatrion import envs +from alphatrion.artifact.artifact import Artifact from alphatrion.storage.sqlstore import SQLStore from alphatrion.storage.tracestore import TraceStore from alphatrion.tracing.clickhouse_exporter import ClickHouseSpanExporter @@ -35,6 +36,7 @@ def __init__(self): database=os.getenv(envs.CLICKHOUSE_DATABASE, "alphatrion_traces"), username=os.getenv(envs.CLICKHOUSE_USERNAME, "alphatrion"), password=os.getenv(envs.CLICKHOUSE_PASSWORD, "alphatr1on"), + # Disable auto-init, use migration job for clusters init_tables=os.getenv(envs.CLICKHOUSE_INIT_TABLES, "false").lower() == "true", ) @@ -54,6 +56,10 @@ def __init__(self): tracer_provider = trace.get_tracer_provider() tracer_provider.add_span_processor(ContextAttributesSpanProcessor()) + artifact_insecure = os.getenv(envs.ARTIFACT_INSECURE, "false").lower() == "true" + if artifact_storage_enabled(): + self._artifact = Artifact(insecure=artifact_insecure) + self._inited = True @property @@ -70,6 +76,10 @@ def flush(self): if isinstance(tracer_provider, TracerProvider): tracer_provider.force_flush(timeout_millis=5000) + @property + def artifact(self): + return self._artifact + def init(): """ @@ -85,3 +95,7 @@ def storage_runtime() -> StorageRuntime: if __STORAGE_RUNTIME__ is None: raise RuntimeError("StorageRuntime is not initialized. Call init() first.") return __STORAGE_RUNTIME__ + + +def artifact_storage_enabled() -> bool: + return os.getenv(envs.ENABLE_ARTIFACT_STORAGE, "true").lower() == "true" diff --git a/alphatrion/storage/sql_models.py b/alphatrion/storage/sql_models.py index 37788ac7..b4604855 100644 --- a/alphatrion/storage/sql_models.py +++ b/alphatrion/storage/sql_models.py @@ -2,7 +2,16 @@ import uuid from datetime import UTC, datetime -from sqlalchemy import JSON, Column, DateTime, Float, Integer, String, UniqueConstraint +from sqlalchemy import ( + JSON, + Column, + DateTime, + Float, + Index, + Integer, + String, + UniqueConstraint, +) from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.ext.mutable import MutableDict from sqlalchemy.orm import declarative_base @@ -79,7 +88,7 @@ class TeamMember(Base): uuid = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) team_id = Column(UUID(as_uuid=True), nullable=False) - user_id = Column(UUID(as_uuid=True), nullable=False) + user_id = Column(UUID(as_uuid=True), nullable=False, index=True) created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) updated_at = Column( @@ -88,7 +97,11 @@ class TeamMember(Base): onupdate=lambda: datetime.now(UTC), ) - __table_args__ = (UniqueConstraint("team_id", "user_id", name="unique_team_user"),) + __table_args__ = ( + # Prevents duplicate team memberships and creates index on (team_id, user_id) + # This index covers queries filtering by team_id (via leftmost prefix rule) + UniqueConstraint("team_id", "user_id", name="unique_team_user"), + ) class ExperimentType(enum.IntEnum): @@ -101,7 +114,9 @@ class Experiment(Base): uuid = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) team_id = Column(UUID(as_uuid=True), nullable=False) - user_id = Column(UUID(as_uuid=True), nullable=True) + user_id = Column( + UUID(as_uuid=True), nullable=True, comment="User who created the experiment" + ) name = Column(String, nullable=False) description = Column(String, nullable=True) meta = Column( @@ -131,6 +146,17 @@ class Experiment(Base): 0: UNKNOWN, 1: PENDING, 2: RUNNING, 9: COMPLETED, \ 10: CANCELLED, 11: FAILED", ) + usage = Column( + MutableDict.as_mutable(JSON), + nullable=True, + comment="The usage information, e.g. for LLM calls: \ + {total_tokens: int, input_tokens: int, output_tokens: int}", + ) + cost = Column( + MutableDict.as_mutable(JSON), + nullable=True, + comment="Cost of the experiment in dollars", + ) created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) updated_at = Column( @@ -140,6 +166,17 @@ class Experiment(Base): ) is_del = Column(Integer, default=0, comment="0 for not deleted, 1 for deleted") + __table_args__ = ( + # For get_exp_by_name() - line 407-412: (team_id, name, is_del) + Index("idx_experiment_team_name", "team_id", "name", "is_del"), + # For list_exps_by_team_id() - line 428-429: (team_id, is_del) + + # ORDER BY created_at + # For list_exps_by_timeframe() - line 521-528: (team_id, created_at range, + # is_del) + # For count_experiments() - line 507-515: (team_id, is_del) + Index("idx_experiment_team_active_time", "team_id", "is_del", "created_at"), + ) + class Run(Base): __tablename__ = "runs" @@ -147,12 +184,15 @@ class Run(Base): uuid = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) team_id = Column(UUID(as_uuid=True), nullable=False) experiment_id = Column(UUID(as_uuid=True), nullable=False) - user_id = Column(UUID(as_uuid=True), nullable=True) + user_id = Column( + UUID(as_uuid=True), nullable=True, comment="User who created the run" + ) meta = Column( MutableDict.as_mutable(JSON), nullable=True, comment="Additional metadata for the run", ) + duration = Column(Float, default=0.0, comment="Duration of the run in seconds") status = Column( Integer, default=Status.PENDING, @@ -161,6 +201,17 @@ class Run(Base): 0: UNKNOWN, 1: PENDING, 2: RUNNING, 9: COMPLETED, \ 10: CANCELLED, 11: FAILED", ) + usage = Column( + MutableDict.as_mutable(JSON), + nullable=True, + comment="The usage information, e.g. for LLM calls: \ + {total_tokens: int, input_tokens: int, output_tokens: int}", + ) + cost = Column( + MutableDict.as_mutable(JSON), + nullable=True, + comment="Cost of the run in dollars", + ) created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) updated_at = Column( @@ -170,6 +221,14 @@ class Run(Base): ) is_del = Column(Integer, default=0, comment="0 for not deleted, 1 for deleted") + __table_args__ = ( + # For list_runs_by_exp_id() - line 592: (experiment_id, is_del) + + # ORDER BY created_at + Index("idx_run_experiment_active", "experiment_id", "is_del", "created_at"), + # For count_runs() - line 606: (team_id, is_del) + Index("idx_run_team_active", "team_id", "is_del"), + ) + # class Model(Base): # __tablename__ = "models" @@ -196,7 +255,6 @@ class Run(Base): class Metric(Base): __tablename__ = "metrics" - __table_args__ = (UniqueConstraint("run_id", "key", name="idx_unique_metric"),) uuid = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) key = Column(String, nullable=False) @@ -206,6 +264,17 @@ class Metric(Base): run_id = Column(UUID(as_uuid=True), nullable=False) created_at = Column(DateTime(timezone=True), default=datetime.now(UTC)) + __table_args__ = ( + # For list_metrics_by_experiment_id() - line 639: filter + ORDER BY created_at + Index("idx_metric_experiment_time", "experiment_id", "created_at"), + # For list_metrics_by_run_id() - line 650: filter + ORDER BY created_at + # Note: UniqueConstraint below provides (run_id, key) index, but not optimal + # for ORDER BY created_at + Index("idx_metric_run_time", "run_id", "created_at"), + # Unique constraint for data integrity + UniqueConstraint("run_id", "key", name="idx_unique_metric"), + ) + class ExperimentLabel(Base): __tablename__ = "experiment_labels" @@ -222,3 +291,115 @@ class ExperimentLabel(Base): default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), ) + + __table_args__ = ( + # For list_labels_by_exp_id() - line 446: filter by experiment_id + # For list_exps_by_label() join - line 464-474: join + filter by + # label_name/value + # This composite index covers both via leftmost prefix rule + Index( + "idx_experiment_label_lookup", "experiment_id", "label_name", "label_value" + ), + ) + + +class ContentSnapshot(Base): + __tablename__ = "content_snapshots" + __table_args__ = ( + Index("ix_content_snapshots_experiment_id_is_del", "experiment_id", "is_del"), + Index( + "ix_content_snapshots_experiment_id_content_uid", + "experiment_id", + "content_uid", + ), + Index( + "ix_content_snapshots_experiment_id_created_at", + "experiment_id", + "created_at", + ), + Index("ix_content_snapshots_team_id", "team_id"), + ) + + uuid = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + team_id = Column(UUID(as_uuid=True), nullable=False, name="team_id") + experiment_id = Column(UUID(as_uuid=True), nullable=False, name="experiment_id") + run_id = Column( + UUID(as_uuid=True), nullable=True, comment="Run ID, null for seed content" + ) + + content_uid = Column( + String, nullable=False, comment="UID from hive ContentDatabase" + ) + content_text = Column(String, nullable=False, comment="Actual code content as text") + + parent_uid = Column( + String, nullable=True, comment="Parent content UID (null for seed)" + ) + co_parent_uids = Column( + JSON, nullable=True, comment="List of co-parent UIDs for crossover" + ) + + fitness = Column(JSON, nullable=True, comment="Multi-dimensional fitness values") + evaluation = Column(JSON, nullable=True, comment="Full evaluation results") + metainfo = Column( + JSON, nullable=True, comment="Additional metadata for the content snapshot" + ) + + language = Column( + String, + nullable=True, + default="python", + comment="Programming language for syntax highlighting", + ) + + created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) + is_del = Column(Integer, default=0, comment="0 for not deleted, 1 for deleted") + + +class ImageBuildCache(Base): + __tablename__ = "image_build_cache" + __table_args__ = ( + Index("ix_image_build_cache_hash", "content_hash", unique=True), + Index("ix_image_build_cache_type_valid", "image_type", "is_valid"), + Index("ix_image_build_cache_last_hit_at", "last_hit_at"), + ) + + uuid = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + content_hash = Column(String(64), nullable=False, comment="SHA256 hex digest") + image_type = Column(String(20), nullable=False, comment="'base' or 'sandbox'") + image_name = Column(String(500), nullable=False, comment="Full image URL") + inputs_summary = Column(JSON, nullable=True, comment="Debug info about hash inputs") + hit_count = Column(Integer, default=0, comment="Number of cache hits") + last_hit_at = Column( + DateTime(timezone=True), nullable=True, comment="Last cache hit" + ) + created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) + is_valid = Column(Integer, default=1, comment="1 for valid, 0 for invalidated") + + +class Dataset(Base): + __tablename__ = "datasets" + + uuid = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + name = Column(String, nullable=False) + description = Column(String, nullable=True) + meta = Column( + MutableDict.as_mutable(JSON), + nullable=True, + comment="Additional metadata for the dataset", + ) + team_id = Column(UUID(as_uuid=True), nullable=False) + experiment_id = Column(UUID(as_uuid=True), nullable=True) + run_id = Column(UUID(as_uuid=True), nullable=True) + user_id = Column( + UUID(as_uuid=True), nullable=False, comment="User who created the dataset" + ) + path = Column(String, nullable=False, comment="Storage path for the dataset") + + created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC)) + updated_at = Column( + DateTime(timezone=True), + default=lambda: datetime.now(UTC), + onupdate=lambda: datetime.now(UTC), + ) + is_del = Column(Integer, default=0, comment="0 for not deleted, 1 for deleted") diff --git a/alphatrion/storage/sqlstore.py b/alphatrion/storage/sqlstore.py index 60586a9c..00b0f6a0 100644 --- a/alphatrion/storage/sqlstore.py +++ b/alphatrion/storage/sqlstore.py @@ -7,6 +7,8 @@ from alphatrion.storage.metastore import MetaStore from alphatrion.storage.sql_models import ( Base, + ContentSnapshot, + Dataset, Experiment, ExperimentLabel, Metric, @@ -346,6 +348,19 @@ def create_experiment( uid = uuid.uuid4() session = self._session() + # # TODO: verify user is in the team + # membership = ( + # session.query(TeamMember) + # .filter( + # TeamMember.user_id == user_id, + # TeamMember.team_id == team_id, + # ) + # .first() + # ) + # if membership is None: + # session.close() + # raise ValueError("User must be a member of the team to create experiment") + new_exp = Experiment( uuid=uid, team_id=team_id, @@ -396,22 +411,22 @@ def get_experiment(self, experiment_id: uuid.UUID) -> Experiment | None: return exp # Different team may have the same experiment name. - def get_exp_by_name(self, name: str, team_id: uuid.UUID) -> Experiment | None: + def get_exp_by_name( + self, name: str, team_id: uuid.UUID, include_deleted: bool = False + ) -> Experiment | None: # make sure the team exists team = self.get_team(team_id) if team is None: return None session = self._session() - trial = ( - session.query(Experiment) - .filter( - Experiment.name == name, - Experiment.team_id == team_id, - Experiment.is_del == 0, - ) - .first() + query = session.query(Experiment).filter( + Experiment.name == name, + Experiment.team_id == team_id, ) + if not include_deleted: + query = query.filter(Experiment.is_del == 0) + trial = query.first() session.close() return trial @@ -532,6 +547,70 @@ def list_exps_by_timeframe( session.close() return exps + def delete_experiment(self, experiment_id: uuid.UUID) -> bool: + session = self._session() + + # Try to delete the experiment + exp = ( + session.query(Experiment) + .filter(Experiment.uuid == experiment_id, Experiment.is_del == 0) + .first() + ) + + if exp and exp.status == Status.RUNNING: + raise ValueError( + "Cannot delete a running experiment. Please stop it first." + ) + + # Delete all runs associated with this experiment + # (regardless of experiment status) + session.query(Run).filter(Run.experiment_id == experiment_id).update( + {Run.is_del: 1}, synchronize_session=False + ) + if exp: + exp.is_del = 1 + session.commit() + session.close() + return True + + # Even if experiment doesn't exist, commit the run deletions + session.commit() + session.close() + return False + + def delete_experiments(self, experiment_ids: list[uuid.UUID]) -> int: + """ + Batch delete experiments by setting is_del flag. + Also deletes all associated runs. + Returns the number of experiments successfully deleted. + """ + session = self._session() + # Delete the experiments + # if experiment is running, skip deletion for that experiment + filtered_exps = ( + session.query(Experiment.uuid) + .filter( + Experiment.uuid.in_(experiment_ids), + Experiment.is_del == 0, + Experiment.status != Status.RUNNING, + ) + .all() + ) + filtered_exp_ids = [exp_id for (exp_id,) in filtered_exps] # unpack tuples + + deleted_count = ( + session.query(Experiment) + .filter(Experiment.uuid.in_(filtered_exp_ids)) + .update({Experiment.is_del: 1}, synchronize_session=False) + ) + # Delete all runs associated with these experiments + session.query(Run).filter(Run.experiment_id.in_(filtered_exp_ids)).update( + {Run.is_del: 1}, synchronize_session=False + ) + session.commit() + session.close() + return deleted_count + # ---------- Run APIs ---------- def create_run( @@ -580,7 +659,7 @@ def get_run(self, run_id: uuid.UUID) -> Run | None: def list_runs_by_exp_id( self, - exp_id: uuid.UUID, + experiment_id: uuid.UUID, page: int = 0, page_size: int = 10, order_by: str = "created_at", @@ -589,7 +668,7 @@ def list_runs_by_exp_id( session = self._session() runs = ( session.query(Run) - .filter(Run.experiment_id == exp_id, Run.is_del == 0) + .filter(Run.experiment_id == experiment_id, Run.is_del == 0) .order_by( getattr(Run, order_by).desc() if order_desc else getattr(Run, order_by) ) @@ -653,3 +732,303 @@ def list_metrics_by_run_id(self, run_id: uuid.UUID) -> list[Metric]: ) session.close() return metrics + + def create_content_snapshot( + self, + team_id: uuid.UUID, + experiment_id: uuid.UUID, + run_id: uuid.UUID | None, + content_uid: str, + content_text: str, + parent_uid: str | None = None, + co_parent_uids: list[str] | None = None, + fitness: dict | list | None = None, + evaluation: dict | None = None, + metainfo: dict | None = None, + language: str = "python", + ) -> uuid.UUID: + with self._session() as session: + new_snapshot = ContentSnapshot( + team_id=team_id, + experiment_id=experiment_id, + run_id=run_id, + content_uid=content_uid, + content_text=content_text, + parent_uid=parent_uid, + co_parent_uids=co_parent_uids, + fitness=fitness, + evaluation=evaluation, + metainfo=metainfo, + language=language, + ) + session.add(new_snapshot) + session.commit() + return new_snapshot.uuid + + def get_content_snapshot(self, snapshot_id: uuid.UUID) -> ContentSnapshot | None: + with self._session() as session: + return ( + session.query(ContentSnapshot) + .filter( + ContentSnapshot.uuid == snapshot_id, + ContentSnapshot.is_del == 0, + ) + .first() + ) + + def get_content_snapshot_by_uid( + self, experiment_id: uuid.UUID, content_uid: str + ) -> ContentSnapshot | None: + with self._session() as session: + return ( + session.query(ContentSnapshot) + .filter( + ContentSnapshot.experiment_id == experiment_id, + ContentSnapshot.content_uid == content_uid, + ContentSnapshot.is_del == 0, + ) + .first() + ) + + def list_content_snapshots_by_experiment_id( + self, experiment_id: uuid.UUID, page: int = 0, page_size: int = 1000 + ) -> list[ContentSnapshot]: + with self._session() as session: + return ( + session.query(ContentSnapshot) + .filter( + ContentSnapshot.experiment_id == experiment_id, + ContentSnapshot.is_del == 0, + ) + .offset(page * page_size) + .limit(page_size) + .all() + ) + + def list_content_snapshots_summary_by_experiment_id( + self, experiment_id: uuid.UUID, page: int = 0, page_size: int = 10000 + ) -> list[dict]: + """ + Returns lightweight content snapshot data without content_text and evaluation. + Used for charts and listings where full content is not needed. + """ + with self._session() as session: + results = ( + session.query( + ContentSnapshot.uuid, + ContentSnapshot.team_id, + ContentSnapshot.experiment_id, + ContentSnapshot.run_id, + ContentSnapshot.content_uid, + ContentSnapshot.parent_uid, + ContentSnapshot.co_parent_uids, + ContentSnapshot.fitness, + ContentSnapshot.language, + ContentSnapshot.metainfo, + ContentSnapshot.created_at, + ) + .filter( + ContentSnapshot.experiment_id == experiment_id, + ContentSnapshot.is_del == 0, + ) + .order_by(ContentSnapshot.created_at.asc()) + .offset(page * page_size) + .limit(page_size) + .all() + ) + return [ + { + "uuid": r.uuid, + "team_id": r.team_id, + "experiment_id": r.experiment_id, + "run_id": r.run_id, + "content_uid": r.content_uid, + "parent_uid": r.parent_uid, + "co_parent_uids": r.co_parent_uids, + "fitness": r.fitness, + "language": r.language, + "metainfo": r.metainfo, + "created_at": r.created_at, + } + for r in results + ] + + def list_fitness_by_experiment_ids( + self, experiment_ids: list[uuid.UUID] + ) -> dict[uuid.UUID, list[dict]]: + """ + Batch-fetch fitness values for multiple trials in a single query. + Returns {trial_id: [{fitness: ..., content_uid: ...}, ...]} for each trial. + Only fetches the fitness and content_uid columns to minimize payload. + """ + if not experiment_ids: + return {} + with self._session() as session: + results = ( + session.query( + ContentSnapshot.experiment_id, + ContentSnapshot.content_uid, + ContentSnapshot.fitness, + ) + .filter( + ContentSnapshot.experiment_id.in_(experiment_ids), + ContentSnapshot.is_del == 0, + ) + .all() + ) + grouped: dict[uuid.UUID, list[dict]] = {} + for r in results: + grouped.setdefault(r.experiment_id, []).append( + {"content_uid": r.content_uid, "fitness": r.fitness} + ) + return grouped + + def get_content_lineage( + self, experiment_id: uuid.UUID, content_uid: str + ) -> list[ContentSnapshot]: + """ + Get the full lineage of a content snapshot, from the given content_uid + back to the seed content (content with no parent). + Fetches all snapshots for the experiment in one query and traverses in Python. + Returns list ordered from seed (oldest first) to child. + """ + with self._session() as session: + all_snapshots = ( + session.query(ContentSnapshot) + .filter( + ContentSnapshot.experiment_id == experiment_id, + ContentSnapshot.is_del == 0, + ) + .all() + ) + + uid_map = {s.content_uid: s for s in all_snapshots} + lineage = [] + current_uid: str | None = content_uid + visited: set[str] = set() + + while current_uid and current_uid not in visited: + visited.add(current_uid) + snapshot = uid_map.get(current_uid) + if snapshot: + lineage.append(snapshot) + current_uid = snapshot.parent_uid + else: + break + + return list(reversed(lineage)) + + def list_metric_keys_by_exp_id(self, exp_id: uuid.UUID) -> list[str]: + """Returns unique metric keys for an experiment.""" + with self._session() as session: + keys = ( + session.query(Metric.key) + .filter(Metric.experiment_id == exp_id) + .distinct() + .all() + ) + return [k[0] for k in keys] + + # ---------- Dataset APIs ---------- + + def create_dataset( + self, + name: str, + team_id: uuid.UUID, + user_id: uuid.UUID, + path: str, + experiment_id: uuid.UUID | None = None, + run_id: uuid.UUID | None = None, + description: str | None = None, + meta: dict | None = None, + ) -> uuid.UUID: + session = self._session() + new_dataset = Dataset( + name=name, + team_id=team_id, + user_id=user_id, + path=path, + description=description, + meta=meta, + experiment_id=experiment_id, + run_id=run_id, + ) + session.add(new_dataset) + session.commit() + dataset_id = new_dataset.uuid + session.close() + return dataset_id + + def get_dataset(self, dataset_id: uuid.UUID) -> Dataset | None: + session = self._session() + dataset = ( + session.query(Dataset) + .filter(Dataset.uuid == dataset_id, Dataset.is_del == 0) + .first() + ) + session.close() + return dataset + + def list_datasets( + self, + team_id: uuid.UUID, + experiment_id: uuid.UUID | None = None, + run_id: uuid.UUID | None = None, + page: int = 0, + page_size: int = 10, + order_by: str = "created_at", + order_desc: bool = True, + ) -> list[Dataset]: + session = self._session() + query = session.query(Dataset).filter( + Dataset.team_id == team_id, Dataset.is_del == 0 + ) + if experiment_id is not None: + query = query.filter(Dataset.experiment_id == experiment_id) + if run_id is not None: + query = query.filter(Dataset.run_id == run_id) + datasets = ( + query.order_by( + getattr(Dataset, order_by).desc() + if order_desc + else getattr(Dataset, order_by) + ) + .offset(page * page_size) + .limit(page_size) + .all() + ) + session.close() + return datasets + + def update_dataset(self, dataset_id: uuid.UUID, **kwargs) -> None: + session = self._session() + dataset = ( + session.query(Dataset) + .filter(Dataset.uuid == dataset_id, Dataset.is_del == 0) + .first() + ) + if dataset: + for key, value in kwargs.items(): + if key == "meta" and isinstance(value, dict): + if dataset.meta is None: + dataset.meta = {} + dataset.meta.update(value) + else: + setattr(dataset, key, value) + session.commit() + session.close() + + def delete_dataset(self, dataset_id: uuid.UUID) -> bool: + session = self._session() + dataset = ( + session.query(Dataset) + .filter(Dataset.uuid == dataset_id, Dataset.is_del == 0) + .first() + ) + if dataset: + dataset.is_del = 1 + session.commit() + session.close() + return True + session.close() + return False diff --git a/alphatrion/storage/tracestore.py b/alphatrion/storage/tracestore.py index 4871edf5..98823845 100644 --- a/alphatrion/storage/tracestore.py +++ b/alphatrion/storage/tracestore.py @@ -28,7 +28,7 @@ def __init__( database: Database name username: Database username password: Database password - init_tables: If True, create tables on initialization + init_tables: If True, create tables on initialization (single-node only) """ self.database = database self._lock = threading.Lock() # Protect concurrent access to ClickHouse client @@ -70,7 +70,7 @@ def _create_database(self) -> None: raise def _create_tables(self) -> None: - """Create the otel_spans table if it doesn't exist.""" + """Create the otel_spans table if it doesn't exist (single-node only).""" create_table_sql = f""" CREATE TABLE IF NOT EXISTS {self.database}.otel_spans ( Timestamp DateTime64(9) CODEC(Delta, ZSTD(1)), diff --git a/alphatrion/tracing/clickhouse_exporter.py b/alphatrion/tracing/clickhouse_exporter.py index 3ee65a0f..392bc91b 100644 --- a/alphatrion/tracing/clickhouse_exporter.py +++ b/alphatrion/tracing/clickhouse_exporter.py @@ -42,7 +42,10 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: filtered_spans = [ span for span in spans - if span.attributes and "traceloop.workflow.name" in span.attributes + if span.attributes + and "traceloop.workflow.name" in span.attributes + and span.attributes.get("traceloop.span.kind") + in ["workflow", "task", "agent", "tool"] ] if not filtered_spans: diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile index 6975e89a..ee5a06b1 100644 --- a/dashboard/Dockerfile +++ b/dashboard/Dockerfile @@ -19,7 +19,13 @@ COPY --from=build /app/static/assets /usr/share/caddy/static/assets COPY Caddyfile /etc/caddy/Caddyfile +# Copy and setup entrypoint script +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + # Switch to non-root user USER caddy -EXPOSE 8080 \ No newline at end of file +EXPOSE 8080 + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] \ No newline at end of file diff --git a/dashboard/docker-entrypoint.sh b/dashboard/docker-entrypoint.sh new file mode 100644 index 00000000..171b5933 --- /dev/null +++ b/dashboard/docker-entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -e + +# Write config.json with userId and teamId from environment variables +cat > /usr/share/caddy/config.json < - + - AlphaTrion + Hiverge diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index ae04a92f..9a4098b3 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -8,22 +8,44 @@ "name": "alphatrion-dashboard", "version": "0.1.0", "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", "@tanstack/react-query": "^5.8.0", + "@tanstack/react-virtual": "^3.13.18", + "@types/diff": "^7.0.2", + "@types/react-syntax-highlighter": "^15.5.13", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", "axios": "^1.6.2", + "class-variance-authority": "^0.7.1", "clsx": "^2.0.0", + "d3": "^7.8.5", "date-fns": "^2.30.0", + "diff": "^8.0.3", + "js-yaml": "^4.1.1", + "jszip": "^3.10.1", "lucide-react": "^0.555.0", "plotly.js": "^3.3.1", + "prism-react-renderer": "^2.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-markdown": "^9.0.1", "react-plotly.js": "^2.6.0", "react-router-dom": "^6.20.0", - "recharts": "^2.15.0", - "tailwind-merge": "^3.4.0" + "react-syntax-highlighter": "^16.1.0", + "recharts": "^3.6.0", + "tailwind-merge": "^3.4.0", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@tanstack/eslint-plugin-query": "^5.8.0", + "@types/d3": "^7.4.3", + "@types/js-yaml": "^4.0.9", + "@types/node": "^25.0.9", "@types/react": "^18.2.42", "@types/react-dom": "^18.2.17", "@typescript-eslint/eslint-plugin": "^6.13.0", @@ -41,7 +63,6 @@ }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -795,6 +816,44 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", + "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.5" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "dev": true, @@ -847,7 +906,6 @@ }, "node_modules/@isaacs/cliui": { "version": "8.0.2", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -863,7 +921,6 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.2.2", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -874,7 +931,6 @@ }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -888,7 +944,6 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -906,7 +961,6 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -914,12 +968,10 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1030,9 +1082,31 @@ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", "license": "ISC" }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -1044,7 +1118,6 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -1052,7 +1125,6 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -1064,7 +1136,6 @@ }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -1199,12 +1270,115 @@ "integrity": "sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==", "license": "MIT" }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1271,6 +1445,39 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", @@ -1356,6 +1563,38 @@ } } }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-portal": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", @@ -1427,7 +1666,7 @@ } } }, - "node_modules/@radix-ui/react-slot": { + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", @@ -1445,47 +1684,87 @@ } } }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -1497,13 +1776,13 @@ } } }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -1515,21 +1794,237 @@ } } }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { "optional": true } } }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@remix-run/router": { "version": "1.23.0", "license": "MIT", @@ -1848,6 +2343,18 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@tanstack/eslint-plugin-query": { "version": "5.91.4", "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.91.4.tgz", @@ -2037,6 +2544,33 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.19", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.19.tgz", + "integrity": "sha512-KzwmU1IbE0IvCZSm6OXkS+kRdrgW2c2P3Ho3NC+zZXWK6oObv/L+lcV/2VuJ+snVESRlMJ+w/fg4WXI/JzoNGQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.19", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.19.tgz", + "integrity": "sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@turf/area": { "version": "7.3.4", "resolved": "https://registry.npmjs.org/@turf/area/-/area-7.3.4.tgz", @@ -2146,55 +2680,290 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "license": "MIT" }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "license": "MIT" + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, "license": "MIT", "dependencies": { - "@types/d3-color": "*" + "@types/d3-selection": "*" } }, - "node_modules/@types/d3-path": { - "version": "3.1.1", + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, "license": "MIT" }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", + "node_modules/@types/d3-color": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, "license": "MIT", "dependencies": { - "@types/d3-time": "*" + "@types/d3-array": "*", + "@types/geojson": "*" } }, - "node_modules/@types/d3-shape": { - "version": "3.1.7", + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, "license": "MIT", "dependencies": { - "@types/d3-path": "*" + "@types/d3-selection": "*" } }, - "node_modules/@types/d3-time": { - "version": "3.0.4", + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, "license": "MIT" }, - "node_modules/@types/d3-timer": { + "node_modules/@types/d3-ease": { "version": "3.0.2", "license": "MIT" }, - "node_modules/@types/estree": { - "version": "1.0.8", + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/diff": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", + "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } }, "node_modules/@types/geojson": { "version": "7946.0.16", @@ -2211,6 +2980,22 @@ "@types/geojson": "*" } }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "dev": true, @@ -2233,20 +3018,49 @@ "@types/pbf": "*" } }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", + "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, "node_modules/@types/pbf": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", "license": "MIT" }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", - "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.26", - "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -2261,6 +3075,15 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/semver": { "version": "7.7.1", "dev": true, @@ -2275,6 +3098,26 @@ "@types/geojson": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "dev": true, @@ -2512,7 +3355,6 @@ }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", - "dev": true, "license": "ISC" }, "node_modules/@vitejs/plugin-react": { @@ -2534,6 +3376,21 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, "node_modules/abs-svg-path": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", @@ -2576,7 +3433,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2584,7 +3440,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2598,12 +3453,10 @@ }, "node_modules/any-promise": { "version": "1.3.0", - "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -2615,12 +3468,10 @@ }, "node_modules/arg": { "version": "5.0.2", - "dev": true, "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { @@ -2874,9 +3725,18 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/base64-arraybuffer": { @@ -2898,7 +3758,6 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2937,7 +3796,6 @@ }, "node_modules/brace-expansion": { "version": "2.0.2", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -2945,7 +3803,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3045,7 +3902,6 @@ }, "node_modules/camelcase-css": { "version": "2.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -3079,6 +3935,16 @@ "element-size": "^1.1.1" } }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "dev": true, @@ -3094,9 +3960,48 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chokidar": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -3119,7 +4024,6 @@ }, "node_modules/chokidar/node_modules/glob-parent": { "version": "5.1.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -3134,6 +4038,18 @@ "integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==", "license": "MIT" }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/clsx": { "version": "2.1.1", "license": "MIT", @@ -3161,7 +4077,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3248,6 +4163,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -3293,7 +4218,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3365,7 +4289,6 @@ }, "node_modules/cssesc": { "version": "3.0.0", - "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -3391,6 +4314,47 @@ "node": ">=0.12" } }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-array": { "version": "3.2.4", "license": "ISC", @@ -3401,6 +4365,43 @@ "node": ">=12" } }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-collection": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", @@ -3414,19 +4415,114 @@ "node": ">=12" } }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-dispatch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", "license": "BSD-3-Clause" }, - "node_modules/d3-ease": { - "version": "3.0.1", + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", "license": "BSD-3-Clause", "engines": { "node": ">=12" } }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-force": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", @@ -3535,12 +4631,30 @@ "node": ">=12" } }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-quadtree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==", "license": "BSD-3-Clause" }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-scale": { "version": "4.0.2", "license": "ISC", @@ -3555,6 +4669,28 @@ "node": ">=12" } }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-shape": { "version": "3.2.0", "license": "ISC", @@ -3592,6 +4728,94 @@ "node": ">=12" } }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3/node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "dev": true, @@ -3656,7 +4880,6 @@ }, "node_modules/debug": { "version": "4.4.3", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3676,6 +4899,19 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "dev": true, @@ -3722,6 +4958,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "license": "MIT", @@ -3729,6 +4974,15 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-kerning": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-kerning/-/detect-kerning-2.1.2.tgz", @@ -3741,11 +4995,32 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/didyoumean": { "version": "1.2.2", - "dev": true, "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "dev": true, @@ -3759,7 +5034,6 @@ }, "node_modules/dlv": { "version": "1.1.3", - "dev": true, "license": "MIT" }, "node_modules/doctrine": { @@ -3773,14 +5047,14 @@ "node": ">=6.0.0" } }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" } }, "node_modules/draw-svg-path": { @@ -3840,7 +5114,6 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", - "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -3865,7 +5138,6 @@ }, "node_modules/emoji-regex": { "version": "9.2.2", - "dev": true, "license": "MIT" }, "node_modules/end-of-stream": { @@ -4034,6 +5306,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/es5-ext": { "version": "0.10.64", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", @@ -4417,6 +5699,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "license": "BSD-2-Clause", @@ -4435,9 +5727,9 @@ } }, "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, "node_modules/events": { @@ -4458,6 +5750,12 @@ "type": "^2.7.2" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/falafel": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.2.5.tgz", @@ -4488,18 +5786,8 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-equals": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", - "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/fast-glob": { "version": "3.3.3", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -4514,7 +5802,6 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -4544,12 +5831,24 @@ }, "node_modules/fastq": { "version": "1.19.1", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "dev": true, @@ -4563,7 +5862,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -4666,7 +5964,6 @@ }, "node_modules/foreground-child": { "version": "3.3.1", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -4693,6 +5990,14 @@ "node": ">= 6" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "dev": true, @@ -4722,7 +6027,6 @@ }, "node_modules/fsevents": { "version": "2.3.3", - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4937,7 +6241,6 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -5415,6 +6718,101 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5455,6 +6853,22 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "dev": true, @@ -5500,6 +6914,12 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/internal-slot": { "version": "1.1.0", "dev": true, @@ -5520,12 +6940,36 @@ "node": ">=12" } }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "dev": true, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" }, @@ -5570,7 +7014,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -5655,9 +7098,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5700,7 +7152,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5726,7 +7177,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -5735,6 +7185,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-iexplorer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-iexplorer/-/is-iexplorer-1.0.0.tgz", @@ -5774,7 +7234,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -5966,7 +7425,6 @@ }, "node_modules/isexe": { "version": "2.0.0", - "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -5987,7 +7445,6 @@ }, "node_modules/jackspeak": { "version": "3.4.3", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -6001,7 +7458,6 @@ }, "node_modules/jiti": { "version": "1.21.7", - "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -6012,8 +7468,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6079,6 +7536,18 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/kdbush": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", @@ -6114,9 +7583,17 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -6127,7 +7604,6 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", - "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -6144,16 +7620,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "license": "MIT", @@ -6164,6 +7644,20 @@ "loose-envify": "cli.js" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "dev": true, @@ -6233,125 +7727,731 @@ "node": ">=6.4.0" } }, - "node_modules/maplibre-gl": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz", - "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==", - "license": "BSD-3-Clause", + "node_modules/maplibre-gl": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz", + "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^20.3.1", + "@types/geojson": "^7946.0.14", + "@types/geojson-vt": "3.2.5", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.0", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.3", + "global-prefix": "^4.0.0", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^3.3.0", + "potpack": "^2.0.0", + "quickselect": "^3.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0", + "vt-pbf": "^3.1.3" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, + "node_modules/maplibre-gl/node_modules/@mapbox/tiny-sdf": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz", + "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==", + "license": "BSD-2-Clause" + }, + "node_modules/maplibre-gl/node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/maplibre-gl/node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/geojson-vt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, + "node_modules/maplibre-gl/node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-log2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-log2/-/math-log2-1.0.1.tgz", + "integrity": "sha512-9W0yGtkaMAkf74XGYVy4Dqw3YUMnTNB2eeiw9aQbUl4A3KmuCEHTt2DgAB07ENzOYAjsYSAYufkAq0Zd+jU7zA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "@mapbox/geojson-rewind": "^0.5.2", - "@mapbox/jsonlint-lines-primitives": "^2.0.2", - "@mapbox/point-geometry": "^0.1.0", - "@mapbox/tiny-sdf": "^2.0.6", - "@mapbox/unitbezier": "^0.0.1", - "@mapbox/vector-tile": "^1.3.1", - "@mapbox/whoots-js": "^3.1.0", - "@maplibre/maplibre-gl-style-spec": "^20.3.1", - "@types/geojson": "^7946.0.14", - "@types/geojson-vt": "3.2.5", - "@types/mapbox__point-geometry": "^0.1.4", - "@types/mapbox__vector-tile": "^1.3.4", - "@types/pbf": "^3.0.5", - "@types/supercluster": "^7.1.3", - "earcut": "^3.0.0", - "geojson-vt": "^4.0.2", - "gl-matrix": "^3.4.3", - "global-prefix": "^4.0.0", - "kdbush": "^4.0.2", - "murmurhash-js": "^1.0.0", - "pbf": "^3.3.0", - "potpack": "^2.0.0", - "quickselect": "^3.0.0", - "supercluster": "^8.0.1", - "tinyqueue": "^3.0.0", - "vt-pbf": "^3.1.3" - }, - "engines": { - "node": ">=16.14.0", - "npm": ">=8.1.0" - }, - "funding": { - "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/maplibre-gl/node_modules/@mapbox/tiny-sdf": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz", - "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==", - "license": "BSD-2-Clause" - }, - "node_modules/maplibre-gl/node_modules/@mapbox/unitbezier": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", - "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", - "license": "BSD-2-Clause" + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } }, - "node_modules/maplibre-gl/node_modules/earcut": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", - "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", - "license": "ISC" + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } }, - "node_modules/maplibre-gl/node_modules/geojson-vt": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", - "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", - "license": "ISC" + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } }, - "node_modules/maplibre-gl/node_modules/potpack": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", - "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", - "license": "ISC" + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/maplibre-gl/node_modules/quickselect": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", - "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", - "license": "ISC" + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/maplibre-gl/node_modules/supercluster": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", - "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", - "license": "ISC", + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "kdbush": "^4.0.2" + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/maplibre-gl/node_modules/tinyqueue": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", - "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", - "license": "ISC" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "micromark-util-types": "^2.0.0" } }, - "node_modules/math-log2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-log2/-/math-log2-1.0.1.tgz", - "integrity": "sha512-9W0yGtkaMAkf74XGYVy4Dqw3YUMnTNB2eeiw9aQbUl4A3KmuCEHTt2DgAB07ENzOYAjsYSAYufkAq0Zd+jU7zA==", + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/merge2": { - "version": "1.4.1", - "dev": true, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">= 8" + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -6403,12 +8503,22 @@ }, "node_modules/minipass": { "version": "7.1.2", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, "node_modules/mouse-change": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/mouse-change/-/mouse-change-1.4.0.tgz", @@ -6453,7 +8563,6 @@ }, "node_modules/mz": { "version": "2.7.0", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -6463,7 +8572,6 @@ }, "node_modules/nanoid": { "version": "3.3.11", - "dev": true, "funding": [ { "type": "github", @@ -6528,7 +8636,6 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6569,7 +8676,6 @@ }, "node_modules/object-hash": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -6730,9 +8836,14 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", - "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "dev": true, @@ -6750,6 +8861,31 @@ "integrity": "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==", "license": "MIT" }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse-rect": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/parse-rect/-/parse-rect-1.2.0.tgz", @@ -6789,7 +8925,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6801,7 +8936,6 @@ }, "node_modules/path-scurry": { "version": "1.11.1", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -6816,7 +8950,6 @@ }, "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.4.3", - "dev": true, "license": "ISC" }, "node_modules/path-type": { @@ -6854,12 +8987,10 @@ }, "node_modules/picocolors": { "version": "1.1.1", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -6870,7 +9001,6 @@ }, "node_modules/pify": { "version": "2.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6878,7 +9008,6 @@ }, "node_modules/pirates": { "version": "4.0.7", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -6988,7 +9117,6 @@ }, "node_modules/postcss": { "version": "8.5.6", - "dev": true, "funding": [ { "type": "opencollective", @@ -7015,7 +9143,6 @@ }, "node_modules/postcss-import": { "version": "15.1.0", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -7031,7 +9158,6 @@ }, "node_modules/postcss-import/node_modules/resolve": { "version": "1.22.10", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -7050,7 +9176,6 @@ }, "node_modules/postcss-js": { "version": "4.1.0", - "dev": true, "funding": [ { "type": "opencollective", @@ -7074,7 +9199,6 @@ }, "node_modules/postcss-load-config": { "version": "6.0.1", - "dev": true, "funding": [ { "type": "opencollective", @@ -7115,7 +9239,6 @@ }, "node_modules/postcss-nested": { "version": "6.2.0", - "dev": true, "funding": [ { "type": "opencollective", @@ -7139,7 +9262,6 @@ }, "node_modules/postcss-selector-parser": { "version": "6.1.2", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -7151,7 +9273,6 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", - "dev": true, "license": "MIT" }, "node_modules/potpack": { @@ -7160,12 +9281,34 @@ "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", "license": "ISC" }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", "license": "MIT", "engines": { - "node": ">= 0.8.0" + "node": ">=6" } }, "node_modules/probe-image-size": { @@ -7194,6 +9337,16 @@ "react-is": "^16.13.1" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/protocol-buffers-schema": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", @@ -7214,7 +9367,6 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "dev": true, "funding": [ { "type": "github", @@ -7271,6 +9423,33 @@ "version": "16.13.1", "license": "MIT" }, + "node_modules/react-markdown": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-plotly.js": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.6.0.tgz", @@ -7284,6 +9463,29 @@ "react": ">0.13.0" } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.17.0", "dev": true, @@ -7367,21 +9569,6 @@ "react-dom": ">=16.8" } }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "license": "MIT", - "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -7404,25 +9591,28 @@ } } }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", + "node_modules/react-syntax-highlighter": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", + "integrity": "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" + "@babel/runtime": "^7.28.4", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.30.0", + "refractor": "^5.0.0" + }, + "engines": { + "node": ">= 16.20.2" }, "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" + "react": ">= 0.14.0" } }, "node_modules/read-cache": { "version": "1.0.0", - "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -7457,7 +9647,6 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -7467,43 +9656,50 @@ } }, "node_modules/recharts": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", - "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", + "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", "license": "MIT", + "workspaces": [ + "www" + ], "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", "license": "MIT", - "dependencies": { - "decimal.js-light": "^2.4.1" + "peerDependencies": { + "redux": "^5.0.0" } }, - "node_modules/recharts/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "dev": true, @@ -7525,6 +9721,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/refractor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^9.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "dev": true, @@ -7642,6 +9854,45 @@ "regl-scatter2d": "^3.2.3" } }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "2.0.0-next.5", "dev": true, @@ -7677,7 +9928,6 @@ }, "node_modules/reusify": { "version": "1.1.0", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -7704,6 +9954,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.52.5", "dev": true, @@ -7746,7 +10002,6 @@ }, "node_modules/run-parallel": { "version": "1.2.0", - "dev": true, "funding": [ { "type": "github", @@ -7917,6 +10172,12 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/shallow-copy": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", @@ -7925,7 +10186,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -7936,7 +10196,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8012,7 +10271,6 @@ }, "node_modules/signal-exit": { "version": "4.1.0", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -8047,12 +10305,21 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stack-trace": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", @@ -8061,6 +10328,12 @@ "node": "*" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/static-eval": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.1.tgz", @@ -8138,7 +10411,6 @@ }, "node_modules/string-width": { "version": "5.1.2", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -8155,7 +10427,6 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -8168,12 +10439,10 @@ }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/string-width/node_modules/ansi-regex": { "version": "6.2.2", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -8184,7 +10453,6 @@ }, "node_modules/string-width/node_modules/strip-ansi": { "version": "7.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -8284,9 +10552,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -8298,7 +10579,6 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -8324,9 +10604,26 @@ "integrity": "sha512-i0TFx4wPcO0FwX+4RkLJi1MxmcTv90jNZgxMu9XRnMXMeFUY1VJlIoXpZunPUvUUqbCT1pg5PEkFqqpcaElNaA==", "license": "MIT" }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/sucrase": { "version": "3.35.0", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -8347,7 +10644,6 @@ }, "node_modules/sucrase/node_modules/commander": { "version": "4.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -8355,7 +10651,6 @@ }, "node_modules/sucrase/node_modules/glob": { "version": "10.4.5", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -8374,7 +10669,6 @@ }, "node_modules/sucrase/node_modules/minimatch": { "version": "9.0.5", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -8480,7 +10774,6 @@ }, "node_modules/tailwindcss": { "version": "3.4.18", - "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -8514,9 +10807,17 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/tailwindcss/node_modules/resolve": { "version": "1.22.10", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -8540,7 +10841,6 @@ }, "node_modules/thenify": { "version": "3.3.1", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -8548,7 +10848,6 @@ }, "node_modules/thenify-all": { "version": "1.6.0", - "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -8650,7 +10949,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -8673,6 +10971,26 @@ "topoquantize": "bin/topoquantize" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "1.4.3", "dev": true, @@ -8686,7 +11004,6 @@ }, "node_modules/ts-interface-checker": { "version": "0.1.13", - "dev": true, "license": "Apache-2.0" }, "node_modules/tslib": { @@ -8838,6 +11155,112 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unquote": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", @@ -8930,14 +11353,51 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "license": "MIT" }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", @@ -9042,7 +11502,6 @@ }, "node_modules/which": { "version": "2.0.2", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -9154,7 +11613,6 @@ }, "node_modules/wrap-ansi": { "version": "8.1.0", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -9171,7 +11629,6 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -9187,12 +11644,10 @@ }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -9205,7 +11660,6 @@ }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.2.2", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -9216,7 +11670,6 @@ }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.3", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -9227,7 +11680,6 @@ }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -9267,6 +11719,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/dashboard/package.json b/dashboard/package.json index cd67e29e..a27eb26f 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -10,22 +10,44 @@ "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0" }, "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", "@tanstack/react-query": "^5.8.0", + "@tanstack/react-virtual": "^3.13.18", + "@types/diff": "^7.0.2", + "@types/react-syntax-highlighter": "^15.5.13", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", "axios": "^1.6.2", + "class-variance-authority": "^0.7.1", "clsx": "^2.0.0", + "d3": "^7.8.5", "date-fns": "^2.30.0", + "diff": "^8.0.3", + "js-yaml": "^4.1.1", + "jszip": "^3.10.1", "lucide-react": "^0.555.0", "plotly.js": "^3.3.1", + "prism-react-renderer": "^2.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-markdown": "^9.0.1", "react-plotly.js": "^2.6.0", "react-router-dom": "^6.20.0", - "recharts": "^2.15.0", - "tailwind-merge": "^3.4.0" + "react-syntax-highlighter": "^16.1.0", + "recharts": "^3.6.0", + "tailwind-merge": "^3.4.0", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@tanstack/eslint-plugin-query": "^5.8.0", + "@types/d3": "^7.4.3", + "@types/js-yaml": "^4.0.9", + "@types/node": "^25.0.9", "@types/react": "^18.2.42", "@types/react-dom": "^18.2.17", "@typescript-eslint/eslint-plugin": "^6.13.0", diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 9e11254a..48b78f60 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, createContext, useContext } from 'react'; import { Route, Routes } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { getUserId } from './lib/config'; @@ -12,9 +12,30 @@ import { ExperimentDetailPage } from './pages/experiments/[id]'; import { ExperimentComparePage } from './pages/experiments/compare'; import { RunsPage } from './pages/runs'; import { RunDetailPage } from './pages/runs/[id]'; +import { ExperimentIDEPage } from './pages/experiments/tracker/[id]'; import { ArtifactsPage } from './pages/artifacts'; import type { Team } from './types'; +// Selection Context for IDE component +interface SelectionContextType { + experimentId: string | null; + setExperimentId: (id: string | null) => void; + experimentName: string | null; + setExperimentName: (name: string | null) => void; + selectedPointName: string | null; + setSelectedPointName: (name: string | null) => void; + chartPanelMode: "results" | "metrics"; + setChartPanelMode: (mode: "results" | "metrics") => void; +} + +const SelectionContext = createContext(null); + +export function useSelection() { + const ctx = useContext(SelectionContext); + if (!ctx) throw new Error("useSelection must be used within App"); + return ctx; +} + function App() { const [currentUser, setCurrentUser] = useState(null); const [loading, setLoading] = useState(true); @@ -22,6 +43,12 @@ function App() { const { selectedTeamId, setSelectedTeamId } = useTeamContext(); const queryClient = useQueryClient(); + // Selection state for IDE component + const [experimentId, setExperimentId] = useState(null); + const [experimentName, setExperimentName] = useState(null); + const [selectedPointName, setSelectedPointName] = useState(null); + const [chartPanelMode, setChartPanelMode] = useState<"results" | "metrics">("results"); + useEffect(() => { async function initialize() { try { @@ -126,24 +153,41 @@ function App() { return null; } + const selectionValue = { + experimentId, + setExperimentId, + experimentName, + setExperimentName, + selectedPointName, + setSelectedPointName, + chartPanelMode, + setChartPanelMode, + }; + return (
- - }> - } /> - - } /> - } /> - } /> - - - } /> - } /> + + + {/* Tracker view - full page without layout */} + } /> + + {/* Main app with layout */} + }> + } /> + + } /> + } /> + } /> + + + } /> + } /> + + } /> - } /> - - + +
); diff --git a/dashboard/src/assets/logo.svg b/dashboard/src/assets/logo.svg new file mode 100644 index 00000000..5faa1e14 --- /dev/null +++ b/dashboard/src/assets/logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/dashboard/src/assets/logotype.svg b/dashboard/src/assets/logotype.svg new file mode 100644 index 00000000..a47e07ad --- /dev/null +++ b/dashboard/src/assets/logotype.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dashboard/src/components/charts/interactive-metrics-chart.tsx b/dashboard/src/components/charts/interactive-metrics-chart.tsx new file mode 100644 index 00000000..f483c8a4 --- /dev/null +++ b/dashboard/src/components/charts/interactive-metrics-chart.tsx @@ -0,0 +1,328 @@ +import { useState, useMemo } from "react"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + Brush, + Area, + ComposedChart, +} from "recharts"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; +import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs-new"; +import { + TrendingUp, + TrendingDown, + Activity, + BarChart3, + LineChartIcon, + Download, +} from "lucide-react"; +import type { Metric } from "../../types"; + +interface InteractiveMetricsChartProps { + metrics: Metric[]; +} + +// Color palette for different metrics +const COLORS = [ + "#3b82f6", // blue + "#10b981", // green + "#f59e0b", // amber + "#ef4444", // red + "#8b5cf6", // purple + "#ec4899", // pink + "#14b8a6", // teal + "#f97316", // orange +]; + +export default function InteractiveMetricsChart({ metrics }: InteractiveMetricsChartProps) { + const [chartType, setChartType] = useState<"line" | "area">("line"); + const [visibleMetrics, setVisibleMetrics] = useState>(new Set()); + + // Group metrics by key and organize data + const { metricKeys, chartData, stats } = useMemo(() => { + const metricsByKey: Record = {}; + metrics.forEach((m) => { + const key = m.key || "unknown"; + if (!metricsByKey[key]) metricsByKey[key] = []; + metricsByKey[key].push(m); + }); + + const keys = Object.keys(metricsByKey); + + // Initialize visible metrics (all visible by default) + if (visibleMetrics.size === 0 && keys.length > 0) { + setVisibleMetrics(new Set(keys)); + } + + // Create chart data - combine all metrics by index + const maxLength = Math.max(...keys.map((k) => metricsByKey[k].length)); + const data = Array.from({ length: maxLength }, (_, index) => { + const point: any = { index: index + 1 }; + keys.forEach((key) => { + const metricList = metricsByKey[key]; + if (index < metricList.length) { + point[key] = metricList[index].value; + // Add timestamp if available + if (metricList[index].createdAt) { + point[`${key}_timestamp`] = metricList[index].createdAt; + } + } + }); + return point; + }); + + // Calculate statistics for each metric + const statistics: Record< + string, + { min: number; max: number; avg: number; latest: number; trend: "up" | "down" | "flat" } + > = {}; + + keys.forEach((key) => { + const values = metricsByKey[key].map((m) => m.value ?? 0); + const min = Math.min(...values); + const max = Math.max(...values); + const avg = values.reduce((a, b) => a + b, 0) / values.length; + const latest = values[values.length - 1] ?? 0; + + // Determine trend + let trend: "up" | "down" | "flat" = "flat"; + if (values.length >= 2) { + const first = values[0]; + const last = values[values.length - 1]; + const change = ((last - first) / (first || 1)) * 100; + if (change > 1) trend = "up"; + else if (change < -1) trend = "down"; + } + + statistics[key] = { min, max, avg, latest, trend }; + }); + + return { + metricKeys: keys, + chartData: data, + stats: statistics, + }; + }, [metrics]); + + const toggleMetric = (key: string) => { + const newVisible = new Set(visibleMetrics); + if (newVisible.has(key)) { + newVisible.delete(key); + } else { + newVisible.add(key); + } + setVisibleMetrics(newVisible); + }; + + const exportData = () => { + const csv = [ + ["Index", ...metricKeys].join(","), + ...chartData.map((row) => + [row.index, ...metricKeys.map((key) => row[key] ?? "")].join(",") + ), + ].join("\n"); + + const blob = new Blob([csv], { type: "text/csv" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "metrics.csv"; + a.click(); + }; + + if (metrics.length === 0) { + return ( + + + +

No metrics data available

+
+
+ ); + } + + return ( +
+ {/* Statistics Cards */} +
+ {metricKeys.slice(0, 4).map((key, idx) => { + const stat = stats[key]; + const TrendIcon = stat.trend === "up" ? TrendingUp : stat.trend === "down" ? TrendingDown : Activity; + const trendColor = stat.trend === "up" ? "text-green-500" : stat.trend === "down" ? "text-red-500" : "text-gray-500"; + + return ( + + + {key} + + + +
{stat.latest.toFixed(4)}
+
+ Min: {stat.min.toFixed(2)} + Max: {stat.max.toFixed(2)} + Avg: {stat.avg.toFixed(2)} +
+
+
+ ); + })} +
+ + {/* Chart Controls */} + + +
+
+ Metrics Over Time + + Interactive visualization of {metricKeys.length} metric(s) across {chartData.length} steps + +
+
+ setChartType(v as any)}> + + + + Line + + + + Area + + + + +
+
+
+ + {/* Metric Toggle Badges */} +
+ {metricKeys.map((key, idx) => ( + toggleMetric(key)} + > + + {key} + + ))} +
+ + {/* Chart */} + + + + + + + toggleMetric(e.dataKey as string)} + /> + + + {metricKeys.map((key, idx) => { + if (!visibleMetrics.has(key)) return null; + + const color = COLORS[idx % COLORS.length]; + + if (chartType === "area") { + return ( + + ); + } else { + return ( + + ); + } + })} + + + + {/* Additional Stats */} +
+ {metricKeys.map((key) => { + const stat = stats[key]; + return ( +
+
+

{key}

+
+ + Range: {stat.min.toFixed(3)} - {stat.max.toFixed(3)} + +
+
+
+

{stat.latest.toFixed(4)}

+

Latest

+
+
+ ); + })} +
+
+
+
+ ); +} diff --git a/dashboard/src/components/charts/metrics-comparison.tsx b/dashboard/src/components/charts/metrics-comparison.tsx new file mode 100644 index 00000000..8eb67faf --- /dev/null +++ b/dashboard/src/components/charts/metrics-comparison.tsx @@ -0,0 +1,359 @@ +import { useState, useMemo } from "react"; +import { + ScatterChart, + Scatter, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + ZAxis, + Legend, +} from "recharts"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; +import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; +import { ArrowRight, Download } from "lucide-react"; +import type { Metric } from "../../types"; + +interface MetricsComparisonProps { + metrics: Metric[]; +} + +export default function MetricsComparison({ metrics }: MetricsComparisonProps) { + const metricKeys = useMemo(() => { + const keys = new Set(); + metrics.forEach((m) => { + if (m.key) keys.add(m.key); + }); + return Array.from(keys).sort(); + }, [metrics]); + + const [xAxis, setXAxis] = useState(metricKeys[0] || ""); + const [yAxis, setYAxis] = useState(metricKeys[1] || metricKeys[0] || ""); + + const chartData = useMemo(() => { + if (!xAxis || !yAxis) return []; + + // Group metrics by run_id to get paired values + const runData: Record = {}; + + metrics.forEach((m) => { + const runKey = m.runId || "default"; + if (!runData[runKey]) runData[runKey] = {}; + + if (m.key === xAxis) { + runData[runKey].x = m.value ?? 0; + runData[runKey].step = m.step ?? 0; + } + if (m.key === yAxis) { + runData[runKey].y = m.value ?? 0; + } + }); + + // Filter to only runs with both x and y values + return Object.entries(runData) + .filter(([_, data]) => data.x !== undefined && data.y !== undefined) + .map(([runId, data], idx) => ({ + x: data.x!, + y: data.y!, + step: data.step, + runId, + index: idx + 1, + })); + }, [metrics, xAxis, yAxis]); + + const stats = useMemo(() => { + if (chartData.length === 0) return null; + + const xValues = chartData.map((d) => d.x); + const yValues = chartData.map((d) => d.y); + + // Calculate correlation coefficient + const n = xValues.length; + const sumX = xValues.reduce((a, b) => a + b, 0); + const sumY = yValues.reduce((a, b) => a + b, 0); + const sumXY = xValues.reduce((sum, x, i) => sum + x * yValues[i], 0); + const sumX2 = xValues.reduce((sum, x) => sum + x * x, 0); + const sumY2 = yValues.reduce((sum, y) => sum + y * y, 0); + + const correlation = + (n * sumXY - sumX * sumY) / + Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY)); + + return { + correlation: isNaN(correlation) ? 0 : correlation, + xMin: Math.min(...xValues), + xMax: Math.max(...xValues), + yMin: Math.min(...yValues), + yMax: Math.max(...yValues), + points: chartData.length, + }; + }, [chartData]); + + const exportData = () => { + const csv = [ + ["Index", xAxis, yAxis, "Step", "Run ID"].join(","), + ...chartData.map((row) => + [row.index, row.x, row.y, row.step, row.runId].join(",") + ), + ].join("\n"); + + const blob = new Blob([csv], { type: "text/csv" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `comparison_${xAxis}_vs_${yAxis}.csv`; + a.click(); + }; + + if (metricKeys.length === 0) { + return ( + + +

No metrics available for comparison

+
+
+ ); + } + + return ( +
+ {/* Controls */} + + + Metric Comparison + + Select two metrics to visualize their relationship + + + +
+
+ + +
+ +
+ +
+ +
+ + +
+ +
+ +
+
+
+
+ + {/* Stats */} + {stats && ( +
+ + + + Correlation + + + +
+ {stats.correlation.toFixed(3)} +
+

+ {Math.abs(stats.correlation) > 0.7 + ? "Strong" + : Math.abs(stats.correlation) > 0.4 + ? "Moderate" + : "Weak"}{" "} + {stats.correlation > 0 ? "positive" : "negative"} +

+
+
+ + + + + {xAxis} Range + + + +
+ {stats.xMin.toFixed(3)} - {stats.xMax.toFixed(3)} +
+

+ Span: {(stats.xMax - stats.xMin).toFixed(3)} +

+
+
+ + + + + {yAxis} Range + + + +
+ {stats.yMin.toFixed(3)} - {stats.yMax.toFixed(3)} +
+

+ Span: {(stats.yMax - stats.yMin).toFixed(3)} +

+
+
+
+ )} + + {/* Scatter Plot */} + + +
+
+ + {yAxis} vs {xAxis} + + + {chartData.length} data points + +
+ {stats && ( + 0.7 + ? "default" + : Math.abs(stats.correlation) > 0.4 + ? "secondary" + : "outline" + } + > + r = {stats.correlation.toFixed(3)} + + )} +
+
+ + {chartData.length === 0 ? ( +
+ No data points available for selected metrics +
+ ) : ( + + + + + + + { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+

+ Point {data.index} +

+
+

+ + {xAxis}: + {" "} + {data.x.toFixed(4)} +

+

+ + {yAxis}: + {" "} + {data.y.toFixed(4)} +

+ {data.step !== undefined && ( +

+ + Step: + {" "} + {data.step} +

+ )} +
+
+ ); + } + return null; + }} + /> + +
+
+ )} +
+
+
+ ); +} diff --git a/dashboard/src/components/charts/metrics-grid.tsx b/dashboard/src/components/charts/metrics-grid.tsx new file mode 100644 index 00000000..678866cd --- /dev/null +++ b/dashboard/src/components/charts/metrics-grid.tsx @@ -0,0 +1,168 @@ +import { useMemo } from "react"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Brush, +} from "recharts"; +import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; +import { Badge } from "../ui/badge"; +import { TrendingUp, TrendingDown, Activity } from "lucide-react"; +import type { Metric } from "../../types"; + +interface MetricsGridProps { + metrics: Metric[]; +} + +export default function MetricsGrid({ metrics }: MetricsGridProps) { + const metricsByKey = useMemo(() => { + const grouped: Record = {}; + metrics.forEach((m) => { + const key = m.key || "unknown"; + if (!grouped[key]) grouped[key] = []; + grouped[key].push(m); + }); + return grouped; + }, [metrics]); + + const metricKeys = Object.keys(metricsByKey); + + if (metrics.length === 0) { + return ( + + + +

No metrics data available

+
+
+ ); + } + + return ( +
+ {metricKeys.map((key) => { + const metricData = metricsByKey[key]; + const chartData = metricData.map((m, idx) => ({ + index: idx + 1, + value: m.value ?? 0, + step: m.step ?? idx + 1, + })); + + const values = metricData.map((m) => m.value ?? 0); + const min = Math.min(...values); + const max = Math.max(...values); + const avg = values.reduce((a, b) => a + b, 0) / values.length; + const latest = values[values.length - 1] ?? 0; + + // Calculate trend + let trend: "up" | "down" | "flat" = "flat"; + if (values.length >= 2) { + const first = values[0]; + const last = values[values.length - 1]; + const change = ((last - first) / (first || 1)) * 100; + if (change > 1) trend = "up"; + else if (change < -1) trend = "down"; + } + + const TrendIcon = + trend === "up" ? TrendingUp : trend === "down" ? TrendingDown : Activity; + const trendColor = + trend === "up" + ? "text-green-500" + : trend === "down" + ? "text-red-500" + : "text-gray-500"; + + return ( + + +
+ {key} + +
+
+ + Latest: {latest.toFixed(4)} + + + {metricData.length} points + +
+
+ + + + + + + `Step: ${value}`} + formatter={(value: any) => [ + value.toFixed(4), + key, + ]} + /> + + {metricData.length > 20 && ( + + )} + + + + {/* Stats */} +
+
+

Min

+

{min.toFixed(4)}

+
+
+

Avg

+

{avg.toFixed(4)}

+
+
+

Max

+

{max.toFixed(4)}

+
+
+
+
+ ); + })} +
+ ); +} diff --git a/dashboard/src/components/charts/metrics-tree.tsx b/dashboard/src/components/charts/metrics-tree.tsx new file mode 100644 index 00000000..6a7c558f --- /dev/null +++ b/dashboard/src/components/charts/metrics-tree.tsx @@ -0,0 +1,582 @@ +import { useState, useMemo, useEffect, type Dispatch, type SetStateAction } from "react"; +import { useQueries } from "@tanstack/react-query"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; +import { Badge } from "../ui/badge"; +import { ChevronRight, ChevronDown, BarChart3, Activity, Search } from "lucide-react"; +import type { Metric } from "../../types"; +import { useMetricKeys } from "../../hooks/use-trial-detail"; +import { fetchMetricsByKey } from "../../lib/graphql-client"; + +interface MetricsTreeProps { + experimentId: string; + isOngoing?: boolean; + experimentCreatedAt?: string; + selectedPaths?: Set; + onSelectedPathsChange?: Dispatch>>; +} + +interface TreeNode { + name: string; + fullPath: string; + children: Map; + isLeaf: boolean; +} + +const COLORS = [ + "#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", + "#ec4899", "#06b6d4", "#84cc16", "#f97316", "#6366f1", +]; + +function formatElapsed(ms: number): string { + if (ms < 1000) return `${Math.round(ms)}ms`; + const totalSeconds = Math.floor(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`; + if (minutes > 0) return `${minutes}m ${seconds}s`; + return `${seconds}s`; +} + +function formatElapsedAxis(seconds: number): string { + if (seconds < 60) return `${seconds}s`; + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + if (hours > 0) return secs > 0 ? `${hours}h${minutes}m${secs}s` : minutes > 0 ? `${hours}h${minutes}m` : `${hours}h`; + return secs > 0 ? `${minutes}m${secs}s` : `${minutes}m`; +} + +function formatTimestamp(iso: string): string { + const d = new Date(iso); + return d.toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); +} + +function makeMetricsTooltip(startTime: number | null) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function MetricsTooltipContent({ active, payload }: any) { + if (!active || !payload || payload.length === 0) return null; + + const meta: Record | undefined = payload[0]?.payload?._meta; + const elapsedSec: number | undefined = payload[0]?.payload?.elapsed; + + // Pick the first available metric to show shared point-level info + const firstMeta = meta + ? Object.values(meta).find((m): m is Metric => !!m) + : undefined; + + return ( +
+
+

+{formatElapsed((elapsedSec ?? 0) * 1000)}

+ {firstMeta?.createdAt && ( +

+ {formatTimestamp(firstMeta.createdAt)} +

+ )} +
+ {payload.map((entry: any) => { + const m = meta?.[entry.dataKey]; + return ( +
+
+ + {entry.name} + + {entry.value != null ? Number(entry.value).toPrecision(6) : "—"} + +
+ {m && ( +
+ {m.step != null && m.step > 0 && ( +
Logged step: {m.step}
+ )} + {m.runId && ( +
Run: {m.runId.slice(0, 8)}
+ )} +
+ )} +
+ ); + })} +
+ ); + }; +} + +function buildTreeFromKeys(keys: string[]): TreeNode { + const root: TreeNode = { + name: "root", + fullPath: "", + children: new Map(), + isLeaf: false, + }; + + keys.forEach((key) => { + const parts = key.split("/"); + let current = root; + + parts.forEach((part, index) => { + const isLastPart = index === parts.length - 1; + const fullPath = parts.slice(0, index + 1).join("/"); + + if (!current.children.has(part)) { + current.children.set(part, { + name: part, + fullPath, + children: new Map(), + isLeaf: isLastPart, + }); + } + + current = current.children.get(part)!; + }); + }); + + return root; +} + +function getAllLeafPaths(node: TreeNode): string[] { + const paths: string[] = []; + if (node.isLeaf) { + paths.push(node.fullPath); + } + for (const child of node.children.values()) { + paths.push(...getAllLeafPaths(child)); + } + return paths; +} + +function TreeNodeComponent({ + node, + level = 0, + selectedPaths, + onToggleSelect, + onToggleSelectAll, +}: { + node: TreeNode; + level?: number; + selectedPaths: Set; + onToggleSelect: (path: string) => void; + onToggleSelectAll: (paths: string[]) => void; +}) { + const [isExpanded, setIsExpanded] = useState(false); + + const hasChildren = node.children.size > 0; + const isSelected = selectedPaths.has(node.fullPath); + + const leafPaths = getAllLeafPaths(node); + const selectedCount = leafPaths.filter((path) => selectedPaths.has(path)).length; + const hasSelectedChildren = selectedCount > 0; + const allChildrenSelected = selectedCount === leafPaths.length && leafPaths.length > 0; + + const handleExpandClick = (e: React.MouseEvent) => { + e.stopPropagation(); + setIsExpanded(!isExpanded); + }; + + const handleClick = () => { + if (node.isLeaf) { + onToggleSelect(node.fullPath); + } else if (hasChildren) { + onToggleSelectAll(leafPaths); + } + }; + + const childNodes = Array.from(node.children.values()).sort((a, b) => + a.name.localeCompare(b.name) + ); + + return ( +
+
+ {hasChildren ? ( + + {isExpanded ? ( + + ) : ( + + )} + + ) : ( + + )} + +
+ {node.isLeaf ? ( + + ) : ( + + )} + + + {node.name} + + + {!node.isLeaf && leafPaths.length > 0 && ( + + {selectedCount}/{leafPaths.length} + + )} +
+
+ + {isExpanded && hasChildren && ( +
+ {childNodes.map((child) => ( + + ))} +
+ )} +
+ ); +} + +function MetricsChart({ + experimentId, + selectedKeys, + isOngoing, + experimentCreatedAt, +}: { + experimentId: string; + selectedKeys: string[]; + isOngoing?: boolean; + experimentCreatedAt?: string; +}) { + const queries = useQueries({ + queries: selectedKeys.map((key) => ({ + queryKey: ["metricsByKey", experimentId, key], + queryFn: () => fetchMetricsByKey(experimentId, key, 1000), + retry: 1, + refetchInterval: isOngoing ? 60_000 : false, + })), + }); + + const isLoading = queries.some((q) => q.isLoading); + const loadedData = selectedKeys + .map((key, idx) => ({ + key, + metrics: (queries[idx]?.data ?? []) as Metric[], + loading: queries[idx]?.isLoading ?? true, + color: COLORS[idx % COLORS.length], + })) + .filter((d) => !d.loading && d.metrics.length > 0); + + const trialStartMs = useMemo( + () => experimentCreatedAt ? new Date(experimentCreatedAt).getTime() : null, + [experimentCreatedAt], + ); + + const { chartData, startTime } = useMemo(() => { + if (loadedData.length === 0) return { chartData: [], startTime: null }; + + // Use trial createdAt as the reference start time, falling back to + // the earliest metric timestamp if unavailable. + let earliest: number | null = trialStartMs; + if (earliest === null) { + for (const { metrics } of loadedData) { + if (metrics.length > 0 && metrics[0].createdAt) { + const t = new Date(metrics[0].createdAt).getTime(); + if (earliest === null || t < earliest) earliest = t; + } + } + } + + // Collect all data points across all series, keyed by their elapsed + // time in seconds so the x-axis represents wall-clock time. + const pointsByElapsed = new Map>(); + + for (const { key, metrics } of loadedData) { + for (const m of metrics) { + if (!m.createdAt) continue; + const elapsedSec = Math.max( + 0, + Math.round((new Date(m.createdAt).getTime() - (earliest ?? 0)) / 1000), + ); + let point = pointsByElapsed.get(elapsedSec); + if (!point) { + point = { elapsed: elapsedSec, _meta: {} }; + pointsByElapsed.set(elapsedSec, point); + } + point[key] = m.value ?? null; + (point._meta as Record)[key] = m; + } + } + + const data = Array.from(pointsByElapsed.values()).sort( + (a, b) => (a.elapsed as number) - (b.elapsed as number), + ); + + return { chartData: data, startTime: earliest }; + }, [loadedData, trialStartMs]); + + const CustomTooltip = useMemo(() => makeMetricsTooltip(startTime), [startTime]); + + if (isLoading && loadedData.length === 0) { + return ( +
+
+
+ ); + } + + return ( + + + + + + } /> + {loadedData.map(({ key, color }) => ( + + ))} + + + ); +} + +export default function MetricsTree({ experimentId, isOngoing, experimentCreatedAt, selectedPaths: externalPaths, onSelectedPathsChange }: MetricsTreeProps) { + const [internalPaths, setInternalPaths] = useState>(new Set()); + const selectedPaths = externalPaths ?? internalPaths; + const setSelectedPaths = onSelectedPathsChange ?? setInternalPaths; + const { metricKeys, metricKeysLoading, metricKeysError } = useMetricKeys(experimentId, { + refetchInterval: isOngoing ? 60_000 : false, + }); + + const tree = useMemo(() => buildTreeFromKeys(metricKeys), [metricKeys]); + + const [regexFilter, setRegexFilter] = useState(""); + const [regexError, setRegexError] = useState(false); + + useEffect(() => { + if (!regexFilter) { + setRegexError(false); + return; + } + try { + const re = new RegExp(regexFilter); + setRegexError(false); + const matched = metricKeys.filter((key) => re.test(key)); + setSelectedPaths(new Set(matched)); + } catch { + setRegexError(true); + } + }, [regexFilter, metricKeys, setSelectedPaths]); + + const handleToggleSelect = (path: string) => { + setSelectedPaths((prev) => { + const next = new Set(prev); + if (next.has(path)) { + next.delete(path); + } else { + next.add(path); + } + return next; + }); + }; + + const handleToggleSelectAll = (paths: string[]) => { + setSelectedPaths((prev) => { + const next = new Set(prev); + const allSelected = paths.every((path) => next.has(path)); + if (allSelected) { + paths.forEach((path) => next.delete(path)); + } else { + paths.forEach((path) => next.add(path)); + } + return next; + }); + }; + + const selectedKeys = Array.from(selectedPaths); + + if (metricKeysLoading) { + return ( +
+
+
+ ); + } + + if (metricKeysError) { + return ( +
+

Failed to load metrics

+

Make sure the backend server is running

+
+ ); + } + + if (metricKeys.length === 0) { + return ( +
+ +

No metrics data available

+
+ ); + } + + return ( +
+ {/* Chart area */} +
+ {selectedKeys.length > 0 ? ( +
+ {/* Selected badges */} +
+ {selectedKeys.map((key, idx) => ( + handleToggleSelect(key)} + > + {key.split("/").pop()} × + + ))} +
+ {/* Chart */} +
+ +
+
+ ) : ( +
+
+ +

Select metrics from below to visualize

+
+
+ )} +
+ + {/* Bottom panels */} +
+ {/* Tree browser */} +
+
+

+ Metrics Browser + {metricKeys.length} metric{metricKeys.length !== 1 ? "s" : ""} +

+
+
+ {Array.from(tree.children.values()).map((child) => ( + + ))} +
+
+ + {/* Regex filter panel */} +
+
+

Regex Select

+
+
+
+ + setRegexFilter(e.target.value)} + placeholder="e.g. loss|accuracy" + className={`w-full pl-7 pr-2 py-1 text-xs rounded border bg-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 ${ + regexError + ? "border-destructive focus:ring-destructive" + : "border-input focus:ring-ring" + }`} + /> +
+

+ Matches select metrics automatically +

+
+
+
+
+ ); +} diff --git a/dashboard/src/components/charts/multi-trial-comparison.tsx b/dashboard/src/components/charts/multi-trial-comparison.tsx new file mode 100644 index 00000000..5c4b1e8d --- /dev/null +++ b/dashboard/src/components/charts/multi-trial-comparison.tsx @@ -0,0 +1,263 @@ +import { useState, useMemo } from "react"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + Brush, +} from "recharts"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; +import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; +import { Download, TrendingUp } from "lucide-react"; +import type { Metric } from "../../types"; + +interface MultiTrialComparisonProps { + metrics: (Metric & { trialId: string })[]; + trialNames: Record; // trialId -> trial name +} + +const COLORS = ["#3b82f6", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#ec4899", "#14b8a6", "#f97316"]; + +export default function MultiTrialComparison({ metrics, trialNames }: MultiTrialComparisonProps) { + // Get available metric keys + const metricKeys = useMemo(() => { + const keys = new Set(); + metrics.forEach((m) => { + if (m.key) keys.add(m.key); + }); + return Array.from(keys).sort(); + }, [metrics]); + + const [selectedMetric, setSelectedMetric] = useState(metricKeys[0] || ""); + + // Get unique trial IDs + const trialIds = useMemo(() => { + const ids = new Set(); + metrics.forEach((m) => ids.add(m.trialId)); + return Array.from(ids); + }, [metrics]); + + // Prepare chart data + const chartData = useMemo(() => { + if (!selectedMetric) return []; + + // Group metrics by trial + const trialMetrics: Record = {}; + metrics.forEach((m) => { + if (m.key === selectedMetric) { + if (!trialMetrics[m.trialId]) trialMetrics[m.trialId] = []; + trialMetrics[m.trialId].push(m); + } + }); + + // Find max length + const maxLength = Math.max(...Object.values(trialMetrics).map((arr) => arr.length), 0); + + // Create aligned data + const data = Array.from({ length: maxLength }, (_, idx) => { + const point: any = { step: idx + 1 }; + Object.entries(trialMetrics).forEach(([trialId, metrics]) => { + if (idx < metrics.length) { + point[trialId] = metrics[idx].value; + } + }); + return point; + }); + + return data; + }, [metrics, selectedMetric]); + + // Calculate statistics + const stats = useMemo(() => { + if (!selectedMetric) return null; + + const trialStats: Record = {}; + + trialIds.forEach((trialId) => { + const trialMetrics = metrics.filter((m) => m.trialId === trialId && m.key === selectedMetric); + if (trialMetrics.length === 0) return; + + const values = trialMetrics.map((m) => m.value ?? 0); + trialStats[trialId] = { + min: Math.min(...values), + max: Math.max(...values), + avg: values.reduce((a, b) => a + b, 0) / values.length, + latest: values[values.length - 1] ?? 0, + }; + }); + + return trialStats; + }, [metrics, selectedMetric, trialIds]); + + const exportData = () => { + if (!selectedMetric) return; + + const csv = [ + ["Step", ...trialIds.map((id) => trialNames[id] || id)].join(","), + ...chartData.map((row) => [row.step, ...trialIds.map((id) => row[id] ?? "")].join(",")), + ].join("\n"); + + const blob = new Blob([csv], { type: "text/csv" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `multi_trial_${selectedMetric}.csv`; + a.click(); + }; + + if (metricKeys.length === 0) { + return ( + + +

No metrics available

+
+
+ ); + } + + return ( +
+ {/* Controls */} + + + Multi-Trial Comparison + Compare the same metric across {trialIds.length} trials + + +
+
+ + +
+
+ +
+
+
+
+ + {/* Trial Stats Cards */} + {stats && ( +
+ {trialIds.map((trialId, idx) => { + const stat = stats[trialId]; + if (!stat) return null; + + return ( + + +
+
+ + {trialNames[trialId] || trialId} + +
+ + +
{stat.latest.toFixed(4)}
+
+
+

Min

+

{stat.min.toFixed(3)}

+
+
+

Avg

+

{stat.avg.toFixed(3)}

+
+
+

Max

+

{stat.max.toFixed(3)}

+
+
+
+ + ); + })} +
+ )} + + {/* Chart */} + + + {selectedMetric || "Select a metric"} - All Trials + + Comparison of {selectedMetric} across {trialIds.length} trials + + + + {chartData.length === 0 ? ( +
+ No data available for selected metric +
+ ) : ( + + + + + + `Step: ${value}`} + formatter={(value: any, name: string) => [ + value.toFixed(4), + trialNames[name] || name, + ]} + /> + trialNames[value] || value} + wrapperStyle={{ paddingTop: "20px" }} + /> + + + {trialIds.map((trialId, idx) => ( + + ))} + + + )} +
+
+
+ ); +} diff --git a/dashboard/src/components/charts/trial-metrics-inline.tsx b/dashboard/src/components/charts/trial-metrics-inline.tsx new file mode 100644 index 00000000..40568bb3 --- /dev/null +++ b/dashboard/src/components/charts/trial-metrics-inline.tsx @@ -0,0 +1,164 @@ +import { memo, useState } from "react"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; +import { Activity } from "lucide-react"; +import { Badge } from "../ui/badge"; +import type { Metric } from "../../types"; + +const COLORS = [ + "#3b82f6", // blue + "#10b981", // green + "#f59e0b", // amber + "#ef4444", // red + "#8b5cf6", // purple + "#ec4899", // pink + "#14b8a6", // teal + "#f97316", // orange +]; + +interface TrialMetricsInlineProps { + metrics: Metric[]; + isLoading: boolean; +} + +export const TrialMetricsInline = memo(function TrialMetricsInline({ + metrics, + isLoading, +}: TrialMetricsInlineProps) { + if (isLoading) { + return ( +
+
+
+ ); + } + + if (metrics.length === 0) { + return ( +
+ +

No metrics recorded

+
+ ); + } + + // Group metrics by key + const metricsByKey: Record = {}; + for (const m of metrics) { + const key = m.key || "unknown"; + if (!metricsByKey[key]) metricsByKey[key] = []; + metricsByKey[key].push(m); + } + const metricKeys = Object.keys(metricsByKey); + + return ; +}); + +// Separate inner component so useState is not conditional +const MetricsChart = memo(function MetricsChart({ + metricsByKey, + metricKeys, +}: { + metricsByKey: Record; + metricKeys: string[]; +}) { + const [visibleMetrics, setVisibleMetrics] = useState>( + () => new Set(metricKeys), + ); + + const toggleMetric = (key: string) => { + setVisibleMetrics((prev) => { + const next = new Set(prev); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } + return next; + }); + }; + + // Build chart data: one row per step index + const maxLength = Math.max( + ...metricKeys.map((k) => metricsByKey[k].length), + ); + const chartData = Array.from({ length: maxLength }, (_, index) => { + const point: Record = { index: index + 1 }; + for (const key of metricKeys) { + const list = metricsByKey[key]; + if (index < list.length) { + point[key] = list[index].value ?? 0; + } + } + return point; + }); + + return ( +
+ {/* Metric key badges */} +
+ {metricKeys.map((key, idx) => ( + toggleMetric(key)} + > + {key} + + ))} +
+ {/* Chart */} +
+ + + + + + + {metricKeys.map((key, idx) => { + if (!visibleMetrics.has(key)) return null; + return ( + + ); + })} + + +
+
+ ); +}); diff --git a/dashboard/src/components/chat/ChatInline.tsx b/dashboard/src/components/chat/ChatInline.tsx new file mode 100644 index 00000000..486157d4 --- /dev/null +++ b/dashboard/src/components/chat/ChatInline.tsx @@ -0,0 +1,190 @@ +import { AlertCircle, ChevronDown, ChevronUp, Loader2, MessageSquare } from "lucide-react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useChat } from "../../hooks/use-chat"; +import { ChatInput } from "./ChatInput"; +import { ChatMessage } from "./ChatMessage"; + +interface ChatInlineProps { + experimentId: string; + isExpanded: boolean; + onToggle: () => void; + onSnapshotClick?: (contentUid: string) => void; +} + +export function ChatInline({ experimentId, isExpanded, onToggle, onSnapshotClick }: ChatInlineProps) { + const { + messages, + isStreaming, + error, + sessionId, + startSession, + sendMessage, + clearChat, + } = useChat(); + const messagesEndRef = useRef(null); + const initializedRef = useRef(false); + const [height, setHeight] = useState(300); + const resizeRef = useRef(null); + + // Start session when expanded + useEffect(() => { + if (isExpanded && !initializedRef.current) { + initializedRef.current = true; + startSession(experimentId).catch((err) => { + console.error("Failed to start chat session:", err); + }); + } + }, [isExpanded, experimentId, startSession]); + + // Reset when trial changes + useEffect(() => { + initializedRef.current = false; + clearChat(); + }, [experimentId, clearChat]); + + // Scroll to bottom on new messages + useEffect(() => { + if (isExpanded) { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + } + }, [messages, isExpanded]); + + // Resize handling + const handleMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + const startY = e.clientY; + const startHeight = height; + + const handleMouseMove = (e: MouseEvent) => { + const delta = startY - e.clientY; + setHeight(Math.max(150, Math.min(600, startHeight + delta))); + }; + + const handleMouseUp = () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }, [height]); + + const suggestedQuestions = [ + "What are the top 3 snapshots?", + "How has fitness improved?", + "Show me the best code", + ]; + + if (!isExpanded) { + return ( +
+
+ + Chat with the Hive about this experiment +
+ +
+ ); + } + + return ( +
+ {/* Resize handle */} +
+ + {/* Header */} +
+
+ + Chat + {isStreaming && } +
+ +
+ + {/* Messages */} +
+ {/* Loading state */} + {!sessionId && !error && ( +
+ + Connecting... +
+ )} + + {/* Error state */} + {error && ( +
+ + {error.message} +
+ )} + + {/* Empty state with suggestions */} + {sessionId && messages.length === 0 && !error && ( +
+

+ Ask about fitness trends, snapshots, code evolution... +

+
+ {suggestedQuestions.map((question, index) => ( + + ))} +
+
+ )} + + {/* Message list */} + {messages.map((msg, index) => ( + + ))} + + {/* Scroll anchor */} +
+
+ + {/* Input */} +
+ +
+
+ ); +} diff --git a/dashboard/src/components/chat/ChatInput.tsx b/dashboard/src/components/chat/ChatInput.tsx new file mode 100644 index 00000000..afc132fc --- /dev/null +++ b/dashboard/src/components/chat/ChatInput.tsx @@ -0,0 +1,77 @@ +import { useState, useRef, useCallback, type KeyboardEvent } from "react"; +import { Send } from "lucide-react"; +import { Button } from "../ui/button"; +import { Textarea } from "../ui/textarea"; + +interface ChatInputProps { + onSend: (message: string) => void; + disabled?: boolean; + placeholder?: string; +} + +export function ChatInput({ + onSend, + disabled = false, + placeholder = "Ask about your trial data...", +}: ChatInputProps) { + const [message, setMessage] = useState(""); + const textareaRef = useRef(null); + + const handleSend = useCallback(() => { + const trimmedMessage = message.trim(); + if (trimmedMessage && !disabled) { + onSend(trimmedMessage); + setMessage(""); + // Reset textarea height + if (textareaRef.current) { + textareaRef.current.style.height = "auto"; + } + } + }, [message, disabled, onSend]); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + // Send on Enter (without Shift) + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }, + [handleSend] + ); + + const handleInput = useCallback(() => { + // Auto-resize textarea + if (textareaRef.current) { + textareaRef.current.style.height = "auto"; + textareaRef.current.style.height = `${Math.min( + textareaRef.current.scrollHeight, + 200 + )}px`; + } + }, []); + + return ( +
+