diff --git a/.github/workflows/ocr.yml b/.github/workflows/ocr.yml new file mode 100644 index 0000000000000000000000000000000000000000..12793624d0c418ffbe2586d3c36e481f42afff29 --- /dev/null +++ b/.github/workflows/ocr.yml @@ -0,0 +1,21 @@ +name: Hybrid OCR offline tests +on: + push: + paths: ['ocr/**', 'augmentoolkit/utils/offline_chunking.py', 'generation/core_components/chunking.py', 'pytest-ocr.ini', '.github/workflows/ocr.yml'] + pull_request: + paths: ['ocr/**', 'augmentoolkit/utils/offline_chunking.py', 'generation/core_components/chunking.py', 'pytest-ocr.ini', '.github/workflows/ocr.yml'] +permissions: + contents: read +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: python -m pip install --require-hashes -r ocr/requirements.lock + - run: python -m pip install -r ocr/requirements-dev.txt + - run: ruff check ocr augmentoolkit/utils/offline_chunking.py + - run: ruff format --check ocr augmentoolkit/utils/offline_chunking.py + - run: python -m pytest -c pytest-ocr.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..f20d1313d6bbed9b911023e6519a8fa696dc8a71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +.vscode +.DS_Store +_old_project +outputs/* +*.pyc +**/__pycache__/ + +**/*.pyc +cache/* +inputs/meta/* +inputs/meta* +**/spicy_prompts +**/hidden* +test_huey* +huey/* +logs/* +!logs/.gitkeep +atk-interface/dist/ +atk-interface/node_modules/ +.venv +.venv/* +**/spicy_obfuscation +generation/core_pipelines/grep_sources +llama.cpp +models/** +helper_process_logs/ \ No newline at end of file diff --git a/.hfignore b/.hfignore new file mode 100644 index 0000000000000000000000000000000000000000..172770ae6141b82b36f29ad221818d5ec60227f1 --- /dev/null +++ b/.hfignore @@ -0,0 +1,48 @@ +.git/ +.venv/ +__pycache__/ +.pytest_cache/ +.ipynb_checkpoints/ +*.pyc +*.pyo +*.pyd +.DS_Store + +# Frontend build artifacts and dependencies +atk-interface/node_modules/ +atk-interface/dist/ + +# Local logs and temp files +helper_process_logs/ +logs/ +npm_dev.log +uvicorn_dev.log +llm_server_dev.log + +# Data and configs (we want a clean slate on HF Space usually, or just default examples) +# Keep only necessary examples if any +inputs/ +outputs/ +external_configs/ +models/ + +# Exclude scripts used for local setup/cleanup +clean_hf_space.py +download_model.py +!inputs/.gitkeep +!outputs/.gitkeep +!external_configs/.gitkeep +!models/.gitkeep + +docs/ +cli_utils/ +images/ +tests/ +api_README.md +linux.sh +local_linux.sh +local_macos.sh +macos.sh +pytest-ocr.ini +spec.md +spec (10).md diff --git a/.pytest_cache/.gitignore b/.pytest_cache/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..bc1a1f6167d09c909aad37280b760bb715d0f1da --- /dev/null +++ b/.pytest_cache/.gitignore @@ -0,0 +1,2 @@ +# Created by pytest automatically. +* diff --git a/.pytest_cache/CACHEDIR.TAG b/.pytest_cache/CACHEDIR.TAG new file mode 100644 index 0000000000000000000000000000000000000000..fce15ad7eaa74e5682b644c84efb75334c112f95 --- /dev/null +++ b/.pytest_cache/CACHEDIR.TAG @@ -0,0 +1,4 @@ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by pytest. +# For information about cache directory tags, see: +# https://bford.info/cachedir/spec.html diff --git a/.pytest_cache/README.md b/.pytest_cache/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b89018ced91c0a8af7f3f23ce8901870da89f3a0 --- /dev/null +++ b/.pytest_cache/README.md @@ -0,0 +1,8 @@ +# pytest cache directory # + +This directory contains data from the pytest's cache plugin, +which provides the `--lf` and `--ff` options, as well as the `cache` fixture. + +**Do not** commit this to version control. + +See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information. diff --git a/.pytest_cache/v/cache/lastfailed b/.pytest_cache/v/cache/lastfailed new file mode 100644 index 0000000000000000000000000000000000000000..9e26dfeeb6e641a33dae4961196235bdb965b21b --- /dev/null +++ b/.pytest_cache/v/cache/lastfailed @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids new file mode 100644 index 0000000000000000000000000000000000000000..93cefc37936f7aa9584babef2716d69c6f0206f6 --- /dev/null +++ b/.pytest_cache/v/cache/nodeids @@ -0,0 +1,493 @@ +[ + "ocr/tests/test_agent_api.py::test_health_public", + "ocr/tests/test_agent_api.py::test_ready_unauthorized", + "ocr/tests/test_agent_api.py::test_v1_chat_completions_invalid_model", + "ocr/tests/test_agent_api.py::test_v1_chat_completions_invalid_url", + "ocr/tests/test_agent_api.py::test_v1_models_authorized", + "ocr/tests/test_agent_api.py::test_v1_ocr_page_wrong_sensitivity", + "ocr/tests/test_alias.py::test_grounding_and_boxes", + "ocr/tests/test_alias.py::test_invalid_reply[response0-alias_empty]", + "ocr/tests/test_alias.py::test_invalid_reply[response1-alias_truncated]", + "ocr/tests/test_alias.py::test_invalid_reply[response2-alias_empty]", + "ocr/tests/test_alias.py::test_invalid_reply[response3-alias_empty]", + "ocr/tests/test_alias.py::test_invalid_reply[response4-alias_reasoning_output]", + "ocr/tests/test_alias.py::test_probe_does_not_qualify_destination[False]", + "ocr/tests/test_alias.py::test_probe_does_not_qualify_destination[True]", + "ocr/tests/test_alias.py::test_retryable[429]", + "ocr/tests/test_alias.py::test_retryable[502]", + "ocr/tests/test_alias.py::test_retryable[503]", + "ocr/tests/test_alias.py::test_untrusted_geometry_rejected", + "ocr/tests/test_anonymization_engine.py::test_contact_patterns[12345 is a number.-12345 is a number.]", + "ocr/tests/test_anonymization_engine.py::test_contact_patterns[Mail: f\\xf6\\xf6@example.de.\\nCall +49 30 12345678!-Mail: [EMAIL].\\nCall [PHONE]!]", + "ocr/tests/test_anonymization_engine.py::test_contact_patterns[test@example.com and 555-0199.-[EMAIL] and [PHONE].]", + "ocr/tests/test_anonymization_engine.py::test_containment_and_duplicate_policy", + "ocr/tests/test_anonymization_engine.py::test_crossing_or_conflicting_overlap_blocks_whole_transformation", + "ocr/tests/test_anonymization_engine.py::test_dehyphenation_preserves_original_character_mapping", + "ocr/tests/test_anonymization_engine.py::test_entity_level_evaluator_separates_labels_coverage_and_partial_masking", + "ocr/tests/test_anonymization_engine.py::test_every_qualification_gold_span_replaces_exactly", + "ocr/tests/test_anonymization_engine.py::test_recognizer_maps_normalized_predictions_without_inventing_offsets", + "ocr/tests/test_anonymization_engine.py::test_repeated_umlaut_hyphenated_broken_words_preserve_other_spacing", + "ocr/tests/test_anonymization_integration.py::test_gate03c_frozen_holdout_canary_acceptance", + "ocr/tests/test_anonymization_integration.py::test_gate03d_recorded_first_holdout_acceptance", + "ocr/tests/test_anonymization_integration.py::test_gate03d_selected_hybrid_development_regression", + "ocr/tests/test_anonymization_integration.py::test_gate03e_recorded_current_candidate_coverage_gate", + "ocr/tests/test_anonymization_integration.py::test_gate03f_recorded_private_correction_utility", + "ocr/tests/test_anonymization_integration.py::test_real_pinned_recognition_and_ocr_handoff", + "ocr/tests/test_anonymization_integration.py::test_real_synthetic_canary_release_qualification", + "ocr/tests/test_anonymization_qualification.py::TestAnonymizationQualification::test_acceptance_fail_closed_handoff", + "ocr/tests/test_anonymization_qualification.py::TestAnonymizationQualification::test_acceptance_malformed_outputs_blocked", + "ocr/tests/test_anonymization_qualification.py::TestAnonymizationQualification::test_baseline_nltk_fail_open_security_flaw", + "ocr/tests/test_anonymization_qualification.py::TestAnonymizationQualification::test_evaluate_synthetic_dataset", + "ocr/tests/test_anonymization_qualification.py::TestAnonymizationQualification::test_ocr_to_anonymization_integration", + "ocr/tests/test_anonymization_qualification.py::TestAnonymizationQualification::test_synthetic_fixtures_spans_are_valid", + "ocr/tests/test_anonymization_release_policy.py::test_approval_tampering_or_expiration_rejected[approval_id-11111111111111111111111111111111]", + "ocr/tests/test_anonymization_release_policy.py::test_approval_tampering_or_expiration_rejected[candidate_digest-0000000000000000000000000000000000000000000000000000000000000000]", + "ocr/tests/test_anonymization_release_policy.py::test_approval_tampering_or_expiration_rejected[destination-value6]", + "ocr/tests/test_anonymization_release_policy.py::test_approval_tampering_or_expiration_rejected[expires_at-0]", + "ocr/tests/test_anonymization_release_policy.py::test_approval_tampering_or_expiration_rejected[format-text]", + "ocr/tests/test_anonymization_release_policy.py::test_approval_tampering_or_expiration_rejected[issued_at-9999999999]", + "ocr/tests/test_anonymization_release_policy.py::test_approval_tampering_or_expiration_rejected[signature-0000000000000000000000000000000000000000000000000000000000000000]", + "ocr/tests/test_anonymization_release_policy.py::test_authority_has_no_default_identity_review_or_replay_authorization", + "ocr/tests/test_anonymization_release_policy.py::test_boolean_review_claims_are_not_verified_evidence", + "ocr/tests/test_anonymization_release_policy.py::test_candidate_changes_and_forged_copies_rejected", + "ocr/tests/test_anonymization_release_policy.py::test_d0_text_requires_separately_scoped_approval", + "ocr/tests/test_anonymization_release_policy.py::test_expired_approval_and_acl_revocation_rechecked_at_export", + "ocr/tests/test_anonymization_release_policy.py::test_export_cannot_change_identity_destination_or_format[changed0]", + "ocr/tests/test_anonymization_release_policy.py::test_export_cannot_change_identity_destination_or_format[changed1]", + "ocr/tests/test_anonymization_release_policy.py::test_export_cannot_change_identity_destination_or_format[changed2]", + "ocr/tests/test_anonymization_release_policy.py::test_export_cannot_change_identity_destination_or_format[changed3]", + "ocr/tests/test_anonymization_release_policy.py::test_export_cannot_change_identity_destination_or_format[changed4]", + "ocr/tests/test_anonymization_release_policy.py::test_export_cannot_change_identity_destination_or_format[changed5]", + "ocr/tests/test_anonymization_release_policy.py::test_incomplete_stale_or_mismatched_review_evidence_rejected[candidate_digest-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb]", + "ocr/tests/test_anonymization_release_policy.py::test_incomplete_stale_or_mismatched_review_evidence_rejected[completed_at-0]", + "ocr/tests/test_anonymization_release_policy.py::test_incomplete_stale_or_mismatched_review_evidence_rejected[completed_at-9999999999]", + "ocr/tests/test_anonymization_release_policy.py::test_incomplete_stale_or_mismatched_review_evidence_rejected[destination_id-private-other]", + "ocr/tests/test_anonymization_release_policy.py::test_incomplete_stale_or_mismatched_review_evidence_rejected[review_id-wrong]", + "ocr/tests/test_anonymization_release_policy.py::test_incomplete_stale_or_mismatched_review_evidence_rejected[reviewer_subject-another-subject]", + "ocr/tests/test_anonymization_release_policy.py::test_incomplete_stale_or_mismatched_review_evidence_rejected[sensitivity-D0]", + "ocr/tests/test_anonymization_release_policy.py::test_issue_requires_authenticated_review_and_approved_private_destination[changed0]", + "ocr/tests/test_anonymization_release_policy.py::test_issue_requires_authenticated_review_and_approved_private_destination[changed1]", + "ocr/tests/test_anonymization_release_policy.py::test_issue_requires_authenticated_review_and_approved_private_destination[changed2]", + "ocr/tests/test_anonymization_release_policy.py::test_issue_requires_authenticated_review_and_approved_private_destination[changed3]", + "ocr/tests/test_anonymization_release_policy.py::test_issue_requires_authenticated_review_and_approved_private_destination[changed4]", + "ocr/tests/test_anonymization_release_policy.py::test_issue_requires_authenticated_review_and_approved_private_destination[changed5]", + "ocr/tests/test_anonymization_release_policy.py::test_issue_requires_authenticated_review_and_approved_private_destination[changed6]", + "ocr/tests/test_anonymization_release_policy.py::test_ledger_unavailability_never_returns_export", + "ocr/tests/test_anonymization_release_policy.py::test_legacy_boolean_review_or_v1_approval_cannot_bypass_new_boundary", + "ocr/tests/test_anonymization_release_policy.py::test_legacy_dictionary_cannot_receive_approval", + "ocr/tests/test_anonymization_release_policy.py::test_replay_ledger_persists_across_authority_instances_and_races", + "ocr/tests/test_anonymization_release_policy.py::test_replay_ledger_rejects_shared_permissions_or_symlink", + "ocr/tests/test_anonymization_release_policy.py::test_review_record_changes_invalidate_previous_approval", + "ocr/tests/test_anonymization_release_policy.py::test_reviewed_export_is_destination_bound_classified_and_single_use[D0]", + "ocr/tests/test_anonymization_release_policy.py::test_reviewed_export_is_destination_bound_classified_and_single_use[D1]", + "ocr/tests/test_anonymization_release_policy.py::test_reviewed_export_is_destination_bound_classified_and_single_use[D2]", + "ocr/tests/test_anonymization_release_policy.py::test_sensitive_public_destination_is_denied_even_by_permissive_application", + "ocr/tests/test_anonymization_release_policy.py::test_signed_approval_json_roundtrip_preserves_identity_and_destination_binding", + "ocr/tests/test_anonymization_runtime_safety.py::test_atomic_candidate_failure_has_no_partial_target_or_staging_file[flush]", + "ocr/tests/test_anonymization_runtime_safety.py::test_atomic_candidate_failure_has_no_partial_target_or_staging_file[fsync]", + "ocr/tests/test_anonymization_runtime_safety.py::test_atomic_candidate_failure_has_no_partial_target_or_staging_file[link]", + "ocr/tests/test_anonymization_runtime_safety.py::test_atomic_candidate_failure_has_no_partial_target_or_staging_file[write]", + "ocr/tests/test_anonymization_runtime_safety.py::test_cached_tokenizer_and_missing_nltk_corpus_preserve_chunking", + "ocr/tests/test_anonymization_runtime_safety.py::test_chunking_import_does_not_download_nltk_or_model_files", + "ocr/tests/test_anonymization_runtime_safety.py::test_missing_cached_tokenizer_uses_reversible_conservative_fallback", + "ocr/tests/test_anonymization_runtime_safety.py::test_publication_is_complete_atomic_private_and_no_overwrite", + "ocr/tests/test_anonymization_runtime_safety.py::test_short_write_never_publishes_partial_candidate", + "ocr/tests/test_anonymization_runtime_safety.py::test_symlink_or_racing_writer_cannot_be_overwritten", + "ocr/tests/test_anonymization_security.py::test_bad_placeholders_block_transformation", + "ocr/tests/test_anonymization_security.py::test_cli_creates_private_classified_candidate_and_never_overwrites", + "ocr/tests/test_anonymization_security.py::test_cli_failure_never_writes_a_candidate", + "ocr/tests/test_anonymization_security.py::test_empty_predictions_require_review_and_never_claim_success", + "ocr/tests/test_anonymization_security.py::test_failure_on_second_page_returns_no_partial_candidate", + "ocr/tests/test_anonymization_security.py::test_incomplete_or_inconsistent_evidence_fails_closed[classification]", + "ocr/tests/test_anonymization_security.py::test_incomplete_or_inconsistent_evidence_fails_closed[duplicate]", + "ocr/tests/test_anonymization_security.py::test_incomplete_or_inconsistent_evidence_fails_closed[failure]", + "ocr/tests/test_anonymization_security.py::test_incomplete_or_inconsistent_evidence_fails_closed[missing]", + "ocr/tests/test_anonymization_security.py::test_incomplete_or_inconsistent_evidence_fails_closed[partial]", + "ocr/tests/test_anonymization_security.py::test_inference_without_runtime_is_sanitized", + "ocr/tests/test_anonymization_security.py::test_invalid_runtime_configuration_is_sanitized[options0]", + "ocr/tests/test_anonymization_security.py::test_invalid_runtime_configuration_is_sanitized[options1]", + "ocr/tests/test_anonymization_security.py::test_invalid_runtime_configuration_is_sanitized[options2]", + "ocr/tests/test_anonymization_security.py::test_invalid_runtime_configuration_is_sanitized[options3]", + "ocr/tests/test_anonymization_security.py::test_invalid_runtime_configuration_is_sanitized[options4]", + "ocr/tests/test_anonymization_security.py::test_legacy_pipeline_flag_blocks_before_processing_or_external_egress", + "ocr/tests/test_anonymization_security.py::test_model_copy_cannot_bypass_span_validation", + "ocr/tests/test_anonymization_security.py::test_model_initialization_exception_is_sanitized", + "ocr/tests/test_anonymization_security.py::test_model_manifest_corruption_is_rejected", + "ocr/tests/test_anonymization_security.py::test_model_unavailable_fails_without_any_download", + "ocr/tests/test_anonymization_security.py::test_new_handoff_retains_classification_and_discards_reidentifying_metadata[D0]", + "ocr/tests/test_anonymization_security.py::test_new_handoff_retains_classification_and_discards_reidentifying_metadata[D1]", + "ocr/tests/test_anonymization_security.py::test_new_handoff_retains_classification_and_discards_reidentifying_metadata[D2]", + "ocr/tests/test_anonymization_security.py::test_unsupported_categories_or_external_recognizer_blocked[categories-value0]", + "ocr/tests/test_anonymization_security.py::test_unsupported_categories_or_external_recognizer_blocked[local-False]", + "ocr/tests/test_anonymization_security.py::test_untrusted_model_outputs_never_become_candidates[bool_offset]", + "ocr/tests/test_anonymization_security.py::test_untrusted_model_outputs_never_become_candidates[exception]", + "ocr/tests/test_anonymization_security.py::test_untrusted_model_outputs_never_become_candidates[extra]", + "ocr/tests/test_anonymization_security.py::test_untrusted_model_outputs_never_become_candidates[invalid_label]", + "ocr/tests/test_anonymization_security.py::test_untrusted_model_outputs_never_become_candidates[invalid_score]", + "ocr/tests/test_anonymization_security.py::test_untrusted_model_outputs_never_become_candidates[nan_score]", + "ocr/tests/test_anonymization_security.py::test_untrusted_model_outputs_never_become_candidates[negative]", + "ocr/tests/test_anonymization_security.py::test_untrusted_model_outputs_never_become_candidates[none]", + "ocr/tests/test_anonymization_security.py::test_untrusted_model_outputs_never_become_candidates[past_end]", + "ocr/tests/test_anonymization_security.py::test_untrusted_model_outputs_never_become_candidates[string]", + "ocr/tests/test_anonymization_security.py::test_untrusted_model_outputs_never_become_candidates[wrong_slice]", + "ocr/tests/test_anonymization_security.py::test_untrusted_safe_error_cannot_smuggle_source_into_error_response", + "ocr/tests/test_api_jobs.py::test_auth_downgrade_idempotency_cancel", + "ocr/tests/test_api_jobs.py::test_out_of_order_mixed_and_partial", + "ocr/tests/test_api_jobs.py::test_ttl_and_capacity", + "ocr/tests/test_api_jobs.py::test_two_lane_out_of_order_assembly", + "ocr/tests/test_api_jobs.py::test_worker_auth_limits_ready", + "ocr/tests/test_facade.py::test_facade_and_rejections", + "ocr/tests/test_gate03c_holdout_utf8.py::test_direct_partial_utf8_decode_fails_instead_of_silently_losing_character", + "ocr/tests/test_gate03c_holdout_utf8.py::test_existing_conversation_suffix_truncation_preserves_utf8_characters", + "ocr/tests/test_gate03c_holdout_utf8.py::test_frozen_holdout_integrity_and_control_unchanged", + "ocr/tests/test_gate03c_holdout_utf8.py::test_full_and_every_truncation_boundary_roundtrip[M\\xfcnchen, Gr\\xfc\\xdfe!]", + "ocr/tests/test_gate03c_holdout_utf8.py::test_full_and_every_truncation_boundary_roundtrip[\\u0928\\u092e\\u0938\\u094d\\u0924\\u0947 \\u0926\\u0941\\u0928\\u093f\\u092f\\u093e]", + "ocr/tests/test_gate03c_holdout_utf8.py::test_full_and_every_truncation_boundary_roundtrip[\\u6f22\\u5b57\\U0001f642a\\xe9\\xdf]", + "ocr/tests/test_gate03c_holdout_utf8.py::test_full_and_every_truncation_boundary_roundtrip[na\\xefve caf\\xe9 \\U0001f30d]", + "ocr/tests/test_gate03c_holdout_utf8.py::test_full_and_every_truncation_boundary_roundtrip[plain ASCII]", + "ocr/tests/test_gate03c_runtime_state.py::test_classified_cleanup_retains_evidence_and_respects_active_writer_lock", + "ocr/tests/test_gate03c_runtime_state.py::test_killed_writer_recovery_removes_only_private_abandoned_stage", + "ocr/tests/test_gate03c_runtime_state.py::test_multi_host_keys_cannot_share_independent_local_ledgers", + "ocr/tests/test_gate03c_runtime_state.py::test_persistent_multiprocess_race_restart_and_rotation", + "ocr/tests/test_gate03c_runtime_state.py::test_storage_failure_never_reinitializes_or_releases[corrupt]", + "ocr/tests/test_gate03c_runtime_state.py::test_storage_failure_never_reinitializes_or_releases[identity_mismatch]", + "ocr/tests/test_gate03c_runtime_state.py::test_storage_failure_never_reinitializes_or_releases[missing]", + "ocr/tests/test_gate03c_runtime_state.py::test_storage_failure_never_reinitializes_or_releases[permissions]", + "ocr/tests/test_gate03c_runtime_state.py::test_storage_failure_never_reinitializes_or_releases[replacement]", + "ocr/tests/test_gate03c_runtime_state.py::test_termination_rolls_back_and_committed_consumption_survives", + "ocr/tests/test_gate03e_outcomes_status.py::test_disable_ocr_and_bounded_operator_policy", + "ocr/tests/test_gate03e_outcomes_status.py::test_general_status_grammar_and_exact_offsets[A religiously uncommitted position is quoted.-expected2]", + "ocr/tests/test_gate03e_outcomes_status.py::test_general_status_grammar_and_exact_offsets[Angabe: keine Konfession.-expected0]", + "ocr/tests/test_gate03e_outcomes_status.py::test_general_status_grammar_and_exact_offsets[Er bestritt 'keiner Religion' anzugeh\\xf6ren.-expected1]", + "ocr/tests/test_gate03e_outcomes_status.py::test_general_status_grammar_and_exact_offsets[Ohne Religion, nicht ohne Humor.-expected4]", + "ocr/tests/test_gate03e_outcomes_status.py::test_general_status_grammar_and_exact_offsets[Scan: konfessi0nsfrei-expected7]", + "ocr/tests/test_gate03e_outcomes_status.py::test_general_status_grammar_and_exact_offsets[Scan: konfessionsfre1-expected6]", + "ocr/tests/test_gate03e_outcomes_status.py::test_general_status_grammar_and_exact_offsets[Status: NONRELIGI0US-expected5]", + "ocr/tests/test_gate03e_outcomes_status.py::test_general_status_grammar_and_exact_offsets[The essay concerns religiously unaffiliated citizens.-expected3]", + "ocr/tests/test_gate03e_outcomes_status.py::test_general_status_grammar_and_exact_offsets[\\U0001f4a1 nonreli-\\ngi0us-expected8]", + "ocr/tests/test_gate03e_outcomes_status.py::test_historical_first_runner_refuses_updated_implementation", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[Archivs]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[Hafenbogen]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[Religiously sophisticated art]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[\\xfcbernonreligi0us]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[a 12-character number]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[keine Religionszugeh\\xf6rigkeit behauptet]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[konfess10nsfre1]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[konfessXonsfrei]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[none religious]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[nonreligi00s]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[nonreligi0us\\u0301]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[nonreligi0us_suffix]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[nonreligiousness]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[ohne Religionsbuch]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[politically unaffiliated]", + "ocr/tests/test_gate03e_outcomes_status.py::test_no_broad_fuzzy_matching_or_attribution[prenom_nonreligi0us]", + "ocr/tests/test_gate03e_outcomes_status.py::test_other_failures_never_count_as_success[anonymization_inference_failed-blocked_model_runtime]", + "ocr/tests/test_gate03e_outcomes_status.py::test_other_failures_never_count_as_success[anonymization_text_limit-other_incomplete]", + "ocr/tests/test_gate03e_outcomes_status.py::test_other_failures_never_count_as_success[incomplete_ocr_document-other_incomplete]", + "ocr/tests/test_gate03e_outcomes_status.py::test_partial_full_and_taxonomy_mismatch_are_distinct", + "ocr/tests/test_gate03e_outcomes_status.py::test_span_conflicts_are_abstentions_not_returned_leaks", + "ocr/tests/test_gate03e_outcomes_status.py::test_unexpected_runtime_failure_is_sanitized_and_abstains", + "ocr/tests/test_gate03e_provenance_review.py::test_blocked_review_record_has_reason_and_original_but_no_candidate", + "ocr/tests/test_gate03e_provenance_review.py::test_caller_reference_rejected_before_host_lookup[../private-file]", + "ocr/tests/test_gate03e_provenance_review.py::test_caller_reference_rejected_before_host_lookup[https://example.invalid]", + "ocr/tests/test_gate03e_provenance_review.py::test_caller_reference_rejected_before_host_lookup[reference3]", + "ocr/tests/test_gate03e_provenance_review.py::test_caller_reference_rejected_before_host_lookup[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx]", + "ocr/tests/test_gate03e_provenance_review.py::test_canonical_provenance_and_no_raw_config_metadata", + "ocr/tests/test_gate03e_provenance_review.py::test_each_policy_change_changes_digest[ocr]", + "ocr/tests/test_gate03e_provenance_review.py::test_each_policy_change_changes_digest[placeholders]", + "ocr/tests/test_gate03e_provenance_review.py::test_each_policy_change_changes_digest[terms]", + "ocr/tests/test_gate03e_provenance_review.py::test_each_policy_change_changes_digest[threshold]", + "ocr/tests/test_gate03e_provenance_review.py::test_legacy_review_cannot_invent_configuration_or_accept_new_normalization", + "ocr/tests/test_gate03e_provenance_review.py::test_modified_provenance_invalidates_new_approval", + "ocr/tests/test_gate03e_provenance_review.py::test_old_candidate_serialization_and_approval_remain_valid", + "ocr/tests/test_gate03e_provenance_review.py::test_private_review_has_classified_source_and_no_approval_power", + "ocr/tests/test_gate03e_provenance_review.py::test_private_review_refuses_missing_authority[acl]", + "ocr/tests/test_gate03e_provenance_review.py::test_private_review_refuses_missing_authority[candidate_missing]", + "ocr/tests/test_gate03e_provenance_review.py::test_private_review_refuses_missing_authority[credentials]", + "ocr/tests/test_gate03e_provenance_review.py::test_private_review_refuses_missing_authority[expired]", + "ocr/tests/test_gate03e_provenance_review.py::test_private_review_refuses_missing_authority[public_environment]", + "ocr/tests/test_gate03e_provenance_review.py::test_private_review_refuses_missing_authority[revoked]", + "ocr/tests/test_gate03e_provenance_review.py::test_private_source_integrity_and_current_authorization[changed_class]", + "ocr/tests/test_gate03e_provenance_review.py::test_private_source_integrity_and_current_authorization[changed_digest]", + "ocr/tests/test_gate03e_provenance_review.py::test_private_source_integrity_and_current_authorization[changed_source]", + "ocr/tests/test_gate03e_provenance_review.py::test_private_source_integrity_and_current_authorization[revoked_during_read]", + "ocr/tests/test_gate03e_provenance_review.py::test_private_source_integrity_and_current_authorization[session_expired_during_read]", + "ocr/tests/test_gate03e_provenance_review.py::test_private_source_integrity_and_current_authorization[url_reference]", + "ocr/tests/test_gate03e_provenance_review.py::test_private_source_preserves_full_unicode[Gr\\xfc\\xdfe aus dem Testlabor]", + "ocr/tests/test_gate03e_provenance_review.py::test_private_source_preserves_full_unicode[\\u0928\\u092e\\u0938\\u094d\\u0924\\u0947]", + "ocr/tests/test_gate03e_provenance_review.py::test_private_source_preserves_full_unicode[\\u6f22\\u5b57\\U0001f642synthetic]", + "ocr/tests/test_gate03e_provenance_review.py::test_replacement_version_and_normalization_configuration_are_committed", + "ocr/tests/test_gate03e_provenance_review.py::test_revocation_while_loading_review_material_prevents_return", + "ocr/tests/test_gate03e_provenance_review.py::test_runtime_configuration_drift_blocks_candidate", + "ocr/tests/test_gate03e_provenance_review.py::test_source_reference_classification_cannot_be_downgraded", + "ocr/tests/test_gate03e_provenance_review.py::test_unknown_recognizer_configuration_is_never_defaulted", + "ocr/tests/test_gate03e_provenance_review.py::test_unsupported_or_forged_candidate_fails_closed[missing_provenance]", + "ocr/tests/test_gate03e_provenance_review.py::test_unsupported_or_forged_candidate_fails_closed[model_mismatch]", + "ocr/tests/test_gate03e_provenance_review.py::test_unsupported_or_forged_candidate_fails_closed[unknown_version]", + "ocr/tests/test_gate03g_security_audit.py::test_corrected_completed_review_must_bind_original_ocr_evidence", + "ocr/tests/test_gate03g_security_audit.py::test_expiry_during_real_sqlite_lock_wait_does_not_consume", + "ocr/tests/test_gate03g_security_audit.py::test_legacy_policy_is_explicit_literal_true_and_current[approve-1]", + "ocr/tests/test_gate03g_security_audit.py::test_legacy_policy_is_explicit_literal_true_and_current[approve-False]", + "ocr/tests/test_gate03g_security_audit.py::test_legacy_policy_is_explicit_literal_true_and_current[approve-None]", + "ocr/tests/test_gate03g_security_audit.py::test_legacy_policy_is_explicit_literal_true_and_current[approve-allow]", + "ocr/tests/test_gate03g_security_audit.py::test_legacy_policy_is_explicit_literal_true_and_current[approve-policy4]", + "ocr/tests/test_gate03g_security_audit.py::test_legacy_policy_is_explicit_literal_true_and_current[export-1]", + "ocr/tests/test_gate03g_security_audit.py::test_legacy_policy_is_explicit_literal_true_and_current[export-False]", + "ocr/tests/test_gate03g_security_audit.py::test_legacy_policy_is_explicit_literal_true_and_current[export-None]", + "ocr/tests/test_gate03g_security_audit.py::test_legacy_policy_is_explicit_literal_true_and_current[export-allow]", + "ocr/tests/test_gate03g_security_audit.py::test_legacy_policy_is_explicit_literal_true_and_current[export-policy4]", + "ocr/tests/test_gate03g_security_audit.py::test_missing_host_legacy_policy_denies_existing_approval", + "ocr/tests/test_gate03g_security_audit.py::test_post_link_sync_failure_leaves_only_complete_private_artifact", + "ocr/tests/test_gate03g_security_audit.py::test_private_delivery_revocation_during_write_prevents_publication", + "ocr/tests/test_gate03g_security_audit.py::test_revocation_during_consumption_burns_approval_without_returning_output[acl]", + "ocr/tests/test_gate03g_security_audit.py::test_revocation_during_consumption_burns_approval_without_returning_output[legacy_policy]", + "ocr/tests/test_gate03g_security_audit.py::test_transfer_rejects_mismatched_completed_review_lookup", + "ocr/tests/test_gate03g_security_audit.py::test_uncorrected_release_requires_explicit_operator_policy", + "ocr/tests/test_gate03g_security_audit.py::test_version_coercion_does_not_bypass_corrected_default_deny[v1]", + "ocr/tests/test_gate03g_security_audit.py::test_version_coercion_does_not_bypass_corrected_default_deny[v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_bad_typed_candidate_cannot_approve_or_export_and_does_not_consume[bounds-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_bad_typed_candidate_cannot_approve_or_export_and_does_not_consume[bounds-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_bad_typed_candidate_cannot_approve_or_export_and_does_not_consume[bounds-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_bad_typed_candidate_cannot_approve_or_export_and_does_not_consume[classification-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_bad_typed_candidate_cannot_approve_or_export_and_does_not_consume[classification-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_bad_typed_candidate_cannot_approve_or_export_and_does_not_consume[classification-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_bad_typed_candidate_cannot_approve_or_export_and_does_not_consume[duplicate-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_bad_typed_candidate_cannot_approve_or_export_and_does_not_consume[duplicate-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_bad_typed_candidate_cannot_approve_or_export_and_does_not_consume[duplicate-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_bad_typed_candidate_cannot_approve_or_export_and_does_not_consume[empty-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_bad_typed_candidate_cannot_approve_or_export_and_does_not_consume[empty-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_bad_typed_candidate_cannot_approve_or_export_and_does_not_consume[empty-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_legacy_archive_unique_indices_are_not_forced_contiguous", + "ocr/tests/test_gate03h_candidate_integrity.py::test_malformed_document_containers_and_classification_fail_closed[v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_malformed_document_containers_and_classification_fail_closed[v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_malformed_document_containers_and_classification_fail_closed[v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_original_review_poc_rejection[test_duplicate_page_manifest_should_be_rejected]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_original_review_poc_rejection[test_invalid_replacement_bounds_should_be_rejected_by_parser]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_private_review_and_correction_reject_corrupt_stored_material[foreign_page]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_private_review_and_correction_reject_corrupt_stored_material[upper_bound]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_private_review_requires_exact_original_page_set[indices0-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_private_review_requires_exact_original_page_set[indices0-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_private_review_requires_exact_original_page_set[indices0-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_private_review_requires_exact_original_page_set[indices1-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_private_review_requires_exact_original_page_set[indices1-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_private_review_requires_exact_original_page_set[indices1-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_private_review_requires_exact_original_page_set[indices2-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_private_review_requires_exact_original_page_set[indices2-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_private_review_requires_exact_original_page_set[indices2-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_private_review_requires_exact_original_page_set[indices3-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_private_review_requires_exact_original_page_set[indices3-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_private_review_requires_exact_original_page_set[indices3-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_schema_rejects_duplicate_and_empty_pages_all_entry_paths[indices0-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_schema_rejects_duplicate_and_empty_pages_all_entry_paths[indices0-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_schema_rejects_duplicate_and_empty_pages_all_entry_paths[indices0-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_schema_rejects_duplicate_and_empty_pages_all_entry_paths[indices1-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_schema_rejects_duplicate_and_empty_pages_all_entry_paths[indices1-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_schema_rejects_duplicate_and_empty_pages_all_entry_paths[indices1-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_schema_rejects_duplicate_and_empty_pages_all_entry_paths[indices2-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_schema_rejects_duplicate_and_empty_pages_all_entry_paths[indices2-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_schema_rejects_duplicate_and_empty_pages_all_entry_paths[indices2-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_source_aware_upper_bound_and_original_coordinate_semantics[v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_source_aware_upper_bound_and_original_coordinate_semantics[v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_source_aware_upper_bound_and_original_coordinate_semantics[v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[-1-2-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[-1-2-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[-1-2-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0--1-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0--1-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0--1-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-0-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-0-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-0-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-2.0-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-2.0-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-2.0-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-2_0-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-2_0-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-2_0-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-2_1-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-2_1-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-2_1-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-None-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-None-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-None-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-True-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-True-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0-True-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0.0-2-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0.0-2-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[0.0-2-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[1-1-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[1-1-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[1-1-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[2-1-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[2-1-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[2-1-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[False-2-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[False-2-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[False-2-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[None-2-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[None-2-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_strict_replacement_bounds_direct_json_and_digest[None-2-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_v3_duplicate_pages_cannot_bypass_with_version_coercion[v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_v3_duplicate_pages_cannot_bypass_with_version_coercion[v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_valid_pre_fix_bytes_digests_and_indexing_remain_identical[0-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_valid_pre_fix_bytes_digests_and_indexing_remain_identical[0-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_valid_pre_fix_bytes_digests_and_indexing_remain_identical[0-v3]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_valid_pre_fix_bytes_digests_and_indexing_remain_identical[1-v1]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_valid_pre_fix_bytes_digests_and_indexing_remain_identical[1-v2]", + "ocr/tests/test_gate03h_candidate_integrity.py::test_valid_pre_fix_bytes_digests_and_indexing_remain_identical[1-v3]", + "ocr/tests/test_ingest.py::test_embedded_skip_and_force", + "ocr/tests/test_ingest.py::test_malformed[%PDF-broken-a.pdf]", + "ocr/tests/test_ingest.py::test_malformed[hello-a.png]", + "ocr/tests/test_ingest.py::test_malformed[hello-a.txt]", + "ocr/tests/test_ingest.py::test_signature_and_limits", + "ocr/tests/test_ingest.py::test_tiff_and_bomb", + "ocr/tests/test_ingest.py::test_twenty_page_mixed", + "ocr/tests/test_limits_provenance.py::test_missing_models_readiness_and_output", + "ocr/tests/test_limits_provenance.py::test_no_token_and_remote_unavailable", + "ocr/tests/test_limits_provenance.py::test_partial_order_and_logs", + "ocr/tests/test_limits_provenance.py::test_quality_evidence_and_handoff", + "ocr/tests/test_limits_provenance.py::test_timeout_and_result_capacity", + "ocr/tests/test_limits_provenance.py::test_upload_chunked_rate_options", + "ocr/tests/test_live.py::test_live_alias", + "ocr/tests/test_live.py::test_real_api_image_and_pdf", + "ocr/tests/test_live.py::test_real_cpu", + "ocr/tests/test_private_host_adapters.py::test_local_delivery_requires_separate_current_authorization_and_retains_classification", + "ocr/tests/test_private_host_adapters.py::test_real_adapter_contract_fails_closed_without_trusting_request_flags[acl_revoked]", + "ocr/tests/test_private_host_adapters.py::test_real_adapter_contract_fails_closed_without_trusting_request_flags[boolean]", + "ocr/tests/test_private_host_adapters.py::test_real_adapter_contract_fails_closed_without_trusting_request_flags[candidate_missing]", + "ocr/tests/test_private_host_adapters.py::test_real_adapter_contract_fails_closed_without_trusting_request_flags[destination_changed]", + "ocr/tests/test_private_host_adapters.py::test_real_adapter_contract_fails_closed_without_trusting_request_flags[destination_revoked]", + "ocr/tests/test_private_host_adapters.py::test_real_adapter_contract_fails_closed_without_trusting_request_flags[expired_identity]", + "ocr/tests/test_private_host_adapters.py::test_real_adapter_contract_fails_closed_without_trusting_request_flags[expired_review]", + "ocr/tests/test_private_host_adapters.py::test_real_adapter_contract_fails_closed_without_trusting_request_flags[incomplete_review]", + "ocr/tests/test_private_host_adapters.py::test_real_adapter_contract_fails_closed_without_trusting_request_flags[requested_identity]", + "ocr/tests/test_private_host_adapters.py::test_real_adapter_contract_fails_closed_without_trusting_request_flags[revoked_review]", + "ocr/tests/test_private_host_adapters.py::test_real_adapter_contract_fails_closed_without_trusting_request_flags[revoked_session]", + "ocr/tests/test_private_host_adapters.py::test_review_evidence_is_immutable_and_candidate_bound", + "ocr/tests/test_private_host_adapters.py::test_revocation_after_export_still_blocks_private_delivery[acl_revoked]", + "ocr/tests/test_private_host_adapters.py::test_revocation_after_export_still_blocks_private_delivery[export_expired]", + "ocr/tests/test_private_host_adapters.py::test_revocation_after_export_still_blocks_private_delivery[export_revoked]", + "ocr/tests/test_private_host_adapters.py::test_revocation_after_export_still_blocks_private_delivery[review_expired]", + "ocr/tests/test_private_host_adapters.py::test_revocation_after_export_still_blocks_private_delivery[review_revoked]", + "ocr/tests/test_private_host_adapters.py::test_transport_prohibits_substitution_fallback_and_unregistered_artifacts[changed_bytes]", + "ocr/tests/test_private_host_adapters.py::test_transport_prohibits_substitution_fallback_and_unregistered_artifacts[extra_raw_metadata]", + "ocr/tests/test_private_host_adapters.py::test_transport_prohibits_substitution_fallback_and_unregistered_artifacts[public_fallback]", + "ocr/tests/test_private_host_adapters.py::test_transport_prohibits_substitution_fallback_and_unregistered_artifacts[redirect]", + "ocr/tests/test_private_host_adapters.py::test_transport_prohibits_substitution_fallback_and_unregistered_artifacts[substituted_directory]", + "ocr/tests/test_private_host_adapters.py::test_transport_prohibits_substitution_fallback_and_unregistered_artifacts[symlink]", + "ocr/tests/test_private_host_adapters.py::test_transport_prohibits_substitution_fallback_and_unregistered_artifacts[url]", + "ocr/tests/test_quality.py::test_blur_proxy_and_blank", + "ocr/tests/test_religious_terminology.py::test_casefold_never_matches_partial_expansion", + "ocr/tests/test_religious_terminology.py::test_custom_operator_terminology_uses_same_provenance_validation", + "ocr/tests/test_religious_terminology.py::test_engine_keeps_d2_and_review_flags_with_only_terminology_evidence", + "ocr/tests/test_religious_terminology.py::test_existing_first_result_refuses_before_initializing_model", + "ocr/tests/test_religious_terminology.py::test_historical_frozen_holdout_is_unchanged_well_formed_and_complete", + "ocr/tests/test_religious_terminology.py::test_length_limit_and_default_is_not_affiliation_inference", + "ocr/tests/test_religious_terminology.py::test_literal_offsets_and_no_attribution[A lecture on Taoism, not a personal assertion.-terms3-expected3]", + "ocr/tests/test_religious_terminology.py::test_literal_offsets_and_no_attribution[An agnos-\\ntisch entry.-terms8-expected8]", + "ocr/tests/test_religious_terminology.py::test_literal_offsets_and_no_attribution[Angabe: OHNE KONFESSION-terms1-expected1]", + "ocr/tests/test_religious_terminology.py::test_literal_offsets_and_no_attribution[Buddh1sm is an OCR error-terms12-expected12]", + "ocr/tests/test_religious_terminology.py::test_literal_offsets_and_no_attribution[He quoted \"Zoroastrianism\".-terms5-expected5]", + "ocr/tests/test_religious_terminology.py::test_literal_offsets_and_no_attribution[Hinduism Hinduism-terms9-expected9]", + "ocr/tests/test_religious_terminology.py::test_literal_offsets_and_no_attribution[Islamabad Islamismus preIslam Islam_suffix Islam\\u0301-terms10-expected10]", + "ocr/tests/test_religious_terminology.py::test_literal_offsets_and_no_attribution[No religious affiliation was stated.-terms2-expected2]", + "ocr/tests/test_religious_terminology.py::test_literal_offsets_and_no_attribution[Pr\\xe9face: Stra\\xdfe / STRASSE.-terms6-expected6]", + "ocr/tests/test_religious_terminology.py::test_literal_offsets_and_no_attribution[She denied being a Jain.-terms4-expected4]", + "ocr/tests/test_religious_terminology.py::test_literal_offsets_and_no_attribution[Status: religionslos.-terms0-expected0]", + "ocr/tests/test_religious_terminology.py::test_literal_offsets_and_no_attribution[\\U0001f4a1 A Bah\\xe1'\\xed reference.-terms7-expected7]", + "ocr/tests/test_religious_terminology.py::test_literal_offsets_and_no_attribution[orthodox sorting; catholic taste; evangelical marketing-terms11-expected11]", + "ocr/tests/test_religious_terminology.py::test_longest_containing_phrase_preserves_offsets_and_has_no_fake_score", + "ocr/tests/test_religious_terminology.py::test_reject_unbounded_or_nonliteral_configuration[Islam]", + "ocr/tests/test_religious_terminology.py::test_reject_unbounded_or_nonliteral_configuration[terms0]", + "ocr/tests/test_religious_terminology.py::test_reject_unbounded_or_nonliteral_configuration[terms10]", + "ocr/tests/test_religious_terminology.py::test_reject_unbounded_or_nonliteral_configuration[terms1]", + "ocr/tests/test_religious_terminology.py::test_reject_unbounded_or_nonliteral_configuration[terms2]", + "ocr/tests/test_religious_terminology.py::test_reject_unbounded_or_nonliteral_configuration[terms3]", + "ocr/tests/test_religious_terminology.py::test_reject_unbounded_or_nonliteral_configuration[terms4]", + "ocr/tests/test_religious_terminology.py::test_reject_unbounded_or_nonliteral_configuration[terms5]", + "ocr/tests/test_religious_terminology.py::test_reject_unbounded_or_nonliteral_configuration[terms6]", + "ocr/tests/test_religious_terminology.py::test_reject_unbounded_or_nonliteral_configuration[terms7]", + "ocr/tests/test_religious_terminology.py::test_reject_unbounded_or_nonliteral_configuration[terms8]", + "ocr/tests/test_religious_terminology.py::test_terminology_does_not_hide_model_failure_or_overlap", + "ocr/tests/test_reviewed_corrections.py::test_all_five_supported_corrections[boundary]", + "ocr/tests/test_reviewed_corrections.py::test_all_five_supported_corrections[category]", + "ocr/tests/test_reviewed_corrections.py::test_all_five_supported_corrections[conflict]", + "ocr/tests/test_reviewed_corrections.py::test_all_five_supported_corrections[false_positive]", + "ocr/tests/test_reviewed_corrections.py::test_all_five_supported_corrections[missing]", + "ocr/tests/test_reviewed_corrections.py::test_approval_cannot_follow_correction_and_old_history_remains_valid[D1]", + "ocr/tests/test_reviewed_corrections.py::test_approval_cannot_follow_correction_and_old_history_remains_valid[D2]", + "ocr/tests/test_reviewed_corrections.py::test_cannot_reuse_span_on_other_document_even_same_text", + "ocr/tests/test_reviewed_corrections.py::test_conflicting_or_silent_overrides_fail_closed[conflict_not_all_targets]", + "ocr/tests/test_reviewed_corrections.py::test_conflicting_or_silent_overrides_fail_closed[overlapping_edits]", + "ocr/tests/test_reviewed_corrections.py::test_conflicting_or_silent_overrides_fail_closed[unacknowledged_prediction]", + "ocr/tests/test_reviewed_corrections.py::test_conflicting_or_silent_overrides_fail_closed[unknown_targets]", + "ocr/tests/test_reviewed_corrections.py::test_corrected_policy_revocation_is_checked_again_at_export", + "ocr/tests/test_reviewed_corrections.py::test_corrupt_private_records_fail_sanitized[classification]", + "ocr/tests/test_reviewed_corrections.py::test_corrupt_private_records_fail_sanitized[malformed]", + "ocr/tests/test_reviewed_corrections.py::test_corrupt_private_records_fail_sanitized[observation]", + "ocr/tests/test_reviewed_corrections.py::test_corrupt_private_records_fail_sanitized[source]", + "ocr/tests/test_reviewed_corrections.py::test_current_permission_and_storage_rechecked_at_publication[acl_at_commit]", + "ocr/tests/test_reviewed_corrections.py::test_current_permission_and_storage_rechecked_at_publication[expired_session]", + "ocr/tests/test_reviewed_corrections.py::test_current_permission_and_storage_rechecked_at_publication[missing_storage]", + "ocr/tests/test_reviewed_corrections.py::test_current_permission_and_storage_rechecked_at_publication[revoked_proposal]", + "ocr/tests/test_reviewed_corrections.py::test_current_permission_and_storage_rechecked_at_publication[revoked_view]", + "ocr/tests/test_reviewed_corrections.py::test_empty_prediction_warning_survives_manual_correction", + "ocr/tests/test_reviewed_corrections.py::test_human_tags_cannot_be_disguised_as_legacy_candidate[v1]", + "ocr/tests/test_reviewed_corrections.py::test_human_tags_cannot_be_disguised_as_legacy_candidate[v2]", + "ocr/tests/test_reviewed_corrections.py::test_incorrect_original_offsets_and_malformed_edits_are_sanitized[digest]", + "ocr/tests/test_reviewed_corrections.py::test_incorrect_original_offsets_and_malformed_edits_are_sanitized[end]", + "ocr/tests/test_reviewed_corrections.py::test_incorrect_original_offsets_and_malformed_edits_are_sanitized[extra_replacement]", + "ocr/tests/test_reviewed_corrections.py::test_incorrect_original_offsets_and_malformed_edits_are_sanitized[page]", + "ocr/tests/test_reviewed_corrections.py::test_incorrect_original_offsets_and_malformed_edits_are_sanitized[raw_text]", + "ocr/tests/test_reviewed_corrections.py::test_incorrect_original_offsets_and_malformed_edits_are_sanitized[start]", + "ocr/tests/test_reviewed_corrections.py::test_incorrect_original_offsets_and_malformed_edits_are_sanitized[wrong_target]", + "ocr/tests/test_reviewed_corrections.py::test_lost_publication_reply_retains_immutable_recovery_pointer", + "ocr/tests/test_reviewed_corrections.py::test_missing_explicit_corrected_release_policy_denies", + "ocr/tests/test_reviewed_corrections.py::test_no_correction_permission_even_with_review_and_release_roles[apply]", + "ocr/tests/test_reviewed_corrections.py::test_no_correction_permission_even_with_review_and_release_roles[bind]", + "ocr/tests/test_reviewed_corrections.py::test_no_correction_permission_even_with_review_and_release_roles[inspect]", + "ocr/tests/test_reviewed_corrections.py::test_no_correction_permission_even_with_review_and_release_roles[inspect_proposal]", + "ocr/tests/test_reviewed_corrections.py::test_no_correction_permission_even_with_review_and_release_roles[submit]", + "ocr/tests/test_reviewed_corrections.py::test_no_request_supplied_identity_or_approval_flag[False]", + "ocr/tests/test_reviewed_corrections.py::test_no_request_supplied_identity_or_approval_flag[None]", + "ocr/tests/test_reviewed_corrections.py::test_no_request_supplied_identity_or_approval_flag[credential2]", + "ocr/tests/test_reviewed_corrections.py::test_no_request_supplied_identity_or_approval_flag[synthetic-author]", + "ocr/tests/test_reviewed_corrections.py::test_runtime_failure_is_not_a_correctable_conflict", + "ocr/tests/test_reviewed_corrections.py::test_second_correction_uses_original_offsets_and_retains_chain", + "ocr/tests/test_reviewed_corrections.py::test_simultaneous_apply_and_proposal_replay_publish_once", + "ocr/tests/test_reviewed_corrections.py::test_stale_or_inconsistent_proposal_rejected[author_subject-another-author]", + "ocr/tests/test_reviewed_corrections.py::test_stale_or_inconsistent_proposal_rejected[context_digest-ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff]", + "ocr/tests/test_reviewed_corrections.py::test_stale_or_inconsistent_proposal_rejected[evidence_digest-ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff]", + "ocr/tests/test_reviewed_corrections.py::test_stale_or_inconsistent_proposal_rejected[expires_at-1]", + "ocr/tests/test_reviewed_corrections.py::test_stale_or_inconsistent_proposal_rejected[machine_snapshot_digest-ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff]", + "ocr/tests/test_reviewed_corrections.py::test_stale_or_inconsistent_proposal_rejected[prior_candidate_digest-ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff]", + "ocr/tests/test_reviewed_corrections.py::test_stale_parallel_proposal_cannot_discard_a_newer_correction", + "ocr/tests/test_reviewed_corrections.py::test_unicode_multipage_corrections_use_original_codepoints[0]", + "ocr/tests/test_reviewed_corrections.py::test_unicode_multipage_corrections_use_original_codepoints[1]", + "ocr/tests/test_reviewed_corrections.py::test_unnecessary_empty_correction_has_no_new_candidate_or_approval", + "ocr/tests/test_security_routing.py::test_bounded_retries_breaker[429]", + "ocr/tests/test_security_routing.py::test_bounded_retries_breaker[502]", + "ocr/tests/test_security_routing.py::test_bounded_retries_breaker[503]", + "ocr/tests/test_security_routing.py::test_budget_health_and_gpu_fallback", + "ocr/tests/test_security_routing.py::test_d2_hidden_substitution_blocked", + "ocr/tests/test_security_routing.py::test_empty_reply_one_attempt_and_no_false_success", + "ocr/tests/test_security_routing.py::test_explicit_no_fallback[cpu_only]", + "ocr/tests/test_security_routing.py::test_explicit_no_fallback[gpu_only]", + "ocr/tests/test_security_routing.py::test_fallback_and_throughput_gates", + "ocr/tests/test_security_routing.py::test_gpu_only_not_blocked_by_cpu_health", + "ocr/tests/test_security_routing.py::test_low_confidence_escalation", + "ocr/tests/test_security_routing.py::test_no_downgrade", + "ocr/tests/test_security_routing.py::test_queue_capacity_and_cancel", + "ocr/tests/test_security_routing.py::test_retry_policy_rechecked", + "ocr/tests/test_security_routing.py::test_security_matrix[cpu-False-D0]", + "ocr/tests/test_security_routing.py::test_security_matrix[cpu-False-D1]", + "ocr/tests/test_security_routing.py::test_security_matrix[cpu-False-D2]", + "ocr/tests/test_security_routing.py::test_security_matrix[cpu-True-D0]", + "ocr/tests/test_security_routing.py::test_security_matrix[cpu-True-D1]", + "ocr/tests/test_security_routing.py::test_security_matrix[cpu-True-D2]", + "ocr/tests/test_security_routing.py::test_security_matrix[gpu-True-D0]", + "ocr/tests/test_security_routing.py::test_security_matrix[gpu-True-D1]", + "ocr/tests/test_security_routing.py::test_security_matrix[gpu-True-D2]" +] \ No newline at end of file diff --git a/Agent.md b/Agent.md new file mode 100644 index 0000000000000000000000000000000000000000..f839a09b685bd708981cf4c96a787a998ca462ba --- /dev/null +++ b/Agent.md @@ -0,0 +1,154 @@ +## 1. Deployment Configuration + +### Target Space +- **Profile:** `Leon4gr45` +- **Space:** `agent-0` +- **Full Identifier:** `Leon4gr45/agent-0` +- **Frontend Port:** `7860` (mandatory for all Hugging Face Spaces) + +### Deployment Method +Choose the correct SDK based on the app type based on the codebase language: + +- **Gradio SDK** — for Gradio applications +- **Streamlit SDK** — for Streamlit applications +- **Docker SDK** — for all other applications (recommended default for flexibility) + +### HF Token +- The environment variable **`$HF_TOKEN` will always be provided at execution time**. +- Never hardcode the token. Always read it from the environment. +- All monitoring and log‑streaming commands rely on `$HF_TOKEN`. + +### Required Files +- `Dockerfile` (or `app.py` for Gradio/Streamlit SDKs) +- `README.md` with Hugging Face YAML frontmatter: + ```yaml + --- + title: + sdk: docker | gradio | streamlit + app_port: 7860 + --- + ``` +- `.hfignore` to exclude unnecessary files +- This `Agent.md` file (must be committed before deployment) + +--- + +## 2. API Exposure and Documentation + +### Mandatory Endpoints +Every deployment **must** expose: + +- **`/health`** + - Returns HTTP 200 when the app is ready. + - Required for Hugging Face to transition the Space from *starting* → *running*. + +- **`/api-docs`** + - Documents **all** available API endpoints. + - Must be reachable at: + `https://HF_PROFILE-agent-0.hf.space/api-docs` + +### Functional Endpoints + +### /health +- Method: GET +- Purpose: Health Check + +### /readiness +- Method: GET +- Purpose: Readiness Check + +### /pipelines/run +- Method: POST +- Purpose: Queue a dataset generation pipeline for execution. + +### /jobs +- Method: GET +- Purpose: List jobs from HF_WORK_REPO + +### /jobs/{job_id}/manifest +- Method: GET +- Purpose: Inspect job manifest + +### /jobs/{job_id}/checkpoints +- Method: GET +- Purpose: Inspect job checkpoints + +### /jobs/{job_id}/outputs +- Method: GET +- Purpose: Inspect job output references + +### /jobs/{job_id}/resume +- Method: POST +- Purpose: Explicitly resume a job from remote state + +### /jobs/{job_id}/fork +- Method: POST +- Purpose: Fork an existing job run into a new durable job + +### /pipelines/available +- Method: GET +- Purpose: Get available pipeline aliases from super_config.yaml. + +### /configs/aliases +- Method: GET +- Purpose: Get available config file aliases from super_config.yaml. + +### /tasks/queue +- Method: GET +- Purpose: Get lists of pending and scheduled tasks. + +### /tasks/{task_id}/status +- Method: GET +- Purpose: Get the status of a pipeline run. + +### /tasks/{task_id}/parameters +- Method: GET +- Purpose: Get the parameters a task was executed with. + +### /tasks/{task_id}/interrupt +- Method: POST +- Purpose: Interrupt a running pipeline task subprocess or revoke a pending task. + +### /tasks/{task_id}/logs +- Method: GET +- Purpose: Get logs for a specific task. + +### /logs +- Method: DELETE +- Purpose: Clear all task log files. + +### /tasks/{task_id}/outputs/download +- Method: GET +- Purpose: Download output directory for a task. + +### /outputs/structure/{relative_path} +- Method: GET +- Purpose: Get structure of a path within the outputs directory. + +### /inputs/structure/{relative_path} +- Method: GET +- Purpose: Get structure of a path within the inputs directory. + +### /configs/structure/{relative_path} +- Method: GET +- Purpose: Get structure of a path within the configs directory. + +--- + +## 3. Deployment Workflow + +Precondition: Use the huggingface hub cli hf to check that the space is empty of files nd delete any which are still in there and not belonging to the project to be uploaded + +### Standard Deployment Command +After any code change, run: + +```bash +hf upload Leon4gr45/agent-0 --repo-type=space +``` + +Scan build and run logs # Get build logs (SSE) curl -N -H "Authorization: Bearer $HF_TOKEN" "https://huggingface.co/api/spaces/Leon4gr45/agent-0/logs/build" + +Get run logs (SSE) once the build logs succeed +curl -N -H "Authorization: Bearer $HF_TOKEN" "https://huggingface.co/api/spaces/Leon4gr45/agent-0/logs/run" + +after 300 seconds to see if the deployment has been successful, and if not, fix the errors of deployment, and redeploy and monitor in a cycle until the space is running and reacts to the api endpoints you created. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..7308f85a2c1ef79f4d1a23bb633e791b109c7a9d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# syntax=docker/dockerfile:1 +FROM python:3.11.17-slim-bookworm +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=1 \ + OCR_MODELS_DIR=/opt/ocr-models HOME=/home/ocr +RUN apt-get update && apt-get install -y --no-install-recommends fonts-dejavu-core && rm -rf /var/lib/apt/lists/* +ARG OCR_SOURCE_REVISION=unknown +ENV OCR_SOURCE_REVISION=$OCR_SOURCE_REVISION +WORKDIR /app +# Build from repository root. Only OCR dependencies and code enter this separate worker. +COPY ocr/requirements.lock /tmp/requirements.lock +RUN --mount=type=secret,id=proxy_ca \ + if [ -f /run/secrets/proxy_ca ]; then export PIP_CERT=/run/secrets/proxy_ca; fi; \ + pip install --no-cache-dir --require-hashes -r /tmp/requirements.lock +COPY ocr /app/ocr +# Download once at build time, verify pinned SHA256s, keep inference offline. +RUN --mount=type=secret,id=proxy_ca \ + if [ -f /run/secrets/proxy_ca ]; then export SSL_CERT_FILE=/run/secrets/proxy_ca; fi; \ + python -m ocr.download_models +RUN useradd --uid 1000 --create-home ocr && chmod -R a+rX /opt/ocr-models /app +USER ocr +EXPOSE 7860 +HEALTHCHECK --interval=30s --timeout=5s CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:7860/health',timeout=3)" +CMD ["uvicorn", "ocr.worker_api:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1", "--no-access-log", "--limit-concurrency", "4"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f5e33c961122915efb06c5a6314806f58cec6c3b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Evan Armstrong + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 39ae160eb8a345adc3b6aa5b7c0b1cfb3d0df4b6..b241d0334cbae1f9aca950e518e213edb2917113 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,287 @@ --- -title: Agent 0 -emoji: 📈 -colorFrom: purple -colorTo: pink -sdk: gradio -sdk_version: 6.30.0 -python_version: '3.13' -app_file: app.py -pinned: false +title: agent-0 +sdk: docker +app_port: 7860 --- -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +# Augmentoolkit - Data for Domain-expert AI +Augmentoolkit creates domain-expert datasets that update an AI's brain (basically, its knowledge cutoff), so that the AI becomes an expert in an area of your choosing. + +You upload documents, and press a button. And get a fully trained custom LLM. Now every aspect of your AI's behavior and understanding is under your control. Better still, Augmentoolkit **optionally works offline on your computer** -- no external API key required* for datagen† on most hardware. + +Maybe you want AI to know the latest research papers in your field, or perhaps you want an LLM that understands your passion deeply and has learned from the same sources as you. Possibly, you dream of creating a lore expert for your favorite obscure fictional universe. Whatever the application is, Augmentoolkit lets you take text and make an LLM's brain inherently learn the information contained within. It also automatically creates a RAG-ready dataset (and can start up an inference server) if you want some traditional grounding as well. + +Get started now (the interface will guide you through generating your first dataset): + +(Be sure to use Python 3.11 when creating the virtual environment to be sure this'll work) + +### MacOS (interface) +```bash +git clone https://github.com/e-p-armstrong/augmentoolkit.git +cd augmentoolkit # Python == 3.11 +bash macos.sh # NOTE: Will attempt to install valkey via brew if not found. +# bash local_macos.sh # use this command if doing local dataset generation +``` + +### Linux (interface) +```bash +git clone https://github.com/e-p-armstrong/augmentoolkit.git +cd augmentoolkit # Python == 3.11 +bash linux.sh # NOTE: will build Valkey from source if a Redis/Valkey server is not running +``` + +**Or for local inference** +```bash +git clone https://github.com/e-p-armstrong/augmentoolkit.git +cd augmentoolkit # Python == 3.11 +bash local_linux.sh normal # or you can write "small" or a custom model name to serve the quantized version (for more consumer hardware) or a model of your choice, respectively. See the quickstart page linked just a bit farther down for a full reference here. +``` + +If you have multiple GPUs, run `local_linux.sh` with the `--tensor-parallelism N` argument. N == number of GPUs you have (even). So: 1, 2, 4, 8... etc. + +> [!IMPORTANT] +> +> Please star the repo. + +### Windows (interface) +> [!NOTE] +> +> If you're on windows, your best bet is to use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install). [The CLI is easier to get running on windows honestly.](docs/quickstart.md#windows-cli) + +*Note that datagen can take a while on a lot of hardware however, don't expect fast datagen on an old mac for instance. And for training you will need either a powerful machine of your own, or to rent (latter is done automatically for you if you so choose). + +†If you want data to generate faster you *can* use an open-source LLM API, and the quickstart encourages you to. In addition to its custom dataset generation model, Augmentoolkit is optimized for open source LLMs like Deepseek or Llama. + +![](images/augmentoolkit-logo.png) + +Augmentoolkit, now that it is on its 3.0 version, has been refined and improved through over a year of professional application and experimentation. It is now the best way in the world to create domain expert LLMs, and it's MIT licensed. + +If you use this project and like it, please consider starring the repo! It's also designed to be extremely customizable so consider **forking** Augmentoolkit! + +> [!IMPORTANT] +> +> The below links contain very useful information. There is a table of contents, that links to extensive documentation pages for any conceivable part of the project, a bit further down. + +[Help Videos](#video-tutorials) I walk through how to do all the cool stuff in this project starting from scratch, including training LLMs with the data and configs you get (takes 10 minutes). Check out the help videos if you want further guidance! + +[Community](https://discord.gg/s6PBfsaVzu) If you have questions, if you are training models, if you are building cool new pipelines or extensions on top of Augmentoolkit's code, or if just want to hang out, I'd love to see you on the Augmentoolkit discord! It's also a good place to reach me. + +[Newsletter](https://promptingweekly.substack.com/) I write about model training and data generation over on a Substack. Totally free, I just want to help people be able to use the tool better. + +[Contact](#contact) I'm doing all kinds of things around this project, if you're interested in the mission and the business of bringing custom, personally-aligned AI to everyone, let's get in touch! + +[Build](docs/pipeline_primer.md) Augmentoolkit is meant to be the go-to tool for people experimenting with training LLMs, whether they're a hobbyist or a professional. To that end, building new pipelines is as simple as writing Python functions (while adhering to about 2 mostly optional conventions). Efficient explainers and pipeline templates are provided for you to build your own dataset generation pipelines, and by extension, your own datasets and your own completely custom LLMs. + +All configs are fully annotated with comments and placeholders to help you understand them as you fill them out. + +## Documentation Pages + +> [!NOTE] +> +> that this documentation page (the main README) has an [important note](#note-for-when-you-start-training) about model training for facts that you should read regardless of your experience level. + +1. [Quickstart](docs/quickstart.md) + - [CLI](docs/quickstart.md#the-cli) + - [Interface](docs/quickstart.md#macos-interface) +1. [Video Help](#video-tutorials) + - [Generate Data and Train an LLM! (12 mins)](#train-a-model-on-your-own-data-in-13-minutes) + - [Detailed exploration (Interface)](#interface-deep-dive) + - [Detailed exploration (CLI)](#cli-and-code-structure-deep-dive) +1. [Vision](docs/vision.md) + - [What is it? (Technical)](docs/vision.md#what-is-it-technical) + - [What is it? (General)](docs/vision.md#what-is-it-general) + - [Ideas Presented](docs/vision.md#ideas-presented-and-hypotheses) + - [Goals](docs/vision.md#goals) +1. Longstart (customization and development guide links) + - [Pipelines Available by Default]() + - [Compositions]() + - [Complete Factual Generation](docs/complete_factual_datagen.md) + - [Meta Datagen](docs/meta.md) + - [Pipelines]() + - [Multi-Source Recall Factual Datagen](docs/multi_source_facts.md) + - [Single-Source Recall Factual Datagen](docs/single_source_recall.md) + - [Representation Variation](docs/representation_variation.md) + - [Traditional Classifier Bootstrapper](docs/classifier_bootstrapper.md) + - [Generic Data Rephrase](docs/generic_data_rephrase.md) + - [GRPO (experimental)](docs/grpo.md) + - [Correction Data (loss-masked mistakes)](docs/corrections.md) + - [Rag Data (preparing for enhanced recall)](docs/rag_data.md) + - [Debug (health check)](docs/debug.md) + - [Starting Point (build your own pipeline!)](docs/example.md) + - [RPToolkit](docs/rptoolkit.md) + - [Style and Persona Fine-Tuning Guide](docs/style_finetuning.md) + - [Utility]() + - [Basic LLM Server](docs/basic_server.md) + - [RAG LLM Server](docs/rag_server.md) + - **[Discord Hosting](docs/discord.md)** + - [Config Common Fields](docs/config_common_fields.md) + + - [Understand the Tool In Detail]() + - [Project Structure](docs/project_structure.md) + - [CLI]() + - [Flows](docs/CLI_flows.md) + - [Upload Documents](docs/CLI_flows.md#upload-documents) + - [Starting Runs](docs/CLI_flows.md#starting-runs) + - [Observing Runs](docs/CLI_flows.md#observing-results) + - [Getting Your Results Back](docs/CLI_flows.md#getting-your-results-back) + - [Interface]() + - [Flows](docs/interface_flows.md) + - [Upload Documents](docs/interface_flows.md#upload-documents) + - [Starting Runs](docs/interface_flows.md#starting-runs) + - [Observing Runs](docs/interface_flows.md#observing-runs) + - [Getting Your Results Back](docs/interface_flows.md#getting-your-results-back) + - [Training with Axolotl Concepts](docs/axolotl_concepts.md) + - [Customize and Develop]() + - [New Pipeline Primer](docs/pipeline_primer.md) + - [Abstractions Primer](docs/abstractions_primer.md) + - [Conventions Commandments](docs/pipeline_conventions.md) + - [Reminder That Conventions Are Minimal and You Can Just Code and It Will Probably Work](docs/conventions_reminder.md) +1. [Discord!](#discord) +1. [Updates and Training/Datagen Tips Blog! Stay in the loop!](#training-and-datagen-tips-blog) +1. [Contributing!](#contributing) +1. [Contact & Client Work](#contact) + +**If you're familiar with LLMs and want a more jargonful rundown of what Augmentoolkit is and what makes it cool, check out [this section](docs/vision.md)** + +Cite: +[![DOI](https://zenodo.org/badge/726083337.svg)](https://zenodo.org/doi/10.5281/zenodo.11525927) + + +> [!NOTE] +> +> If you don't want to do model training, but just want to generate a dataset, turn `do_train` off in your dataset generation config. + +### Video Tutorials + +#### [Train a Model on your Own Data in 13 Minutes](https://youtu.be/E9TyyZzIMyY) + +#### [Interface Deep Dive!](https://youtu.be/M-OFVwHPfeU) + +#### [CLI and Code Structure Deep Dive!](https://youtu.be/cEkgw7sYqMw) + +^ This one is useful if you're going to make modifications to the code + +### Benefits +**Augmentoolkit makes LLM data easy.** +- **Cheap:** Augmentoolkit pipelines use open-source LLMs, and so can be run on consumer hardware for hardly any cost, or cheaply via APIs like Deepinfra *(the "local" prompt sets should enable usage of most pipelines by reasoning models, too)* +- **Effortless:** Any Augmentoolkit pipeline can be run with an intuitive interface that is started by running a start script. Alternatively, you can make data by putting some files in a folder, and then running a Python script. If that's too much, you can also use the graphical user interface, now a first-class citizen in Augmentoolkit 3 (and in fact, the recommended way to run Augmentoolkit). Previously-started runs are continued automatically, so you don't need to worry about interruptions costing you time and/or money. +- **Fast:** when using APIs, you can quickly generate millions of trainable tokens. Fully async code lets you get results quickly. Reading and chunking caches ensure that even large-scale workloads are quick to use. Models are automatically trained after the data is ready, and are even automatically downloaded and prepared for inference on your local machine. All the hard or annoying parts of the process have been automated and made efficient. In the past creating datasets and iterating and testing and learning could have taken a skilled person months; now, anyone can press a button, come back in a day, and chat with a newly-trained model. +- **Innovative, Effective Approach to Factual Training:** Augmentoolkit has a production-tested method of creating domain-expert LLMs that can understand entirely new subjects. Many separate pipelines are composed together to produce quality datasets that teach capabilities such as answering factual questions, acknowledging when something is not known by the model, correcting mistakes, etc. You can be confident in getting high-quality specialist models when you use Augmentoolkit. + +We've also done our best to **facilitate the step after you generate your data -- training your LLM:** +- **Production-Scale:** Datasets that are gigabytes-large have been generated with Augmentoolkit -- it is battle-hardened, it works at scale without annoying inefficiencies costing immense time, and it is ready for the stresses of production. +- **Train an AI for the cost of a dinner:** you can generate data on your own hardware for what is basically free. Augmentoolkit can then automatically perform a full finetune of an AI, on your own data, for a tiny sum of money (roughly $20 for the finetuning part of the process). +- **Create your LLM in less than a day:** with a fully automated process for turning documents into datasets, and only a single button-click needed to kick off training, making a subject matter expert LLM is *fast* (especially when you use API for the dataset generation). Iterate quickly and cheaply. +- **When you use the same recipe, you get the same bread:** Augmentoolkit datasets have been used successfully for professional consulting projects. Video documentation is linked in this README that shows exactly how to use this tool to do the same. The code, settings, and prompts you need is all here. Examples, templates, comments, marked-out placeholders, and extensive documentation is all available. +- **Train AI with confidence, *especially* if it's your first time:** between the battle-tested process, extensive video docs, in-depth README, and Discord community, you can be confident you'll get a good LLM out of this. + +**Do it all locally** +With a custom-trained 7b model built to run these pipelines specifically, Augmentoolkit can generate data on consumer hardware, and can do so at incredible scale, with incredible parallelism, when on higher-performance computers. Budget does not need to be a constraint -- just passion and time. Of course, if you want immediate results/speed, you can use an API too. + +Finally, **using the model you create should be easy and valuable:** +- **AI that understands your facts:** For the professionals and the passionate: training an LLM with Augmentoolkit's Complete Factual Datagen "composition" pipeline creates an assistant that understands the big picture of the data you're training on. If RAG is like giving an LLM an open-book test on a textbook it hasn't read before, then training on Augmentoolkit data gives it some time to study before the test as well. This pipeline has been battle-tested in consulting projects across different industries. Compared to earlier versions of Augmentoolkit, Augmentoolkit's 3.0 version generates a wide variety of different domain data, and it even automatically balances this data with the generic data it uses. +- **Individual Alignment:** Use GPRO (the same algorithm that made Deepseek R1 as good as it is) to align a model to any task imaginable without modifying any code. Augmentoolkit adopts an innovative approach of letting you use an LLM as a reward function -- you write a prompt that grades certain outputs higher, and then those reward scores teach the model to behave more like that in the future. Want your model to do a task better? Explain what "better" is and then the model will learn it. Want your model to be more emotional and human-like? Explain how to grade responses based on their emotional content, and the model will [learn it](https://huggingface.co/Heralax/llama-gRPo-emotions-nothoughts). Want your model to write like a pirate? Explain in your grading prompt what makes a good pirate-like response, and the model will learn it. You can also change code and use traditional reward functions if you want to. The GRPO pipeline is experimental and in beta, but early results are promising. +- **Make sense of massive data without using human annotators:** For the heavy-duty ML professionals: if you have a large dataset with tons of unlabelled text (like the Enron emails dataset, IMDB, or fineweb, etc.) you can now write a sentence or two that describes two classes which exist in that data. Augmentoolkit's classifier creator pipeline will then use an LLM to make a full classification dataset, based on a subset of the input data and your specified classes; it'll then train a classifier and evaluate it and take more data and retrain, in a loop, until validation loss is below a specified threshold. Classifiers trained using this pipeline seem to achieve similar performance to classifiers trained on human-labelled data. Be advised that data is not yet automatically balanced between different labels. +- **AI inspired by your favorite fiction:** For the creatives and entertainers: using RPToolkit, you can create detailed and varied multi-turn roleplaying data with the themes of any story you can think of. If you're creating custom AI for creative or entertainment purposes, you can now specialize it in any genre you want. Want a depressing and dark specialist in mecha stories? Feed in some stories and you can get a ton of data for that. How about an AI writer of wholesome slice of life? You can get data for that too. Create as broad or as narrow of a writing AI as you want from whatever inspiration you can find. + +*Clarification: Augmentoolkit, the project, has multiple pipelines: the original pipeline (QA), RPtoolkit (rich multiturn roleplaying data), and the classifier creator. If it is said that "Augmentoolkit can make [some kind of data]" then I mean that one of Augmentoolkit's pipelines can do so.* + +### NOTE For When You Start Training + +Factual finetuning requires a certain number of optimizer steps to stick. If training is where the LLM's brain "moves" towards a place where it understands your new domain, "optimizer steps" are the number of times the LLM moves. **If your dataset is small you may not have enough optimizer steps for the LLM to learn the new domain well.** + +Because of this, ironically, it can be easier to teach LLMs large new domains rather than small ones, with training. However, there are tools at your disposal for turning a small dataset into a large one when you use Augmentoolkit. + +In [complete factual dataset](docs/complete_factual_datagen.md) you have the `number_of_factual_sft_generations_to_do` setting for the whole pipeline, and the `variation_generation_counts` which you can customize per input dir. The one that is customized per dir makes the data from a specific input dir represented more in the continued pretraining data; the other setting increases the overall amount of SFT data made from all input dirs together. With these two levers you can make a small dataset as large as you need — though some of the data may be very similar, you can still scale it up in this way to teach it to an LLM without catastrophic drawbacks. + +As a "break glass in case of emergency" option, if your dataset is exceptionally small, you may want to consider turning sample packing off. This an be done by modifying the pretrain and finetune kwargs to set sample packing off (do this in the complete factual datagen config). + +```yaml +other_pretrain_kwargs: {sample_packing: False} +other_finetune_kwargs: {sample_packing: False} +``` + +Turning off sample packing has not been tested with the current iteration of Augmentoolkit's settings yet, so success with that emergency approach cannot be guaranteed for extremely small datasets, but since the main problem with extremely small datasets is a lack of per-epoch optimizer steps causing the LLM to not learn the data enough, *theoretically*, this should work. + +Most of the configuration of Augmentoolkit you'll do, besides changing input/output paths for different models, is probably going to be related to the optimizer step. With very large input datasets you'll want to reduce things that increase the optimizer step because otherwise you'll be training for a long time, whereas with very small ones you'll have to pull out tricks to increase it. That's why this has a section and is marked out as important -- be cognizant of the size of your dataset as you create it! + +If you have any questions about your specific use case, consider heading over to the [Discord](https://discord.gg/s6PBfsaVzu) + +### Temporary Announcement if You've Been Here Before +If you're returning to look at this repo after a while, I want to make a few things clear! + +Firstly, a *lot* has changed. I disappeared for six months, half of it was spent on research and the other half was spent building. I wanted the tool to fundamentally work better. Now, Augmentoolkit reliably produces great domain experts across datasets of different sizes. It can even teach a model something that it has not seen at all during pretraining. This experimentation cost thousands of dollars out of pocket, but I believe it was worth it, since anyone can now make domain experts about arbitrary subjects with very little technical experience. + +Secondly, things are *much* easier to use. The interface is robust and is not a second-class citizen anymore. Start scripts, automatically generated and balanced training configs, better error messages, and a host of other improvements should make Augmentoolkit much nicer to use. + +Not much code from the original survived, though making older pipelines fit into the project as it is now, is pretty simple ([see the new pipeline example and you'll get a picture of how they look like now](/docs/example.md)). Also, if you have custom prompts from before, they should work with the new pipelines without modification. The older pipeline compared to this one is like a rat compared to a human -- they're technically related and have a lot of the same DNA, but the human is much more evolved and more capable. I hope you enjoy using the new project and getting great results. + +The bad news is that since so much has changed, some new bugs probably got introduced. Please report bugs so that they can be fixed. I had not been keeping too close an eye on the issues these past 4 months since the entire project was being ripped apart anyway -- now that it is in a more final form, and frankly now that I have better discipline with this sort of thing, I'll be focused on the Discord and GitHub issues to correct any mistakes that you point out. If any of the new documentation is unclear about parts of the project, please let me know. And, if you have [custom pipelines]() that you want to add, or bugfixes, please check out [Contributing]() and make a PR! + + +### Discord + +Custom-built models are (usually) not meant to be enjoyed only by their creators. There's a [new feature in Augmentoolkit](docs/discord.md) where you can easily make your custom models into Discord bots! Now you can share your custom AI creations with your friends or community! Also, all the code runs on your own computer so no worry about recurring costs. + +Speaking of Discord... + +Augmentoolkit is partly about democratizing dataset generation, so community is hugely important to the project! There's a Discord server where you can get help with custom model creation, as well as share new pipelines, prompt sets, or projects you're creating! [Come hang out and be part of a useful community of like-minded people!](https://discord.gg/s6PBfsaVzu) + +### Training and Datagen Tips Blog + +I write about model training and data generation over on a [free substack](https://promptingweekly.substack.com/)! If you want read access to my brain as I continue to experiment and explore with dataset generation, consider signing up to up your model creation game. If you're planning on building your own dataset generation pipelines using the tools and abstractions provided by Augmentoolkit, some of the advice there might be very useful. + +Now that the new Augmentoolkit version is out, I finally have time to post again (and new ideas to post as well). + +### Contributing! + +PRs for bugfixes, new pipelines, and improvements are welcome! If you have an experiment you're proud of, consider opening a PR. The rules are pretty standard: + +- Contributors can open PRs +- Collaborators can push to branches and merge PRs into the master branch +- Collaborators may either be chosen depending on contributions, or may be chosen internally within Augmentoolkit (the company) +- [The example pipeline and its documentation](docs/example.md) contain useful information for making your own pipeline. You are encouraged to fork Augmentoolkit and experiment! +- Code with the style you want, just test thoroughly before making a PR + - Caveat: failing silently or continuing is worse than explicitly erroring if an impossible state is reached + - Asserts are your friend + - `black .` makes even MY code look formatted nice, it can do the same for yours. + +### Useful Commands for Datagen and Training Workflows + +Copy-paste these when appropriate or use them as a reference. + +Copy files over to a different computer (such as a GPU instance on runpod) + +``` +scp -P [port] -r ./outputs/your-output-dir/pretraining_run root@123.456.78.9:/workspace/axolotl +``` + +Kick off a training run: +``` +accelerate launch -m axolotl.cli.train [your_config].yaml +``` + +Convert and quantize with llama.cpp +``` +python ~/llama.cpp/convert_hf_to_gguf.py --outtype q8_0 +``` + +## Contact! + +- Email me at: evanpeterarmstrong@gmail.com (NOTE: my inbox is flooded, your message might not get through, prefer booking a call for serious discussions) +- [For serious and urgent discussions, we can schedule a call!](https://calendly.com/evanpeterarmstrong/30min) +- [I'm pretty active on the Augmentoolkit discord server and a bunch of other AI discords. Find me as @heralax!](https://discord.gg/s6PBfsaVzu) +- [I sometimes post stuff on X/Twitter](https://twitter.com/e_p_armstrong) +- [Substack! I am finally posting again.](https://promptingweekly.substack.com/) +- [YouTube -- The source of the help videos](https://www.youtube.com/@Heralax) +- [Let's connect on LinkedIn!](https://www.linkedin.com/in/evan-armstrong-1a84b3200/) + +If you have a company or organization that wants to serve custom domain-expert AI to internal users (to get employees the information they need to do their jobs great) or to external users (for instance, to answer community questions or increase product awareness) then we should [get in touch](https://calendly.com/evanpeterarmstrong/discovery-call). + +Also, if you have an **AI Chat Wrapper Startup or Company** that is currently being extorted by OpenAI, [we should also talk](https://calendly.com/evanpeterarmstrong/discovery-call), because I'm fairly certain **I can save you a ton on API costs while maintaining or improving answer quality**. Not only will I use this tool to produce a quality model, but I also have the means to run it at scale, which is not a trivial problem to solve. + +The Augmentoolkit project is going to continue to be developed. It has been consistently developed for a long time -- the many-months gap from this major update to the previous one is because I was busy researching and developing the techniques, and later, preparing this release itself (this update has been in the works a long time). I am open to and would appreciate organizations sponsoring this open-source project -- I'd like nothing more than to research and build the tools for creating custom LLMs all day! + +I am also working on ambitious commercial solutions involving Augmentoolkit and Augmentoolkit tech. This project is part of a larger master-plan. If you're an investor, I'm very open to discussions here! My [Calendly](https://calendly.com/evanpeterarmstrong/30min) is always open. + +Current Generalized AI means hallucinations, platitudes, and sycophancy; domain-expert AI writes with your knowledge, understanding, and is aligned to **your** tastes. As more people use the same AI, more of the world sounds the same and thinks the same--it is my belief that individually-customized LLMs are the only way to avoid this world of slop. diff --git a/api.py b/api.py new file mode 100644 index 0000000000000000000000000000000000000000..c05443f343d176c6ad08d14172a2ae2278d832b6 --- /dev/null +++ b/api.py @@ -0,0 +1,737 @@ +from enum import Enum +import json +import logging +from pathlib import Path as PyPath +import traceback +import yaml +import sys +import shutil +import zipfile +import httpx +import os +import tempfile +import time +import subprocess + +from fastapi import ( + FastAPI, + HTTPException, + UploadFile, + File, + BackgroundTasks, + Path as FastApiPath, + Query, + Body, + Request, +) +from fastapi.responses import FileResponse, StreamingResponse, JSONResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from typing import List, Optional, Dict, Any +from pydantic import BaseModel, Field +from fastapi.middleware.cors import CORSMiddleware + +from huey_config import huey +from tasks import ( + run_pipeline_task, + set_final_status, + resolve_job_output_dir, +) +from huey.exceptions import ( + HueyException, + TaskException, +) +from redis_config import ( + get_progress, + redis_client, + set_progress, +) +import signal + +from resolve_path import resolve_path + +from file_operation_helpers import ( + get_safe_path, + zip_directory, + get_dir_structure, + FileStructure, + MoveItemRequest, + handle_get_structure, + handle_download_item, + handle_delete_item, + handle_move_item, + handle_create_directory, +) + +from augmentoolkit.hf_persistence import ( + HFHubManager, + CheckpointManager, + JobManifestManager, + IncompatibleResumeError, + JobManifest, +) + + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +SUPER_CONFIG_PATH = PyPath("super_config.yaml") +PATH_ALIASES = {} + +try: + with open(SUPER_CONFIG_PATH, "r", encoding="utf-8") as f: + super_config = yaml.safe_load(f) + PATH_ALIASES = super_config.get("path_aliases", {}) +except FileNotFoundError: + print(f"ERROR: Super config file not found at {SUPER_CONFIG_PATH}.") +except yaml.YAMLError as e: + print(f"ERROR: Error parsing super config file {SUPER_CONFIG_PATH}: {e}") + +INPUTS_DIR = PyPath("./inputs").resolve() +OUTPUTS_DIR = PyPath("./outputs").resolve() +GENERATION_DIR = PyPath("./generation").resolve() +CONFIGS_DIR = PyPath("./external_configs").resolve() +MAX_FILE_SIZE = 1024 * 1024 * 1000 + +LOGS_DIR = PyPath("./logs").resolve() + +INPUTS_DIR.mkdir(exist_ok=True) +OUTPUTS_DIR.mkdir(exist_ok=True) +GENERATION_DIR.mkdir(exist_ok=True) +CONFIGS_DIR.mkdir(exist_ok=True) +LOGS_DIR.mkdir(exist_ok=True) + + +class PipelineRunRequest(BaseModel): + node_path: str + config_path: Optional[str] = None + parameters: Optional[Dict[str, Any]] = None + + +class PipelineRunResponse(BaseModel): + pipeline_id: str + message: str + + +class PipelineStatus(str, Enum): + PENDING = "PENDING" + RUNNING = "RUNNING" + COMPLETED = "COMPLETED" + FAILED = "FAILED" + REVOKED = "REVOKED" + + +class PipelineStatusResponse(BaseModel): + task_id: str + status: PipelineStatus + message: Optional[str] = None + progress: Optional[float] = Field(None, ge=0.0, le=1.0) + details: Optional[Dict[str, Any]] = None + + +class QueueStatusResponse(BaseModel): + pending_tasks: List[str] = Field(..., description="List of task IDs currently pending execution.") + scheduled_tasks: List[str] = Field(..., description="List of task IDs scheduled for future execution.") + message: str + + +class CreateDirectoryRequest(BaseModel): + relative_path: str = Field(..., description="The relative path within base directory.") + + +class DuplicateConfigRequest(BaseModel): + source_alias: str = Field(..., description="Alias from super_config.yaml.") + destination_relative_path: str = Field(..., description="Desired relative path.") + + +class TaskParametersResponse(BaseModel): + task_id: str + parameters: Dict[str, Any] + + +app = FastAPI( + title="Augmentoolkit Resumable Dataset Factory API", + description="API for managing and running Augmentoolkit dataset pipelines with Hugging Face Hub remote persistence.", + version="2.0", +) + +origins = [ + "http://localhost:5173", + "http://127.0.0.1:5173", + "http://localhost:5174", + "http://127.0.0.1:5174", + "http://localhost:3000", +] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.on_event("startup") +async def startup_event(): + """At startup, reconnects to HF_WORK_REPO, discovers incomplete jobs, reconciles state, and auto-resumes if enabled.""" + logger.info("Augmentoolkit API starting up...") + work_repo = os.environ.get("HF_WORK_REPO") + if work_repo: + try: + ckpt_mgr = CheckpointManager() + incomplete_jobs = ckpt_mgr.discover_incomplete_jobs(work_repo=work_repo) + logger.info(f"Discovered {len(incomplete_jobs)} incomplete job(s) in HF_WORK_REPO '{work_repo}'.") + auto_resume = os.environ.get("HF_AUTO_RESUME", "false").lower() in ("true", "1", "yes") + if auto_resume: + for manifest in incomplete_jobs: + logger.info(f"Auto-resuming durable job {manifest.job_id}...") + run_pipeline_task( + node_path=manifest.pipeline, + parameters={ + "job_id": manifest.job_id, + "hf_source_repo": manifest.source_repo, + "hf_work_repo": manifest.work_repo, + "hf_output_repo": manifest.output_repo, + "source_revision": manifest.source_revision, + }, + ) + except Exception as e: + logger.error(f"Error during startup HF recovery discovery: {e}") + + +@app.get("/health", summary="Health Check") +async def health_check(): + """Basic health check endpoint for Hugging Face.""" + return {"status": "ok", "message": "Augmentoolkit API is running."} + + +@app.get("/readiness", summary="Readiness Check") +async def readiness_check(): + """ + Comprehensive readiness check reporting Redis/Huey connection, Hugging Face authentication, + repository accessibility (HF_SOURCE_REPO, HF_WORK_REPO, HF_OUTPUT_REPO), and worker status. + """ + redis_ok = False + redis_error = None + try: + redis_client.ping() + redis_ok = True + except Exception as e: + redis_error = str(e) + + hf_mgr = HFHubManager() + hf_health = hf_mgr.check_all_health() + + worker_ok = False + try: + huey.pending() + worker_ok = True + except Exception: + pass + + overall_ready = redis_ok and worker_ok and hf_health.get("healthy", False) + + return { + "status": "ready" if overall_ready else "degraded", + "redis": {"status": "ok" if redis_ok else "error", "error": redis_error}, + "huey_queue": {"status": "ok" if worker_ok else "error"}, + "huggingface": hf_health, + } + + +@app.get("/api-docs", include_in_schema=False) +async def api_docs_redirect(): + return RedirectResponse(url="/docs") + + +@app.post( + "/pipelines/run", + response_model=PipelineRunResponse, + status_code=202, + summary="Queue a dataset generation pipeline for execution.", +) +def queue_pipeline_run(request: PipelineRunRequest): + try: + task = run_pipeline_task( + node_path=request.node_path, + config_path=request.config_path, + parameters=request.parameters, + ) + return PipelineRunResponse( + pipeline_id=task.id, + message="Pipeline run queued successfully.", + ) + except Exception as e: + logger.error(f"ERROR during pipeline queueing: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Failed to enqueue pipeline task: {e}", + ) + + +@app.get("/jobs", summary="List jobs from HF_WORK_REPO") +def list_jobs(work_repo: Optional[str] = Query(None)): + target_repo = work_repo or os.environ.get("HF_WORK_REPO", "") + if not target_repo: + return {"work_repo": "", "jobs": []} + hf_mgr = HFHubManager() + hf_jobs = hf_mgr.list_jobs(work_repo=target_repo) + return {"work_repo": target_repo, "jobs": hf_jobs} + + +@app.get("/jobs/{job_id}/manifest", summary="Inspect job manifest") +def inspect_job_manifest(job_id: str, work_repo: Optional[str] = Query(None)): + target_repo = work_repo or os.environ.get("HF_WORK_REPO", "") + hf_mgr = HFHubManager() + manifest = hf_mgr.fetch_manifest(job_id=job_id, work_repo=target_repo) + if not manifest: + raise HTTPException(status_code=404, detail=f"Manifest for job '{job_id}' not found.") + return manifest + + +@app.get("/jobs/{job_id}/checkpoints", summary="Inspect job checkpoints") +def inspect_job_checkpoints(job_id: str, work_repo: Optional[str] = Query(None)): + target_repo = work_repo or os.environ.get("HF_WORK_REPO", "") + if not target_repo: + raise HTTPException(status_code=400, detail="HF_WORK_REPO is not configured.") + hf_mgr = HFHubManager() + try: + files = hf_mgr.api.list_repo_files(repo_id=target_repo, repo_type="dataset", token=hf_mgr.token) + ckpts = [f for f in files if f.startswith(f"jobs/{job_id}/checkpoints/")] + return {"job_id": job_id, "checkpoints": ckpts} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error checking checkpoints: {e}") + + +@app.get("/jobs/{job_id}/outputs", summary="Inspect job output references") +def inspect_job_outputs(job_id: str, output_repo: Optional[str] = Query(None)): + target_repo = output_repo or os.environ.get("HF_OUTPUT_REPO", "") + if not target_repo: + raise HTTPException(status_code=400, detail="HF_OUTPUT_REPO is not configured.") + hf_mgr = HFHubManager() + try: + files = hf_mgr.api.list_repo_files(repo_id=target_repo, repo_type="dataset", token=hf_mgr.token) + outputs = [f for f in files if f.startswith(f"datasets/{job_id}/")] + return {"job_id": job_id, "output_repo": target_repo, "outputs": outputs} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error checking outputs: {e}") + + +@app.post("/jobs/{job_id}/resume", summary="Explicitly resume a job from remote state") +def resume_job(job_id: str, parameters: Optional[Dict[str, Any]] = Body(None)): + work_repo = os.environ.get("HF_WORK_REPO", "") + ckpt_mgr = CheckpointManager() + manifest = ckpt_mgr.load_remote_manifest(job_id=job_id, work_repo=work_repo) + if not manifest: + raise HTTPException(status_code=404, detail=f"Job '{job_id}' not found in {work_repo}.") + + target_output_dir = resolve_job_output_dir( + node_path=manifest.pipeline, + config_path=parameters.get("config_path") if parameters else None, + parameters=parameters, + durable_job_id=job_id, + ) + + live_prompt_hash = JobManifestManager.compute_live_prompt_hash( + prompt_folder=parameters.get("prompt_folder") if parameters else None, + default_prompt_folder=parameters.get("default_prompt_folder") if parameters else None, + ) + + try: + reconciled_manifest, completed_shards, restored_data = ckpt_mgr.reconcile_and_resume_job( + job_id=job_id, + target_pipeline=manifest.pipeline, + target_config_dict=parameters or {}, + target_prompt_hash=live_prompt_hash, + target_source_repo=manifest.source_repo, + target_source_revision=manifest.source_revision, + target_source_path=manifest.source_path, + target_source_split=manifest.source_split, + target_output_schema_version=manifest.output_schema_version, + work_repo=work_repo, + output_dir=target_output_dir, + ) + except IncompatibleResumeError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to reconcile job state: {e}") + + run_params = parameters or {} + run_params.update({ + "job_id": job_id, + "output_dir": target_output_dir, + "hf_source_repo": manifest.source_repo, + "hf_work_repo": manifest.work_repo, + "hf_output_repo": manifest.output_repo, + "source_revision": manifest.source_revision, + "restored_shards": list(completed_shards), + }) + + task = run_pipeline_task( + node_path=manifest.pipeline, + parameters=run_params, + ) + return { + "job_id": job_id, + "task_id": task.id, + "output_dir": target_output_dir, + "message": f"Resume task for durable job '{job_id}' enqueued successfully.", + "completed_shards": list(completed_shards), + } + + +@app.post("/jobs/{job_id}/fork", summary="Fork an existing job run into a new durable job") +def fork_job( + job_id: str, + new_job_id: Optional[str] = Query(None), + parameters: Optional[Dict[str, Any]] = Body(None), +): + work_repo = os.environ.get("HF_WORK_REPO", "") + ckpt_mgr = CheckpointManager() + parent_manifest = ckpt_mgr.load_remote_manifest(job_id=job_id, work_repo=work_repo) + if not parent_manifest: + raise HTTPException(status_code=404, detail=f"Parent job '{job_id}' not found in {work_repo}.") + + forked_id = new_job_id or f"{job_id}_fork_{int(time.time())}" + + fork_params = parameters or {} + run_params = { + "job_id": forked_id, + "forked_from_job_id": job_id, + "hf_source_repo": fork_params.get("hf_source_repo", parent_manifest.source_repo), + "hf_work_repo": work_repo, + "hf_output_repo": fork_params.get("hf_output_repo", parent_manifest.output_repo), + "source_revision": fork_params.get("source_revision", parent_manifest.source_revision), + "source_path": fork_params.get("source_path", parent_manifest.source_path), + "source_split": fork_params.get("source_split", parent_manifest.source_split), + } + run_params.update(fork_params) + + task = run_pipeline_task( + node_path=parent_manifest.pipeline, + parameters=run_params, + ) + + return { + "forked_job_id": forked_id, + "parent_job_id": job_id, + "task_id": task.id, + "message": "Forked job queued successfully.", + } + + +@app.get( + "/pipelines/available", + response_model=List[str], + summary="Get available pipeline aliases from super_config.yaml.", +) +def get_available_pipelines(): + available_pipelines = [] + if not SUPER_CONFIG_PATH.exists(): + return [] + + try: + with open(SUPER_CONFIG_PATH, "r", encoding="utf-8") as f: + super_config = yaml.safe_load(f) or {} + + aliases = super_config.get("path_aliases", {}) + if not isinstance(aliases, dict): + return [] + + for alias, path_value in aliases.items(): + if isinstance(path_value, str) and not path_value.strip().lower().endswith(".yaml"): + available_pipelines.append(alias) + + return sorted(available_pipelines) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error parsing super config: {e}") + + +@app.get( + "/configs/aliases", + response_model=List[str], + summary="Get available config file aliases from super_config.yaml.", +) +def get_available_config_aliases(): + config_aliases = [] + if not SUPER_CONFIG_PATH.exists(): + return [] + + try: + with open(SUPER_CONFIG_PATH, "r", encoding="utf-8") as f: + super_config = yaml.safe_load(f) or {} + + aliases = super_config.get("path_aliases", {}) + if not isinstance(aliases, dict): + return [] + + for alias, path_value in aliases.items(): + if isinstance(path_value, str) and path_value.strip().lower().endswith(".yaml"): + config_aliases.append(alias) + + return sorted(config_aliases) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error parsing super config: {e}") + + +@app.get( + "/tasks/queue", + response_model=QueueStatusResponse, + summary="Get lists of pending and scheduled tasks.", +) +def get_queue_status(): + try: + pending_tasks = [task.id for task in huey.pending()] + scheduled_tasks = [task.id for task in huey.scheduled()] + return QueueStatusResponse( + pending_tasks=pending_tasks, + scheduled_tasks=scheduled_tasks, + message=f"Found {len(pending_tasks)} pending and {len(scheduled_tasks)} scheduled tasks.", + ) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to retrieve queue status: {e}") + + +@app.get( + "/tasks/{task_id}/status", + response_model=PipelineStatusResponse, + summary="Get the status of a pipeline run.", +) +def get_pipeline_status(task_id: str): + redis_status_key = f"status_for_task:{task_id}" + try: + final_status_json = redis_client.get(redis_status_key) + if final_status_json: + try: + status_data = json.loads(final_status_json) + final_status_enum = PipelineStatus(status_data["status"]) + return PipelineStatusResponse( + task_id=task_id, + status=final_status_enum, + message=status_data.get("message"), + progress=1.0, + details=status_data.get("details"), + ) + except Exception: + pass + + pipeline_progress = get_progress(task_id) + if pipeline_progress: + return PipelineStatusResponse( + task_id=task_id, + status=PipelineStatus.RUNNING, + message=pipeline_progress.get("message", "Task is running."), + progress=min(pipeline_progress.get("progress", 0.0), 1.0), + details={"source": "progress_tracker"}, + ) + + pending_ids = {task.id for task in huey.pending()} + scheduled_ids = {task.id for task in huey.scheduled()} + if task_id in pending_ids or task_id in scheduled_ids: + return PipelineStatusResponse( + task_id=task_id, + status=PipelineStatus.PENDING, + message="Task is pending in the queue.", + progress=0.0, + details={"source": "huey_queue"}, + ) + + if huey.is_revoked(task_id): + return PipelineStatusResponse( + task_id=task_id, + status=PipelineStatus.REVOKED, + message="Task was revoked.", + progress=0.0, + details={"source": "huey_revoked"}, + ) + + raise HTTPException(status_code=404, detail=f"Task with ID '{task_id}' not found.") + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error retrieving status: {e}") + + +@app.get( + "/tasks/{task_id}/parameters", + response_model=TaskParametersResponse, + summary="Get the parameters a task was executed with.", +) +def get_task_parameters(task_id: str): + redis_key = f"parameters_for_task:{task_id}" + try: + params_json = redis_client.get(redis_key) + if params_json is None: + raise HTTPException(status_code=404, detail=f"Parameters for task '{task_id}' not found.") + parameters = json.loads(params_json) + return TaskParametersResponse(task_id=task_id, parameters=parameters) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error retrieving parameters: {e}") + + +def _find_and_kill_run_augmentoolkit_process() -> Optional[Dict[str, Any]]: + try: + result = subprocess.run(["ps", "-ef"], capture_output=True, text=True, check=True) + for line in result.stdout.splitlines(): + if "run_augmentoolkit.py" in line and "grep" not in line: + fields = line.split() + if len(fields) >= 2: + try: + pid = int(fields[1]) + os.kill(pid, signal.SIGINT) + time.sleep(2) + return {"pid": pid, "killed": True, "method": "SIGINT"} + except Exception: + continue + return None + except Exception: + return None + + +@app.post( + "/tasks/{task_id}/interrupt", + status_code=200, + summary="Interrupt a running pipeline task subprocess or revoke a pending task.", +) +def interrupt_or_revoke_task(task_id: str): + redis_pid_key = f"worker_pid_for_task:{task_id}" + redis_status_key = f"status_for_task:{task_id}" + + try: + final_status_json = redis_client.get(redis_status_key) + if final_status_json: + try: + status_data = json.loads(final_status_json) + existing_status = status_data.get("status", "UNKNOWN").upper() + if existing_status in ["COMPLETED", "FAILED", "REVOKED"]: + raise HTTPException( + status_code=409, + detail=f"Task {task_id} has already finished with status: {existing_status}.", + ) + except HTTPException: + raise + except Exception: + pass + + pid_bytes = redis_client.get(redis_pid_key) + if pid_bytes: + try: + pid = int(pid_bytes) + os.kill(pid, signal.SIGINT) + time.sleep(1) + set_final_status(task_id, "REVOKED", "Task interrupted via SIGINT.") + return {"message": f"Task {task_id} interrupted via SIGINT."} + except Exception: + pass + + was_revoked = huey.revoke(task_id, revoke_once=True) + if was_revoked: + set_final_status(task_id, "REVOKED", "Task revoked while pending.") + return {"message": f"Task {task_id} was pending and has been revoked."} + + raise HTTPException(status_code=404, detail=f"Task {task_id} not running or not found.") + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error interrupting task: {e}") + + +@app.get("/tasks/{task_id}/logs", summary="Get logs for a specific task.") +def get_task_logs(task_id: str, tail: Optional[int] = Query(None, ge=1)): + log_file_path = LOGS_DIR / f"{task_id}.log" + if not log_file_path.exists(): + raise HTTPException(status_code=404, detail=f"Log file for task {task_id} not found.") + + try: + with open(log_file_path, "r", encoding="utf-8") as f: + lines = f.readlines() + log_content = "".join(lines[-tail:]) if tail else "".join(lines) + return JSONResponse(content={"task_id": task_id, "logs": log_content}) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to read log file: {e}") + + +@app.delete("/logs", status_code=200, summary="Clear all task log files.") +def clear_all_logs(): + deleted_count = 0 + try: + for item in LOGS_DIR.iterdir(): + if item.is_file() and item.suffix == ".log": + item.unlink() + deleted_count += 1 + return {"message": f"Successfully deleted {deleted_count} log file(s)."} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to clear logs: {e}") + + +@app.get( + "/tasks/{task_id}/outputs/download", summary="Download output directory for a task." +) +def download_task_output(task_id: str): + redis_key = f"output_dir_for_task:{task_id}" + output_dir_str = redis_client.get(redis_key) + if not output_dir_str: + raise HTTPException(status_code=404, detail=f"Output directory not found for task {task_id}.") + + task_output_dir = PyPath(output_dir_str).resolve() + if not task_output_dir.is_dir(): + raise HTTPException(status_code=404, detail="Output directory does not exist.") + + zip_filename = f"task_{task_id}_output.zip" + temp_zip_path = PyPath(tempfile.gettempdir()) / zip_filename + zip_directory(task_output_dir, temp_zip_path) + return FileResponse(temp_zip_path, media_type="application/zip", filename=zip_filename) + + +@app.get( + "/outputs/structure/{relative_path:path}", + response_model=List[FileStructure], + summary="Get structure of a path within the outputs directory.", +) +def get_output_structure(relative_path: str = "."): + return handle_get_structure(OUTPUTS_DIR, relative_path) + + +@app.get( + "/inputs/structure/{relative_path:path}", + response_model=List[FileStructure], + summary="Get structure of a path within the inputs directory.", +) +def get_input_structure(relative_path: str = "."): + return handle_get_structure(INPUTS_DIR, relative_path) + + +@app.get( + "/configs/structure/{relative_path:path}", + response_model=List[FileStructure], + summary="Get structure of a path within the configs directory.", +) +def get_config_structure(relative_path: str = "."): + return handle_get_structure(CONFIGS_DIR, relative_path) + + +FRONTEND_DIST_DIR = PyPath("atk-interface/dist") + +@app.get("/{full_path:path}", include_in_schema=False) +async def serve_frontend(full_path: str): + file_path = FRONTEND_DIST_DIR / full_path + if file_path.is_file(): + return FileResponse(file_path) + + assets_path = FRONTEND_DIST_DIR / "assets" / full_path + if assets_path.is_file(): + return FileResponse(assets_path) + + index_html = FRONTEND_DIST_DIR / "index.html" + if index_html.is_file(): + return FileResponse(index_html) + + if not full_path or full_path == "/": + return {"status": "ok", "message": "Augmentoolkit API is running (frontend not built)."} + + raise HTTPException(status_code=404, detail="Not found") diff --git a/api_README.md b/api_README.md new file mode 100644 index 0000000000000000000000000000000000000000..b7074e49242e81063af8798fc1a6b6a537e2fb02 --- /dev/null +++ b/api_README.md @@ -0,0 +1,500 @@ +# Augmentoolkit API Documentation + +This document describes the REST API endpoints for interacting with the Augmentoolkit server, allowing you to manage and run data generation pipelines, monitor their status, and manage associated files (inputs, outputs, logs, configs). + +The API is built using FastAPI and utilizes Huey for background task queueing and Redis for state management (task status, PID mapping, output directory mapping). + +**Base URL:** (Assuming the server runs locally on port 8000) `http://127.0.0.1:8000` + +--- + +## Core Concepts + +* **Pipelines:** Defined sequences of data processing steps, typically corresponding to Python functions within the codebase (e.g., in a `pipelines` directory). +* **Tasks:** An instance of a pipeline run, managed by the Huey background task queue. Each task has a unique `task_id`. +* **Nodes:** Refers to the importable path of the Python function that executes a pipeline (e.g., `pipelines.my_pipeline.run`). +* **Configs:** YAML configuration files (stored in `./external_configs/`) that provide parameters for pipeline runs. Can be overridden by parameters in the API request. +* **Super Config (`super_config.yaml`):** Defines path aliases used for resolving node and config paths. +* **Directories:** + * `./inputs/`: Stores input data files used by pipelines. + * `./outputs/`: Stores output files generated by pipelines. Output directories are often named using the `task_id`. + * `./external_configs/`: Stores reusable pipeline configuration files. + * `./logs/`: Stores log files (`.log`) for each pipeline task run. + +--- + +## Endpoints + +### Health Check + +* **GET /** + * **Summary:** Health Check + * **Description:** Basic health check endpoint. Returns a simple message indicating the API is running. + * **Response:** `{"message": "Augmentoolkit API is running."}` + +--- + +### Pipeline Execution + +* **POST /pipelines/run** + * **Summary:** Queue a dataset generation pipeline for execution. + * **Status Code:** 202 Accepted + * **Description:** Resolves node and config paths using aliases from `super_config.yaml` and enqueues a pipeline task using Huey (`tasks.run_pipeline_task`). The task executes the pipeline defined by `node_path` potentially using configuration from `config_path` and `parameters` via `run_augmentoolkit.py`. + * **Request Body:** + ```json + { + "node_path": "string (required, can use aliases)", + "config_path": "string (optional, relative to ./external_configs/ or absolute, can use aliases)", + "parameters": { /* JSON object of key-value pairs (optional) */ } + } + ``` + * **Response:** + ```json + { + "pipeline_id": "string (Huey Task ID)", + "message": "Pipeline run queued successfully." + } + ``` + +* **GET /pipelines/available** + * **Summary:** Get available pipeline aliases from super_config.yaml. + * **Description:** Reads the `super_config.yaml` file and returns a sorted list of path aliases (keys) whose corresponding values do not end with `.yaml`. These aliases typically represent the runnable entry points for pipelines that can be passed to the `node_path` field in the `/pipelines/run` endpoint. + * **Path Parameters:** None + * **Query Parameters:** None + * **Response:** A JSON list of strings. + ```json + [ + "alias1", + "alias2", + "another-pipeline-alias", + ... + ] + ``` + * **Error Responses:** + * 500 Internal Server Error: If `super_config.yaml` cannot be found or parsed, or an unexpected error occurs. (Note: If the file is found but empty or `path_aliases` is missing/invalid, it currently returns an empty list `[]` gracefully). + +--- + +### Task Status & Management + +* **GET /tasks/{task_id}/status** + * **Summary:** Get the status of a pipeline run. + * **Description:** Retrieves the current status (PENDING, RUNNING, COMPLETED, FAILED, REVOKED) and result (if finished) of a specific pipeline task using its `task_id`. Checks Huey's state and potentially Redis for progress information. + * **Path Parameters:** + * `task_id`: The Huey task ID obtained from `/pipelines/run`. + * **Response:** + ```json + { + "task_id": "string", + "status": "string (PENDING | RUNNING | COMPLETED | FAILED | REVOKED)", + "message": "string (optional)", + "progress": "float (optional, 0.0-1.0)", + "details": { /* Optional details, e.g., error info or result */ } + } + ``` + * **Error Responses:** + * 404 Not Found: If `task_id` does not exist. + * 500 Internal Server Error: If status retrieval fails. + +* **GET /tasks/{task_id}/parameters** + * **Summary:** Get the parameters a task was executed with. + * **Description:** Retrieves the parameters (combined from config file and request overrides) that were used to start a specific task run. Parameters are stored in Redis when the task begins execution and persist based on a configured timeout (typically several days). + * **Path Parameters:** + * `task_id`: The Huey task ID obtained from `/pipelines/run`. + * **Response:** + ```json + { + "task_id": "string", + "parameters": { /* JSON object of key-value pairs used for the task */ } + } + ``` + * **Error Responses:** + * 404 Not Found: If the `task_id` does not exist or if the parameters were not stored/have expired. + * 500 Internal Server Error: If retrieving or parsing the parameters from Redis fails (e.g., data corruption). + +* **POST /tasks/{task_id}/interrupt** + * **Summary:** Interrupt a running pipeline task subprocess or revoke a pending task. + * **Description:** Attempts to interrupt a running task or revoke a pending one. + * If the task is **RUNNING**: Looks up the subprocess PID associated with the `task_id` in Redis (set by `tasks.py`) and sends a `SIGINT` signal to it using `os.kill`. + * If the task is **PENDING**: Revokes the task using `huey.revoke`. + * Reports an error if the task is already finished or revoked. + * **Path Parameters:** + * `task_id`: The Huey task ID. + * **Response:** + * `{"message": "Sent SIGINT signal..."}` (for running tasks) + * `{"message": "Task ... was pending and has been successfully revoked."}` (for pending tasks) + * **Error Responses:** + * 404 Not Found: If `task_id` does not exist or the subprocess PID is gone. + * 409 Conflict: If the task is already finished/revoked or fails to revoke. + * 500 Internal Server Error: If signaling fails or other errors occur. + +* **GET /tasks/queue** + * **Summary:** Get lists of pending and scheduled tasks. + * **Description:** Retrieves the IDs of tasks currently pending or scheduled in the Huey queue using `huey.pending()` and `huey.scheduled()`. Note: This does not reliably show tasks that are *actively running*. + * **Path Parameters:** None + * **Query Parameters:** None + * **Response:** + ```json + { + "pending_tasks": ["string (task_id)", ...], + "scheduled_tasks": ["string (task_id)", ...], + "message": "string (e.g., Found X pending and Y scheduled tasks.)" + } + ``` + * **Error Responses:** + * 500 Internal Server Error: If retrieving queue status fails. + +--- + +### Log Management + +* **GET /tasks/{task_id}/logs** + * **Summary:** Get logs for a specific task. + * **Description:** Retrieves the content of the log file (`./logs/.log`) generated by a specific task run. + * **Path Parameters:** + * `task_id`: The Huey task ID. + * **Query Parameters:** + * `tail` (int, optional): Return only the last N lines of the log. + * **Response:** + ```json + { + "task_id": "string", + "message": "string", + "logs": "string (log content)" + } + ``` + * **Error Responses:** + * 404 Not Found: If the log file doesn't exist. + * 500 Internal Server Error: If reading the file fails. + +* **DELETE /tasks/{task_id}/logs** + * **Summary:** Delete log file for a specific task. + * **Description:** Deletes the specific log file (`./logs/.log`). + * **Path Parameters:** + * `task_id`: The Huey task ID. + * **Response:** `{"message": "Successfully deleted log file..."}` + * **Error Responses:** + * 404 Not Found: If the log file doesn't exist. + * 500 Internal Server Error: If deletion fails. + +* **GET /logs** + * **Summary:** List all available log files. + * **Description:** Returns a list of all `.log` filenames currently in the `./logs/` directory. + * **Response:** `{"log_files": ["task_id1.log", "task_id2.log", ...]}` + * **Error Responses:** + * 500 Internal Server Error: If listing the directory fails. + +* **DELETE /logs** + * **Summary:** Clear all task log files. + * **Description:** Deletes all files ending with `.log` within the `./logs/` directory. + * **Response:** Reports the number of deleted files and any errors encountered. + ```json + { + "message": "Successfully deleted X log file(s)...", + "errors": [ /* list of error strings, if any */ ] + } + ``` + * **Error Responses:** + * 500 Internal Server Error: If accessing or clearing the directory fails. + +--- + +### Output File Management + +* **GET /tasks/{task_id}/outputs/download** + * **Summary:** Download output directory for a task. + * **Description:** Retrieves the path of the output directory associated with the `task_id` from Redis (set by `tasks.py`), zips the entire directory content, and returns it as a downloadable file (`task__output.zip`). + * **Path Parameters:** + * `task_id`: The Huey task ID. + * **Response:** A zip file (`application/zip`). + * **Error Responses:** + * 404 Not Found: If the task ID mapping or the directory itself doesn't exist. + * 500 Internal Server Error: If zipping or file access fails. + +* **GET /tasks/{task_id}/outputs/structure** + * **Summary:** Get structure of a task's output directory. + * **Description:** Retrieves the path of the output directory associated with the `task_id` from Redis and returns its file/folder structure as a JSON list. Paths are relative to the specific task's output directory root. + * **Path Parameters:** + * `task_id`: The Huey task ID. + * **Response:** List of `FileStructure` objects (see below). + * **Error Responses:** + * 404 Not Found: If the task ID mapping or the directory itself doesn't exist. + * 500 Internal Server Error: If reading the directory fails. + +* **GET /outputs/structure/{relative_path:path}** + * **Summary:** Get structure of a path within the main outputs directory. + * **Description:** Retrieves the file/folder structure for a given path relative to the root `./outputs/` directory. Uses the shared `handle_get_structure` helper. + * **Path Parameters:** + * `relative_path`: The path relative to `./outputs/` (e.g., `my_folder/sub_folder` or leave empty/`.` for the root). + * **Response:** List of `FileStructure` objects: + ```json + [ + { + "path": "string (relative path)", + "is_dir": true, + "children": [ /* nested FileStructure objects */ ] + }, + { + "path": "string (relative path)", + "is_dir": false, + "children": null + } + ] + ``` + * **Error Responses:** + * 404 Not Found: If the path doesn't exist. + * 400 Bad Request: If the path is not a directory. + * 500 Internal Server Error: If listing fails. + +* **GET /outputs/download/{relative_path:path}** + * **Summary:** Download a specific file or folder from outputs. + * **Description:** Downloads a specific file or zips and downloads a specific folder relative to the root `./outputs/` directory. Uses the shared `handle_download_item` helper. + * **Path Parameters:** + * `relative_path`: The path relative to `./outputs/`. + * **Response:** A file (`application/octet-stream` or detected type) or a zip archive (`application/zip`). + * **Error Responses:** + * 404 Not Found: If the path doesn't exist. + * 500 Internal Server Error: If reading/zipping fails. + +* **DELETE /outputs/{relative_path:path}** + * **Summary:** Delete a file or folder from outputs. + * **Description:** Deletes a specific file or folder relative to the root `./outputs/` directory. Uses the shared `handle_delete_item` helper. **Warning:** This is permanent. + * **Path Parameters:** + * `relative_path`: The path relative to `./outputs/`. + * **Response:** `{"message": "Successfully deleted..."}` + * **Error Responses:** + * 404 Not Found: If the path doesn't exist. + * 400 Bad Request: If attempting to delete the root `./outputs` directory. + * 500 Internal Server Error: If deletion fails. + +* **POST /outputs/move** + * **Summary:** Move/Rename a file or folder within outputs. + * **Description:** Moves or renames a file or folder within the `./outputs/` directory using `shutil.move`. + * The source path must exist. + * The destination path must *not* already exist (to prevent overwrites). + * The parent directory of the destination path must exist. + * Cannot be used to move the root `./outputs` directory itself. + * **Request Body:** + ```json + { + "source_relative_path": "string (required, current relative path)", + "destination_relative_path": "string (required, new relative path)" + } + ``` + * **Response (Status 200):** + ```json + { + "message": "Successfully moved {file/directory} from '{source}' to '{destination}'." + } + ``` + * **Error Responses:** + * 400 Bad Request: If source/destination paths are invalid, if the destination parent doesn't exist, or if attempting to move the root directory. + * 404 Not Found: If the source path does not exist. + * 409 Conflict: If the destination path already exists. + * 500 Internal Server Error: If the move operation fails for other reasons (e.g., permissions). + +--- + +### Input File Management + +*Input file management endpoints mirror the Output File Management endpoints, but operate on the `./inputs/` directory.* Also, it has a route to create a directory, too. + +* **GET /inputs/structure/{relative_path:path}** + * **Summary:** Get structure of a path within the inputs directory. + * **Description:** Retrieves structure relative to `./inputs/`. + * **Response:** List of `FileStructure` objects. + +* **GET /inputs/download/{relative_path:path}** + * **Summary:** Download a specific file or folder from inputs. + * **Description:** Downloads file/folder relative to `./inputs/`. + * **Response:** File or Zip archive. + +* **DELETE /inputs/{relative_path:path}** + * **Summary:** Delete a file or folder from inputs. + * **Description:** Deletes file/folder relative to `./inputs/`. **Warning:** Permanent. + * **Response:** `{"message": "Successfully deleted..."}` + +* **POST /inputs/move** + * **Summary:** Move/Rename a file or folder within inputs. + * **Description:** Moves or renames a file or folder within the `./inputs/` directory using `shutil.move`. See `/outputs/move` for detailed behavior and error conditions. + * **Request Body:** + ```json + { + "source_relative_path": "string (required, current relative path)", + "destination_relative_path": "string (required, new relative path)" + } + ``` + * **Response (Status 200):** + ```json + { + "message": "Successfully moved {file/directory} from '{source}' to '{destination}'." + } + ``` + * **Error Responses:** 400, 404, 409, 500 (see `/outputs/move`) + +* **POST /inputs/directory** + * **Summary:** Create a new directory within the inputs folder. + * **Description:** Creates a new directory at the specified `relative_path` within the `./inputs/` directory. The path can include parent directories (e.g., `existing_folder/new_subfolder`). + * **Request Body:** + ```json + { + "relative_path": "string (required, relative path)" + } + ``` + * **Response (Status 201):** + ```json + { + "message": "Successfully created directory '{relative_path}' in inputs." + } + ``` + * **Error Responses:** + * 400 Bad Request: If `relative_path` is invalid (empty, '.', targets root) or if the parent directory specified in the path does not exist. + * 409 Conflict: If a file or directory already exists at the target `relative_path`. + * 500 Internal Server Error: If directory creation fails for other reasons (e.g., permissions). + +* **POST /inputs/upload/{relative_path:path}** + * **Summary:** Upload files to a specific path within inputs, automatically extracting zip files. + * **Description:** Uploads one or more files to a specified path within `./inputs/`. The path is created if it doesn't exist. + * If a file ends with `.zip` (case-insensitive), it is automatically extracted into the target `relative_path`. + * Upon successful extraction, the original `.zip` file is deleted. + * If extraction fails, the original `.zip` file is kept, and an error is reported. + * Non-zip files are saved directly to the `relative_path`. + * Allows uploading directly to the root `./inputs/` directory (e.g., by specifying `.` as the `relative_path`). + * **Path Parameters:** + * `relative_path`: Target path relative to `./inputs/`. Files (including extracted zip contents) will be placed here. + * **Request Body:** `multipart/form-data` containing one or more files. + * **Response (Status 201 or 207):** + ```json + { + "message": "Upload process completed...", + "uploaded_files": [ + {"filename": "string", "size": int, "path": "string (relative path)", "error": "string (optional, only on failed extraction)"} + ], + "extracted_zips": [ + {"filename": "string (original zip)", "extracted_to": "string (relative path)"} + ], + "errors": [ /* list of general error strings, if any */ ] + } + ``` + * **Error Responses:** + * 500 Internal Server Error: If directory creation, file writing, or extraction fails unexpectedly. + +--- + +### Config File Management + +*Configuration file management endpoints operate on the `./external_configs/` directory.* + +* **GET /configs/aliases** + * **Summary:** Get available config file aliases from super_config.yaml. + * **Description:** Reads the `super_config.yaml` file and returns a sorted list of path aliases (keys) whose corresponding values *do* end with `.yaml` (case-insensitive). These aliases can be used to identify existing configuration files for duplication or other purposes. + * **Path Parameters:** None + * **Query Parameters:** None + * **Response:** A JSON list of strings. + ```json + [ + "config-alias1", + "another_config.yaml", + "path/to/config_alias_3", + ... + ] + ``` + * **Error Responses:** + * 500 Internal Server Error: If `super_config.yaml` cannot be found or parsed, or an unexpected error occurs. (Note: If the file is found but empty or `path_aliases` is missing/invalid, it returns an empty list `[]` gracefully). + +* **GET /configs/structure/{relative_path:path}** + * **Summary:** Get structure of a path within the configs directory. + * **Description:** Retrieves structure relative to `./external_configs/`. + * **Response:** List of `FileStructure` objects. + +* **GET /configs/content/{relative_path:path}** + * **Summary:** Get content of a specific config file. + * **Description:** Retrieves the plain text content of a specific file relative to `./external_configs/`. + * **Path Parameters:** + * `relative_path`: The path relative to `./external_configs/`. + * **Response:** Plain text file content (`text/plain`). + * **Error Responses:** + * 404 Not Found: If the file doesn't exist. + * 400 Bad Request: If the path is not a file. + +* **POST /configs/content/{relative_path:path}** + * **Summary:** Create or update a config file. + * **Description:** Creates a new file or overwrites an existing file at the specified path relative to `./external_configs/`. Parent directories are created if they don't exist. + * **Path Parameters:** + * `relative_path`: The path relative to `./external_configs/`. + * **Request Body:** Raw text content (`text/plain`). + * **Response:** `{"message": "Successfully saved config..."}` + * **Error Responses:** + * 400 Bad Request: If attempting to write to the root. + * 500 Internal Server Error: If writing fails. + +* **DELETE /configs/{relative_path:path}** + * **Summary:** Delete a specific config file or directory. + * **Description:** Deletes a specific file or directory (recursively) relative to `./external_configs/`. **Warning:** Permanent. + * **Path Parameters:** + * `relative_path`: The path relative to `./external_configs/`. + * **Response:** `{"message": "Successfully deleted config {file|directory}..."}` + * **Error Responses:** + * 404 Not Found: If the file or directory doesn't exist. + * 400 Bad Request: If attempting to delete the root `./external_configs` directory. + * 500 Internal Server Error: If deletion fails. + +* **POST /configs/move** + * **Summary:** Move/Rename a file or folder within configs. + * **Description:** Moves or renames a file or folder within the `./external_configs/` directory using `shutil.move`. See `/outputs/move` for detailed behavior and error conditions. + * **Request Body:** + ```json + { + "source_relative_path": "string (required, current relative path)", + "destination_relative_path": "string (required, new relative path)" + } + ``` + * **Response (Status 200):** + ```json + { + "message": "Successfully moved {file/directory} from '{source}' to '{destination}'." + } + ``` + * **Error Responses:** 400, 404, 409, 500 (see `/outputs/move`) + +* **POST /configs/directory** + * **Summary:** Create a new directory within the configs folder. + * **Description:** Creates a new directory at the specified `relative_path` within the `./external_configs/` directory. Uses the shared `handle_create_directory` helper. + * **Request Body:** + ```json + { + "relative_path": "string (required, relative path)" + } + ``` + * **Response (Status 201):** + ```json + { + "message": "Successfully created directory '{relative_path}' in external_configs." + } + ``` + * **Error Responses:** + * 400 Bad Request: If `relative_path` is invalid (empty, '.', targets root) or if the parent directory specified in the path does not exist. + * 409 Conflict: If a file or directory already exists at the target `relative_path`. + * 500 Internal Server Error: If directory creation fails for other reasons (e.g., permissions). + +* **POST /configs/duplicate** + * **Summary:** Duplicate a pipeline config into external_configs. + * **Description:** Duplicates a configuration file identified by a `source_alias` (from `super_config.yaml`) into the `./external_configs/` directory at the specified `destination_relative_path`. The source alias must resolve to an existing `.yaml` file. The destination path must not already exist, and its parent directory must exist. + * **Request Body:** + ```json + { + "source_alias": "string (required, alias from super_config.yaml)", + "destination_relative_path": "string (required, new relative path in external_configs)" + } + ``` + * **Response (Status 201):** + ```json + { + "message": "Successfully duplicated config from alias '{source_alias}' to '{destination_relative_path}' in external_configs." + } + ``` + * **Error Responses:** + * 400 Bad Request: If the source alias doesn't point to a `.yaml` file, if destination path is invalid, or if destination parent directory doesn't exist. + * 404 Not Found: If the source alias is not found in `super_config.yaml` or the resolved source file doesn't exist. + * 409 Conflict: If the destination path already exists. + * 500 Internal Server Error: If resolving paths, reading the source, or writing the destination file fails. diff --git a/atk-interface/README.md b/atk-interface/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7059a962adb0138b65dd10e0aee66ccfe984b8c6 --- /dev/null +++ b/atk-interface/README.md @@ -0,0 +1,12 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/atk-interface/api_README.md b/atk-interface/api_README.md new file mode 100644 index 0000000000000000000000000000000000000000..462da8fe74bc81667a84c8681043d1429f02c2cc --- /dev/null +++ b/atk-interface/api_README.md @@ -0,0 +1,500 @@ +# Augmentoolkit API Documentation + +This document describes the REST API endpoints for interacting with the Augmentoolkit server, allowing you to manage and run data generation pipelines, monitor their status, and manage associated files (inputs, outputs, logs, configs). + +The API is built using FastAPI and utilizes Huey for background task queueing and Redis for state management (task status, PID mapping, output directory mapping). + +**Base URL:** (Assuming the server runs locally on port 8000) `http://127.0.0.1:8000` + +--- + +## Core Concepts + +* **Pipelines:** Defined sequences of data processing steps, typically corresponding to Python functions within the codebase (e.g., in a `pipelines` directory). +* **Tasks:** An instance of a pipeline run, managed by the Huey background task queue. Each task has a unique `task_id`. +* **Nodes:** Refers to the importable path of the Python function that executes a pipeline (e.g., `pipelines.my_pipeline.run`). +* **Configs:** YAML configuration files (stored in `./external_configs/`) that provide parameters for pipeline runs. Can be overridden by parameters in the API request. +* **Super Config (`super_config.yaml`):** Defines path aliases used for resolving node and config paths. +* **Directories:** + * `./inputs/`: Stores input data files used by pipelines. + * `./outputs/`: Stores output files generated by pipelines. Output directories are often named using the `task_id`. + * `./external_configs/`: Stores reusable pipeline configuration files. + * `./logs/`: Stores log files (`.log`) for each pipeline task run. + +--- + +## Endpoints + +### Health Check + +* **GET /** + * **Summary:** Health Check + * **Description:** Basic health check endpoint. Returns a simple message indicating the API is running. + * **Response:** `{"message": "Augmentoolkit API is running."}` + +--- + +### Pipeline Execution + +* **POST /pipelines/run** + * **Summary:** Queue a dataset generation pipeline for execution. + * **Status Code:** 202 Accepted + * **Description:** Resolves node and config paths using aliases from `super_config.yaml` and enqueues a pipeline task using Huey (`tasks.run_pipeline_task`). The task executes the pipeline defined by `node_path` potentially using configuration from `config_path` and `parameters` via `run_augmentoolkit.py`. + * **Request Body:** + ```json + { + "node_path": "string (required, can use aliases)", + "config_path": "string (optional, relative to ./external_configs/ or absolute, can use aliases)", + "parameters": { /* JSON object of key-value pairs (optional) */ } + } + ``` + * **Response:** + ```json + { + "pipeline_id": "string (Huey Task ID)", + "message": "Pipeline run queued successfully." + } + ``` + +* **GET /pipelines/available** + * **Summary:** Get available pipeline aliases from super_config.yaml. + * **Description:** Reads the `super_config.yaml` file and returns a sorted list of path aliases (keys) whose corresponding values do not end with `.yaml`. These aliases typically represent the runnable entry points for pipelines that can be passed to the `node_path` field in the `/pipelines/run` endpoint. + * **Path Parameters:** None + * **Query Parameters:** None + * **Response:** A JSON list of strings. + ```json + [ + "alias1", + "alias2", + "another-pipeline-alias", + ... + ] + ``` + * **Error Responses:** + * 500 Internal Server Error: If `super_config.yaml` cannot be found or parsed, or an unexpected error occurs. (Note: If the file is found but empty or `path_aliases` is missing/invalid, it currently returns an empty list `[]` gracefully). + +--- + +### Task Status & Management + +* **GET /tasks/{task_id}/status** + * **Summary:** Get the status of a pipeline run. + * **Description:** Retrieves the current status (PENDING, RUNNING, COMPLETED, FAILED, REVOKED) and result (if finished) of a specific pipeline task using its `task_id`. Checks Huey's state and potentially Redis for progress information. + * **Path Parameters:** + * `task_id`: The Huey task ID obtained from `/pipelines/run`. + * **Response:** + ```json + { + "task_id": "string", + "status": "string (PENDING | RUNNING | COMPLETED | FAILED | REVOKED)", + "message": "string (optional)", + "progress": "float (optional, 0.0-1.0)", + "details": { /* Optional details, e.g., error info or result */ } + } + ``` + * **Error Responses:** + * 404 Not Found: If `task_id` does not exist. + * 500 Internal Server Error: If status retrieval fails. + +* **GET /tasks/{task_id}/parameters** + * **Summary:** Get the parameters a task was executed with. + * **Description:** Retrieves the parameters (combined from config file and request overrides) that were used to start a specific task run. Parameters are stored in Redis when the task begins execution and persist based on a configured timeout (typically several days). + * **Path Parameters:** + * `task_id`: The Huey task ID obtained from `/pipelines/run`. + * **Response:** + ```json + { + "task_id": "string", + "parameters": { /* JSON object of key-value pairs used for the task */ } + } + ``` + * **Error Responses:** + * 404 Not Found: If the `task_id` does not exist or if the parameters were not stored/have expired. + * 500 Internal Server Error: If retrieving or parsing the parameters from Redis fails (e.g., data corruption). + +* **POST /tasks/{task_id}/interrupt** + * **Summary:** Interrupt a running pipeline task subprocess or revoke a pending task. + * **Description:** Attempts to interrupt a running task or revoke a pending one. + * If the task is **RUNNING**: Looks up the subprocess PID associated with the `task_id` in Redis (set by `tasks.py`) and sends a `SIGINT` signal to it using `os.kill`. + * If the task is **PENDING**: Revokes the task using `huey.revoke`. + * Reports an error if the task is already finished or revoked. + * **Path Parameters:** + * `task_id`: The Huey task ID. + * **Response:** + * `{"message": "Sent SIGINT signal..."}` (for running tasks) + * `{"message": "Task ... was pending and has been successfully revoked."}` (for pending tasks) + * **Error Responses:** + * 404 Not Found: If `task_id` does not exist or the subprocess PID is gone. + * 409 Conflict: If the task is already finished/revoked or fails to revoke. + * 500 Internal Server Error: If signaling fails or other errors occur. + +* **GET /tasks/queue** + * **Summary:** Get lists of pending and scheduled tasks. + * **Description:** Retrieves the IDs of tasks currently pending or scheduled in the Huey queue using `huey.pending()` and `huey.scheduled()`. Note: This does not reliably show tasks that are *actively running*. + * **Path Parameters:** None + * **Query Parameters:** None + * **Response:** + ```json + { + "pending_tasks": ["string (task_id)", ...], + "scheduled_tasks": ["string (task_id)", ...], + "message": "string (e.g., Found X pending and Y scheduled tasks.)" + } + ``` + * **Error Responses:** + * 500 Internal Server Error: If retrieving queue status fails. + +--- + +### Log Management + +* **GET /tasks/{task_id}/logs** + * **Summary:** Get logs for a specific task. + * **Description:** Retrieves the content of the log file (`./logs/.log`) generated by a specific task run. + * **Path Parameters:** + * `task_id`: The Huey task ID. + * **Query Parameters:** + * `tail` (int, optional): Return only the last N lines of the log. + * **Response:** + ```json + { + "task_id": "string", + "message": "string", + "logs": "string (log content)" + } + ``` + * **Error Responses:** + * 404 Not Found: If the log file doesn't exist. + * 500 Internal Server Error: If reading the file fails. + +* **DELETE /tasks/{task_id}/logs** + * **Summary:** Delete log file for a specific task. + * **Description:** Deletes the specific log file (`./logs/.log`). + * **Path Parameters:** + * `task_id`: The Huey task ID. + * **Response:** `{"message": "Successfully deleted log file..."}` + * **Error Responses:** + * 404 Not Found: If the log file doesn't exist. + * 500 Internal Server Error: If deletion fails. + +* **GET /logs** + * **Summary:** List all available log files. + * **Description:** Returns a list of all `.log` filenames currently in the `./logs/` directory. + * **Response:** `{"log_files": ["task_id1.log", "task_id2.log", ...]}` + * **Error Responses:** + * 500 Internal Server Error: If listing the directory fails. + +* **DELETE /logs** + * **Summary:** Clear all task log files. + * **Description:** Deletes all files ending with `.log` within the `./logs/` directory. + * **Response:** Reports the number of deleted files and any errors encountered. + ```json + { + "message": "Successfully deleted X log file(s)...", + "errors": [ /* list of error strings, if any */ ] + } + ``` + * **Error Responses:** + * 500 Internal Server Error: If accessing or clearing the directory fails. + +--- + +### Output File Management + +* **GET /tasks/{task_id}/outputs/download** + * **Summary:** Download output directory for a task. + * **Description:** Retrieves the path of the output directory associated with the `task_id` from Redis (set by `tasks.py`), zips the entire directory content, and returns it as a downloadable file (`task__output.zip`). + * **Path Parameters:** + * `task_id`: The Huey task ID. + * **Response:** A zip file (`application/zip`). + * **Error Responses:** + * 404 Not Found: If the task ID mapping or the directory itself doesn't exist. + * 500 Internal Server Error: If zipping or file access fails. + +* **GET /tasks/{task_id}/outputs/structure** + * **Summary:** Get structure of a task's output directory. + * **Description:** Retrieves the path of the output directory associated with the `task_id` from Redis and returns its file/folder structure as a JSON list. Paths are relative to the specific task's output directory root. + * **Path Parameters:** + * `task_id`: The Huey task ID. + * **Response:** List of `FileStructure` objects (see below). + * **Error Responses:** + * 404 Not Found: If the task ID mapping or the directory itself doesn't exist. + * 500 Internal Server Error: If reading the directory fails. + +* **GET /outputs/structure/{relative_path:path}** + * **Summary:** Get structure of a path within the main outputs directory. + * **Description:** Retrieves the file/folder structure for a given path relative to the root `./outputs/` directory. Uses the shared `handle_get_structure` helper. + * **Path Parameters:** + * `relative_path`: The path relative to `./outputs/` (e.g., `my_folder/sub_folder` or leave empty/`.` for the root). + * **Response:** List of `FileStructure` objects: + ```json + [ + { + "path": "string (relative path)", + "is_dir": true, + "children": [ /* nested FileStructure objects */ ] + }, + { + "path": "string (relative path)", + "is_dir": false, + "children": null + } + ] + ``` + * **Error Responses:** + * 404 Not Found: If the path doesn't exist. + * 400 Bad Request: If the path is not a directory. + * 500 Internal Server Error: If listing fails. + +* **GET /outputs/download/{relative_path:path}** + * **Summary:** Download a specific file or folder from outputs. + * **Description:** Downloads a specific file or zips and downloads a specific folder relative to the root `./outputs/` directory. Uses the shared `handle_download_item` helper. + * **Path Parameters:** + * `relative_path`: The path relative to `./outputs/`. + * **Response:** A file (`application/octet-stream` or detected type) or a zip archive (`application/zip`). + * **Error Responses:** + * 404 Not Found: If the path doesn't exist. + * 500 Internal Server Error: If reading/zipping fails. + +* **DELETE /outputs/{relative_path:path}** + * **Summary:** Delete a file or folder from outputs. + * **Description:** Deletes a specific file or folder relative to the root `./outputs/` directory. Uses the shared `handle_delete_item` helper. **Warning:** This is permanent. + * **Path Parameters:** + * `relative_path`: The path relative to `./outputs/`. + * **Response:** `{"message": "Successfully deleted..."}` + * **Error Responses:** + * 404 Not Found: If the path doesn't exist. + * 400 Bad Request: If attempting to delete the root `./outputs` directory. + * 500 Internal Server Error: If deletion fails. + +* **POST /outputs/move** + * **Summary:** Move/Rename a file or folder within outputs. + * **Description:** Moves or renames a file or folder within the `./outputs/` directory using `shutil.move`. + * The source path must exist. + * The destination path must *not* already exist (to prevent overwrites). + * The parent directory of the destination path must exist. + * Cannot be used to move the root `./outputs` directory itself. + * **Request Body:** + ```json + { + "source_relative_path": "string (required, current relative path)", + "destination_relative_path": "string (required, new relative path)" + } + ``` + * **Response (Status 200):** + ```json + { + "message": "Successfully moved {file/directory} from '{source}' to '{destination}'." + } + ``` + * **Error Responses:** + * 400 Bad Request: If source/destination paths are invalid, if the destination parent doesn't exist, or if attempting to move the root directory. + * 404 Not Found: If the source path does not exist. + * 409 Conflict: If the destination path already exists. + * 500 Internal Server Error: If the move operation fails for other reasons (e.g., permissions). + +--- + +### Input File Management + +*Input file management endpoints mirror the Output File Management endpoints, but operate on the `./inputs/` directory.* + +* **GET /inputs/structure/{relative_path:path}** + * **Summary:** Get structure of a path within the inputs directory. + * **Description:** Retrieves structure relative to `./inputs/`. + * **Response:** List of `FileStructure` objects. + +* **GET /inputs/download/{relative_path:path}** + * **Summary:** Download a specific file or folder from inputs. + * **Description:** Downloads file/folder relative to `./inputs/`. + * **Response:** File or Zip archive. + +* **DELETE /inputs/{relative_path:path}** + * **Summary:** Delete a file or folder from inputs. + * **Description:** Deletes file/folder relative to `./inputs/`. **Warning:** Permanent. + * **Response:** `{"message": "Successfully deleted..."}` + +* **POST /inputs/move** + * **Summary:** Move/Rename a file or folder within inputs. + * **Description:** Moves or renames a file or folder within the `./inputs/` directory using `shutil.move`. See `/outputs/move` for detailed behavior and error conditions. + * **Request Body:** + ```json + { + "source_relative_path": "string (required, current relative path)", + "destination_relative_path": "string (required, new relative path)" + } + ``` + * **Response (Status 200):** + ```json + { + "message": "Successfully moved {file/directory} from '{source}' to '{destination}'." + } + ``` + * **Error Responses:** 400, 404, 409, 500 (see `/outputs/move`) + +* **POST /inputs/directory** + * **Summary:** Create a new directory within the inputs folder. + * **Description:** Creates a new directory at the specified `relative_path` within the `./inputs/` directory. The path can include parent directories (e.g., `existing_folder/new_subfolder`). + * **Request Body:** + ```json + { + "relative_path": "string (required, relative path)" + } + ``` + * **Response (Status 201):** + ```json + { + "message": "Successfully created directory '{relative_path}' in inputs." + } + ``` + * **Error Responses:** + * 400 Bad Request: If `relative_path` is invalid (empty, '.', targets root) or if the parent directory specified in the path does not exist. + * 409 Conflict: If a file or directory already exists at the target `relative_path`. + * 500 Internal Server Error: If directory creation fails for other reasons (e.g., permissions). + +* **POST /inputs/upload/{relative_path:path}** + * **Summary:** Upload files to a specific path within inputs, automatically extracting zip files. + * **Description:** Uploads one or more files to a specified path within `./inputs/`. The path is created if it doesn't exist. + * If a file ends with `.zip` (case-insensitive), it is automatically extracted into the target `relative_path`. + * Upon successful extraction, the original `.zip` file is deleted. + * If extraction fails, the original `.zip` file is kept, and an error is reported. + * Non-zip files are saved directly to the `relative_path`. + * Allows uploading directly to the root `./inputs/` directory (e.g., by specifying `.` as the `relative_path`). + * **Path Parameters:** + * `relative_path`: Target path relative to `./inputs/`. Files (including extracted zip contents) will be placed here. + * **Request Body:** `multipart/form-data` containing one or more files. + * **Response (Status 201 or 207):** + ```json + { + "message": "Upload process completed...", + "uploaded_files": [ + {"filename": "string", "size": int, "path": "string (relative path)", "error": "string (optional, only on failed extraction)"} + ], + "extracted_zips": [ + {"filename": "string (original zip)", "extracted_to": "string (relative path)"} + ], + "errors": [ /* list of general error strings, if any */ ] + } + ``` + * **Error Responses:** + * 500 Internal Server Error: If directory creation, file writing, or extraction fails unexpectedly. + +--- + +### Config File Management + +*Configuration file management endpoints operate on the `./external_configs/` directory.* + +* **GET /configs/aliases** + * **Summary:** Get available config file aliases from super_config.yaml. + * **Description:** Reads the `super_config.yaml` file and returns a sorted list of path aliases (keys) whose corresponding values *do* end with `.yaml` (case-insensitive). These aliases can be used to identify existing configuration files for duplication or other purposes. + * **Path Parameters:** None + * **Query Parameters:** None + * **Response:** A JSON list of strings. + ```json + [ + "config-alias1", + "another_config.yaml", + "path/to/config_alias_3", + ... + ] + ``` + * **Error Responses:** + * 500 Internal Server Error: If `super_config.yaml` cannot be found or parsed, or an unexpected error occurs. (Note: If the file is found but empty or `path_aliases` is missing/invalid, it returns an empty list `[]` gracefully). + +* **GET /configs/structure/{relative_path:path}** + * **Summary:** Get structure of a path within the configs directory. + * **Description:** Retrieves structure relative to `./external_configs/`. + * **Response:** List of `FileStructure` objects. + +* **GET /configs/content/{relative_path:path}** + * **Summary:** Get content of a specific config file. + * **Description:** Retrieves the plain text content of a specific file relative to `./external_configs/`. + * **Path Parameters:** + * `relative_path`: The path relative to `./external_configs/`. + * **Response:** Plain text file content (`text/plain`). + * **Error Responses:** + * 404 Not Found: If the file doesn't exist. + * 400 Bad Request: If the path is not a file. + +* **POST /configs/content/{relative_path:path}** + * **Summary:** Create or update a config file. + * **Description:** Creates a new file or overwrites an existing file at the specified path relative to `./external_configs/`. Parent directories are created if they don't exist. + * **Path Parameters:** + * `relative_path`: The path relative to `./external_configs/`. + * **Request Body:** Raw text content (`text/plain`). + * **Response:** `{"message": "Successfully saved config..."}` + * **Error Responses:** + * 400 Bad Request: If attempting to write to the root. + * 500 Internal Server Error: If writing fails. + +* **DELETE /configs/{relative_path:path}** + * **Summary:** Delete a specific config file or directory. + * **Description:** Deletes a specific file or directory (recursively) relative to `./external_configs/`. **Warning:** Permanent. + * **Path Parameters:** + * `relative_path`: The path relative to `./external_configs/`. + * **Response:** `{"message": "Successfully deleted config {file|directory}..."}` + * **Error Responses:** + * 404 Not Found: If the file or directory doesn't exist. + * 400 Bad Request: If attempting to delete the root `./external_configs` directory. + * 500 Internal Server Error: If deletion fails. + +* **POST /configs/move** + * **Summary:** Move/Rename a file or folder within configs. + * **Description:** Moves or renames a file or folder within the `./external_configs/` directory using `shutil.move`. See `/outputs/move` for detailed behavior and error conditions. + * **Request Body:** + ```json + { + "source_relative_path": "string (required, current relative path)", + "destination_relative_path": "string (required, new relative path)" + } + ``` + * **Response (Status 200):** + ```json + { + "message": "Successfully moved {file/directory} from '{source}' to '{destination}'." + } + ``` + * **Error Responses:** 400, 404, 409, 500 (see `/outputs/move`) + +* **POST /configs/directory** + * **Summary:** Create a new directory within the configs folder. + * **Description:** Creates a new directory at the specified `relative_path` within the `./external_configs/` directory. Uses the shared `handle_create_directory` helper. + * **Request Body:** + ```json + { + "relative_path": "string (required, relative path)" + } + ``` + * **Response (Status 201):** + ```json + { + "message": "Successfully created directory '{relative_path}' in external_configs." + } + ``` + * **Error Responses:** + * 400 Bad Request: If `relative_path` is invalid (empty, '.', targets root) or if the parent directory specified in the path does not exist. + * 409 Conflict: If a file or directory already exists at the target `relative_path`. + * 500 Internal Server Error: If directory creation fails for other reasons (e.g., permissions). + +* **POST /configs/duplicate** + * **Summary:** Duplicate a pipeline config into external_configs. + * **Description:** Duplicates a configuration file identified by a `source_alias` (from `super_config.yaml`) into the `./external_configs/` directory at the specified `destination_relative_path`. The source alias must resolve to an existing `.yaml` file. The destination path must not already exist, and its parent directory must exist. + * **Request Body:** + ```json + { + "source_alias": "string (required, alias from super_config.yaml)", + "destination_relative_path": "string (required, new relative path in external_configs)" + } + ``` + * **Response (Status 201):** + ```json + { + "message": "Successfully duplicated config from alias '{source_alias}' to '{destination_relative_path}' in external_configs." + } + ``` + * **Error Responses:** + * 400 Bad Request: If the source alias doesn't point to a `.yaml` file, if destination path is invalid, or if destination parent directory doesn't exist. + * 404 Not Found: If the source alias is not found in `super_config.yaml` or the resolved source file doesn't exist. + * 409 Conflict: If the destination path already exists. + * 500 Internal Server Error: If resolving paths, reading the source, or writing the destination file fails. diff --git a/atk-interface/eslint.config.js b/atk-interface/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..ec2b712d301e7ddeca8327fc4b967069cc82047b --- /dev/null +++ b/atk-interface/eslint.config.js @@ -0,0 +1,33 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/atk-interface/index.html b/atk-interface/index.html new file mode 100644 index 0000000000000000000000000000000000000000..8d9b2778b358cdf7b70156ddea974e30dce204a5 --- /dev/null +++ b/atk-interface/index.html @@ -0,0 +1,13 @@ + + + + + + + Augmentoolkit + + +
+ + + diff --git a/atk-interface/package-lock.json b/atk-interface/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..95af210489ea2858dc6c8ec08fb4e51d6053ac16 --- /dev/null +++ b/atk-interface/package-lock.json @@ -0,0 +1,4554 @@ +{ + "name": "atk-interface", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "atk-interface", + "version": "0.0.0", + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "file-saver": "^2.0.5", + "framer-motion": "^12.7.4", + "jszip": "^3.10.1", + "react": "^19.0.0", + "react-arborist": "^3.4.3", + "react-dom": "^19.0.0", + "react-dropzone": "^14.3.8", + "react-icons": "^5.5.0", + "react-router-dom": "^7.5.1" + }, + "devDependencies": { + "@eslint/js": "^9.22.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.21", + "eslint": "^9.22.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "vite": "^6.3.1" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", + "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.25.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.0.tgz", + "integrity": "sha512-iWhsUS8Wgxz9AXNfvfOPFSW4VfMXdVhp1hjkZVhXCrpgh/aLcc45rX6MPu+tIVUWDw0HfNwth7O28M1xDxNf9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@monaco-editor/loader": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", + "integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==", + "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", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@react-dnd/asap": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz", + "integrity": "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==", + "license": "MIT" + }, + "node_modules/@react-dnd/invariant": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==", + "license": "MIT" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", + "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", + "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", + "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", + "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", + "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", + "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", + "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", + "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", + "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", + "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", + "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", + "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", + "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", + "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", + "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", + "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", + "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", + "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", + "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", + "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", + "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz", + "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", + "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.10", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dnd-core": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.1.tgz", + "integrity": "sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==", + "license": "MIT", + "dependencies": { + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.1.1" + } + }, + "node_modules/dnd-core/node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.139", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.139.tgz", + "integrity": "sha512-GGnRYOTdN5LYpwbIr0rwP/ZHOQSvAF6TG0LSzp28uCBb9JiXHJGmaaKw29qjNJc5bGnnp6kXJqRnGMQoELwi5w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.25.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.0.tgz", + "integrity": "sha512-MsBdObhM4cEwkzCiraDv7A6txFXEqtNXOb877TsSp2FCkBNl8JfVQrmiuDqC1IkejT6JLPzYBXx/xAiYhyzgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.25.0", + "@eslint/plugin-kit": "^0.2.8", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", + "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.7.4", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.7.4.tgz", + "integrity": "sha512-jX0bPsTmU0oPZTYz/dVyD0dmOyEOEJvdn0TaZBE5I8g2GvVnnQnW9f65cJnoVfUkY3WZWNXGXnPbVA9YnaIfVA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.7.4", + "motion-utils": "^12.7.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", + "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "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/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/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", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/monaco-editor": { + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "license": "MIT", + "peer": true + }, + "node_modules/motion-dom": { + "version": "12.7.4", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.7.4.tgz", + "integrity": "sha512-1ZUHAoSUMMxP6jPqyxlk9XUfb6NxMsnWPnH2YGhrOhTURLcXWbETi6eemoKb60Pe32NVJYduL4B62VQSO5Jq8Q==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.7.2" + } + }, + "node_modules/motion-utils": { + "version": "12.7.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.7.2.tgz", + "integrity": "sha512-XhZwqctxyJs89oX00zn3OGCuIIpVevbTa+u82usWBC6pSHUd2AoNWiYa7Du8tJxJy9TFbZ82pcn5t7NOm1PHAw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "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", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-arborist": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/react-arborist/-/react-arborist-3.4.3.tgz", + "integrity": "sha512-yFnq1nIQhT2uJY4TZVz2tgAiBb9lxSyvF4vC3S8POCK8xLzjGIxVv3/4dmYquQJ7AHxaZZArRGHiHKsEewKdTQ==", + "license": "MIT", + "dependencies": { + "react-dnd": "^14.0.3", + "react-dnd-html5-backend": "^14.0.3", + "react-window": "^1.8.11", + "redux": "^5.0.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": ">= 16.14", + "react-dom": ">= 16.14" + } + }, + "node_modules/react-dnd": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.5.tgz", + "integrity": "sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==", + "license": "MIT", + "dependencies": { + "@react-dnd/invariant": "^2.0.0", + "@react-dnd/shallowequal": "^2.0.0", + "dnd-core": "14.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz", + "integrity": "sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==", + "license": "MIT", + "dependencies": { + "dnd-core": "14.0.1" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-dropzone": { + "version": "14.3.8", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz", + "integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.1.tgz", + "integrity": "sha512-/jjU3fcYNd2bwz9Q0xt5TwyiyoO8XjSEFXJY4O/lMAlkGTHWuHRAbR9Etik+lSDqMC7A7mz3UlXzgYT6Vl58sA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.1.tgz", + "integrity": "sha512-5DPSPc7ENrt2tlKPq0FtpG80ZbqA9aIKEyqX6hSNJDlol/tr6iqCK4crqdsusmOSSotq6zDsn0y3urX9TuTNmA==", + "license": "MIT", + "dependencies": { + "react-router": "7.5.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-window": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz", + "integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "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/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", + "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.0", + "@rollup/rollup-android-arm64": "4.40.0", + "@rollup/rollup-darwin-arm64": "4.40.0", + "@rollup/rollup-darwin-x64": "4.40.0", + "@rollup/rollup-freebsd-arm64": "4.40.0", + "@rollup/rollup-freebsd-x64": "4.40.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", + "@rollup/rollup-linux-arm-musleabihf": "4.40.0", + "@rollup/rollup-linux-arm64-gnu": "4.40.0", + "@rollup/rollup-linux-arm64-musl": "4.40.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-musl": "4.40.0", + "@rollup/rollup-linux-s390x-gnu": "4.40.0", + "@rollup/rollup-linux-x64-gnu": "4.40.0", + "@rollup/rollup-linux-x64-musl": "4.40.0", + "@rollup/rollup-win32-arm64-msvc": "4.40.0", + "@rollup/rollup-win32-ia32-msvc": "4.40.0", + "@rollup/rollup-win32-x64-msvc": "4.40.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz", + "integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.3", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.12" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/atk-interface/package.json b/atk-interface/package.json new file mode 100644 index 0000000000000000000000000000000000000000..071cafdee2ba7a202d6a1cb2a9202c20ec24be5a --- /dev/null +++ b/atk-interface/package.json @@ -0,0 +1,38 @@ +{ + "name": "atk-interface", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "file-saver": "^2.0.5", + "framer-motion": "^12.7.4", + "jszip": "^3.10.1", + "react": "^19.0.0", + "react-arborist": "^3.4.3", + "react-dom": "^19.0.0", + "react-dropzone": "^14.3.8", + "react-icons": "^5.5.0", + "react-router-dom": "^7.5.1" + }, + "devDependencies": { + "@eslint/js": "^9.22.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.21", + "eslint": "^9.22.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "vite": "^6.3.1" + } +} diff --git a/atk-interface/pages_README.md b/atk-interface/pages_README.md new file mode 100644 index 0000000000000000000000000000000000000000..4acb681374dd1be8225809c7e90426bd2279a576 --- /dev/null +++ b/atk-interface/pages_README.md @@ -0,0 +1,134 @@ +This codebase will contain an interface for interacting with a locally-running Augmentoolkit server. The structure of the Augmentoolkit server's API is described in api_README.md. + +A basic sketch of pages is laid out below: +pages are probably +1. navigation. Configs, Inputs, and Outputs? And pipeline execution babysitting. +2. Config editing. Modify config fields directly, oh and if you point input_dir or output_dir at something that does not exist, it warns you. + - this page will have the ability for executing a pipeline too. +3. Input editing. Upload files, view what files are there, delete files. Main thing is sorta like a filebrowser over the structure we get back as a result. Download button on the selected item, alternatively shift click or drag select and download all selected items, everything will feel nice and snappy. +4. Output editing. Same operations as inputs plus it shows a list of all task IDs this session and lets you navigate to any output dir from there. +5. pipeline execution babysitting! We have this big progress bar at the top as well as an expandable details box off to the side showing (read only) the config we ran with with syntax highlighting. details key will include execution stats, that's what I'll use that reserved field for -- and the interface will proudly display whatever key-value pairs are added to the details as part of pipeline execution. It polls details and progress once a second when this screen is on (single page app you can click between screens and go back to the last one and it will be where you left off on it). A big download output dir button. A big cancel execution button with a confirm modal. And in a window, also updating (maybe once every .5 seconds or less) we have the logs streaming in (efficiently, using the cursor). + +Some design brainstorming is below: +Tactile feedback and style: +1. dark mode maybe gradient for modern feel. Motion/replaying things in the background, maybe just moving shapes, maybe something else that exudes AI techological feel like light effects against a dark background, falling symbols or whatever, just give the ADD brains something to be mesmerized by during lulls. Idea: not falling symbols. There'll be one or mulitple beads of light travelling along the paths created by a diagram of a neural network. You know, the thing with all the nodes interconnected with lines? Would allow for a lot of variation, is topical, is hopefully easy to code? Hmm.) +2. buttons shake and move when pressed and generally satisfying +3. highlights, different color based on actions +4. stylize things like the drag select, download, modals, make you feel the actions +5. make errors less painful by cheerful presentation while displaying all important information. +6. Rounded corners etc. +7. When datagen runs finish, the mood is triumphant. +8. High numbers in the details box change its color and effects? +9. Particle effects? +10. text boxes smooth and elegant Whatever makes typing seem smoother or whatever, more fluid and flowing, do that. Colored typing cursor? +11. and have a "boring mode" toggle in the 6. settings page that disables all the fun stuff. +12. actions, when taken, should highlight/draw attention to the natural next action through design. When a dataset generation run finishes, the "download output dir" (or however it is more concisely worded) button should react, be highlighted, appealing, etc. When files are selected in the input or output editing pages (such as potentially for downloading) the download button reacts. If a config is being edited, and the input_dir or output_dir is pointing to something which, we can tell from the structure of those does not exist, and the interface is warning you, the "run this config" should be less obvious or imply that it is a bad idea. This makes using the interface self-explanatory and easier and more fun and also makes doing so more engaging. + +### Specific tools we will use to achieve visual impact: ### +Particle effects +coloration changes based on page and activity +Lighting following interaction +3d objects that move with the mouse +Shaking background for important actions +Background fades for transition from one page to another +...more... + +NOTE things like particle effects, which are cool but are unusual for this sort of thing, will get used only for the MOST important things (like progress bar) so that we don't jar/look pretentious and overdesigned/so that it remains tasteful + +### + +General actions: +modals autofocus the first text box and each step in a modal guides you to the next +new files that are created should be highlighted until some action is taken on the page +IF you can't tell the effect is there, the effect is either extraneous and should be removed, or should be strengthened +too many effects (or too many effects in different places) when an action is taken is confusing. Draw their attention to one place (see the or double click to open OVER THE DELETE BUTTON in configs management. + + +Everything should be very performant to keep the project fast to clone and set up. No large clunky videos in the background playing on loop -- generate things. Maybe we can use a simple web physics thing or lighting thing or whatever -- think like an svg over a png, we will make the most excessive style things be done in elegant ways, code- and space-wise. + +Style thoughts: +We're going to make this nice to look at, engaging and stylistic, and make it give feedback on the basic actions in order to make the work more engaging, fun, and viral. We're going to take lessons from game mechanics and make the "second-to-second" experience really fun, in fact absurdly fun for software use. However this new approach must be done with style and panache. Quips in modals must be funny not forced. Effects must be sexy not garish and obnoxious. The fine line will be walked and we will not fall off. +But as we walk it we ascend. + +The unconventional philosophy with regards to style here is taking a page from video game creation. Apparently great care goes into making the second-to-second experience of games great, addicting, appealing, enjoyable. For instance, in a card game when you mouse over a card, ig might get bigger; its background might animate; a sound might play; particle effects may surround it; and when you click it, the places where you may play it also light up, new sounds play etc. Every action suggests the next action through effects, and everything feels cool to do. Ideally this software, though iti s for LLM dataset generation, would also be fun/cool to use. So we are adopting that approach. + + +-- How this runs -- + +This will be run on a user's local machine and connect to a local Augmentoolkit server. +The tech stack should be optimized for smooth, stylish operation and **easy setup** without catastrophic dependency bloat. +Web or app based, either way is fine + +Notably, any AI agent taking on development of this will lack a lot of context on what Augmentoolkit is and does. For most explanatory strings you will want to stub them or leave todos. We'll have to collaborate w.r.t. what information is important to show and how, and what the workflows are, though much of this is either revealed via the api structure or the description here. + +Flexible design is key, and being able to change things without messing up the entire project is important. We want flexibility in UI updates, in state updates -- fragile programming is a big nono since this is being hacked together and hacked on. We want the AK47 of tech stack choice, design, and approach. It should work even if I drag it hrough the figurative mud. The API it is using is pretty simple after all. + +### TECH STACK!!! + +The local interface for the Augmentoolkit server will be a **web-based application** (specifically a Single Page Application, or SPA) built with the following technologies: + +- **Type**: Web-based application (SPA) +- **Framework**: React with Vite +- **State Management**: Context API +- **Styling**: Tailwind CSS +- **Animations**: Framer Motion +- **Config Editing**: Monaco Editor +- **File Management**: Custom components with `react arborist` for performance, customization, and modernity. +- **API Communication**: Fetch API +- **Real-Time Updates**: Polling with `setInterval` +- **Development Tools**: ESLint, Prettier, Jest, React Testing Library + +#### Thought Process Behind Tech Stack Decisions + +1. **Web-Based Application (SPA)** + - **Why**: A web-based interface simplifies deployment and user setup. Users can run the Augmentoolkit server and access the interface through a browser, eliminating the need for platform-specific builds or complex installations. This aligns with the goal of making setup easy and accessible. + - **Reasoning**: Leveraging HTML, CSS, and JavaScript ensures cross-platform compatibility and rapid development, as these are widely supported and familiar technologies. + +2. **React with Vite** + - **Why**: React’s component-based architecture is perfect for building a modular SPA with distinct pages like Configs, Inputs, Outputs, and Pipeline Execution. Vite enhances this by offering faster builds and a modern development experience compared to alternatives like Create React App. + - **Reasoning**: React’s ecosystem and Vite’s performance make this combination ideal for quick iteration and scalability. + +3. **Context API for State Management** + - **Why**: The Context API, built into React, provides a lightweight solution for managing state (e.g., task IDs, config data) across components. It avoids the complexity of heavier tools like Redux for this project’s scope. + - **Reasoning**: Simplicity and integration with React make it a practical choice for a focused interface. + +4. **Tailwind CSS for Styling** + - **Why**: Tailwind CSS, a utility-first framework, replaces Styled Components in this stack. It’s elegant, concise, and familiar to you, offering rapid styling with extensive customization options. Unlike Styled Components, which you’re less comfortable with, Tailwind’s utility classes allow you to style directly in your markup, streamlining development. + - **Reasoning**: Tailwind supports quick iteration and consistency while enabling creative UI designs (e.g., gradients, dark mode, tactile feedback), aligning with your preference and the goal of an engaging, game-like interface. + +5. **Framer Motion for Animations** + - **Why**: Framer Motion integrates seamlessly with React and simplifies the creation of sophisticated animations (e.g., moving beads of light, particle effects) to make the interface fun and dynamic. + - **Reasoning**: Its ease of use and power enhance the user experience without adding unnecessary complexity. + +6. **Monaco Editor for Config Editing** + - **Why**: The Monaco Editor (the engine behind VS Code) offers a robust, feature-rich environment for editing YAML configs, complete with syntax highlighting and validation to catch errors like invalid paths. + - **Reasoning**: It’s a proven tool that elevates the config-editing experience for users. + +7. **Custom Components with `react-folder-tree` for File Management** + - **Why**: This lightweight library enables a clear, interactive directory structure for the Inputs and Outputs pages, supporting actions like browsing, uploading, downloading, and deleting files. + - **Reasoning**: It’s simple yet effective for file-related functionality without overcomplicating the stack. + +8. **Fetch API for API Communication** + - **Why**: The Fetch API, built into browsers, is straightforward and sufficient for REST API interactions, avoiding the need for external libraries like Axios. + - **Reasoning**: It’s lightweight and meets the project’s communication needs efficiently. + +9. **Polling with `setInterval` for Real-Time Updates** + - **Why**: Polling with `setInterval` provides a simple way to fetch task status, progress, and logs in real time. Intervals can be adjusted dynamically based on task activity for optimal performance. + - **Reasoning**: It’s easy to implement and effective for this use case, balancing simplicity and responsiveness. + +10. **Development Tools** + - **Why**: ESLint and Prettier enforce code quality and consistent formatting, while Jest and React Testing Library enable unit and integration testing for reliability. + - **Reasoning**: These tools ensure maintainability and robustness, supporting a smooth development process. + +This tech stack strikes a balance between **performance**, **ease of development**, and **flexibility**. It leverages Tailwind CSS for elegant, efficient styling per your preference, while delivering a functional, engaging interface that’s quick to set up and maintain. + +**Start the development server**: +```bash +npm run dev +``` + + +Note to self -- when you run at start the top tabs will react based on common flows. Usually you'll go to configs first, to run something. So, that one will be reacting. Pipeline monitor may be more demure than usually. + + +So beyond pipeline monitoring, we have a config revision to do (easyish), and also log management (easy) \ No newline at end of file diff --git a/atk-interface/postcss.config.js b/atk-interface/postcss.config.js new file mode 100644 index 0000000000000000000000000000000000000000..2e7af2b7f1a6f391da1631d93968a9d487ba977d --- /dev/null +++ b/atk-interface/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/atk-interface/public/vite.svg b/atk-interface/public/vite.svg new file mode 100644 index 0000000000000000000000000000000000000000..e7b8dfb1b2a60bd50538bec9f876511b9cac21e3 --- /dev/null +++ b/atk-interface/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/atk-interface/src/.gitignore b/atk-interface/src/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a547bf36d8d11a4f89c59c144f24795749086dd1 --- /dev/null +++ b/atk-interface/src/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/atk-interface/src/App.css b/atk-interface/src/App.css new file mode 100644 index 0000000000000000000000000000000000000000..b9d355df2a5956b526c004531b7b0ffe412461e0 --- /dev/null +++ b/atk-interface/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/atk-interface/src/App.jsx b/atk-interface/src/App.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6b74e94cf27d0c8b60358e3ed95fbf6b6e57c575 --- /dev/null +++ b/atk-interface/src/App.jsx @@ -0,0 +1,40 @@ +import { Routes, Route } from 'react-router-dom'; +import { TaskHistoryProvider } from './context/TaskHistoryContext'; +import { FileManagerProvider } from './context/FileManagerContext'; +import Layout from './components/Layout'; +import HomePage from './pages/HomePage'; +import ConfigSelector from './pages/ConfigSelector'; +import ConfigEditor from './pages/ConfigEditor'; +import InputManager from './pages/InputManager'; +import OutputManager from './pages/OutputManager'; +import PipelineMonitor from './pages/PipelineMonitor'; +import NotFound from './pages/NotFound'; +import LogManager from './pages/LogManager'; +import LogViewer from './pages/LogViewer'; +import Chat from './pages/Chat'; +// import './App.css'; // Keep or remove based on whether it has useful base styles + +function App() { + return ( + + + + }> + } /> {/* Default route is now HomePage */} + } /> {/* Route for selecting configs */} + } /> {/* Route for editing a specific config */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> {/* Catch-all for 404 */} + + + + + ); +} + +export default App; diff --git a/atk-interface/src/api.js b/atk-interface/src/api.js new file mode 100644 index 0000000000000000000000000000000000000000..a2add039e02e70f46cb6bbcd277ac0e0d93b8311 --- /dev/null +++ b/atk-interface/src/api.js @@ -0,0 +1,490 @@ +import { handleApiError, getApiUrl } from './utils/apiUtils'; + +const API_BASE_URL = window.location.origin; // Use current origin for deployment + +// Helper to handle common fetch logic and errors +async function handleApiResponse(response) { + if (!response.ok) { + let errorMsg = `API Error: ${response.status} ${response.statusText}`; + let errorData = null; + try { + // Attempt to parse error detail from backend + errorData = await response.json(); + errorMsg = errorData.detail || JSON.stringify(errorData) || errorMsg; + } catch (e) { + // If parsing fails, try reading as text + try { + errorMsg = await response.text() || errorMsg; + } catch (textError) { /* Ignore text reading error */ } + } + console.error('API Error Data:', errorData); + const error = new Error(errorMsg); + error.status = response.status; + error.data = errorData; + throw error; + } + // If response has content, parse as JSON, otherwise return the response object + const contentType = response.headers.get("content-type"); + if (contentType && contentType.indexOf("application/json") !== -1) { + return response.json(); + } + if (contentType && contentType.indexOf("text/") !== -1) { + return response.text(); // Handle text responses like config content + } + // For downloads or other types, return the raw response + return response; +} + +// --- Config API --- + +export async function fetchConfigStructure(relativePath = '') { + const path = relativePath ? `/${encodeURIComponent(relativePath)}` : '.'; + const response = await fetch(`${API_BASE_URL}/configs/structure/${path}`); + return handleApiResponse(response); +} + +export async function fetchConfigContent(relativePath) { + if (!relativePath) throw new Error('Relative path is required for fetchConfigContent'); + const response = await fetch(`${API_BASE_URL}/configs/content/${encodeURIComponent(relativePath)}`); + return handleApiResponse(response); +} + +export async function saveConfigContent(relativePath, content) { + if (!relativePath) throw new Error('Relative path is required for saveConfigContent'); + const response = await fetch(`${API_BASE_URL}/configs/content/${encodeURIComponent(relativePath)}`, { + method: 'POST', + headers: { 'Content-Type': 'text/plain' }, + body: content, + }); + return handleApiResponse(response); +} + +/** + * Creates a new directory within the configs folder. + * @param {string} relativePath - The relative path for the new directory. + * @returns {Promise} - API response. + */ +export const createConfigDirectory = async (relativePath) => { + const url = getApiUrl('/configs/directory'); + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ relative_path: relativePath }), + }); + // Use handleApiResponse which includes error handling + return handleApiResponse(response); +}; + +/** + * Deletes a specific config file. + * @param {string} relativePath - The relative path of the file to delete. + * @returns {Promise} - API response. + */ +export const deleteConfigItem = async (relativePath) => { + const url = getApiUrl(`/configs/${encodeURIComponent(relativePath)}`); + const response = await fetch(url, { method: 'DELETE' }); + // Use handleApiResponse which includes error handling + return handleApiResponse(response); +}; + +/** + * Moves/Renames a file or folder within the configs directory. + * @param {string} sourceRelativePath - Current relative path. + * @param {string} destinationRelativePath - New relative path. + * @returns {Promise} - API response. + */ +export const moveConfigItem = async (sourceRelativePath, destinationRelativePath) => { + const url = getApiUrl('/configs/move'); + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + source_relative_path: sourceRelativePath, + destination_relative_path: destinationRelativePath, + }), + }); + // Use handleApiResponse which includes error handling + return handleApiResponse(response); +}; + +/** + * Duplicates a config file from a source alias to a destination path. + * @param {string} sourceAlias - The alias of the config file to duplicate. + * @param {string} destinationRelativePath - The destination path for the new file. + * @returns {Promise} - API response. + */ +export const duplicateConfig = async (sourceAlias, destinationRelativePath) => { + const url = getApiUrl('/configs/duplicate'); + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + source_alias: sourceAlias, + destination_relative_path: destinationRelativePath, + }), + }); + // Use handleApiResponse which includes error handling + return handleApiResponse(response); +}; + +/** + * Fetches available config aliases (e.g., from super_config.yaml) that can be used as sources for duplication. + * NOTE: Assumes a backend endpoint GET /configs/aliases exists. + * @returns {Promise} - A list of config alias strings. + */ +export const fetchAvailableConfigAliases = async () => { + // TODO: Verify/Implement the corresponding backend endpoint GET /configs/aliases + const url = getApiUrl('/configs/aliases'); // Assuming this endpoint exists + const response = await fetch(url); + // Use handleApiResponse which includes error handling + return handleApiResponse(response); +}; + +// --- Pipeline API --- + +export const fetchAvailablePipelines = async () => { + const response = await fetch(getApiUrl('/pipelines/available')); + if (!response.ok) throw new Error('Failed to fetch available pipelines'); + return response.json(); +}; + +export async function runPipeline(nodePath, parameters) { + const response = await fetch(`${API_BASE_URL}/pipelines/run`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ node_path: nodePath, parameters: parameters }), + }); + return handleApiResponse(response); +} + +// --- Input API --- + +export const fetchInputStructure = async (relativePath = '.') => { + const url = getApiUrl(`/inputs/structure/${encodeURIComponent(relativePath)}`); + const response = await fetch(url); + if (!response.ok) { + await handleApiError(response, 'fetch input structure'); + } + return response.json(); +}; + +export const uploadInputFiles = async (relativePath, files) => { + const url = getApiUrl(`/inputs/upload/${encodeURIComponent(relativePath)}`); + const formData = new FormData(); + files.forEach(file => formData.append('files', file, file.name)); // Use 'files' as key + + const response = await fetch(url, { + method: 'POST', + body: formData, + // No 'Content-Type' header, browser sets it with boundary for multipart/form-data + }); + + if (!response.ok && response.status !== 207) { // 207 Multi-Status might indicate partial success + await handleApiError(response, 'upload input files'); + } + // Even on 207, parse the body for details + const result = await response.json(); + if (response.status === 207 && result.errors?.length > 0) { + // Optionally throw a more specific error or handle partial failures + console.warn('Partial upload failure:', result.errors); + // Example: throw new Error(`Upload completed with errors: ${result.errors.join(', ')}`); + } + if (!response.ok && response.status !== 207) { + // Throw if it wasn't OK and wasn't a 207 + throw new Error(result.message || 'Upload failed'); + } + return result; +}; + +export const downloadInputItem = async (relativePath) => { + const url = getApiUrl(`/inputs/download/${encodeURIComponent(relativePath)}`); + const response = await fetch(url); + if (!response.ok) { + await handleApiError(response, `download input item: ${relativePath}`); + } + // Don't parse JSON, return the raw response for blob processing + return response; +}; + +export const deleteInputItem = async (relativePath) => { + const url = getApiUrl(`/inputs/${encodeURIComponent(relativePath)}`); + const response = await fetch(url, { method: 'DELETE' }); + if (!response.ok) { + await handleApiError(response, `delete input item: ${relativePath}`); + } + return response.json(); // Or just check status if no body expected on success +}; + +export const createInputDirectory = async (relativePath) => { + const url = getApiUrl('/inputs/directory'); + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ relative_path: relativePath }), + }); + if (!response.ok) { + await handleApiError(response, `create input directory: ${relativePath}`); + } + return response.json(); +}; + +/** + * Moves a file or folder within the inputs directory. + * @param {string} sourceRelativePath - The current relative path of the item to move. + * @param {string} destinationRelativePath - The new relative path for the item. + * @returns {Promise} - The response object from the API. + */ +export const moveInputItem = async (sourceRelativePath, destinationRelativePath) => { + const url = getApiUrl('/inputs/move'); + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + source_relative_path: sourceRelativePath, + destination_relative_path: destinationRelativePath, + }), + }); + if (!response.ok) { + await handleApiError(response, `move input item from ${sourceRelativePath} to ${destinationRelativePath}`); + } + return response.json(); +}; + +// --- Output API --- + +export const fetchOutputStructure = async (relativePath = '.') => { + const url = getApiUrl(`/outputs/structure/${encodeURIComponent(relativePath)}`); + const response = await fetch(url); + if (!response.ok) { + await handleApiError(response, 'fetch output structure'); + } + return response.json(); +}; + +export const downloadOutputItem = async (relativePath) => { + const url = getApiUrl(`/outputs/download/${encodeURIComponent(relativePath)}`); + const response = await fetch(url); + if (!response.ok) { + await handleApiError(response, `download output item: ${relativePath}`); + } + // Return raw response for blob processing + return response; +}; + +export const deleteOutputItem = async (relativePath) => { + const url = getApiUrl(`/outputs/${encodeURIComponent(relativePath)}`); + const response = await fetch(url, { method: 'DELETE' }); + if (!response.ok) { + await handleApiError(response, `delete output item: ${relativePath}`); + } + return response.json(); // Or check status +}; + +export const moveOutputItem = async (sourceRelativePath, destinationRelativePath) => { + const url = getApiUrl('/outputs/move'); + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + source_relative_path: sourceRelativePath, + destination_relative_path: destinationRelativePath, + }), + }); + if (!response.ok) { + await handleApiError(response, `move output item from ${sourceRelativePath} to ${destinationRelativePath}`); + } + return response.json(); +}; + +// --- Task API --- + +/** + * Fetches the status of a specific task. + * @param {string} taskId - The ID of the task. + * @returns {Promise} - Task status object { task_id, status, message?, progress?, details? }. + */ +export const fetchTaskStatus = async (taskId) => { + if (!taskId) throw new Error('Task ID is required for fetchTaskStatus'); + const url = getApiUrl(`/tasks/${encodeURIComponent(taskId)}/status`); + const response = await fetch(url); + if (!response.ok) { + await handleApiError(response, `fetch task status for ${taskId}`); + } + return response.json(); +}; + +/** + * Fetches the parameters used for a specific task run. + * @param {string} taskId - The ID of the task. + * @returns {Promise} - Object containing { task_id, parameters }. + */ +export const fetchTaskParameters = async (taskId) => { + if (!taskId) throw new Error('Task ID is required for fetchTaskParameters'); + const url = getApiUrl(`/tasks/${encodeURIComponent(taskId)}/parameters`); + const response = await fetch(url); + // Handle 404 specifically for parameters, as it might just mean they weren't stored/expired + if (response.status === 404) { + console.warn(`Parameters not found for task ${taskId}. They might not have been stored or may have expired.`); + return { task_id: taskId, parameters: null }; // Return null for parameters + } + if (!response.ok) { + await handleApiError(response, `fetch task parameters for ${taskId}`); + } + return response.json(); +}; + +/** + * Interrupts a running task or revokes a pending one. + * @param {string} taskId - The ID of the task. + * @returns {Promise} - API response message. + */ +export const interruptTask = async (taskId) => { + if (!taskId) throw new Error('Task ID is required for interruptTask'); + const url = getApiUrl(`/tasks/${encodeURIComponent(taskId)}/interrupt`); + const response = await fetch(url, { method: 'POST' }); + if (!response.ok) { + await handleApiError(response, `interrupt task ${taskId}`); + } + return response.json(); +}; + +/** + * Fetches the list of pending and scheduled tasks. + * @returns {Promise} - Object containing { pending_tasks, scheduled_tasks, message }. + */ +export const fetchTaskQueue = async () => { + const url = getApiUrl('/tasks/queue'); + const response = await fetch(url); + if (!response.ok) { + await handleApiError(response, 'fetch task queue'); + } + return response.json(); +}; + +/** + * Fetches the logs for a specific task. + * @param {string} taskId - The ID of the task. + * @param {number} [tail] - Optional number of lines to tail. + * @returns {Promise} - Object containing { task_id, message, logs }. + */ +export const fetchTaskLogs = async (taskId, tail) => { + if (!taskId) throw new Error('Task ID is required for fetchTaskLogs'); + let url = getApiUrl(`/tasks/${encodeURIComponent(taskId)}/logs`); + if (tail !== undefined && tail !== null) { + url += `?tail=${tail}`; + } + const response = await fetch(url); + // Handle 404 gracefully, log file might not exist yet + if (response.status === 404) { + console.log(`Log file for task ${taskId} not found.`); + return { task_id: taskId, message: "Log file not found.", logs: "" }; + } + if (!response.ok) { + await handleApiError(response, `fetch task logs for ${taskId}`); + } + return response.json(); +}; + +/** + * Deletes the log file for a specific task. + * @param {string} taskId - The ID of the task. + * @returns {Promise} - API response message. + */ +export const deleteTaskLogs = async (taskId) => { + if (!taskId) throw new Error('Task ID is required for deleteTaskLogs'); + const url = getApiUrl(`/tasks/${encodeURIComponent(taskId)}/logs`); + const response = await fetch(url, { method: 'DELETE' }); + if (!response.ok) { + await handleApiError(response, `delete task logs for ${taskId}`); + } + return response.json(); +}; + +/** + * Downloads the output directory for a specific task as a zip file. + * @param {string} taskId - The ID of the task. + * @returns {Promise} - The raw fetch Response object for blob processing. + */ +export const downloadTaskOutput = async (taskId) => { + if (!taskId) throw new Error('Task ID is required for downloadTaskOutput'); + const url = getApiUrl(`/tasks/${encodeURIComponent(taskId)}/outputs/download`); + const response = await fetch(url); + if (!response.ok) { + // Handle 404 specifically - output might not exist + if (response.status === 404) { + throw new Error(`Output directory not found for task ${taskId}.`); + } + await handleApiError(response, `download task output for ${taskId}`); + } + // Return raw response for blob processing + return response; +}; + +/** + * Fetches the list of available log files. + * GET /logs + * @returns {Promise} A promise that resolves to an array of log filenames. + */ +export const fetchLogFiles = async () => { + const response = await fetchWithErrorHandling(`${API_BASE_URL}/logs`); + const data = await response.json(); + // Ensure we return an array, even if the API response structure is slightly off + return Array.isArray(data?.log_files) ? data.log_files : []; +}; + +/** + * Deletes a specific log file based on its task ID. + * DELETE /tasks/{task_id}/logs + * @param {string} taskId - The task ID (extracted from the log filename). + * @returns {Promise} A promise that resolves to the API response message. + */ +export const deleteLogFile = async (taskId) => { + if (!taskId) { + throw new Error("Task ID is required to delete a log file."); + } + // Basic check to prevent deleting files without UUID-like format, adjust if task IDs differ + // if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(taskId)) { + // console.warn(`Attempting to delete log for potentially invalid Task ID: ${taskId}`); + // } + const response = await fetchWithErrorHandling(`${API_BASE_URL}/tasks/${encodeURIComponent(taskId)}/logs`, { + method: 'DELETE', + }); + return response.json(); +}; + +/** + * Clears all log files in the logs directory. + * DELETE /logs + * @returns {Promise} A promise that resolves to the API response (message and potential errors). + */ +export const clearAllLogs = async () => { + const response = await fetchWithErrorHandling(`${API_BASE_URL}/logs`, { + method: 'DELETE', + }); + return response.json(); // Example: {"message": "Successfully deleted X log file(s)...", "errors": []} +}; + +// Utility function assumed to exist from other files (InputManager/ConfigSelector context) +// If it doesn't exist, it needs to be created. +async function fetchWithErrorHandling(url, options = {}) { + const response = await fetch(url, options); + if (!response.ok) { + let errorPayload; + try { + errorPayload = await response.json(); // Try to parse JSON error detail + } catch (e) { + // Ignore parsing error, use status text + } + const errorMessage = errorPayload?.detail || response.statusText || `Request failed with status ${response.status}`; + console.error(`API Error (${response.status}) for ${options.method || 'GET'} ${url}:`, errorMessage, errorPayload); + // Throw an error that includes the status code and message + const error = new Error(errorMessage); + error.status = response.status; + error.payload = errorPayload; + throw error; + } + return response; +} + +// Utility to get the base API URL (if needed elsewhere, or for env vars) +// export const getBaseApiUrl = () => API_BASE_URL; \ No newline at end of file diff --git a/atk-interface/src/assets/atk-logo-notscuffed.png b/atk-interface/src/assets/atk-logo-notscuffed.png new file mode 100644 index 0000000000000000000000000000000000000000..48e60a8bb283f8e9b266d30c1f0dbe00c4a18540 Binary files /dev/null and b/atk-interface/src/assets/atk-logo-notscuffed.png differ diff --git a/atk-interface/src/assets/react.svg b/atk-interface/src/assets/react.svg new file mode 100644 index 0000000000000000000000000000000000000000..6c87de9bb3358469122cc991d5cf578927246184 --- /dev/null +++ b/atk-interface/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/atk-interface/src/components/ButtonParticles.jsx b/atk-interface/src/components/ButtonParticles.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b74dfd368e654ec2dd271b81fe71a2854570a413 --- /dev/null +++ b/atk-interface/src/components/ButtonParticles.jsx @@ -0,0 +1,79 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { motion } from 'framer-motion'; + +const random = (min, max) => Math.random() * (max - min) + min; + +const Particle = ({ id, color, size, duration }) => { + const angle = random(0, Math.PI * 2); + const radius = random(40, 85); // How far particles travel (Increased) + const endX = Math.cos(angle) * radius; + const endY = Math.sin(angle) * radius; + + return ( + + ); +}; + +const ButtonParticles = ({ trigger, color = '#ffffff', count = 8, size = 3.5, duration = 0.45 }) => { + const [particles, setParticles] = useState([]); + + console.log(`ButtonParticles: Rendered. Current trigger=${trigger}, particles=${particles.length}`); + + useEffect(() => { + console.log(`ButtonParticles: useEffect triggered. trigger=${trigger}`); + if (trigger > 0) { // trigger increments on each click + console.log(`ButtonParticles: Trigger > 0 (${trigger}). Generating ${count} particles.`); + const newParticles = Array.from({ length: count }).map((_, i) => ({ + id: `${trigger}-${i}`, // Unique key based on trigger and index + color, + size, + duration, + })); + setParticles(newParticles); + console.log("ButtonParticles: Particles state updated:", newParticles); + + // Optional: Clear particles after animation - not strictly needed as they animate to opacity 0 + const timer = setTimeout(() => { + console.log(`ButtonParticles: Clearing particles after ${duration}s timeout.`); + setParticles([]); + }, duration * 1000); // Clear particles after animation + return () => { + console.log("ButtonParticles: useEffect cleanup running."); + clearTimeout(timer) + }; + } + }, [trigger, color, count, size, duration]); + + // Render only the particles currently animating + return ( + <> + {particles.map(p => ( + {/* Use Fragment to add log without extra div */} + {/* {console.log(`ButtonParticles: Rendering Particle id=${p.id}`)} */} + + + ))} + + ); +}; + +export default ButtonParticles; \ No newline at end of file diff --git a/atk-interface/src/components/DocumentStackBackground.jsx b/atk-interface/src/components/DocumentStackBackground.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c8c999fd8a3b879db793ba7e6e07a99668609603 --- /dev/null +++ b/atk-interface/src/components/DocumentStackBackground.jsx @@ -0,0 +1,315 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { motion, useAnimation, AnimatePresence } from 'framer-motion'; + +// Helper function for random numbers +const random = (min, max) => Math.random() * (max - min) + min; + +/* ──────────────────────────────────────────────────────────────────────────── + Document: single paper in the stack + ------------------------------------------------------------------------*/ +const Document = ({ + id, + yOffset, + zIndex, + onAnimationComplete, + onSortMoveComplete, + isTop, + isBeingRemoved, + isExploding, + isSortingMove, + targetYOffset, + dimensions, +}) => { + const controls = useAnimation(); + + // Random explosion vectors (memoised per mount) + const explodeX = useRef(random(-dimensions.width * 0.4, dimensions.width * 0.4)).current; + const explodeY = useRef(random(-dimensions.height * 0.4, dimensions.height * 0.4)).current; + const explodeRotate = useRef(random(-180, 180)).current; + + /* --------------------------------------------------------------------- + Imperative animation orchestrator + ---------------------------------------------------------------------*/ + const animateDocument = useCallback(async () => { + if (isExploding) { + // Radial blast + await controls.start({ + x: explodeX, + translateY: yOffset + explodeY, + rotate: explodeRotate, + opacity: 0, + transition: { duration: 1.5, ease: 'easeOut' }, + }); + } else if (isBeingRemoved && isTop) { + // Toss off-screen + await controls.start({ + x: random(dimensions.width * 0.3, dimensions.width * 0.6) * (Math.random() > 0.5 ? 1 : -1), + translateY: -dimensions.height * 0.6, + rotate: random(-90, 90), + opacity: 0, + transition: { duration: 0.8, ease: 'easeOut' }, + }); + onAnimationComplete?.(); + } else if (isSortingMove) { + /* --- Three-stage sideways shuffle --- */ + const sideOffset = 250; + const moveOutDuration = 0.3; + const moveDownDuration = 0.4; + const moveInDuration = 0.3; + + // Bring to front instantly + await controls.start({ opacity: 1, x: 0, translateY: yOffset, rotate: 0, zIndex: zIndex + 10 }, { duration: 0 }); + // 1. Sideways + await controls.start({ x: sideOffset, transition: { duration: moveOutDuration, ease: 'easeOut' } }); + // 2. Downwards to slot + await controls.start({ translateY: targetYOffset, transition: { duration: moveDownDuration, ease: 'circIn' } }); + // 3. Back in + await controls.start({ x: 0, transition: { duration: moveInDuration, ease: 'easeOut' } }); + + onSortMoveComplete?.(id); + } else { + // Idle re-stacking movement + await controls.start({ + x: 0, + translateY: yOffset, + zIndex, + rotate: 0, + opacity: 1, + transition: { duration: 0.5, ease: 'easeOut' }, + }); + onAnimationComplete?.(); + } + }, [controls, dimensions, explodeRotate, explodeX, explodeY, id, isBeingRemoved, isExploding, isSortingMove, isTop, targetYOffset, yOffset, zIndex, onAnimationComplete, onSortMoveComplete]); + + /* --------------------------------------------------------------------- + Run animator only *after* the motion.div has mounted. + ---------------------------------------------------------------------*/ + useEffect(() => { + const raf = requestAnimationFrame(animateDocument); + return () => cancelAnimationFrame(raf); + }, [animateDocument]); + + /* --------------------------------------------------------------------- + Disable Framer‑Motion layout magic while this doc is under manual + control (sorting, toss, or explosion). This prevents FM from measuring + a fresh layout on every React re‑render – the culprit behind the + "teleport‑back‑to‑centre" behaviour when the parent component updates. + ---------------------------------------------------------------------*/ + const enableLayout = !isSortingMove && !isBeingRemoved && !isExploding; + + return ( + + ); +}; + +/* ──────────────────────────────────────────────────────────────────────────── + DocumentStackBackground: orchestrates the pile of papers + ------------------------------------------------------------------------*/ +const DocumentStackBackground = ({ + triggerExplode = false, + triggerTossTop = false, + onExplodeComplete, + onTossComplete, +}) => { + const NUM_DOCUMENTS = 15; + const STACK_OFFSET_Y = 30; + + const [documents, setDocuments] = useState([]); + const [isAnimating, setIsAnimating] = useState(false); + const [isExploded, setIsExploded] = useState(false); + + const intervalRef = useRef(null); + const [movingDocInfo, setMovingDocInfo] = useState({ id: null, targetYOffset: 0 }); + + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); + const containerRef = useRef(null); + + /* ------------------------------------------------------------------- + Helpers: size, (re)initialisation, periodic shuffle + -------------------------------------------------------------------*/ + useEffect(() => { + const handleResize = () => { + if (containerRef.current) { + setDimensions({ + width: containerRef.current.offsetWidth, + height: containerRef.current.offsetHeight, + }); + } + }; + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + /* (re)build the stack */ + const initializeDocuments = useCallback(() => { + if (dimensions.height === 0) return; + + const initialDocs = Array.from({ length: NUM_DOCUMENTS }).map((_, i) => ({ + id: `doc-${i}-${Date.now()}`, + yOffset: (i - NUM_DOCUMENTS / 2 + 0.5) * STACK_OFFSET_Y, + zIndex: i, + isBeingRemoved: false, + })); + + setDocuments(initialDocs); + setMovingDocInfo({ id: null, targetYOffset: 0 }); + setIsExploded(false); + setIsAnimating(false); + }, [NUM_DOCUMENTS, STACK_OFFSET_Y, dimensions.height]); + + /* kick-off on mount */ + useEffect(() => initializeDocuments(), [initializeDocuments]); + + /* --------------------------------------------------------------- + Idle shuffling logic (unchanged) + ----------------------------------------------------------------*/ + const sortStep = useCallback(() => { + if (isAnimating || documents.length < 2 || isExploded || movingDocInfo.id) return; + + const indexToRemove = Math.floor(random(1, documents.length - 1)); + const docToMove = documents[indexToRemove]; + if (!docToMove) return; + + const targetYOffset = (documents.length - 1 - documents.length / 2 + 0.5) * STACK_OFFSET_Y; + + setIsAnimating(true); + setMovingDocInfo({ id: docToMove.id, targetYOffset }); + }, [STACK_OFFSET_Y, documents, isAnimating, isExploded, movingDocInfo.id]); + + /* … interval helpers (unchanged) … */ + const stopSortInterval = useCallback(() => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }, []); + + const startSortInterval = useCallback(() => { + stopSortInterval(); + if (!isAnimating && !isExploded) { + intervalRef.current = setInterval(sortStep, 1000); + } + }, [sortStep, stopSortInterval, isAnimating, isExploded]); + + /* start-stop idle shuffle based on explosion and animation state */ + useEffect(() => { + if (!isExploded && !isAnimating) { + startSortInterval(); + } else { + stopSortInterval(); + } + return stopSortInterval; + }, [isExploded, isAnimating, startSortInterval, stopSortInterval]); + + /* ----------------------------------------------------------------- + Toss-top trigger + - Stop interval implicitly by setting isAnimating flag + - Remove the doc from state *after* animation completes via onAnimationComplete + - Interval restart is handled by the useEffect watching isAnimating + ----------------------------------------------------------------*/ + useEffect(() => { + if (triggerTossTop && documents.length > 0 && !isAnimating && !isExploded) { + setIsAnimating(true); + setDocuments((prev) => { + const next = [...prev]; + if (next.length > 0) { + next[next.length - 1] = { ...next[next.length - 1], isBeingRemoved: true }; + } + return next; + }); + } + }, [triggerTossTop, documents.length, isAnimating, isExploded]); + + /* ----------------------------------------------------------------- + Explode trigger – *do not* immediately clear the list so that the + papers have something to animate! + ----------------------------------------------------------------*/ + useEffect(() => { + if (triggerExplode && !isExploded && !isAnimating) { + setIsAnimating(true); + setIsExploded(true); + setMovingDocInfo({ id: null, targetYOffset: 0 }); + } + }, [triggerExplode, isExploded, isAnimating]); + + /* once exploded, wait, then rebuild the stack */ + useEffect(() => { + if (!isExploded) return; + + const explosionDuration = 1500; + const reinitDelay = 1000; + const tid = setTimeout(() => { + initializeDocuments(); + onExplodeComplete?.(); + }, explosionDuration + reinitDelay); + + return () => clearTimeout(tid); + }, [isExploded, initializeDocuments, onExplodeComplete]); + + /* Callback for when *any* document's animation finishes */ + const handleDocumentAnimationComplete = useCallback((docId, isRemoval = false) => { + if (isRemoval) { + setDocuments((prev) => prev.filter(doc => doc.id !== docId)); + onTossComplete?.(); + setIsAnimating(false); + } + }, [onTossComplete]); + + /* -----------------------------------------------------------------*/ + return ( +
+ +
+ {documents.map((doc, idx) => ( + handleDocumentAnimationComplete(doc.id, doc.isBeingRemoved)} + onSortMoveComplete={(id) => { + setDocuments((prev) => { + const i = prev.findIndex((d) => d.id === id); + if (i === -1) return prev; + const copy = [...prev]; + const [moved] = copy.splice(i, 1); + copy.push(moved); + return copy.map((d, j) => ({ + ...d, + yOffset: (j - copy.length / 2 + 0.5) * STACK_OFFSET_Y, + zIndex: j, + })); + }); + setMovingDocInfo({ id: null, targetYOffset: 0 }); + setIsAnimating(false); + }} + dimensions={dimensions} + /> + ))} +
+
+
+ ); +}; + +// Wrap the component with React.memo +const MemoizedDocumentStackBackground = React.memo(DocumentStackBackground); + +export default MemoizedDocumentStackBackground; diff --git a/atk-interface/src/components/EnhancedButton.jsx b/atk-interface/src/components/EnhancedButton.jsx new file mode 100644 index 0000000000000000000000000000000000000000..dd26f0ef6ca12aa895d28efc62965b871b8590d8 --- /dev/null +++ b/atk-interface/src/components/EnhancedButton.jsx @@ -0,0 +1,96 @@ +import React, { useState } from "react"; +import { motion, useAnimation } from "framer-motion"; +import ButtonParticles from "./ButtonParticles"; + +const EnhancedButton = ({ + onClick, + children, + className = "", + disabled = false, + title = "", + particleColor = "#ffffff", + particleCount = 50, + particleSize = 10, + particleDuration = 3, + ...props +}) => { + const [isHovering, setIsHovering] = useState(false); + const [hasBeenClickedSinceHover, setHasBeenClickedSinceHover] = useState(false); + const [particleTrigger, setParticleTrigger] = useState(0); + + /* --- controller that drives ONLY the shake --- */ + const shake = useAnimation(); + + const fireShake = () => + shake.start({ + x: [0, -3, 3, -3, 3, 0], + y: [0, 3, -3, 3, -3, 0], + transition: { duration: 0.25, ease: "easeInOut" } + }); + + const handleClick = (e) => { + if (disabled) return; + onClick?.(e); + setHasBeenClickedSinceHover(true); + setParticleTrigger((t) => t + 1); + fireShake(); + }; + + /* -------- hover / normal variants (scale only) -------- */ + const variants = { + initial: { scale: 1, filter: "brightness(1)" }, + hoverInitial: { + scale: [1, 1.06, 1], + transition: { scale: { duration: 0.8, repeat: Infinity, ease: "easeInOut" } } + }, + hoverClicked: { scale: 1.03, transition: { duration: 0.2 } } + }; + + const state = + disabled + ? "initial" + : isHovering + ? hasBeenClickedSinceHover + ? "hoverClicked" + : "hoverInitial" + : "initial"; + + /* ---------------- render ---------------- */ + return ( + /* wrapper receives the shake controller */ + + { setIsHovering(true); setHasBeenClickedSinceHover(false); }} + onHoverEnd={() => setIsHovering(false)} + onClick={handleClick} + {...props} + > + + {children} + + + {/* particles */} +
+ +
+
+
+ ); +}; + +export default EnhancedButton; diff --git a/atk-interface/src/components/FireworksBackground.jsx b/atk-interface/src/components/FireworksBackground.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ff20e22850e30771e5c3918c22b2656f27fcdf0f --- /dev/null +++ b/atk-interface/src/components/FireworksBackground.jsx @@ -0,0 +1,261 @@ +import React, { + useState, + useEffect, + useRef, + useMemo, + useCallback, + } from 'react'; + import { motion } from 'framer-motion'; + + /* ---------- helpers ---------- */ + + const random = (min, max) => Math.random() * (max - min) + min; + const uuid = () => + typeof crypto !== 'undefined' && crypto.randomUUID + ? crypto.randomUUID() + : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; + +// Define possible firework colors +const fireworkColors = [ +// '#FF5733', // Orange-Red. NOTE we'll use a round of red fireworks for deletion and a round of bright green ones for downloading. Also we want the download/delete buttons to follow along the side of the page when scrolling far down due to a lot of items -- I Will make that change in a bit. +// '#FFC300', // Vivid Yellow +// '#DAF7A6', // Light Green +// '#A0E7E5', // Light Blue +// '#B4A0E7', // Lavender +// '#E7A0C4', // Pink + '#FFFFFF', // White +]; +/* =================================================================== */ +/* MAIN */ +/* =================================================================== */ + +const FireworksBackground = ({ + launchInterval = 5000, + triggerBurst = false, + burstColorOverride = null, +}) => { + /* ---------- size ---------- */ + + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); + const containerRef = useRef(null); + + useEffect(() => { + const handleResize = () => { + if (containerRef.current) { + setDimensions({ + width: containerRef.current.offsetWidth, + height: containerRef.current.offsetHeight, + }); + } + }; + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + /* ---------- pre-computed launch paths ---------- */ + + const numPaths = 7; + const paths = useMemo(() => { + const { width, height } = dimensions; + if (!width || !height) return []; + + return Array.from({ length: numPaths }, (_, id) => ({ + id, + startX: width / 2, + startY: height, + endX: random(width * 0.2, width * 0.8), + endY: random(height * 0.1, height * 0.4), + launchDuration: random(0.8, 3), // s + explosionDuration: random(1, 5), // s + numParticles: Math.floor(random(15, 30)), + numRings: Math.floor(random(2, 7)), + particleRadius: random(80, 180), + defaultColor: + fireworkColors[Math.floor(random(0, fireworkColors.length))], + })); + }, [dimensions]); + + /* ---------- bookkeeping ---------- */ + + const [activeFireworks, setActiveFireworks] = useState([]); // {key, pathId, color} + const activePathIds = useRef(new Set()); + + const removeFirework = useCallback((key, pathId) => { + setActiveFireworks((prev) => prev.filter((fw) => fw.key !== key)); + activePathIds.current.delete(pathId); + }, []); + + /* ---------- launch logic ---------- */ + + const launchFirework = useCallback( + (specificPathId = null, overrideColor = null) => { + if (!paths.length) return; + + let pathId = specificPathId; + if (pathId == null) { + const available = paths + .map((p) => p.id) + .filter((id) => !activePathIds.current.has(id)); + if (!available.length) return; // all busy + pathId = available[Math.floor(random(0, available.length))]; + } + + const path = paths.find(p => p.id === pathId); + if (!path || activePathIds.current.has(pathId)) return; // already occupied or path not found + + const key = uuid(); + const color = overrideColor || path.defaultColor; + + activePathIds.current.add(pathId); + setActiveFireworks((prev) => [...prev, { key, pathId, color }]); + }, + [paths] + ); + + /* interval launch */ + const intervalRef = useRef(); + useEffect(() => { + clearInterval(intervalRef.current); + if (!paths.length) return; + + intervalRef.current = setInterval(() => launchFirework(null, null), launchInterval); + return () => clearInterval(intervalRef.current); + }, [launchInterval, launchFirework, paths.length]); + + /* burst launch */ + useEffect(() => { + if (triggerBurst && paths.length) { + paths.forEach((p, i) => + setTimeout(() => launchFirework(p.id, burstColorOverride), i * 50) + ); + } + }, [triggerBurst, burstColorOverride, paths, launchFirework]); + + /* ================================================================= */ + /* render */ + /* ================================================================= */ + + return ( +
+ {dimensions.width > 0 && ( + + {activeFireworks.map(({ key, pathId, color }) => { + const path = paths.find((p) => p.id === pathId); + if (!path) return null; + return ( + removeFirework(key, pathId)} + /> + ); + })} + + )} +
+ ); + }; + + export default FireworksBackground; + + /* =================================================================== */ + /* Firework (declarative) */ + /* =================================================================== */ + + const Firework = ({ path, color, onDone }) => { + const [phase, setPhase] = useState('launch'); // launch → explode → done + + /* phase timers */ + useEffect(() => { + const t1 = setTimeout( + () => setPhase('explode'), + path.launchDuration * 1000 + ); + const t2 = setTimeout( + () => setPhase('done'), + (path.launchDuration + path.explosionDuration) * 1000 + ); + return () => { + clearTimeout(t1); + clearTimeout(t2); + }; + }, [path]); + + /* notify parent */ + useEffect(() => { + if (phase === 'done') onDone(); + }, [phase, onDone]); + + /* launch particle style */ + const launchMotion = { + translateX: path.endX - path.startX, + translateY: path.endY - path.startY, + opacity: 0, + }; + + return ( + + {/* launch particle */} + + + {/* explosion group */} + + {/* Create multiple rings for a fuller explosion */} + {Array.from({ length: path.numRings }).flatMap((_, ringIndex) => { + const ringDelay = ringIndex * 0.1; // Delay subsequent rings + const ringRadiusMultiplier = 1 - ringIndex * 0.2; // Inner rings are slightly smaller (optional) + + return Array.from({ length: path.numParticles }).map((_, particleIndex) => { + const angle = (particleIndex / path.numParticles) * 2 * Math.PI; + const particleRadius = path.particleRadius * ringRadiusMultiplier; + + return ( + + ); + }); + })} + + + ); + }; \ No newline at end of file diff --git a/atk-interface/src/components/IdleMonitorGraphic.jsx b/atk-interface/src/components/IdleMonitorGraphic.jsx new file mode 100644 index 0000000000000000000000000000000000000000..957ca1aff07de5d0133da2fa113ac7dafa42b2aa --- /dev/null +++ b/atk-interface/src/components/IdleMonitorGraphic.jsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { FiSettings } from 'react-icons/fi'; // Keep gear + +const IdleMonitorGraphic = () => { + return ( +
{/* Container size (controls gear size) */} + {/* Spinning Gear */} + + {/* Gear takes color from parent div */} + {/* Increased size */} + + + {/* Stationary Binoculars SVG - Centered and Resized */} + {/* Container centers the SVG within the gear */} +
+ {/* SVG adjusted: size, viewBox might need tuning based on visual */} + + {/* Group takes fill from parent div's text color */} + + + {/* Path data remains the same */} + + + + +
+
+ ); +}; + +export default IdleMonitorGraphic; \ No newline at end of file diff --git a/atk-interface/src/components/Layout.jsx b/atk-interface/src/components/Layout.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e21cf209dee493932af907d66bfc9e8ac626d21b --- /dev/null +++ b/atk-interface/src/components/Layout.jsx @@ -0,0 +1,147 @@ +import React, { useEffect, useState } from 'react'; +import { NavLink, Outlet, useLocation } from 'react-router-dom'; +import { useNavigation } from '../context/NavigationContext'; +import atkLogo from '../assets/atk-logo-notscuffed.png'; + +const animationStyles = ` + @keyframes pulse-medium-white { + 0%, 100% { background-color: rgba(255,255,255,0.1); } + 50% { background-color: rgba(255,255,255,0.3); } + } + .animate-pulse-medium-white { + animation: pulse-medium-white 1.8s infinite; + } +`; + +function Layout() { + const { highlightedNavLink, setHighlightedNavLink } = useNavigation(); + const location = useLocation(); + const [serverOnline, setServerOnline] = useState(false); + const [datagenServerOnline, setDatagenServerOnline] = useState(false); + + // Check for server on port 8003 + useEffect(() => { + const checkServerStatus = async () => { + try { + const baseUrl = window.location.hostname === '127.0.0.1' || window.location.hostname === 'localhost' + ? 'http://127.0.0.1:8003' + : `${window.location.origin}/llm`; + const response = await fetch(`${baseUrl}/health`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + const isOnline = response.ok; + setServerOnline(isOnline); + + // Auto-highlight chat button when server comes online (but not if user is already on chat page) + if (isOnline && location.pathname !== '/chat') { + setHighlightedNavLink('/chat'); + } else if (!isOnline && highlightedNavLink === '/chat') { + // Remove highlight when server goes offline + setHighlightedNavLink(null); + } + } catch (error) { + setServerOnline(false); + // Remove highlight when server is unreachable + if (highlightedNavLink === '/chat') { + setHighlightedNavLink(null); + } + } + }; + + // Initial check + checkServerStatus(); + + // Poll every 5 seconds + const interval = setInterval(checkServerStatus, 5000); + + return () => clearInterval(interval); + }, [location.pathname, highlightedNavLink, setHighlightedNavLink]); + + // Check for datagen inference server on port 8082 + useEffect(() => { + const checkDatagenServerStatus = async () => { + try { + // Try OpenAI-compatible endpoint + const response = await fetch('http://127.0.0.1:8082/v1/models', { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + setDatagenServerOnline(response.ok); + } catch (error) { + setDatagenServerOnline(false); + } + }; + + // Initial check + checkDatagenServerStatus(); + + // Poll every 5 seconds + const interval = setInterval(checkDatagenServerStatus, 5000); + + return () => clearInterval(interval); + }, []); + + const navLinkClass = (path) => ({ isActive }) => { + const base = 'px-4 py-2 rounded hover:bg-gray-700 transition-colors'; + const active = isActive ? 'bg-blue-600 text-white font-bold' : 'text-gray-300'; + const pulse = highlightedNavLink === path && !isActive ? 'animate-pulse-medium-white' : ''; + return `${base} ${active} ${pulse}`; + }; + + return ( +
+ +
+ {/* Child routes will render here */} +
+ +
+ ); +} + +export default Layout; \ No newline at end of file diff --git a/atk-interface/src/components/Modal.jsx b/atk-interface/src/components/Modal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..044d1ff6c2aab099b6a2e20c2dbcc83a0addfea4 --- /dev/null +++ b/atk-interface/src/components/Modal.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { FiX } from 'react-icons/fi'; + +function Modal({ isOpen, onClose, title, children, width = 'max-w-lg' }) { + + // Handle Escape key press - Hooks must be called unconditionally + React.useEffect(() => { + const handleEscape = (event) => { + if (event.key === 'Escape') { + onClose(); + } + }; + // Only add listener if modal is open + if (isOpen) { + document.addEventListener('keydown', handleEscape); + } + // Cleanup function removes listener + return () => { + document.removeEventListener('keydown', handleEscape); + }; + }, [isOpen, onClose]); // Depend on isOpen and onClose + + // Early return if not open, *after* hooks + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()} // Prevent closing when clicking inside modal + > + +

{title}

+
+ {children} +
+
+
+ ); +} + +// Add simple fade-in animation (requires Tailwind config update) +// In tailwind.config.js -> theme -> extend -> keyframes & animation: +/* +keyframes: { + 'fade-scale-in': { + '0%': { opacity: '0', transform: 'scale(0.95)' }, + '100%': { opacity: '1', transform: 'scale(1)' }, + } +}, +animation: { + 'fade-scale-in': 'fade-scale-in 0.2s ease-out forwards', +} +*/ + +export default Modal; \ No newline at end of file diff --git a/atk-interface/src/components/PourOverBackground.jsx b/atk-interface/src/components/PourOverBackground.jsx new file mode 100644 index 0000000000000000000000000000000000000000..834f1b059af203ceafc123f43d872d17f72541f6 --- /dev/null +++ b/atk-interface/src/components/PourOverBackground.jsx @@ -0,0 +1,224 @@ +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +// Placeholder SVGs (we can refine these later) +const FunnelSVG = () => ( + + {/* Funnel body */} + {/* Spout */} + {/* Simple static "water" line */} + + +); + +const ContainerSVG = () => ( + + {/* Taller coffee level */} + {/* Coffee */} + {/* Taller container outline */} + {/* Glass */} + {/* Handle */} + + +); + +const SpillPuddleSVG = () => ( + + + +); + +const DripSVG = () => ( + + {/* Water drip */} + +); + +const LidSVG = () => ( + + + +); + + +const PourOverBackground = ({ + isActive = false, + triggerDownload = false, + triggerDelete = false, + onDownloadComplete, + onDeleteComplete, +}) => { + const [containerState, setContainerState] = useState('idle'); // 'idle', 'downloading', 'spilled' + + // --- Animation Variants --- + + // Drip Animation + const dripVariants = { + idle: { + y: [0, 130], // Start above container, end further down (Adjusted for scale) + opacity: [1, 1, 0], + scaleY: [0.5, 1, 1], + transition: { duration: 1.5, repeat: Infinity, ease: "linear", times: [0, 0.8, 1] } + }, + active: { + // y: [0, 130], // Removed y animation for stationary effect + y: 50, + opacity: [0.6, 1, 0.6], // Pulsating opacity + scaleY: [0.1, 4, 0.1], // Pulse scale from near zero to full height + transition: { duration: 0.4, repeat: Infinity, ease: "easeInOut", times: [0, 0.5, 1] } // Symmetrical pulse timing + }, + hidden: { opacity: 0, y: 0, transition: { duration: 0.1 } } // Hide drip during transitions + }; + + // Container Animation + const containerVariants = { + idle: { x: 0, rotate: 0, opacity: 1, transition: { duration: 0.5, ease: "easeOut" } }, + exitRight: { x: '150%', opacity: 0, transition: { duration: 0.8, ease: "easeIn" } }, + spill: { rotate: -90, x: -40, y: 20, transition: { duration: 0.4, ease: "easeIn" } }, // Tilt further left + hidden: { opacity: 0, transition: { duration: 0.1 } } + }; + + // Lid Animation + const lidVariants = { + hidden: { y: -20, opacity: 0, transition: { duration: 0.3, ease: "easeOut" } }, + visible: { y: -5, opacity: 1, transition: { duration: 0.3, ease: "easeIn" } } // Position above container + }; + + // Spill Puddle Animation + const spillPuddleVariants = { + hidden: { opacity: 0, scale: 0.8, transition: { duration: 0.3, ease: "easeOut" } }, + visible: { opacity: 1, scale: 1, transition: { duration: 0.3, delay: 0.2, ease: "easeIn" } } // Appear slightly after spill starts + }; + + // --- Funnel Animation Variants --- + const funnelVariants = { + bobbing: { + x: "-50%", // Maintain horizontal center (instead of relying on CSS translate) + y: [-3, 3, -3], // Bob up and down slightly + transition: { duration: 2.5, repeat: Infinity, ease: "easeInOut", repeatType: "loop" } + } + }; + + // --- State Management --- + + // Handle Download Trigger + useEffect(() => { + if (triggerDownload) { + setContainerState('downloading'); + // Sequence: Show lid -> Move container -> Hide lid -> Reset -> Callback + const timer1 = setTimeout(() => { // Allow time for lid animation + const timer2 = setTimeout(() => { // Allow time for container exit + setContainerState('idle'); // Bring back a fresh container + onDownloadComplete?.(); + }, 800); // Duration of container exit + return () => clearTimeout(timer2); + }, 300); // Duration of lid appear + return () => clearTimeout(timer1); + } + }, [triggerDownload, onDownloadComplete]); + + // Handle Delete Trigger + useEffect(() => { + if (triggerDelete) { + setContainerState('spilled'); + // Sequence: Spill -> Wait -> Reset -> Callback + const timer1 = setTimeout(() => { // Let spill animation play + setContainerState('idle'); // Bring back fresh container + onDeleteComplete?.(); + }, 1500); // Duration to show spilled state + return () => clearTimeout(timer1); + } + }, [triggerDelete, onDeleteComplete]); + + const showDrip = containerState === 'idle'; + const showLid = containerState === 'downloading'; + const showSpill = containerState === 'spilled'; + + return ( +
+
{/* Positioning context - Increased size */} + + {/* Funnel (Static for now) */} + + + + + {/* Drip */} + + {showDrip && ( + + + + )} + + + + {/* Container Group (Lid + Container) */} + + {/* Lid */} + + + + + {/* Container */} + + + + {/* Spill Puddle - Positioned independently below where container spills */} + + + + +
+
+ ); +}; + +export default PourOverBackground; diff --git a/atk-interface/src/components/ProgressBarParticles.jsx b/atk-interface/src/components/ProgressBarParticles.jsx new file mode 100644 index 0000000000000000000000000000000000000000..50a554dccaa31cac2e3aaf1d61a3c67315db1112 --- /dev/null +++ b/atk-interface/src/components/ProgressBarParticles.jsx @@ -0,0 +1,384 @@ +import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; +import { motion, useAnimation } from 'framer-motion'; + +// --- Helper Functions --- +const random = (min, max) => Math.random() * (max - min) + min; +const uuid = () => + typeof crypto !== 'undefined' && crypto.randomUUID + ? crypto.randomUUID() + : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; + +// Interpolate between two colors +const interpolateColor = (color1, color2, factor) => { + if (factor < 0) factor = 0; + if (factor > 1) factor = 1; + + const c1 = parseInt(color1.substring(1), 16); + const c2 = parseInt(color2.substring(1), 16); + + const r1 = (c1 >> 16) & 0xff; + const g1 = (c1 >> 8) & 0xff; + const b1 = c1 & 0xff; + + const r2 = (c2 >> 16) & 0xff; + const g2 = (c2 >> 8) & 0xff; + const b2 = c2 & 0xff; + + const r = Math.round(r1 + (r2 - r1) * factor); + const g = Math.round(g1 + (g2 - g1) * factor); + const b = Math.round(b1 + (b2 - b1) * factor); + + return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`; +}; + +const START_COLOR = '#4ade80'; // Dull Green (Tailwind green-400) +const END_COLOR = '#00FF00'; // Bright Neon Green + +// --- Particle Component --- +const Particle = ({ id, initialX, initialY, targetPointX, targetPointY, color, status, screenHeight, screenWidth, onComplete }) => { + const controls = useAnimation(); + const [currentState, setCurrentState] = useState(status); + const particleRef = useRef({ isComplete: false }); // Ref to track completion status + + useEffect(() => { + setCurrentState(status); // Update state if prop changes + }, [status]); + + useEffect(() => { + if (particleRef.current.isComplete) return; // Don't restart animation if already completing + + let animationSequence; + const durationBase = random(6, 12); // Base duration for normal movement + const fadeDuration = 1.5; // Normal fade duration + + // Slow fade durations for lingering particles + const lingerFadeDuration = random(8, 15); // Longer fade for failed/revoked + const lingerDelay = random(3, 7); // Delay before slow fade starts + + switch (currentState) { + case 'FAILED': + // Drop to bottom, linger as smaller "corpse", then fade slowly + animationSequence = async () => { + // 1. Drop to near the bottom + await controls.start({ + x: random(0, screenWidth), // Scatter slightly at the bottom + y: screenHeight - random(10, 15), // Settle very close to the bottom edge + opacity: 1, + scale: 0.8, // Slightly smaller "corpse" + transition: { duration: random(1, 2.5), ease: "easeIn" } + }); + // 2. Linger, then fade slowly + await controls.start({ + opacity: 0, + transition: { duration: lingerFadeDuration, delay: lingerDelay } + }); + if (!particleRef.current.isComplete) { + particleRef.current.isComplete = true; + onComplete(id); + } + }; + break; + + case 'REVOKED': + // Flee to edges, linger smaller/huddled, then fade slowly + { + const edgeX = initialX < screenWidth / 2 ? random(-15, 0) : random(screenWidth, screenWidth + 15); + const edgeY = random(0, screenHeight); + animationSequence = async () => { + // 1. Move quickly to the edge + await controls.start({ + x: edgeX, + y: edgeY, + opacity: 1, + scale: 0.6, // Smaller, huddled look + transition: { duration: random(0.5, 1.5), ease: "easeOut" } + }); + // 2. Linger, then fade slowly + await controls.start({ + opacity: 0, + transition: { duration: lingerFadeDuration, delay: lingerDelay } + }); + if (!particleRef.current.isComplete) { + particleRef.current.isComplete = true; + onComplete(id); + } + }; + } + break; + + case 'RUNNING': + case 'PENDING': + case 'COMPLETED': + default: + // Move towards the target point on the bar and fade (original behavior) + animationSequence = async () => { + // 1. Quick fade-in + await controls.start({ + opacity: 1, + scale: 1, // Start at full size immediately + transition: { duration: 0.3, ease: "easeOut" } // Fast fade-in + }); + + // 2. Move towards the target point + await controls.start({ + x: targetPointX, + y: targetPointY, + // Opacity and scale are already 1 from the previous step + transition: { duration: durationBase, ease: "circOut" } + }); + + // 3. Fade out near the target + await controls.start({ + opacity: 0, + scale: 0.3, // Shrink slightly as it fades + transition: { duration: fadeDuration } + }); + if (!particleRef.current.isComplete) { + particleRef.current.isComplete = true; + onComplete(id); + } + }; + break; + } + + animationSequence().catch(err => { + // Handle potential animation errors (e.g., component unmounted) + if (err.name !== 'AbortError' && !particleRef.current.isComplete) { + console.error("Particle animation error:", err); + // Ensure cleanup happens even on error if not already completed + particleRef.current.isComplete = true; + onComplete(id); + } + }); + + // Cleanup function for when component unmounts or dependencies change + // This helps prevent calling onComplete multiple times if status changes rapidly + return () => { + controls.stop(); // Stop any ongoing animation + // If the component unmounts before the animation naturally completes, + // ensure we call onComplete if it hasn't been called yet. + // This is important for states like RUNNING->FAILED transition. + if (!particleRef.current.isComplete) { + // We might not call onComplete here directly, + // as the state change might trigger a new animation + // that WILL eventually call onComplete. Let the new effect handle it. + // If we *do* call it here, it might remove the particle before the + // FAILED/REVOKED animation can even start. + // Let's rely on the isComplete flag within the async sequences. + } + }; + + // Ensure dependencies cover all inputs for the animation logic + }, [controls, id, initialX, initialY, targetPointX, targetPointY, color, currentState, screenHeight, screenWidth, onComplete]); + + + return ( + + ); +}; + +// --- Main Particle Container --- +const ProgressBarParticles = ({ taskId, progress, status, progressBarElement, downloadButtonElement, color }) => { + const [particles, setParticles] = useState([]); + const containerRef = useRef(null); + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); + const [barRect, setBarRect] = useState(null); + const [buttonRect, setButtonRect] = useState(null); // State for button position + + // Get container dimensions + useEffect(() => { + const updateDimensions = () => { + if (containerRef.current) { + setDimensions({ + width: containerRef.current.offsetWidth, + height: containerRef.current.offsetHeight, + }); + } + }; + updateDimensions(); + window.addEventListener('resize', updateDimensions); + return () => window.removeEventListener('resize', updateDimensions); + }, []); + + // Get progress bar dimensions/position relative to the viewport + useEffect(() => { + const updateBarRect = () => { + if (progressBarElement) { + setBarRect(progressBarElement.getBoundingClientRect()); + } else { + setBarRect(null); + } + }; + + updateBarRect(); // Initial read + + // Use ResizeObserver for efficiency if needed, or just poll on interval/scroll + const intervalId = setInterval(updateBarRect, 200); // Check position periodically + + window.addEventListener('scroll', updateBarRect); // Update on scroll + + return () => { + clearInterval(intervalId); + window.removeEventListener('scroll', updateBarRect); + }; + }, [progressBarElement]); // Re-run if the element itself changes + + // Get download button dimensions/position relative to the viewport + useEffect(() => { + const updateButtonRect = () => { + if (downloadButtonElement) { + setButtonRect(downloadButtonElement.getBoundingClientRect()); + } else { + setButtonRect(null); + } + }; + + updateButtonRect(); // Initial read + + // Use ResizeObserver for efficiency if needed, or just poll on interval/scroll + const intervalId = setInterval(updateButtonRect, 200); // Check position periodically + + window.addEventListener('scroll', updateButtonRect); // Update on scroll + + return () => { + clearInterval(intervalId); + window.removeEventListener('scroll', updateButtonRect); + }; + }, [downloadButtonElement]); // Re-run if the button element itself changes + + const removeParticle = useCallback((id) => { + setParticles(prev => prev.filter(p => p.id !== id)); + }, []); + + // Particle Generation Logic + useEffect(() => { + if (!taskId || !dimensions.width || !dimensions.height || !barRect) { + if (!taskId && particles.length > 0) { + setParticles([]); + } + return; + } + + // --- Update existing particles status --- + // Do this regardless of the current task status, so particles + // correctly transition to FAILED/REVOKED animations etc. + setParticles(prev => prev.map(p => p.status !== status ? { ...p, status: status } : p)); + + // --- Particle Generation --- + // Always generate particles, but their behavior is determined by the status prop passed to Particle + + // Increase max particles significantly & faster generation rate + const maxParticles = 15 + Math.floor((progress ?? 0) * 150); // Halved: Max ~165 particles at 100% + // Faster base rate, scales more aggressively with progress. Min 20ms interval. + const generationRate = Math.max(40, 200 - (progress ?? 0) * 160); // Slower generation: interval 40ms (100%) to 200ms (0%) + + + const intervalId = setInterval(() => { + setParticles(prev => { + // Check against maxParticles (use the length *after* potential updates) + if (prev.length < maxParticles) { + const edge = Math.floor(random(0, 4)); // 0: top, 1: right, 2: bottom, 3: left + let startX, startY; + switch (edge) { + case 0: // top + startX = random(0, dimensions.width); + startY = -10; + break; + case 1: // right + startX = dimensions.width + 10; + startY = random(0, dimensions.height); + break; + case 2: // bottom + startX = random(0, dimensions.width); + startY = dimensions.height + 10; + break; + case 3: // left + default: + startX = -10; + startY = random(0, dimensions.height); + break; + } + + // Calculate target point within the *filled* portion of the bar OR the button + const containerRect = containerRef.current?.getBoundingClientRect(); + if (!containerRect) return prev; // Exit if container not ready + + let targetX, targetY; + + // Target button if status is COMPLETED and button exists + if (status === 'COMPLETED' && buttonRect) { + const buttonStartX = buttonRect.left - containerRect.left; + const buttonStartY = buttonRect.top - containerRect.top; + targetX = buttonStartX + random(0, buttonRect.width); + targetY = buttonStartY + random(0, buttonRect.height); + } + // Otherwise, target the progress bar (if it exists) + else if (barRect) { + const barStartX = barRect.left - containerRect.left; + const barStartY = barRect.top - containerRect.top; + const filledWidth = barRect.width * (progress ?? 0); + + // Target a random X within the filled area, or close to start if progress is 0 + targetX = barStartX + random(0, Math.max(5, filledWidth)); // Ensure minimum target width + // Target Y somewhere vertically within the bar height + targetY = barStartY + random(0, barRect.height); + } + // Fallback if neither is ready (should be rare) + else { + targetX = startX; // Stay near start position + targetY = startY; + } + + return [ + ...prev, // Use the already updated array if status changed, otherwise original prev + { + id: uuid(), + initialX: startX, + initialY: startY, + targetPointX: targetX, + targetPointY: targetY, + color: color, + status: status, // Pass current task status + } + ]; + } + // If max particles reached, just return the potentially status-updated array + return prev; + }); + }, generationRate); + + return () => clearInterval(intervalId); + }, [taskId, progress, status, dimensions, barRect, buttonRect, downloadButtonElement, removeParticle, color]); + + return ( +
+ {particles.map(p => ( + + ))} +
+ ); +}; + +export default ProgressBarParticles; \ No newline at end of file diff --git a/atk-interface/src/components/ScrollingTextBackground.jsx b/atk-interface/src/components/ScrollingTextBackground.jsx new file mode 100644 index 0000000000000000000000000000000000000000..65c28d1f27902a41575fea3b2e71f8fe77ec331e --- /dev/null +++ b/atk-interface/src/components/ScrollingTextBackground.jsx @@ -0,0 +1,92 @@ +import React, { useMemo, useEffect, useRef } from 'react'; +import { motion, useMotionValue, useTransform, useAnimationFrame } from 'framer-motion'; + +// Function to generate a random number within a range +const random = (min, max) => Math.random() * (max - min) + min; + +// Create a single line element +const Line = ({ width, delay }) => ( + +); + +// Create the scrolling background component +const ScrollingTextBackground = ({ duration = 30 }) => { + const numLines = 50; + + const lines = useMemo(() => { + return Array.from({ length: numLines }).map((_, i) => ({ + id: i, + width: random(20, 80), + delay: random(0, 1), + })); + }, []); + + // --- Manual Animation Logic --- + const yProgress = useMotionValue(0); // Tracks progress from 0 to 1 + const yPercentage = useTransform(yProgress, [0, 1], ["0%", "-50%"]); // Maps progress to Y offset + + const lastFrameTime = useRef(performance.now()); + const animationFrameId = useRef(null); // To store the request ID + const currentDuration = useRef(duration); // Store duration in a ref + + // Update the duration ref whenever the prop changes + useEffect(() => { + currentDuration.current = duration; + }, [duration]); + + useAnimationFrame((timestamp) => { + const deltaTime = timestamp - lastFrameTime.current; + + // Guard against potentially huge deltaTime after remount or tab backgrounding. + // Only update animation if deltaTime is reasonable (e.g., < 1 second). + // Also handle potential negative deltaTime if clocks adjust. + if (deltaTime < 0 || deltaTime > 1000) { + lastFrameTime.current = timestamp; // Reset for the next frame + return; // Skip animation update for this frame + } + + lastFrameTime.current = timestamp; // Update time for the next frame calculation + + // Calculate progress increment based on current duration + // duration is in seconds, deltaTime in ms. Convert duration to ms. + const progressIncrement = deltaTime / (currentDuration.current * 1000); + + // Update progress, wrapping around using modulo + const newProgress = (yProgress.get() + progressIncrement) % 1; + yProgress.set(newProgress); + }); + + // --- End Manual Animation Logic --- + + return ( +
+ + {/* First block of lines */} +
+ {lines.map(line => ( + + ))} +
+
+ {/* Second block of lines */} +
+ {lines.map(line => ( + + ))} +
+
+
+ ); +}; + +export default ScrollingTextBackground; \ No newline at end of file diff --git a/atk-interface/src/context/FileManagerContext.jsx b/atk-interface/src/context/FileManagerContext.jsx new file mode 100644 index 0000000000000000000000000000000000000000..545dbd97c7204c16560a347003c291b6a9eb6aa7 --- /dev/null +++ b/atk-interface/src/context/FileManagerContext.jsx @@ -0,0 +1,112 @@ +import React, { createContext, useState, useContext, useCallback } from 'react'; +import { fetchInputStructure, fetchOutputStructure } from '../api'; // Assuming API functions exist + +const FileManagerContext = createContext(); + +export const useFileManager = () => useContext(FileManagerContext); + +/** + * Adapts API data structure to the structure expected by react-arborist. + * Sorts directories first, then alphabetically. + * @param {Array} apiData - Data from the API + * @returns {Array} Adapted and sorted data + */ +const adaptAndSortApiData = (apiData) => { + if (!Array.isArray(apiData)) return []; + const adapted = apiData.map(item => ({ + id: item.path, + name: item.path.split('/').pop() || item.path, + relativePath: item.path, + is_dir: item.is_dir, + children: item.is_dir ? (item.children ? adaptAndSortApiData(item.children) : null) : undefined, + })); + // Sort: directories first, then alphabetically + adapted.sort((a, b) => { + if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1; + return a.name.localeCompare(b.name); + }); + return adapted; +}; + + +export const FileManagerProvider = ({ children }) => { + // --- Input State --- + const [inputTreeData, setInputTreeData] = useState(null); + const [isInputLoading, setIsInputLoading] = useState(false); + const [inputError, setInputError] = useState(null); + + // --- Output State --- + const [outputTreeData, setOutputTreeData] = useState(null); + const [isOutputLoading, setIsOutputLoading] = useState(false); + const [outputError, setOutputError] = useState(null); + + // --- Fetch Input Data (with caching) --- + const fetchInputData = useCallback(async (forceRefresh = false) => { + if (inputTreeData && !forceRefresh) { + // console.log("Using cached input data"); + return; // Use cached data if available and not forcing refresh + } + // console.log("Fetching input data...", { forceRefresh }); + setIsInputLoading(true); + setInputError(null); + try { + const structure = await fetchInputStructure(); + const adaptedData = adaptAndSortApiData(structure); + setInputTreeData(adaptedData); + } catch (e) { + console.error("Failed to fetch input structure:", e); + setInputError(e.message || 'Failed to load input files structure.'); + setInputTreeData([]); // Set empty on error to stop loading indicators + } finally { + setIsInputLoading(false); + } + }, [inputTreeData]); // Re-run if cached data changes (relevant for forceRefresh logic) + + // --- Fetch Output Data (with caching) --- + const fetchOutputData = useCallback(async (forceRefresh = false) => { + if (outputTreeData && !forceRefresh) { + // console.log("Using cached output data"); + return; // Use cached data + } + // console.log("Fetching output data...", { forceRefresh }); + setIsOutputLoading(true); + setOutputError(null); + try { + const structure = await fetchOutputStructure(); + const adaptedData = adaptAndSortApiData(structure); + setOutputTreeData(adaptedData); + } catch (e) { + console.error("Failed to fetch output structure:", e); + setOutputError(e.message || 'Failed to load output files structure.'); + setOutputTreeData([]); // Set empty on error + } finally { + setIsOutputLoading(false); + } + }, [outputTreeData]); // Re-run if cached data changes + + // --- Refresh Functions --- + // These simply call the fetch functions with forceRefresh=true + const refreshInputData = useCallback(() => fetchInputData(true), [fetchInputData]); + const refreshOutputData = useCallback(() => fetchOutputData(true), [fetchOutputData]); + + + const value = { + inputTreeData, + isInputLoading, + inputError, + fetchInputData, + refreshInputData, + + outputTreeData, + isOutputLoading, + outputError, + fetchOutputData, + refreshOutputData, + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/atk-interface/src/context/NavigationContext.jsx b/atk-interface/src/context/NavigationContext.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6c762765de6533684726aaa845f4a89b4b5be36b --- /dev/null +++ b/atk-interface/src/context/NavigationContext.jsx @@ -0,0 +1,19 @@ +import React, { createContext, useContext, useState } from 'react'; + +// Context to store which nav link should be highlighted (in addition to the active one) +const NavigationContext = createContext({ + highlightedNavLink: null, + setHighlightedNavLink: () => {}, +}); + +export const NavigationProvider = ({ children }) => { + const [highlightedNavLink, setHighlightedNavLink] = useState(null); + + return ( + + {children} + + ); +}; + +export const useNavigation = () => useContext(NavigationContext); \ No newline at end of file diff --git a/atk-interface/src/context/TaskHistoryContext.jsx b/atk-interface/src/context/TaskHistoryContext.jsx new file mode 100644 index 0000000000000000000000000000000000000000..29cad1352f703dee862d52d973275a51140777f7 --- /dev/null +++ b/atk-interface/src/context/TaskHistoryContext.jsx @@ -0,0 +1,60 @@ +import React, { createContext, useState, useContext, useCallback, useEffect } from 'react'; + +const TaskHistoryContext = createContext(); + +export const useTaskHistory = () => useContext(TaskHistoryContext); + +export const TaskHistoryProvider = ({ children }) => { + // Initialize state from localStorage if available, otherwise empty array + const [taskHistory, setTaskHistory] = useState(() => { + try { + const storedHistory = localStorage.getItem('atk_taskHistory'); + return storedHistory ? JSON.parse(storedHistory) : []; + } catch (error) { + console.error("Error reading task history from localStorage:", error); + return []; + } + }); + + // Persist history to localStorage whenever it changes + useEffect(() => { + try { + localStorage.setItem('atk_taskHistory', JSON.stringify(taskHistory)); + } catch (error) { + console.error("Error saving task history to localStorage:", error); + } + }, [taskHistory]); + + const addTaskToHistory = useCallback((taskId) => { + if (!taskId) return; + console.log(`[TaskHistoryContext] addTaskToHistory called with taskId: ${taskId}`); + setTaskHistory(prevHistory => { + console.log(`[TaskHistoryContext] Previous history:`, prevHistory); + // Avoid duplicates and add to the beginning (most recent first) + const newHistory = [taskId, ...prevHistory.filter(id => id !== taskId)]; + // Optional: Limit history size + // const MAX_HISTORY_SIZE = 20; + // return newHistory.slice(0, MAX_HISTORY_SIZE); + console.log(`[TaskHistoryContext] New history:`, newHistory); + return newHistory; + }); + }, []); + + const clearTaskHistory = useCallback(() => { + console.log(`[TaskHistoryContext] clearTaskHistory called.`); + setTaskHistory([]); + localStorage.removeItem('atk_taskHistory'); // Clear from storage too + }, []); + + const value = { + taskHistory, + addTaskToHistory, + clearTaskHistory, + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/atk-interface/src/index.css b/atk-interface/src/index.css new file mode 100644 index 0000000000000000000000000000000000000000..bd6213e1dfe6b0a79ce7d8b37d0d2dc70f0250bb --- /dev/null +++ b/atk-interface/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/atk-interface/src/main.jsx b/atk-interface/src/main.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7d11abc9325e405eca667a3fe69499abf8a714e2 --- /dev/null +++ b/atk-interface/src/main.jsx @@ -0,0 +1,16 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import { NavigationProvider } from './context/NavigationContext' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + + + + + , +) diff --git a/atk-interface/src/pages/Chat.jsx b/atk-interface/src/pages/Chat.jsx new file mode 100644 index 0000000000000000000000000000000000000000..00a584e723d44cfe9fab03ca64f81210b19f28f0 --- /dev/null +++ b/atk-interface/src/pages/Chat.jsx @@ -0,0 +1,550 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { FiSend, FiEdit2, FiCopy, FiCheck, FiTrash2, FiSettings, FiRefreshCw } from 'react-icons/fi'; + +function Chat() { + const [messages, setMessages] = useState([]); + const [inputMessage, setInputMessage] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [serverPort, setServerPort] = useState('8003'); + const [showPortSettings, setShowPortSettings] = useState(false); + const [serverStatus, setServerStatus] = useState('checking'); // 'checking', 'online', 'offline' + const [serverType, setServerType] = useState('unknown'); // 'openai', 'rag', 'unknown' + const [editingMessageId, setEditingMessageId] = useState(null); + const [copiedMessageId, setCopiedMessageId] = useState(null); + const [isRefreshingStatus, setIsRefreshingStatus] = useState(false); + const [isStreaming, setIsStreaming] = useState(true); // New streaming toggle, default true + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + + // Check server status + useEffect(() => { + const checkServerStatus = async () => { + try { + // Use the health endpoint instead of generate with empty messages + const baseUrl = window.location.hostname === '127.0.0.1' || window.location.hostname === 'localhost' + ? `http://127.0.0.1:${serverPort}` + : `${window.location.origin}/llm`; + const response = await fetch(`${baseUrl}/health`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + if (response.ok) { + const data = await response.json(); + setServerStatus('online'); + + // Detect server type based on response message + if (data.message && data.message.includes('RAG')) { + setServerType('rag'); + } else if (data.message && data.message.includes('OpenAI')) { + setServerType('openai'); + } else { + setServerType('unknown'); + } + } else { + setServerStatus('offline'); + setServerType('unknown'); + } + } catch (error) { + setServerStatus('offline'); + setServerType('unknown'); + } + }; + + // Check immediately when port changes + checkServerStatus(); + + // Set up interval for periodic checks + const interval = setInterval(checkServerStatus, 3000); // Check every 3 seconds + return () => clearInterval(interval); + }, [serverPort]); // Re-run when serverPort changes + + // Auto-scroll to bottom when new messages arrive + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + const handleSendMessage = async () => { + if (!inputMessage.trim() || isLoading || serverStatus !== 'online') return; + + const userMessage = { role: 'user', content: inputMessage.trim(), id: Date.now() }; + setMessages(prev => [...prev, userMessage]); + setInputMessage(''); + setIsLoading(true); + + try { + // Prepare messages for API (remove id field) + const apiMessages = messages.concat(userMessage).map(({ role, content }) => ({ role, content })); + + if (isStreaming) { + // Handle streaming response + const assistantMessageId = Date.now() + 1; + const assistantMessage = { + role: 'assistant', + content: '', + id: assistantMessageId + }; + + // Add empty assistant message that we'll update + setMessages(prev => [...prev, assistantMessage]); + + const baseUrl = window.location.hostname === '127.0.0.1' || window.location.hostname === 'localhost' + ? `http://127.0.0.1:${serverPort}` + : `${window.location.origin}/llm`; + const response = await fetch(`${baseUrl}/generate-stream`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ messages: apiMessages }) + }); + + if (!response.ok) { + let errorText = `Server responded with ${response.status}`; + try { + const errorData = await response.text(); + if (errorData) { + errorText += `: ${errorData}`; + } + } catch (e) { + // Ignore if we can't read the error body + } + throw new Error(errorText); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let accumulatedContent = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.slice(6)); + if (data.error) { + throw new Error(data.error); + } + if (data.text) { + accumulatedContent += data.text; + // Update the assistant message with accumulated content + setMessages(prev => prev.map(msg => + msg.id === assistantMessageId + ? { ...msg, content: accumulatedContent } + : msg + )); + } + if (data.done) { + break; + } + } catch (parseError) { + console.warn('Failed to parse SSE data:', line); + } + } + } + } + } finally { + reader.releaseLock(); + } + } else { + // Handle non-streaming response (original logic) + const baseUrl = window.location.hostname === '127.0.0.1' || window.location.hostname === 'localhost' + ? `http://127.0.0.1:${serverPort}` + : `${window.location.origin}/llm`; + const response = await fetch(`${baseUrl}/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ messages: apiMessages }) + }); + + if (!response.ok) { + let errorText = `Server responded with ${response.status}`; + try { + const errorData = await response.text(); + if (errorData) { + errorText += `: ${errorData}`; + } + } catch (e) { + // Ignore if we can't read the error body + } + throw new Error(errorText); + } + + const responseText = await response.text(); + + // Try to parse as JSON first, in case the server wraps the response in quotes + let finalResponseText; + try { + // If the response is JSON-wrapped (e.g., "\"Hello world\""), parse it + const parsed = JSON.parse(responseText); + finalResponseText = typeof parsed === 'string' ? parsed : responseText; + } catch (e) { + // If it's not JSON, use as-is + finalResponseText = responseText; + } + + const assistantMessage = { + role: 'assistant', + content: finalResponseText, + id: Date.now() + 1 + }; + + setMessages(prev => [...prev, assistantMessage]); + } + } catch (error) { + console.error('Error sending message:', error); + const errorMessage = { + role: 'assistant', + content: `Error: ${error.message}. Please check if the server is running on port ${serverPort} and try refreshing the connection.`, + id: Date.now() + 1, + isError: true + }; + setMessages(prev => [...prev, errorMessage]); + + // Also refresh server status to check if server went offline + handleManualRefresh(); + } finally { + setIsLoading(false); + inputRef.current?.focus(); + } + }; + + const handleKeyPress = (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + const handleCopyMessage = (message) => { + navigator.clipboard.writeText(message.content); + setCopiedMessageId(message.id); + setTimeout(() => setCopiedMessageId(null), 2000); + }; + + const handleEditMessage = (message) => { + setEditingMessageId(message.id); + }; + + const handleSaveEdit = (messageId, newContent) => { + setMessages(prev => prev.map(msg => + msg.id === messageId ? { ...msg, content: newContent } : msg + )); + setEditingMessageId(null); + }; + + const handleDeleteMessage = (messageId) => { + setMessages(prev => prev.filter(msg => msg.id !== messageId)); + }; + + const handleClearChat = () => { + if (window.confirm('Are you sure you want to clear all messages?')) { + setMessages([]); + } + }; + + // Manual refresh function + const handleManualRefresh = async () => { + setIsRefreshingStatus(true); + try { + const baseUrl = window.location.hostname === '127.0.0.1' || window.location.hostname === 'localhost' + ? `http://127.0.0.1:${serverPort}` + : `${window.location.origin}/llm`; + const response = await fetch(`${baseUrl}/health`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + if (response.ok) { + const data = await response.json(); + setServerStatus('online'); + + if (data.message && data.message.includes('RAG')) { + setServerType('rag'); + } else if (data.message && data.message.includes('OpenAI')) { + setServerType('openai'); + } else { + setServerType('unknown'); + } + } else { + setServerStatus('offline'); + setServerType('unknown'); + } + } catch (error) { + setServerStatus('offline'); + setServerType('unknown'); + } finally { + setIsRefreshingStatus(false); + } + }; + + const MessageComponent = ({ message }) => { + const [editContent, setEditContent] = useState(message.content); + const isEditing = editingMessageId === message.id; + const isCopied = copiedMessageId === message.id; + + return ( +
+
+
+ + {message.role === 'user' ? 'You' : 'Assistant'} + +
+ + + +
+
+ + {isEditing ? ( +
+