From 8a313971711d3368e2cc33d1383be9b50f705836 Mon Sep 17 00:00:00 2001 From: maziggy Date: Wed, 6 May 2026 09:10:35 +0200 Subject: [PATCH] feat(slicer): bundle-aware preview slice for accurate gram estimates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SliceModal's preview slice runs against unsliced project files to discover per-plate AMS slot consumption. Until now it always used slice_without_profiles — accurate slot mapping (a model property) but gram numbers were derived from the file's embedded process settings, which can drift from the triplet the real print will use. When the caller provides a bundle id + printer/process/filament preset names, get_preview_filaments now routes through slice_with_bundle so the preview's gram numbers match what the real print will produce. Cache key picks up a bundle-context fingerprint so different bundle picks on the same file occupy distinct entries. Backend: - slice_preview.get_preview_filaments: optional bundle_* params - library.py + archives.py: forward params via /filament-requirements Frontend (forward-compat for the upcoming SliceModal Bundle tier): - api.getLibraryFileFilamentRequirements / getArchiveFilamentRequirements accept an optional 4th-arg bundle context object --- backend/app/api/routes/archives.py | 34 +- backend/app/api/routes/library.py | 37 + backend/app/services/slice_preview.py | 122 ++- .../tests/unit/services/test_slice_preview.py | 184 +++- .../components/SlicerBundlesPanel.test.tsx | 214 ++++ frontend/src/api/client.ts | 86 +- .../src/components/SlicerBundlesPanel.tsx | 198 ++++ frontend/src/i18n/locales/de.ts | 16 + frontend/src/i18n/locales/en.ts | 16 + frontend/src/i18n/locales/fr.ts | 16 + frontend/src/i18n/locales/it.ts | 16 + frontend/src/i18n/locales/ja.ts | 16 + frontend/src/i18n/locales/pt-BR.ts | 16 + frontend/src/i18n/locales/zh-CN.ts | 16 + frontend/src/i18n/locales/zh-TW.ts | 16 + frontend/src/pages/SettingsPage.tsx | 7 + .../{index-DA6QDhrM.js => index-B2mJSviN.js} | 959 +++++++++--------- static/assets/index-CzFVl3Rw.css | 1 + static/assets/index-N3WJn-iA.css | 1 - static/index.html | 4 +- 20 files changed, 1464 insertions(+), 511 deletions(-) create mode 100644 frontend/src/__tests__/components/SlicerBundlesPanel.test.tsx create mode 100644 frontend/src/components/SlicerBundlesPanel.tsx rename static/assets/{index-DA6QDhrM.js => index-B2mJSviN.js} (53%) create mode 100644 static/assets/index-CzFVl3Rw.css delete mode 100644 static/assets/index-N3WJn-iA.css diff --git a/backend/app/api/routes/archives.py b/backend/app/api/routes/archives.py index 9e103a0ee..8ff97ff22 100644 --- a/backend/app/api/routes/archives.py +++ b/backend/app/api/routes/archives.py @@ -3129,12 +3129,21 @@ async def _try_preview_slice_filaments( plate_id: int, file_path: Path, request_id: str | None = None, + bundle_id: str | None = None, + printer_name: str | None = None, + process_name: str | None = None, + filament_names: list[str] | None = None, ) -> list[dict] | None: """Run a preview slice via the user's configured sidecar so the filament list endpoint can return real per-plate filaments for unsliced project files. Returns ``None`` on any failure — the caller falls back to the painted-face heuristic. ``request_id`` flows through to the sidecar - for live progress on the SliceModal's inline spinner + toast.""" + for live progress on the SliceModal's inline spinner + toast. + + Bundle context (id + preset names) is forwarded to the preview helper + so the preview can mirror the real-print profile triplet when supplied + — see ``slice_preview.get_preview_filaments`` for the full contract. + """ from backend.app.api.routes.settings import get_setting from backend.app.services.slice_preview import get_preview_filaments @@ -3162,6 +3171,10 @@ async def _try_preview_slice_filaments( file_name=file_path.name, api_url=api_url, request_id=request_id, + bundle_id=bundle_id, + printer_name=printer_name, + process_name=process_name, + filament_names=filament_names, ) @@ -3170,6 +3183,10 @@ async def get_filament_requirements( archive_id: int, plate_id: int | None = None, request_id: str | None = None, + bundle_id: str | None = None, + printer_name: str | None = None, + process_name: str | None = None, + filament_names: str | None = None, db: AsyncSession = Depends(get_db), _: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ), ): @@ -3181,6 +3198,12 @@ async def get_filament_requirements( Args: archive_id: The archive ID plate_id: Optional plate index to filter filaments for (for multi-plate files) + bundle_id / printer_name / process_name / filament_names: Optional + bundle context. When all four are supplied, the preview slice + (run for unsliced project files) uses ``slice_with_bundle`` + against the named preset triplet instead of the embedded- + settings fallback. ``filament_names`` is comma- or semicolon- + separated. """ import defusedxml.ElementTree as ET @@ -3284,6 +3307,11 @@ async def get_filament_requirements( project_filaments = extract_project_filaments_from_3mf(zf) used_slot_ids: set[int] = set() if project_filaments and plate_id is not None: + parsed_filament_names: list[str] | None = None + if filament_names: + parsed_filament_names = [ + n.strip() for n in filament_names.replace(";", ",").split(",") if n.strip() + ] or None preview = await _try_preview_slice_filaments( db, kind="archive", @@ -3291,6 +3319,10 @@ async def get_filament_requirements( plate_id=plate_id, file_path=file_path, request_id=request_id, + bundle_id=bundle_id, + printer_name=printer_name, + process_name=process_name, + filament_names=parsed_filament_names, ) if preview is not None: used_slot_ids = {f["slot_id"] for f in preview} diff --git a/backend/app/api/routes/library.py b/backend/app/api/routes/library.py index 23efbe1be..9d2306cb3 100644 --- a/backend/app/api/routes/library.py +++ b/backend/app/api/routes/library.py @@ -2363,6 +2363,10 @@ async def _try_preview_slice_filaments( plate_id: int, file_path: Path, request_id: str | None = None, + bundle_id: str | None = None, + printer_name: str | None = None, + process_name: str | None = None, + filament_names: list[str] | None = None, ) -> list[dict] | None: """Run a preview slice via the user's configured sidecar. Same shape as the matching helper in archives.py — see that module for rationale. @@ -2370,6 +2374,13 @@ async def _try_preview_slice_filaments( ``request_id``: when supplied, forwarded to the sidecar so the SliceModal's inline spinner + toast can poll the matching progress endpoint and show "Generating G-code (45%)" for the preview as well. + + ``bundle_id`` / ``printer_name`` / ``process_name`` / ``filament_names``: + when all are supplied, the preview uses ``slice_with_bundle`` against + the named bundle's preset triplet so the preview's gram numbers reflect + the same profiles the real print will use. Partial context falls back + to the embedded-settings path so a half-completed Bundle-tier selection + in the modal doesn't error out. """ from backend.app.api.routes.settings import get_setting from backend.app.services.slice_preview import get_preview_filaments @@ -2398,6 +2409,10 @@ async def _try_preview_slice_filaments( file_name=file_path.name, api_url=api_url, request_id=request_id, + bundle_id=bundle_id, + printer_name=printer_name, + process_name=process_name, + filament_names=filament_names, ) @@ -2406,6 +2421,10 @@ async def get_library_file_filament_requirements( file_id: int, plate_id: int | None = None, request_id: str | None = None, + bundle_id: str | None = None, + printer_name: str | None = None, + process_name: str | None = None, + filament_names: str | None = None, db: AsyncSession = Depends(get_db), _: User | None = Depends(require_permission_if_auth_enabled(Permission.LIBRARY_READ)), ): @@ -2417,6 +2436,12 @@ async def get_library_file_filament_requirements( Args: file_id: The library file ID plate_id: Optional plate index to get filaments for a specific plate + bundle_id / printer_name / process_name / filament_names: Optional + bundle context. When all four are supplied, the preview slice + (run for unsliced project files) uses ``slice_with_bundle`` + against the named preset triplet instead of the embedded- + settings fallback. ``filament_names`` is comma- or semicolon- + separated to mirror the slice route's multi-color form. """ import defusedxml.ElementTree as ET @@ -2536,6 +2561,14 @@ async def get_library_file_filament_requirements( project_filaments = extract_project_filaments_from_3mf(zf) used_slot_ids: set[int] = set() if project_filaments and plate_id is not None: + # Bundle context flows through optional query params so + # callers without a Bundle-tier selection (the common + # case) hit the same path as before. + parsed_filament_names: list[str] | None = None + if filament_names: + parsed_filament_names = [ + n.strip() for n in filament_names.replace(";", ",").split(",") if n.strip() + ] or None preview = await _try_preview_slice_filaments( db, kind="library_file", @@ -2543,6 +2576,10 @@ async def get_library_file_filament_requirements( plate_id=plate_id, file_path=file_path, request_id=request_id, + bundle_id=bundle_id, + printer_name=printer_name, + process_name=process_name, + filament_names=parsed_filament_names, ) if preview is not None: used_slot_ids = {f["slot_id"] for f in preview} diff --git a/backend/app/services/slice_preview.py b/backend/app/services/slice_preview.py index db46bbc2c..867d37d70 100644 --- a/backend/app/services/slice_preview.py +++ b/backend/app/services/slice_preview.py @@ -6,14 +6,26 @@ the ``/filament-requirements`` endpoint can read it directly. For unsliced project files it doesn't exist yet — only the slicer can produce it, since Bambu Studio applies its own pruning to painted-face data at slice time. -This module wraps the sidecar's ``slice_without_profiles`` call so the -endpoint can run a preview slice with the project's embedded settings, -parse the result's slice_info, and return the actual filament list. Results -are cached by ``(kind, source_id, plate_id, content_hash)`` so repeat -opens of the modal on the same plate are instant; LRU eviction keeps the -cache bounded. Hash invalidation handles in-place file replacement; no TTL -is used because preview-slice output is deterministic for a given file -content. +This module wraps the sidecar's slice call so the endpoint can run a preview +slice, parse the result's slice_info, and return the actual filament list. +Two slice modes are supported: + + * "embedded settings" mode (default) — calls ``slice_without_profiles`` so + the slicer falls back on the file's own ``Metadata/project_settings.config``. + Used when the SliceModal opens before the user has picked a profile + triplet and we just want the slot-mapping (which is a model property, + independent of process settings). + + * "bundle" mode — when the caller passes a bundle id + per-category preset + names, calls ``slice_with_bundle`` so the preview reflects the same + triplet the real print will use. More accurate gram numbers; same slot + mapping. Used after the SliceModal's Bundle tier resolves. + +Results are cached by ``(kind, source_id, plate_id, content_hash, bundle_key)`` +so different bundle picks on the same file don't collide and repeat opens +on the same plate + same bundle are instant. LRU eviction keeps the cache +bounded. Hash invalidation handles in-place file replacement; no TTL is +used because preview-slice output is deterministic for a given input. """ from __future__ import annotations @@ -35,22 +47,46 @@ from backend.app.services.slicer_api import ( logger = logging.getLogger(__name__) _PREVIEW_CACHE_MAX = 256 +# Cache key includes a bundle-context fingerprint (or "" when no bundle was +# supplied) so a "preview without profiles" result and a "preview with +# bundle X" result for the same file/plate occupy distinct entries instead +# of clobbering each other. +_PreviewCacheKey = tuple[str, int, int, str, str] # Cache values: list[dict] on success, [] on parsed-but-empty (slicer # returned a 3MF without filament data for this plate — caching the negative # avoids burning 30s+ per modal open on a known-bad input). -_preview_cache: OrderedDict[tuple[str, int, int, str], list[dict]] = OrderedDict() -# Per-key locks prevent N concurrent modal opens on the same (file, plate) -# from launching N redundant preview slices — only the first one runs, the -# rest wait and read from the cache. Locks are evicted alongside cache -# entries to keep the dict bounded; we do NOT cache transient sidecar +_preview_cache: OrderedDict[_PreviewCacheKey, list[dict]] = OrderedDict() +# Per-key locks prevent N concurrent modal opens on the same (file, plate, +# bundle) from launching N redundant preview slices — only the first one +# runs, the rest wait and read from the cache. Locks are evicted alongside +# cache entries to keep the dict bounded; we do NOT cache transient sidecar # failures (network errors etc.) so those retry naturally on next request. -_preview_locks: dict[tuple[str, int, int, str], asyncio.Lock] = {} +_preview_locks: dict[_PreviewCacheKey, asyncio.Lock] = {} def _content_hash(file_bytes: bytes) -> str: return hashlib.sha256(file_bytes).hexdigest()[:16] +def _bundle_context_fingerprint( + bundle_id: str | None, + printer_name: str | None, + process_name: str | None, + filament_names: list[str] | None, +) -> str: + """Derive a stable cache-key fragment for the bundle context. Empty + string when no bundle is supplied — preserves cache compatibility with + the no-bundle ("embedded settings") path so existing entries remain + valid. SHA-256 prefix keeps the key short while collision-resistant + enough for a 256-entry LRU. + """ + if not (bundle_id and printer_name and process_name and filament_names): + return "" + parts = [bundle_id, printer_name, process_name, *filament_names] + raw = "\x1f".join(parts).encode("utf-8") + return hashlib.sha256(raw).hexdigest()[:12] + + async def get_preview_filaments( *, kind: str, @@ -60,16 +96,33 @@ async def get_preview_filaments( file_name: str, api_url: str, request_id: str | None = None, + bundle_id: str | None = None, + printer_name: str | None = None, + process_name: str | None = None, + filament_names: list[str] | None = None, ) -> list[dict] | None: - """Run a preview slice for ``plate_id`` using the file's embedded settings, - parse the resulting slice_info, and return the per-plate filament list. + """Run a preview slice for ``plate_id``, parse the resulting slice_info, + and return the per-plate filament list. + + By default uses the file's embedded settings (``slice_without_profiles``). + When all four ``bundle_*`` params are provided, uses ``slice_with_bundle`` + so the preview matches the profile triplet the real print will use — + same slot mapping, more-accurate gram numbers. Partial bundle context + (e.g. id without preset names) falls back to the embedded path rather + than failing, so an in-progress modal selection doesn't surface errors. Returns ``None`` when the preview slice fails — the caller should fall back to whatever heuristic it has (typically the project_filaments + painted-face approach in ``threemf_tools``). """ h = _content_hash(file_bytes) - key = (kind, source_id, plate_id, h) + bundle_fp = _bundle_context_fingerprint( + bundle_id, + printer_name, + process_name, + filament_names, + ) + key: _PreviewCacheKey = (kind, source_id, plate_id, h, bundle_fp) cached = _preview_cache.get(key) if cached is not None: _preview_cache.move_to_end(key) @@ -86,19 +139,38 @@ async def get_preview_filaments( try: async with SlicerApiService(base_url=api_url) as svc: - result = await svc.slice_without_profiles( - model_bytes=file_bytes, - model_filename=file_name, - plate=plate_id, - export_3mf=True, - request_id=request_id, - ) + if bundle_fp: + # All four bundle params present (guaranteed non-None by + # _bundle_context_fingerprint returning non-empty); + # the type-checker can't see that, so assert for narrowing. + assert bundle_id and printer_name and process_name + assert filament_names is not None + result = await svc.slice_with_bundle( + model_bytes=file_bytes, + model_filename=file_name, + bundle_id=bundle_id, + printer_name=printer_name, + process_name=process_name, + filament_names=filament_names, + plate=plate_id, + export_3mf=True, + request_id=request_id, + ) + else: + result = await svc.slice_without_profiles( + model_bytes=file_bytes, + model_filename=file_name, + plate=plate_id, + export_3mf=True, + request_id=request_id, + ) except SlicerApiError as e: logger.warning( - "Preview slice failed for %s/%s plate %s: %s", + "Preview slice failed for %s/%s plate %s (bundle=%s): %s", kind, source_id, plate_id, + bundle_id or "-", e, ) return None diff --git a/backend/tests/unit/services/test_slice_preview.py b/backend/tests/unit/services/test_slice_preview.py index 6988d5afc..1467f81c7 100644 --- a/backend/tests/unit/services/test_slice_preview.py +++ b/backend/tests/unit/services/test_slice_preview.py @@ -72,7 +72,18 @@ class _StubService: return False async def slice_without_profiles(self, **kw): - self.calls.append(kw) + self.calls.append({"method": "slice_without_profiles", **kw}) + if self.raise_exc is not None: + raise self.raise_exc + return SliceResult( + content=self.response_bytes or b"", + print_time_seconds=0, + filament_used_g=0.0, + filament_used_mm=0.0, + ) + + async def slice_with_bundle(self, **kw): + self.calls.append({"method": "slice_with_bundle", **kw}) if self.raise_exc is not None: raise self.raise_exc return SliceResult( @@ -254,3 +265,174 @@ class TestGetPreviewFilaments: assert len(slice_preview._preview_cache) == _PREVIEW_CACHE_MAX # Lock dict is also pruned (no leak): same size as cache. assert len(slice_preview._preview_locks) == _PREVIEW_CACHE_MAX + + +# --------------------------------------------------------------------------- +# Bundle-aware preview path — when bundle context is supplied, the preview +# routes through `slice_with_bundle` so its gram numbers reflect the same +# triplet the real print will use. Cache must distinguish between bundle +# picks so a fresh selection doesn't re-serve a prior preview's output. +# --------------------------------------------------------------------------- + + +class TestBundleAwarePreview: + @pytest.mark.asyncio + async def test_full_bundle_context_uses_slice_with_bundle(self): + body = _make_sliced_3mf(plate_id=1, filaments=[{"id": "1", "type": "PLA", "color": "#000"}]) + stub = _StubService(response_bytes=body) + with patch.object(slice_preview, "SlicerApiService", lambda **kw: stub): + result = await get_preview_filaments( + kind="library_file", + source_id=42, + plate_id=1, + file_bytes=b"abc", + file_name="x.3mf", + api_url="http://sidecar", + bundle_id="abc123", + printer_name="# Bambu Lab H2D 0.4 nozzle", + process_name="# 0.20mm Standard @BBL H2D", + filament_names=["# Bambu PLA Basic @BBL H2D"], + ) + assert result is not None + assert result[0]["slot_id"] == 1 + # The bundle path engaged — slice_with_bundle was called, not the + # embedded-settings fallback. + assert len(stub.calls) == 1 + assert stub.calls[0]["method"] == "slice_with_bundle" + assert stub.calls[0]["bundle_id"] == "abc123" + assert stub.calls[0]["filament_names"] == ["# Bambu PLA Basic @BBL H2D"] + + @pytest.mark.asyncio + async def test_partial_bundle_context_falls_back_to_embedded(self): + # Modal-in-progress case: user picked a bundle id but hasn't yet + # picked the filament. Falling back to embedded settings keeps + # the preview's slot mapping fresh while gram numbers will firm + # up once the selection completes. + body = _make_sliced_3mf(plate_id=1, filaments=[{"id": "1", "type": "PLA", "color": "#000"}]) + stub = _StubService(response_bytes=body) + with patch.object(slice_preview, "SlicerApiService", lambda **kw: stub): + await get_preview_filaments( + kind="library_file", + source_id=42, + plate_id=1, + file_bytes=b"abc", + file_name="x.3mf", + api_url="http://sidecar", + bundle_id="abc123", + printer_name="# Bambu Lab H2D 0.4 nozzle", + process_name="# 0.20mm Standard @BBL H2D", + # filament_names missing + ) + assert len(stub.calls) == 1 + assert stub.calls[0]["method"] == "slice_without_profiles" + + @pytest.mark.asyncio + async def test_empty_filament_names_list_falls_back(self): + # Empty list (vs None) is treated as "incomplete context" since + # passing `[]` to slice_with_bundle would yield no + # --load-filaments arg and confuse the CLI. + body = _make_sliced_3mf(plate_id=1, filaments=[{"id": "1", "type": "PLA", "color": "#000"}]) + stub = _StubService(response_bytes=body) + with patch.object(slice_preview, "SlicerApiService", lambda **kw: stub): + await get_preview_filaments( + kind="library_file", + source_id=42, + plate_id=1, + file_bytes=b"abc", + file_name="x.3mf", + api_url="http://sidecar", + bundle_id="abc123", + printer_name="P", + process_name="Q", + filament_names=[], + ) + assert stub.calls[0]["method"] == "slice_without_profiles" + + @pytest.mark.asyncio + async def test_cache_separates_bundle_picks(self): + # Same file/plate, two different bundle picks → two distinct cache + # entries → two slices run. Without the bundle-fingerprint cache key, + # the second call would erroneously serve the first's output. + body = _make_sliced_3mf(plate_id=1, filaments=[{"id": "1", "type": "PLA", "color": "#000"}]) + stub = _StubService(response_bytes=body) + with patch.object(slice_preview, "SlicerApiService", lambda **kw: stub): + await get_preview_filaments( + kind="library_file", + source_id=42, + plate_id=1, + file_bytes=b"abc", + file_name="x.3mf", + api_url="http://sidecar", + bundle_id="bundleA", + printer_name="P", + process_name="Q", + filament_names=["F"], + ) + await get_preview_filaments( + kind="library_file", + source_id=42, + plate_id=1, + file_bytes=b"abc", + file_name="x.3mf", + api_url="http://sidecar", + bundle_id="bundleB", + printer_name="P", + process_name="Q", + filament_names=["F"], + ) + assert len(stub.calls) == 2 + assert stub.calls[0]["bundle_id"] == "bundleA" + assert stub.calls[1]["bundle_id"] == "bundleB" + + @pytest.mark.asyncio + async def test_cache_separates_bundle_vs_embedded(self): + # Same file/plate, one call without bundle and one with bundle → + # both must run. The embedded-settings cache entry must NOT be + # served as the bundle-picked result (gram numbers would be wrong). + body = _make_sliced_3mf(plate_id=1, filaments=[{"id": "1", "type": "PLA", "color": "#000"}]) + stub = _StubService(response_bytes=body) + with patch.object(slice_preview, "SlicerApiService", lambda **kw: stub): + await get_preview_filaments( + kind="library_file", + source_id=42, + plate_id=1, + file_bytes=b"abc", + file_name="x.3mf", + api_url="http://sidecar", + ) + await get_preview_filaments( + kind="library_file", + source_id=42, + plate_id=1, + file_bytes=b"abc", + file_name="x.3mf", + api_url="http://sidecar", + bundle_id="bundleA", + printer_name="P", + process_name="Q", + filament_names=["F"], + ) + methods = [c["method"] for c in stub.calls] + assert methods == ["slice_without_profiles", "slice_with_bundle"] + + @pytest.mark.asyncio + async def test_bundle_repeat_call_hits_cache(self): + # Sanity check that the new cache key is otherwise stable: same + # bundle pick on the same file → cache hit on second call. + body = _make_sliced_3mf(plate_id=1, filaments=[{"id": "1", "type": "PLA", "color": "#000"}]) + stub = _StubService(response_bytes=body) + with patch.object(slice_preview, "SlicerApiService", lambda **kw: stub): + for _ in range(2): + await get_preview_filaments( + kind="library_file", + source_id=42, + plate_id=1, + file_bytes=b"abc", + file_name="x.3mf", + api_url="http://sidecar", + bundle_id="bundleA", + printer_name="P", + process_name="Q", + filament_names=["F"], + ) + assert len(stub.calls) == 1 diff --git a/frontend/src/__tests__/components/SlicerBundlesPanel.test.tsx b/frontend/src/__tests__/components/SlicerBundlesPanel.test.tsx new file mode 100644 index 000000000..6efe1bdd5 --- /dev/null +++ b/frontend/src/__tests__/components/SlicerBundlesPanel.test.tsx @@ -0,0 +1,214 @@ +/** + * Tests for the SlicerBundlesPanel — Settings panel for managing + * BambuStudio Printer Preset Bundles (.bbscfg) on the slicer sidecar. + * + * Coverage: + * - Empty state when the sidecar has no bundles imported yet. + * - List rendering with summary line (process / filament counts). + * - Upload happy path → success toast + list invalidation. + * - Upload error → error toast. + * - Delete with confirmation → success toast + list invalidation. + * - Delete error → error toast. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { render } from '../utils'; +import { api } from '../../api/client'; +import { SlicerBundlesPanel } from '../../components/SlicerBundlesPanel'; + +vi.mock('../../api/client', async () => { + const actual: typeof import('../../api/client') = await vi.importActual( + '../../api/client', + ); + return { + ...actual, + api: { + ...actual.api, + listSlicerBundles: vi.fn(), + importSlicerBundle: vi.fn(), + deleteSlicerBundle: vi.fn(), + }, + getAuthToken: vi.fn(() => null), + }; +}); + +const SAMPLE_BUNDLE = { + id: 'abc123def456abcd', + printer_preset_name: '# Bambu Lab H2D 0.4 nozzle', + printer: ['# Bambu Lab H2D 0.4 nozzle'], + process: [ + '# 0.20mm Standard @BBL H2D', + '# 0.16mm Standard @BBL H2D', + ], + filament: [ + '# Bambu PLA Basic @BBL H2D', + '# Bambu PETG HF @BBL H2D 0.4 nozzle', + '# Bambu ABS @BBL H2D', + ], + version: '02.06.00.50', +}; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('SlicerBundlesPanel — empty state', () => { + it('renders the empty-state message when no bundles exist', async () => { + vi.mocked(api.listSlicerBundles).mockResolvedValueOnce([]); + + render(); + + await waitFor(() => + expect(api.listSlicerBundles).toHaveBeenCalled(), + ); + expect( + await screen.findByText(/no bundles imported yet/i), + ).toBeInTheDocument(); + }); +}); + +describe('SlicerBundlesPanel — list rendering', () => { + it('renders bundle name + summary (process and filament counts)', async () => { + vi.mocked(api.listSlicerBundles).mockResolvedValueOnce([SAMPLE_BUNDLE]); + + render(); + + expect( + await screen.findByText('# Bambu Lab H2D 0.4 nozzle'), + ).toBeInTheDocument(); + // Summary should reflect 2 process + 3 filament from the fixture. + expect( + await screen.findByText(/2 process · 3 filament/i), + ).toBeInTheDocument(); + // Version suffix appended after the summary. + expect(screen.getByText(/v02\.06\.00\.50/)).toBeInTheDocument(); + }); +}); + +describe('SlicerBundlesPanel — upload flow', () => { + it('imports a selected file and refreshes the list on success', async () => { + // First listing call returns empty so the test can detect the post-import + // re-fetch (second call) returning the new bundle. + vi.mocked(api.listSlicerBundles) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([SAMPLE_BUNDLE]); + vi.mocked(api.importSlicerBundle).mockResolvedValueOnce(SAMPLE_BUNDLE); + + const { container } = render(); + + // The file input is hidden (display: none for styling); grab it directly. + const fileInput = container.querySelector( + 'input[type="file"]', + ) as HTMLInputElement; + expect(fileInput).toBeTruthy(); + + const file = new File([new Uint8Array([0x50, 0x4b, 0x03, 0x04])], 'H2D.bbscfg', { + type: 'application/zip', + }); + + fireEvent.change(fileInput, { target: { files: [file] } }); + + await waitFor(() => + expect(api.importSlicerBundle).toHaveBeenCalledWith(file), + ); + // After the success, the list call should fire a second time (cache + // invalidation by react-query). + await waitFor(() => + expect(api.listSlicerBundles).toHaveBeenCalledTimes(2), + ); + // The newly imported bundle should now be visible in the list. + expect( + await screen.findByText('# Bambu Lab H2D 0.4 nozzle'), + ).toBeInTheDocument(); + }); + + it('shows an error and does not refresh on upload failure', async () => { + vi.mocked(api.listSlicerBundles).mockResolvedValueOnce([]); + vi.mocked(api.importSlicerBundle).mockRejectedValueOnce( + new Error('Bundle is missing bundle_structure.json'), + ); + + const { container } = render(); + + await waitFor(() => expect(api.listSlicerBundles).toHaveBeenCalled()); + + const fileInput = container.querySelector( + 'input[type="file"]', + ) as HTMLInputElement; + const file = new File([new Uint8Array([0])], 'bad.bbscfg', { + type: 'application/zip', + }); + fireEvent.change(fileInput, { target: { files: [file] } }); + + await waitFor(() => + expect(api.importSlicerBundle).toHaveBeenCalled(), + ); + // Listing should NOT be re-called on failure — only the initial load. + expect(api.listSlicerBundles).toHaveBeenCalledTimes(1); + // Empty state still showing. + expect( + screen.getByText(/no bundles imported yet/i), + ).toBeInTheDocument(); + }); +}); + +describe('SlicerBundlesPanel — delete flow', () => { + it('deletes a bundle after confirmation and refreshes the list', async () => { + vi.mocked(api.listSlicerBundles) + .mockResolvedValueOnce([SAMPLE_BUNDLE]) + .mockResolvedValueOnce([]); + vi.mocked(api.deleteSlicerBundle).mockResolvedValueOnce(undefined); + + render(); + + // Wait for the bundle to render. + await screen.findByText('# Bambu Lab H2D 0.4 nozzle'); + + // Click the trash button (aria-label="Delete"). + fireEvent.click(screen.getByRole('button', { name: /delete/i })); + + // ConfirmModal should appear with the bundle name in the message. + const confirmMessage = await screen.findByText( + /Slice requests that reference "# Bambu Lab H2D 0.4 nozzle" will fail/i, + ); + expect(confirmMessage).toBeInTheDocument(); + + // The modal renders its own "Delete" button — there are now two buttons + // matching /delete/i. Click the one inside the dialog (last in document + // order, since the modal portal renders after the panel). + const deleteButtons = screen.getAllByRole('button', { name: /delete/i }); + fireEvent.click(deleteButtons[deleteButtons.length - 1]); + + await waitFor(() => + expect(api.deleteSlicerBundle).toHaveBeenCalledWith( + 'abc123def456abcd', + ), + ); + // Cache invalidation should re-fire the list query. + await waitFor(() => + expect(api.listSlicerBundles).toHaveBeenCalledTimes(2), + ); + }); + + it('keeps the bundle in the list when the user cancels the delete dialog', async () => { + vi.mocked(api.listSlicerBundles).mockResolvedValueOnce([SAMPLE_BUNDLE]); + + render(); + + await screen.findByText('# Bambu Lab H2D 0.4 nozzle'); + fireEvent.click(screen.getByRole('button', { name: /delete/i })); + + // Cancel by clicking the "Cancel" button on the ConfirmModal. + const cancelButton = await screen.findByRole('button', { name: /cancel/i }); + fireEvent.click(cancelButton); + + // Delete API never called, list never re-fetched. + expect(api.deleteSlicerBundle).not.toHaveBeenCalled(); + expect(api.listSlicerBundles).toHaveBeenCalledTimes(1); + // Bundle still rendered. + expect( + screen.getByText('# Bambu Lab H2D 0.4 nozzle'), + ).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index e0d504b15..deaa49b58 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1171,6 +1171,20 @@ export interface SliceRequest { export_3mf?: boolean; } +// GET /api/v1/slicer/bundles — Printer Preset Bundles imported from +// BambuStudio's "File → Export → Export Preset Bundle" dialog. Each bundle +// is a .bbscfg zip the user uploads once per printer, after which the +// SliceModal can pick its inner presets by name (no re-upload per slice). +// Backend: backend/app/api/routes/slicer_presets.py — bundle endpoints. +export interface SlicerBundle { + id: string; + printer_preset_name: string; + printer: string[]; + process: string[]; + filament: string[]; + version: string | null; +} + // GET /api/v1/slicer/presets — unified listing across cloud / local / standard. export type SlicerCloudStatus = 'ok' | 'not_authenticated' | 'expired' | 'unreachable'; export interface UnifiedPreset { @@ -3776,10 +3790,31 @@ export const api = { }, getArchivePlates: (archiveId: number) => request(`/archives/${archiveId}/plates`), - getArchiveFilamentRequirements: (archiveId: number, plateId?: number, requestId?: string) => { + getArchiveFilamentRequirements: ( + archiveId: number, + plateId?: number, + requestId?: string, + // Optional bundle context: when supplied, the backend's preview slice + // (run for unsliced project files) uses slice_with_bundle so gram + // numbers reflect the same triplet the real print will use. All four + // fields must be set for the bundle path to engage; partial context + // falls back to the embedded-settings preview without erroring. + bundle?: { + bundle_id: string; + printer_name: string; + process_name: string; + filament_names: string[]; + }, + ) => { const qs = new URLSearchParams(); if (plateId !== undefined) qs.set('plate_id', String(plateId)); if (requestId) qs.set('request_id', requestId); + if (bundle) { + qs.set('bundle_id', bundle.bundle_id); + qs.set('printer_name', bundle.printer_name); + qs.set('process_name', bundle.process_name); + qs.set('filament_names', bundle.filament_names.join(';')); + } return request<{ archive_id: number; filename: string; @@ -5141,10 +5176,28 @@ export const api = { }), getLibraryFilePlates: (fileId: number) => request(`/library/files/${fileId}/plates`), - getLibraryFileFilamentRequirements: (fileId: number, plateId?: number, requestId?: string) => { + getLibraryFileFilamentRequirements: ( + fileId: number, + plateId?: number, + requestId?: string, + // Optional bundle context — see getArchiveFilamentRequirements above + // for the contract. Same shape so callers can share a builder helper. + bundle?: { + bundle_id: string; + printer_name: string; + process_name: string; + filament_names: string[]; + }, + ) => { const qs = new URLSearchParams(); if (plateId !== undefined) qs.set('plate_id', String(plateId)); if (requestId) qs.set('request_id', requestId); + if (bundle) { + qs.set('bundle_id', bundle.bundle_id); + qs.set('printer_name', bundle.printer_name); + qs.set('process_name', bundle.process_name); + qs.set('filament_names', bundle.filament_names.join(';')); + } return request<{ file_id: number; filename: string; @@ -5269,6 +5322,35 @@ export const api = { getSlicerPresets: () => request('/slicer/presets'), + // Slicer Bundles (.bbscfg) — Printer Preset Bundles imported from BambuStudio. + // Settings → Slicer Bundles uploads/lists/deletes; the SliceModal picks + // presets by name from a chosen bundle (separate follow-up). + listSlicerBundles: () => + request('/slicer/bundles'), + importSlicerBundle: (file: File) => { + // The /slicer/bundles upload accepts multipart with field name "file" + // (matches the FastAPI route's UploadFile parameter). Bypass `request` + // because it always JSON-stringifies the body — multipart needs the + // browser to set the boundary in the Content-Type header. + const fd = new FormData(); + fd.append('file', file); + return fetch(`${API_BASE}/slicer/bundles`, { + method: 'POST', + headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}, + body: fd, + }).then(async (res) => { + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.detail || `HTTP ${res.status}`); + } + return res.json() as Promise; + }); + }, + deleteSlicerBundle: (bundleId: string) => + request(`/slicer/bundles/${encodeURIComponent(bundleId)}`, { + method: 'DELETE', + }), + // Local Presets (OrcaSlicer imports) getLocalPresets: () => request('/local-presets/'), diff --git a/frontend/src/components/SlicerBundlesPanel.tsx b/frontend/src/components/SlicerBundlesPanel.tsx new file mode 100644 index 000000000..1114f8f53 --- /dev/null +++ b/frontend/src/components/SlicerBundlesPanel.tsx @@ -0,0 +1,198 @@ +import { useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { Loader2, Package, Trash2, Upload } from 'lucide-react'; +import { api, type SlicerBundle } from '../api/client'; +import { Card, CardContent, CardHeader } from './Card'; +import { Button } from './Button'; +import { ConfirmModal } from './ConfirmModal'; +import { useToast } from '../contexts/ToastContext'; + +// Settings panel for managing BambuStudio "Printer Preset Bundles" +// (.bbscfg) on the slicer sidecar. Sits below the slicer-API URL panel +// in SettingsPage and is hidden when use_slicer_api is off — without a +// configured sidecar there's nowhere to upload bundles to. +// +// Backend wiring: backend/app/api/routes/slicer_presets.py exposes +// /api/v1/slicer/bundles (POST/GET/DELETE). The list call returns [] +// when no sidecar is configured, so an empty render is the natural +// "first-run" state for users who haven't enabled the sidecar yet. +export function SlicerBundlesPanel() { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + const { showToast } = useToast(); + const fileInputRef = useRef(null); + const [pendingDelete, setPendingDelete] = useState(null); + + const { data: bundles, isLoading } = useQuery({ + queryKey: ['slicer-bundles'], + queryFn: api.listSlicerBundles, + }); + + const importMutation = useMutation({ + mutationFn: (file: File) => api.importSlicerBundle(file), + onSuccess: (bundle) => { + queryClient.invalidateQueries({ queryKey: ['slicer-bundles'] }); + showToast( + t('settings.slicerBundles.uploadSuccess', { + defaultValue: 'Imported {{name}}', + name: bundle.printer_preset_name, + }), + 'success', + ); + // Reset the file input so the same file can be re-selected after a + // failed retry. (Without this, a second click on the same file + // doesn't trigger onChange and looks like the panel is broken.) + if (fileInputRef.current) fileInputRef.current.value = ''; + }, + onError: (err: Error) => { + showToast( + t('settings.slicerBundles.uploadError', { + defaultValue: 'Bundle upload failed: {{message}}', + message: err.message, + }), + 'error', + ); + if (fileInputRef.current) fileInputRef.current.value = ''; + }, + }); + + const deleteMutation = useMutation({ + mutationFn: (bundleId: string) => api.deleteSlicerBundle(bundleId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['slicer-bundles'] }); + setPendingDelete(null); + showToast( + t('settings.slicerBundles.deleteSuccess', { + defaultValue: 'Bundle removed', + }), + 'success', + ); + }, + onError: (err: Error) => { + showToast( + t('settings.slicerBundles.deleteError', { + defaultValue: 'Bundle delete failed: {{message}}', + message: err.message, + }), + 'error', + ); + setPendingDelete(null); + }, + }); + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + importMutation.mutate(file); + }; + + return ( + + +

+ + {t('settings.slicerBundles.title', { defaultValue: 'Slicer Bundles' })} +

+
+ +

+ {t('settings.slicerBundles.description', { + defaultValue: + 'Import a Printer Preset Bundle (.bbscfg) exported from BambuStudio (File → Export → Export Preset Bundle → "Printer preset bundle"). Once imported, slice requests can pick presets from the bundle by name without re-uploading the JSON profile triplet.', + })} +

+ +
+ + +
+ + {isLoading ? ( +
+ + {t('settings.slicerBundles.loading', { defaultValue: 'Loading bundles…' })} +
+ ) : bundles && bundles.length > 0 ? ( +
    + {bundles.map((b) => ( +
  • +
    +

    {b.printer_preset_name}

    +

    + {t('settings.slicerBundles.summary', { + defaultValue: + '{{processCount}} process · {{filamentCount}} filament presets', + processCount: b.process.length, + filamentCount: b.filament.length, + })} + {b.version && ` · v${b.version}`} +

    +
    + +
  • + ))} +
+ ) : ( +

+ {t('settings.slicerBundles.empty', { + defaultValue: 'No bundles imported yet.', + })} +

+ )} +
+ + {pendingDelete && ( + deleteMutation.mutate(pendingDelete.id)} + onCancel={() => setPendingDelete(null)} + /> + )} +
+ ); +} diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index c69168aaf..e27380fc5 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -1852,6 +1852,22 @@ export default { orcaslicerApiUrl: 'OrcaSlicer Sidecar-URL', bambuStudioApiUrl: 'Bambu Studio Sidecar-URL', slicerApiUrlDescription: 'URL des Slicer-API-Sidecar-Containers. Leer lassen, um die SLICER_API_URL- bzw. BAMBU_STUDIO_API_URL-Umgebungsvariablen zu nutzen.', + slicerBundles: { + title: 'Slicer-Bundles', + description: 'Importiere ein Drucker-Voreinstellungspaket (.bbscfg), das aus BambuStudio exportiert wurde (Datei → Exportieren → Voreinstellungspaket exportieren → "Drucker-Voreinstellungspaket"). Nach dem Import können Slice-Anfragen Voreinstellungen aus dem Bundle per Name auswählen, ohne das JSON-Profil-Trio erneut hochzuladen.', + uploadButton: 'Bundle hochladen', + uploading: 'Hochladen…', + loading: 'Bundles werden geladen…', + empty: 'Noch keine Bundles importiert.', + summary: '{{processCount}} Prozess · {{filamentCount}} Filament-Voreinstellungen', + delete: 'Löschen', + uploadSuccess: '{{name}} importiert', + uploadError: 'Bundle-Upload fehlgeschlagen: {{message}}', + deleteSuccess: 'Bundle entfernt', + deleteError: 'Löschen des Bundles fehlgeschlagen: {{message}}', + confirmDeleteTitle: 'Dieses Bundle entfernen?', + confirmDeleteMessage: 'Slice-Anfragen, die "{{name}}" referenzieren, schlagen fehl, bis das Bundle erneut importiert wird.', + }, externalCameras: 'Externe Kameras', costTracking: 'Kostenverfolgung', printsOnly: 'Nur Drucke', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 3b0bd5cfa..aaf319f21 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1855,6 +1855,22 @@ export default { orcaslicerApiUrl: 'OrcaSlicer sidecar URL', bambuStudioApiUrl: 'Bambu Studio sidecar URL', slicerApiUrlDescription: 'URL of the slicer-API sidecar container. Leave blank to use the SLICER_API_URL / BAMBU_STUDIO_API_URL env var defaults.', + slicerBundles: { + title: 'Slicer Bundles', + description: 'Import a Printer Preset Bundle (.bbscfg) exported from BambuStudio (File → Export → Export Preset Bundle → "Printer preset bundle"). Once imported, slice requests can pick presets from the bundle by name without re-uploading the JSON profile triplet.', + uploadButton: 'Upload bundle', + uploading: 'Uploading…', + loading: 'Loading bundles…', + empty: 'No bundles imported yet.', + summary: '{{processCount}} process · {{filamentCount}} filament presets', + delete: 'Delete', + uploadSuccess: 'Imported {{name}}', + uploadError: 'Bundle upload failed: {{message}}', + deleteSuccess: 'Bundle removed', + deleteError: 'Bundle delete failed: {{message}}', + confirmDeleteTitle: 'Remove this bundle?', + confirmDeleteMessage: 'Slice requests that reference "{{name}}" will fail until the bundle is re-imported.', + }, externalCameras: 'External Cameras', costTracking: 'Cost Tracking', printsOnly: 'Prints Only', diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index c4235a9a0..0decc8e5e 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -1809,6 +1809,22 @@ export default { orcaslicerApiUrl: 'OrcaSlicer sidecar URL', bambuStudioApiUrl: 'Bambu Studio sidecar URL', slicerApiUrlDescription: 'URL of the slicer-API sidecar container. Leave blank to use the SLICER_API_URL / BAMBU_STUDIO_API_URL env var defaults.', + slicerBundles: { + title: 'Slicer Bundles', + description: 'Import a Printer Preset Bundle (.bbscfg) exported from BambuStudio (File → Export → Export Preset Bundle → "Printer preset bundle"). Once imported, slice requests can pick presets from the bundle by name without re-uploading the JSON profile triplet.', + uploadButton: 'Upload bundle', + uploading: 'Uploading…', + loading: 'Loading bundles…', + empty: 'No bundles imported yet.', + summary: '{{processCount}} process · {{filamentCount}} filament presets', + delete: 'Delete', + uploadSuccess: 'Imported {{name}}', + uploadError: 'Bundle upload failed: {{message}}', + deleteSuccess: 'Bundle removed', + deleteError: 'Bundle delete failed: {{message}}', + confirmDeleteTitle: 'Remove this bundle?', + confirmDeleteMessage: 'Slice requests that reference "{{name}}" will fail until the bundle is re-imported.', + }, externalCameras: 'Caméras externes', costTracking: 'Suivi des coûts', printsOnly: 'Impressions uniquement', diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index a813dc399..f4c3a9c87 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -1809,6 +1809,22 @@ export default { orcaslicerApiUrl: 'OrcaSlicer sidecar URL', bambuStudioApiUrl: 'Bambu Studio sidecar URL', slicerApiUrlDescription: 'URL of the slicer-API sidecar container. Leave blank to use the SLICER_API_URL / BAMBU_STUDIO_API_URL env var defaults.', + slicerBundles: { + title: 'Slicer Bundles', + description: 'Import a Printer Preset Bundle (.bbscfg) exported from BambuStudio (File → Export → Export Preset Bundle → "Printer preset bundle"). Once imported, slice requests can pick presets from the bundle by name without re-uploading the JSON profile triplet.', + uploadButton: 'Upload bundle', + uploading: 'Uploading…', + loading: 'Loading bundles…', + empty: 'No bundles imported yet.', + summary: '{{processCount}} process · {{filamentCount}} filament presets', + delete: 'Delete', + uploadSuccess: 'Imported {{name}}', + uploadError: 'Bundle upload failed: {{message}}', + deleteSuccess: 'Bundle removed', + deleteError: 'Bundle delete failed: {{message}}', + confirmDeleteTitle: 'Remove this bundle?', + confirmDeleteMessage: 'Slice requests that reference "{{name}}" will fail until the bundle is re-imported.', + }, externalCameras: 'Camere esterne', costTracking: 'Tracciamento costi', printsOnly: 'Solo stampe', diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 2b012a2aa..8c86b3346 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -1851,6 +1851,22 @@ export default { orcaslicerApiUrl: 'OrcaSlicer sidecar URL', bambuStudioApiUrl: 'Bambu Studio sidecar URL', slicerApiUrlDescription: 'URL of the slicer-API sidecar container. Leave blank to use the SLICER_API_URL / BAMBU_STUDIO_API_URL env var defaults.', + slicerBundles: { + title: 'Slicer Bundles', + description: 'Import a Printer Preset Bundle (.bbscfg) exported from BambuStudio (File → Export → Export Preset Bundle → "Printer preset bundle"). Once imported, slice requests can pick presets from the bundle by name without re-uploading the JSON profile triplet.', + uploadButton: 'Upload bundle', + uploading: 'Uploading…', + loading: 'Loading bundles…', + empty: 'No bundles imported yet.', + summary: '{{processCount}} process · {{filamentCount}} filament presets', + delete: 'Delete', + uploadSuccess: 'Imported {{name}}', + uploadError: 'Bundle upload failed: {{message}}', + deleteSuccess: 'Bundle removed', + deleteError: 'Bundle delete failed: {{message}}', + confirmDeleteTitle: 'Remove this bundle?', + confirmDeleteMessage: 'Slice requests that reference "{{name}}" will fail until the bundle is re-imported.', + }, externalCameras: '外部カメラ', costTracking: 'コスト追跡', printsOnly: '印刷のみ', diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index 89d688469..f8796ea2c 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/frontend/src/i18n/locales/pt-BR.ts @@ -1809,6 +1809,22 @@ export default { orcaslicerApiUrl: 'OrcaSlicer sidecar URL', bambuStudioApiUrl: 'Bambu Studio sidecar URL', slicerApiUrlDescription: 'URL of the slicer-API sidecar container. Leave blank to use the SLICER_API_URL / BAMBU_STUDIO_API_URL env var defaults.', + slicerBundles: { + title: 'Slicer Bundles', + description: 'Import a Printer Preset Bundle (.bbscfg) exported from BambuStudio (File → Export → Export Preset Bundle → "Printer preset bundle"). Once imported, slice requests can pick presets from the bundle by name without re-uploading the JSON profile triplet.', + uploadButton: 'Upload bundle', + uploading: 'Uploading…', + loading: 'Loading bundles…', + empty: 'No bundles imported yet.', + summary: '{{processCount}} process · {{filamentCount}} filament presets', + delete: 'Delete', + uploadSuccess: 'Imported {{name}}', + uploadError: 'Bundle upload failed: {{message}}', + deleteSuccess: 'Bundle removed', + deleteError: 'Bundle delete failed: {{message}}', + confirmDeleteTitle: 'Remove this bundle?', + confirmDeleteMessage: 'Slice requests that reference "{{name}}" will fail until the bundle is re-imported.', + }, externalCameras: 'Câmeras Externas', costTracking: 'Rastreamento de Custos', printsOnly: 'Apenas Impressões', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index a1649be26..f9fc9e482 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -1853,6 +1853,22 @@ export default { orcaslicerApiUrl: 'OrcaSlicer sidecar URL', bambuStudioApiUrl: 'Bambu Studio sidecar URL', slicerApiUrlDescription: 'URL of the slicer-API sidecar container. Leave blank to use the SLICER_API_URL / BAMBU_STUDIO_API_URL env var defaults.', + slicerBundles: { + title: 'Slicer Bundles', + description: 'Import a Printer Preset Bundle (.bbscfg) exported from BambuStudio (File → Export → Export Preset Bundle → "Printer preset bundle"). Once imported, slice requests can pick presets from the bundle by name without re-uploading the JSON profile triplet.', + uploadButton: 'Upload bundle', + uploading: 'Uploading…', + loading: 'Loading bundles…', + empty: 'No bundles imported yet.', + summary: '{{processCount}} process · {{filamentCount}} filament presets', + delete: 'Delete', + uploadSuccess: 'Imported {{name}}', + uploadError: 'Bundle upload failed: {{message}}', + deleteSuccess: 'Bundle removed', + deleteError: 'Bundle delete failed: {{message}}', + confirmDeleteTitle: 'Remove this bundle?', + confirmDeleteMessage: 'Slice requests that reference "{{name}}" will fail until the bundle is re-imported.', + }, externalCameras: '外部摄像头', costTracking: '成本追踪', printsOnly: '仅打印', diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts index a90f54e01..65067a53e 100644 --- a/frontend/src/i18n/locales/zh-TW.ts +++ b/frontend/src/i18n/locales/zh-TW.ts @@ -1853,6 +1853,22 @@ export default { orcaslicerApiUrl: 'OrcaSlicer sidecar URL', bambuStudioApiUrl: 'Bambu Studio sidecar URL', slicerApiUrlDescription: 'URL of the slicer-API sidecar container. Leave blank to use the SLICER_API_URL / BAMBU_STUDIO_API_URL env var defaults.', + slicerBundles: { + title: 'Slicer Bundles', + description: 'Import a Printer Preset Bundle (.bbscfg) exported from BambuStudio (File → Export → Export Preset Bundle → "Printer preset bundle"). Once imported, slice requests can pick presets from the bundle by name without re-uploading the JSON profile triplet.', + uploadButton: 'Upload bundle', + uploading: 'Uploading…', + loading: 'Loading bundles…', + empty: 'No bundles imported yet.', + summary: '{{processCount}} process · {{filamentCount}} filament presets', + delete: 'Delete', + uploadSuccess: 'Imported {{name}}', + uploadError: 'Bundle upload failed: {{message}}', + deleteSuccess: 'Bundle removed', + deleteError: 'Bundle delete failed: {{message}}', + confirmDeleteTitle: 'Remove this bundle?', + confirmDeleteMessage: 'Slice requests that reference "{{name}}" will fail until the bundle is re-imported.', + }, externalCameras: '外部攝影機', costTracking: '成本追蹤', printsOnly: '僅列印', diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 7f0f02cc8..212f09214 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -8,6 +8,7 @@ import { formatDateOnly } from '../utils/date'; import { getCurrencySymbol, SUPPORTED_CURRENCIES } from '../utils/currency'; import type { APIKey, AppSettings, AppSettingsUpdate, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus, GitHubBackupStatus, CloudAuthStatus, UserCreate, UserUpdate, UserResponse, StorageUsageResponse } from '../api/client'; import { Card, CardContent, CardDensityProvider, CardHeader } from '../components/Card'; +import { SlicerBundlesPanel } from '../components/SlicerBundlesPanel'; import { CameraTokensSection } from './CameraTokensPage'; import { Collapsible } from '../components/Collapsible'; import { Button } from '../components/Button'; @@ -4262,6 +4263,12 @@ export function SettingsPage() { + {/* Slicer Preset Bundles — only meaningful when the sidecar is in use, + since uploads / lists round-trip through it. Hide it entirely when + use_slicer_api is off so the Settings page doesn't show a panel that + can't do anything. */} + {(localSettings.use_slicer_api ?? false) && } + {/* Auto-Drying */} diff --git a/static/assets/index-DA6QDhrM.js b/static/assets/index-B2mJSviN.js similarity index 53% rename from static/assets/index-DA6QDhrM.js rename to static/assets/index-B2mJSviN.js index c96689edf..8a2e6337b 100644 --- a/static/assets/index-DA6QDhrM.js +++ b/static/assets/index-B2mJSviN.js @@ -1,42 +1,42 @@ -function cue(t,e){for(var n=0;nr[i]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function r(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();var nk=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function hc(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var TM={exports:{}},Fv={};var Y8;function due(){if(Y8)return Fv;Y8=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function n(r,i,s){var o=null;if(s!==void 0&&(o=""+s),i.key!==void 0&&(o=""+i.key),"key"in i){s={};for(var l in i)l!=="key"&&(s[l]=i[l])}else s=i;return i=s.ref,{$$typeof:t,type:r,key:o,ref:i!==void 0?i:null,props:s}}return Fv.Fragment=e,Fv.jsx=n,Fv.jsxs=n,Fv}var Q8;function uue(){return Q8||(Q8=1,TM.exports=due()),TM.exports}var a=uue(),AM={exports:{}},Gn={};var J8;function mue(){if(J8)return Gn;J8=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),s=Symbol.for("react.consumer"),o=Symbol.for("react.context"),l=Symbol.for("react.forward_ref"),c=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),u=Symbol.for("react.lazy"),m=Symbol.for("react.activity"),p=Symbol.iterator;function f(K){return K===null||typeof K!="object"?null:(K=p&&K[p]||K["@@iterator"],typeof K=="function"?K:null)}var x={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},v=Object.assign,g={};function b(K,ie,pe){this.props=K,this.context=ie,this.refs=g,this.updater=pe||x}b.prototype.isReactComponent={},b.prototype.setState=function(K,ie){if(typeof K!="object"&&typeof K!="function"&&K!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,K,ie,"setState")},b.prototype.forceUpdate=function(K){this.updater.enqueueForceUpdate(this,K,"forceUpdate")};function S(){}S.prototype=b.prototype;function C(K,ie,pe){this.props=K,this.context=ie,this.refs=g,this.updater=pe||x}var P=C.prototype=new S;P.constructor=C,v(P,b.prototype),P.isPureReactComponent=!0;var N=Array.isArray;function A(){}var T={H:null,A:null,T:null,S:null},E=Object.prototype.hasOwnProperty;function k(K,ie,pe){var xe=pe.ref;return{$$typeof:t,type:K,key:ie,ref:xe!==void 0?xe:null,props:pe}}function M(K,ie){return k(K.type,ie,K.props)}function H(K){return typeof K=="object"&&K!==null&&K.$$typeof===t}function O(K){var ie={"=":"=0",":":"=2"};return"$"+K.replace(/[=:]/g,function(pe){return ie[pe]})}var X=/\/+/g;function L(K,ie){return typeof K=="object"&&K!==null&&K.key!=null?O(""+K.key):ie.toString(36)}function ee(K){switch(K.status){case"fulfilled":return K.value;case"rejected":throw K.reason;default:switch(typeof K.status=="string"?K.then(A,A):(K.status="pending",K.then(function(ie){K.status==="pending"&&(K.status="fulfilled",K.value=ie)},function(ie){K.status==="pending"&&(K.status="rejected",K.reason=ie)})),K.status){case"fulfilled":return K.value;case"rejected":throw K.reason}}throw K}function re(K,ie,pe,xe,fe){var B=typeof K;(B==="undefined"||B==="boolean")&&(K=null);var Y=!1;if(K===null)Y=!0;else switch(B){case"bigint":case"string":case"number":Y=!0;break;case"object":switch(K.$$typeof){case t:case e:Y=!0;break;case u:return Y=K._init,re(Y(K._payload),ie,pe,xe,fe)}}if(Y)return fe=fe(K),Y=xe===""?"."+L(K,0):xe,N(fe)?(pe="",Y!=null&&(pe=Y.replace(X,"$&/")+"/"),re(fe,ie,pe,"",function(U){return U})):fe!=null&&(H(fe)&&(fe=M(fe,pe+(fe.key==null||K&&K.key===fe.key?"":(""+fe.key).replace(X,"$&/")+"/")+Y)),ie.push(fe)),1;Y=0;var F=xe===""?".":xe+":";if(N(K))for(var D=0;D>>1,ce=re[se];if(0>>1;sei(pe,ue))xei(fe,pe)?(re[se]=fe,re[xe]=ue,se=xe):(re[se]=pe,re[ie]=ue,se=ie);else if(xei(fe,ue))re[se]=fe,re[xe]=ue,se=xe;else break e}}return J}function i(re,J){var ue=re.sortIndex-J.sortIndex;return ue!==0?ue:re.id-J.id}if(t.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var s=performance;t.unstable_now=function(){return s.now()}}else{var o=Date,l=o.now();t.unstable_now=function(){return o.now()-l}}var c=[],d=[],u=1,m=null,p=3,f=!1,x=!1,v=!1,g=!1,b=typeof setTimeout=="function"?setTimeout:null,S=typeof clearTimeout=="function"?clearTimeout:null,C=typeof setImmediate<"u"?setImmediate:null;function P(re){for(var J=n(d);J!==null;){if(J.callback===null)r(d);else if(J.startTime<=re)r(d),J.sortIndex=J.expirationTime,e(c,J);else break;J=n(d)}}function N(re){if(v=!1,P(re),!x)if(n(c)!==null)x=!0,A||(A=!0,O());else{var J=n(d);J!==null&&ee(N,J.startTime-re)}}var A=!1,T=-1,E=5,k=-1;function M(){return g?!0:!(t.unstable_now()-kre&&M());){var se=m.callback;if(typeof se=="function"){m.callback=null,p=m.priorityLevel;var ce=se(m.expirationTime<=re);if(re=t.unstable_now(),typeof ce=="function"){m.callback=ce,P(re),J=!0;break t}m===n(c)&&r(c),P(re)}else r(c);m=n(c)}if(m!==null)J=!0;else{var K=n(d);K!==null&&ee(N,K.startTime-re),J=!1}}break e}finally{m=null,p=ue,f=!1}J=void 0}}finally{J?O():A=!1}}}var O;if(typeof C=="function")O=function(){C(H)};else if(typeof MessageChannel<"u"){var X=new MessageChannel,L=X.port2;X.port1.onmessage=H,O=function(){L.postMessage(null)}}else O=function(){b(H,0)};function ee(re,J){T=b(function(){re(t.unstable_now())},J)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(re){re.callback=null},t.unstable_forceFrameRate=function(re){0>re||125se?(re.sortIndex=ue,e(d,re),n(c)===null&&re===n(d)&&(v?(S(T),T=-1):v=!0,ee(N,ue-se))):(re.sortIndex=ce,e(c,re),x||f||(x=!0,A||(A=!0,O()))),re},t.unstable_shouldYield=M,t.unstable_wrapCallback=function(re){var J=p;return function(){var ue=p;p=J;try{return re.apply(this,arguments)}finally{p=ue}}}})(EM)),EM}var tH;function fue(){return tH||(tH=1,MM.exports=pue()),MM.exports}var DM={exports:{}},Fs={};var nH;function gue(){if(nH)return Fs;nH=1;var t=qg();function e(c){var d="https://react.dev/errors/"+c;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(e){console.error(e)}}return t(),DM.exports=gue(),DM.exports}var aH;function bue(){if(aH)return Rv;aH=1;var t=fue(),e=qg(),n=hQ();function r(h){var y="https://react.dev/errors/"+h;if(1ce||(h.current=se[ce],se[ce]=null,ce--)}function pe(h,y){ce++,se[ce]=h.current,h.current=y}var xe=K(null),fe=K(null),B=K(null),Y=K(null);function F(h,y){switch(pe(B,y),pe(fe,h),pe(xe,null),y.nodeType){case 9:case 11:h=(h=y.documentElement)&&(h=h.namespaceURI)?y8(h):0;break;default:if(h=y.tagName,y=y.namespaceURI)y=y8(y),h=v8(y,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}ie(xe),pe(xe,h)}function D(){ie(xe),ie(fe),ie(B)}function U(h){h.memoizedState!==null&&pe(Y,h);var y=xe.current,_=v8(y,h.type);y!==_&&(pe(fe,h),pe(xe,_))}function Q(h){fe.current===h&&(ie(xe),ie(fe)),Y.current===h&&(ie(Y),jv._currentValue=ue)}var I,de;function z(h){if(I===void 0)try{throw Error()}catch(_){var y=_.stack.trim().match(/\n( *(at )?)/);I=y&&y[1]||"",de=-1<_.stack.indexOf(` +function xue(t,e){for(var n=0;na[i]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))a(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&a(o)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function a(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();var lk=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function fc(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var OM={exports:{}},Iv={};var sH;function yue(){if(sH)return Iv;sH=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function n(a,i,s){var o=null;if(s!==void 0&&(o=""+s),i.key!==void 0&&(o=""+i.key),"key"in i){s={};for(var l in i)l!=="key"&&(s[l]=i[l])}else s=i;return i=s.ref,{$$typeof:t,type:a,key:o,ref:i!==void 0?i:null,props:s}}return Iv.Fragment=e,Iv.jsx=n,Iv.jsxs=n,Iv}var oH;function vue(){return oH||(oH=1,OM.exports=yue()),OM.exports}var r=vue(),zM={exports:{}},Gn={};var lH;function wue(){if(lH)return Gn;lH=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),s=Symbol.for("react.consumer"),o=Symbol.for("react.context"),l=Symbol.for("react.forward_ref"),c=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),u=Symbol.for("react.lazy"),m=Symbol.for("react.activity"),h=Symbol.iterator;function f(K){return K===null||typeof K!="object"?null:(K=h&&K[h]||K["@@iterator"],typeof K=="function"?K:null)}var x={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},v=Object.assign,g={};function b(K,se,pe){this.props=K,this.context=se,this.refs=g,this.updater=pe||x}b.prototype.isReactComponent={},b.prototype.setState=function(K,se){if(typeof K!="object"&&typeof K!="function"&&K!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,K,se,"setState")},b.prototype.forceUpdate=function(K){this.updater.enqueueForceUpdate(this,K,"forceUpdate")};function S(){}S.prototype=b.prototype;function C(K,se,pe){this.props=K,this.context=se,this.refs=g,this.updater=pe||x}var P=C.prototype=new S;P.constructor=C,v(P,b.prototype),P.isPureReactComponent=!0;var N=Array.isArray;function T(){}var A={H:null,A:null,T:null,S:null},E=Object.prototype.hasOwnProperty;function k(K,se,pe){var xe=pe.ref;return{$$typeof:t,type:K,key:se,ref:xe!==void 0?xe:null,props:pe}}function j(K,se){return k(K.type,se,K.props)}function B(K){return typeof K=="object"&&K!==null&&K.$$typeof===t}function O(K){var se={"=":"=0",":":"=2"};return"$"+K.replace(/[=:]/g,function(pe){return se[pe]})}var G=/\/+/g;function L(K,se){return typeof K=="object"&&K!==null&&K.key!=null?O(""+K.key):se.toString(36)}function ee(K){switch(K.status){case"fulfilled":return K.value;case"rejected":throw K.reason;default:switch(typeof K.status=="string"?K.then(T,T):(K.status="pending",K.then(function(se){K.status==="pending"&&(K.status="fulfilled",K.value=se)},function(se){K.status==="pending"&&(K.status="rejected",K.reason=se)})),K.status){case"fulfilled":return K.value;case"rejected":throw K.reason}}throw K}function ne(K,se,pe,xe,fe){var H=typeof K;(H==="undefined"||H==="boolean")&&(K=null);var Y=!1;if(K===null)Y=!0;else switch(H){case"bigint":case"string":case"number":Y=!0;break;case"object":switch(K.$$typeof){case t:case e:Y=!0;break;case u:return Y=K._init,ne(Y(K._payload),se,pe,xe,fe)}}if(Y)return fe=fe(K),Y=xe===""?"."+L(K,0):xe,N(fe)?(pe="",Y!=null&&(pe=Y.replace(G,"$&/")+"/"),ne(fe,se,pe,"",function(U){return U})):fe!=null&&(B(fe)&&(fe=j(fe,pe+(fe.key==null||K&&K.key===fe.key?"":(""+fe.key).replace(G,"$&/")+"/")+Y)),se.push(fe)),1;Y=0;var F=xe===""?".":xe+":";if(N(K))for(var D=0;D>>1,ce=ne[ae];if(0>>1;aei(pe,oe))xei(fe,pe)?(ne[ae]=fe,ne[xe]=oe,ae=xe):(ne[ae]=pe,ne[se]=oe,ae=se);else if(xei(fe,oe))ne[ae]=fe,ne[xe]=oe,ae=xe;else break e}}return J}function i(ne,J){var oe=ne.sortIndex-J.sortIndex;return oe!==0?oe:ne.id-J.id}if(t.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var s=performance;t.unstable_now=function(){return s.now()}}else{var o=Date,l=o.now();t.unstable_now=function(){return o.now()-l}}var c=[],d=[],u=1,m=null,h=3,f=!1,x=!1,v=!1,g=!1,b=typeof setTimeout=="function"?setTimeout:null,S=typeof clearTimeout=="function"?clearTimeout:null,C=typeof setImmediate<"u"?setImmediate:null;function P(ne){for(var J=n(d);J!==null;){if(J.callback===null)a(d);else if(J.startTime<=ne)a(d),J.sortIndex=J.expirationTime,e(c,J);else break;J=n(d)}}function N(ne){if(v=!1,P(ne),!x)if(n(c)!==null)x=!0,T||(T=!0,O());else{var J=n(d);J!==null&&ee(N,J.startTime-ne)}}var T=!1,A=-1,E=5,k=-1;function j(){return g?!0:!(t.unstable_now()-kne&&j());){var ae=m.callback;if(typeof ae=="function"){m.callback=null,h=m.priorityLevel;var ce=ae(m.expirationTime<=ne);if(ne=t.unstable_now(),typeof ce=="function"){m.callback=ce,P(ne),J=!0;break t}m===n(c)&&a(c),P(ne)}else a(c);m=n(c)}if(m!==null)J=!0;else{var K=n(d);K!==null&&ee(N,K.startTime-ne),J=!1}}break e}finally{m=null,h=oe,f=!1}J=void 0}}finally{J?O():T=!1}}}var O;if(typeof C=="function")O=function(){C(B)};else if(typeof MessageChannel<"u"){var G=new MessageChannel,L=G.port2;G.port1.onmessage=B,O=function(){L.postMessage(null)}}else O=function(){b(B,0)};function ee(ne,J){A=b(function(){ne(t.unstable_now())},J)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(ne){ne.callback=null},t.unstable_forceFrameRate=function(ne){0>ne||125ae?(ne.sortIndex=oe,e(d,ne),n(c)===null&&ne===n(d)&&(v?(S(A),A=-1):v=!0,ee(N,oe-ae))):(ne.sortIndex=ce,e(c,ne),x||f||(x=!0,T||(T=!0,O()))),ne},t.unstable_shouldYield=j,t.unstable_wrapCallback=function(ne){var J=h;return function(){var oe=h;h=J;try{return ne.apply(this,arguments)}finally{h=oe}}}})(HM)),HM}var uH;function kue(){return uH||(uH=1,BM.exports=_ue()),BM.exports}var qM={exports:{}},Fs={};var mH;function Nue(){if(mH)return Fs;mH=1;var t=Vg();function e(c){var d="https://react.dev/errors/"+c;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(e){console.error(e)}}return t(),qM.exports=Nue(),qM.exports}var pH;function Cue(){if(pH)return Ov;pH=1;var t=kue(),e=Vg(),n=kY();function a(p){var y="https://react.dev/errors/"+p;if(1ce||(p.current=ae[ce],ae[ce]=null,ce--)}function pe(p,y){ce++,ae[ce]=p.current,p.current=y}var xe=K(null),fe=K(null),H=K(null),Y=K(null);function F(p,y){switch(pe(H,y),pe(fe,p),pe(xe,null),y.nodeType){case 9:case 11:p=(p=y.documentElement)&&(p=p.namespaceURI)?A8(p):0;break;default:if(p=y.tagName,y=y.namespaceURI)y=A8(y),p=j8(y,p);else switch(p){case"svg":p=1;break;case"math":p=2;break;default:p=0}}se(xe),pe(xe,p)}function D(){se(xe),se(fe),se(H)}function U(p){p.memoizedState!==null&&pe(Y,p);var y=xe.current,_=j8(y,p.type);y!==_&&(pe(fe,p),pe(xe,_))}function Q(p){fe.current===p&&(se(xe),se(fe)),Y.current===p&&(se(Y),Dv._currentValue=oe)}var I,ue;function z(p){if(I===void 0)try{throw Error()}catch(_){var y=_.stack.trim().match(/\n( *(at )?)/);I=y&&y[1]||"",ue=-1<_.stack.indexOf(` at`)?" ()":-1<_.stack.indexOf("@")?"@unknown:0:0":""}return` -`+I+h+de}var G=!1;function $(h,y){if(!h||G)return"";G=!0;var _=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var j={DetermineComponentFrameRoot:function(){try{if(y){var Bt=function(){throw Error()};if(Object.defineProperty(Bt.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(Bt,[])}catch(Dt){var Ct=Dt}Reflect.construct(h,[],Bt)}else{try{Bt.call()}catch(Dt){Ct=Dt}h.call(Bt.prototype)}}else{try{throw Error()}catch(Dt){Ct=Dt}(Bt=h())&&typeof Bt.catch=="function"&&Bt.catch(function(){})}}catch(Dt){if(Dt&&Ct&&typeof Dt.stack=="string")return[Dt.stack,Ct.stack]}return[null,null]}};j.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var V=Object.getOwnPropertyDescriptor(j.DetermineComponentFrameRoot,"name");V&&V.configurable&&Object.defineProperty(j.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var Z=j.DetermineComponentFrameRoot(),ke=Z[0],qe=Z[1];if(ke&&qe){var ct=ke.split(` -`),wt=qe.split(` -`);for(V=j=0;jV||ct[j]!==wt[V]){var Lt=` -`+ct[j].replace(" at new "," at ");return h.displayName&&Lt.includes("")&&(Lt=Lt.replace("",h.displayName)),Lt}while(1<=j&&0<=V);break}}}finally{G=!1,Error.prepareStackTrace=_}return(_=h?h.displayName||h.name:"")?z(_):""}function W(h,y){switch(h.tag){case 26:case 27:case 5:return z(h.type);case 16:return z("Lazy");case 13:return h.child!==y&&y!==null?z("Suspense Fallback"):z("Suspense");case 19:return z("SuspenseList");case 0:case 15:return $(h.type,!1);case 11:return $(h.type.render,!1);case 1:return $(h.type,!0);case 31:return z("Activity");default:return""}}function ne(h){try{var y="",_=null;do y+=W(h,_),_=h,h=h.return;while(h);return y}catch(j){return` -Error generating stack: `+j.message+` -`+j.stack}}var ae=Object.prototype.hasOwnProperty,be=t.unstable_scheduleCallback,he=t.unstable_cancelCallback,me=t.unstable_shouldYield,q=t.unstable_requestPaint,R=t.unstable_now,te=t.unstable_getCurrentPriorityLevel,ge=t.unstable_ImmediatePriority,_e=t.unstable_UserBlockingPriority,Ce=t.unstable_NormalPriority,Ae=t.unstable_LowPriority,ye=t.unstable_IdlePriority,Re=t.log,Ie=t.unstable_setDisableYieldValue,Ne=null,ze=null;function je(h){if(typeof Re=="function"&&Ie(h),ze&&typeof ze.setStrictMode=="function")try{ze.setStrictMode(Ne,h)}catch{}}var Ue=Math.clz32?Math.clz32:De,We=Math.log,tt=Math.LN2;function De(h){return h>>>=0,h===0?32:31-(We(h)/tt|0)|0}var nt=256,Pe=262144,Ke=4194304;function Se(h){var y=h&42;if(y!==0)return y;switch(h&-h){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return h&261888;case 262144:case 524288:case 1048576:case 2097152:return h&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return h&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return h}}function le(h,y,_){var j=h.pendingLanes;if(j===0)return 0;var V=0,Z=h.suspendedLanes,ke=h.pingedLanes;h=h.warmLanes;var qe=j&134217727;return qe!==0?(j=qe&~Z,j!==0?V=Se(j):(ke&=qe,ke!==0?V=Se(ke):_||(_=qe&~h,_!==0&&(V=Se(_))))):(qe=j&~Z,qe!==0?V=Se(qe):ke!==0?V=Se(ke):_||(_=j&~h,_!==0&&(V=Se(_)))),V===0?0:y!==0&&y!==V&&(y&Z)===0&&(Z=V&-V,_=y&-y,Z>=_||Z===32&&(_&4194048)!==0)?y:V}function we(h,y){return(h.pendingLanes&~(h.suspendedLanes&~h.pingedLanes)&y)===0}function He(h,y){switch(h){case 1:case 2:case 4:case 8:case 64:return y+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return y+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function st(){var h=Ke;return Ke<<=1,(Ke&62914560)===0&&(Ke=4194304),h}function ft(h){for(var y=[],_=0;31>_;_++)y.push(h);return y}function _t(h,y){h.pendingLanes|=y,y!==268435456&&(h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0)}function Ve(h,y,_,j,V,Z){var ke=h.pendingLanes;h.pendingLanes=_,h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0,h.expiredLanes&=_,h.entangledLanes&=_,h.errorRecoveryDisabledLanes&=_,h.shellSuspendCounter=0;var qe=h.entanglements,ct=h.expirationTimes,wt=h.hiddenUpdates;for(_=ke&~_;0<_;){var Lt=31-Ue(_),Bt=1<"u")return null;try{return h.activeElement||h.body}catch{return h.body}}var Cr=/[\n"\\]/g;function On(h){return h.replace(Cr,function(y){return"\\"+y.charCodeAt(0).toString(16)+" "})}function Fa(h,y,_,j,V,Z,ke,qe){h.name="",ke!=null&&typeof ke!="function"&&typeof ke!="symbol"&&typeof ke!="boolean"?h.type=ke:h.removeAttribute("type"),y!=null?ke==="number"?(y===0&&h.value===""||h.value!=y)&&(h.value=""+sn(y)):h.value!==""+sn(y)&&(h.value=""+sn(y)):ke!=="submit"&&ke!=="reset"||h.removeAttribute("value"),y!=null?Or(h,ke,sn(y)):_!=null?Or(h,ke,sn(_)):j!=null&&h.removeAttribute("value"),V==null&&Z!=null&&(h.defaultChecked=!!Z),V!=null&&(h.checked=V&&typeof V!="function"&&typeof V!="symbol"),qe!=null&&typeof qe!="function"&&typeof qe!="symbol"&&typeof qe!="boolean"?h.name=""+sn(qe):h.removeAttribute("name")}function qn(h,y,_,j,V,Z,ke,qe){if(Z!=null&&typeof Z!="function"&&typeof Z!="symbol"&&typeof Z!="boolean"&&(h.type=Z),y!=null||_!=null){if(!(Z!=="submit"&&Z!=="reset"||y!=null)){Nn(h);return}_=_!=null?""+sn(_):"",y=y!=null?""+sn(y):_,qe||y===h.value||(h.value=y),h.defaultValue=y}j=j??V,j=typeof j!="function"&&typeof j!="symbol"&&!!j,h.checked=qe?h.checked:!!j,h.defaultChecked=!!j,ke!=null&&typeof ke!="function"&&typeof ke!="symbol"&&typeof ke!="boolean"&&(h.name=ke),Nn(h)}function Or(h,y,_){y==="number"&&gr(h.ownerDocument)===h||h.defaultValue===""+_||(h.defaultValue=""+_)}function Gr(h,y,_,j){if(h=h.options,y){y={};for(var V=0;V<_.length;V++)y["$"+_[V]]=!0;for(_=0;_"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Ii=!1;if(qo)try{var vl={};Object.defineProperty(vl,"passive",{get:function(){Ii=!0}}),window.addEventListener("test",vl,vl),window.removeEventListener("test",vl,vl)}catch{Ii=!1}var yo=null,ha=null,bc=null;function Bm(){if(bc)return bc;var h,y=ha,_=y.length,j,V="value"in yo?yo.value:yo.textContent,Z=V.length;for(h=0;h<_&&y[h]===V[h];h++);var ke=_-h;for(j=1;j<=ke&&y[_-j]===V[Z-j];j++);return bc=V.slice(h,1=uu),bt=" ",Rt=!1;function ot(h,y){switch(h){case"keyup":return Wm.indexOf(y.keyCode)!==-1;case"keydown":return y.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function on(h){return h=h.detail,typeof h=="object"&&"data"in h?h.data:null}var En=!1;function fn(h,y){switch(h){case"compositionend":return on(y);case"keypress":return y.which!==32?null:(Rt=!0,bt);case"textInput":return h=y.data,h===bt&&Rt?null:h;default:return null}}function Rr(h,y){if(En)return h==="compositionend"||!du&&ot(h,y)?(h=Bm(),bc=ha=yo=null,En=!1,h):null;switch(h){case"paste":return null;case"keypress":if(!(y.ctrlKey||y.altKey||y.metaKey)||y.ctrlKey&&y.altKey){if(y.char&&1=y)return{node:_,offset:y-h};h=j}e:{for(;_;){if(_.nextSibling){_=_.nextSibling;break e}_=_.parentNode}_=void 0}_=LA(_)}}function Jp(h,y){return h&&y?h===y?!0:h&&h.nodeType===3?!1:y&&y.nodeType===3?Jp(h,y.parentNode):"contains"in h?h.contains(y):h.compareDocumentPosition?!!(h.compareDocumentPosition(y)&16):!1:!1}function ev(h){h=h!=null&&h.ownerDocument!=null&&h.ownerDocument.defaultView!=null?h.ownerDocument.defaultView:window;for(var y=gr(h.document);y instanceof h.HTMLIFrameElement;){try{var _=typeof y.contentWindow.location.href=="string"}catch{_=!1}if(_)h=y.contentWindow;else break;y=gr(h.document)}return y}function tb(h){var y=h&&h.nodeName&&h.nodeName.toLowerCase();return y&&(y==="input"&&(h.type==="text"||h.type==="search"||h.type==="tel"||h.type==="url"||h.type==="password")||y==="textarea"||h.contentEditable==="true")}var IA=qo&&"documentMode"in document&&11>=document.documentMode,nb=null,OA=null,tv=null,zA=!1;function B5(h,y,_){var j=_.window===_?_.document:_.nodeType===9?_:_.ownerDocument;zA||nb==null||nb!==gr(j)||(j=nb,"selectionStart"in j&&tb(j)?j={start:j.selectionStart,end:j.selectionEnd}:(j=(j.ownerDocument&&j.ownerDocument.defaultView||window).getSelection(),j={anchorNode:j.anchorNode,anchorOffset:j.anchorOffset,focusNode:j.focusNode,focusOffset:j.focusOffset}),tv&&Sn(tv,j)||(tv=j,j=B_(OA,"onSelect"),0>=ke,V-=ke,dd=1<<32-Ue(y)+V|_<Yn?(xr=_n,_n=null):xr=_n.sibling;var jr=Ct(gt,_n,vt[Yn],zt);if(jr===null){_n===null&&(_n=xr);break}h&&_n&&jr.alternate===null&&y(gt,_n),ht=Z(jr,ht,Yn),Ar===null?jn=jr:Ar.sibling=jr,Ar=jr,_n=xr}if(Yn===vt.length)return _(gt,_n),wr&&pu(gt,Yn),jn;if(_n===null){for(;YnYn?(xr=_n,_n=null):xr=_n.sibling;var fh=Ct(gt,_n,jr.value,zt);if(fh===null){_n===null&&(_n=xr);break}h&&_n&&fh.alternate===null&&y(gt,_n),ht=Z(fh,ht,Yn),Ar===null?jn=fh:Ar.sibling=fh,Ar=fh,_n=xr}if(jr.done)return _(gt,_n),wr&&pu(gt,Yn),jn;if(_n===null){for(;!jr.done;Yn++,jr=vt.next())jr=Bt(gt,jr.value,zt),jr!==null&&(ht=Z(jr,ht,Yn),Ar===null?jn=jr:Ar.sibling=jr,Ar=jr);return wr&&pu(gt,Yn),jn}for(_n=j(_n);!jr.done;Yn++,jr=vt.next())jr=Dt(_n,gt,Yn,jr.value,zt),jr!==null&&(h&&jr.alternate!==null&&_n.delete(jr.key===null?Yn:jr.key),ht=Z(jr,ht,Yn),Ar===null?jn=jr:Ar.sibling=jr,Ar=jr);return h&&_n.forEach(function(lue){return y(gt,lue)}),wr&&pu(gt,Yn),jn}function Xr(gt,ht,vt,zt){if(typeof vt=="object"&&vt!==null&&vt.type===v&&vt.key===null&&(vt=vt.props.children),typeof vt=="object"&&vt!==null){switch(vt.$$typeof){case f:e:{for(var jn=vt.key;ht!==null;){if(ht.key===jn){if(jn=vt.type,jn===v){if(ht.tag===7){_(gt,ht.sibling),zt=V(ht,vt.props.children),zt.return=gt,gt=zt;break e}}else if(ht.elementType===jn||typeof jn=="object"&&jn!==null&&jn.$$typeof===E&&lf(jn)===ht.type){_(gt,ht.sibling),zt=V(ht,vt.props),ov(zt,vt),zt.return=gt,gt=zt;break e}_(gt,ht);break}else y(gt,ht);ht=ht.sibling}vt.type===v?(zt=nf(vt.props.children,gt.mode,zt,vt.key),zt.return=gt,gt=zt):(zt=a_(vt.type,vt.key,vt.props,null,gt.mode,zt),ov(zt,vt),zt.return=gt,gt=zt)}return ke(gt);case x:e:{for(jn=vt.key;ht!==null;){if(ht.key===jn)if(ht.tag===4&&ht.stateNode.containerInfo===vt.containerInfo&&ht.stateNode.implementation===vt.implementation){_(gt,ht.sibling),zt=V(ht,vt.children||[]),zt.return=gt,gt=zt;break e}else{_(gt,ht);break}else y(gt,ht);ht=ht.sibling}zt=GA(vt,gt.mode,zt),zt.return=gt,gt=zt}return ke(gt);case E:return vt=lf(vt),Xr(gt,ht,vt,zt)}if(ee(vt))return gn(gt,ht,vt,zt);if(O(vt)){if(jn=O(vt),typeof jn!="function")throw Error(r(150));return vt=jn.call(vt),In(gt,ht,vt,zt)}if(typeof vt.then=="function")return Xr(gt,ht,u_(vt),zt);if(vt.$$typeof===C)return Xr(gt,ht,o_(gt,vt),zt);m_(gt,vt)}return typeof vt=="string"&&vt!==""||typeof vt=="number"||typeof vt=="bigint"?(vt=""+vt,ht!==null&&ht.tag===6?(_(gt,ht.sibling),zt=V(ht,vt),zt.return=gt,gt=zt):(_(gt,ht),zt=VA(vt,gt.mode,zt),zt.return=gt,gt=zt),ke(gt)):_(gt,ht)}return function(gt,ht,vt,zt){try{sv=0;var jn=Xr(gt,ht,vt,zt);return hb=null,jn}catch(_n){if(_n===mb||_n===c_)throw _n;var Ar=Wo(29,_n,null,gt.mode);return Ar.lanes=zt,Ar.return=gt,Ar}}}var df=dU(!0),uU=dU(!1),Jm=!1;function aj(h){h.updateQueue={baseState:h.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function ij(h,y){h=h.updateQueue,y.updateQueue===h&&(y.updateQueue={baseState:h.baseState,firstBaseUpdate:h.firstBaseUpdate,lastBaseUpdate:h.lastBaseUpdate,shared:h.shared,callbacks:null})}function Zm(h){return{lane:h,tag:0,payload:null,callback:null,next:null}}function eh(h,y,_){var j=h.updateQueue;if(j===null)return null;if(j=j.shared,(Fr&2)!==0){var V=j.pending;return V===null?y.next=y:(y.next=V.next,V.next=y),j.pending=y,y=r_(h),K5(h,null,_),y}return n_(h,j,y,_),r_(h)}function lv(h,y,_){if(y=y.updateQueue,y!==null&&(y=y.shared,(_&4194048)!==0)){var j=y.lanes;j&=h.pendingLanes,_|=j,y.lanes=_,Pt(h,_)}}function sj(h,y){var _=h.updateQueue,j=h.alternate;if(j!==null&&(j=j.updateQueue,_===j)){var V=null,Z=null;if(_=_.firstBaseUpdate,_!==null){do{var ke={lane:_.lane,tag:_.tag,payload:_.payload,callback:null,next:null};Z===null?V=Z=ke:Z=Z.next=ke,_=_.next}while(_!==null);Z===null?V=Z=y:Z=Z.next=y}else V=Z=y;_={baseState:j.baseState,firstBaseUpdate:V,lastBaseUpdate:Z,shared:j.shared,callbacks:j.callbacks},h.updateQueue=_;return}h=_.lastBaseUpdate,h===null?_.firstBaseUpdate=y:h.next=y,_.lastBaseUpdate=y}var oj=!1;function cv(){if(oj){var h=ub;if(h!==null)throw h}}function dv(h,y,_,j){oj=!1;var V=h.updateQueue;Jm=!1;var Z=V.firstBaseUpdate,ke=V.lastBaseUpdate,qe=V.shared.pending;if(qe!==null){V.shared.pending=null;var ct=qe,wt=ct.next;ct.next=null,ke===null?Z=wt:ke.next=wt,ke=ct;var Lt=h.alternate;Lt!==null&&(Lt=Lt.updateQueue,qe=Lt.lastBaseUpdate,qe!==ke&&(qe===null?Lt.firstBaseUpdate=wt:qe.next=wt,Lt.lastBaseUpdate=ct))}if(Z!==null){var Bt=V.baseState;ke=0,Lt=wt=ct=null,qe=Z;do{var Ct=qe.lane&-536870913,Dt=Ct!==qe.lane;if(Dt?(br&Ct)===Ct:(j&Ct)===Ct){Ct!==0&&Ct===db&&(oj=!0),Lt!==null&&(Lt=Lt.next={lane:0,tag:qe.tag,payload:qe.payload,callback:null,next:null});e:{var gn=h,In=qe;Ct=y;var Xr=_;switch(In.tag){case 1:if(gn=In.payload,typeof gn=="function"){Bt=gn.call(Xr,Bt,Ct);break e}Bt=gn;break e;case 3:gn.flags=gn.flags&-65537|128;case 0:if(gn=In.payload,Ct=typeof gn=="function"?gn.call(Xr,Bt,Ct):gn,Ct==null)break e;Bt=m({},Bt,Ct);break e;case 2:Jm=!0}}Ct=qe.callback,Ct!==null&&(h.flags|=64,Dt&&(h.flags|=8192),Dt=V.callbacks,Dt===null?V.callbacks=[Ct]:Dt.push(Ct))}else Dt={lane:Ct,tag:qe.tag,payload:qe.payload,callback:qe.callback,next:null},Lt===null?(wt=Lt=Dt,ct=Bt):Lt=Lt.next=Dt,ke|=Ct;if(qe=qe.next,qe===null){if(qe=V.shared.pending,qe===null)break;Dt=qe,qe=Dt.next,Dt.next=null,V.lastBaseUpdate=Dt,V.shared.pending=null}}while(!0);Lt===null&&(ct=Bt),V.baseState=ct,V.firstBaseUpdate=wt,V.lastBaseUpdate=Lt,Z===null&&(V.shared.lanes=0),ih|=ke,h.lanes=ke,h.memoizedState=Bt}}function mU(h,y){if(typeof h!="function")throw Error(r(191,h));h.call(y)}function hU(h,y){var _=h.callbacks;if(_!==null)for(h.callbacks=null,h=0;h<_.length;h++)mU(_[h],y)}var pb=K(null),h_=K(0);function pU(h,y){h=Nu,pe(h_,h),pe(pb,y),Nu=h|y.baseLanes}function lj(){pe(h_,Nu),pe(pb,pb.current)}function cj(){Nu=h_.current,ie(pb),ie(h_)}var Ko=K(null),jl=null;function th(h){var y=h.alternate;pe(ti,ti.current&1),pe(Ko,h),jl===null&&(y===null||pb.current!==null||y.memoizedState!==null)&&(jl=h)}function dj(h){pe(ti,ti.current),pe(Ko,h),jl===null&&(jl=h)}function fU(h){h.tag===22?(pe(ti,ti.current),pe(Ko,h),jl===null&&(jl=h)):nh()}function nh(){pe(ti,ti.current),pe(Ko,Ko.current)}function Xo(h){ie(Ko),jl===h&&(jl=null),ie(ti)}var ti=K(0);function p_(h){for(var y=h;y!==null;){if(y.tag===13){var _=y.memoizedState;if(_!==null&&(_=_.dehydrated,_===null||gM(_)||bM(_)))return y}else if(y.tag===19&&(y.memoizedProps.revealOrder==="forwards"||y.memoizedProps.revealOrder==="backwards"||y.memoizedProps.revealOrder==="unstable_legacy-backwards"||y.memoizedProps.revealOrder==="together")){if((y.flags&128)!==0)return y}else if(y.child!==null){y.child.return=y,y=y.child;continue}if(y===h)break;for(;y.sibling===null;){if(y.return===null||y.return===h)return null;y=y.return}y.sibling.return=y.return,y=y.sibling}return null}var bu=0,Kn=null,Wr=null,li=null,f_=!1,fb=!1,uf=!1,g_=0,uv=0,gb=null,Jce=0;function Ha(){throw Error(r(321))}function uj(h,y){if(y===null)return!1;for(var _=0;_Z?Z:8;var ke=re.T,qe={};re.T=qe,Cj(h,!1,y,_);try{var ct=V(),wt=re.S;if(wt!==null&&wt(qe,ct),ct!==null&&typeof ct=="object"&&typeof ct.then=="function"){var Lt=Qce(ct,j);hv(h,y,Lt,Jo(h))}else hv(h,y,j,Jo(h))}catch(Bt){hv(h,y,{then:function(){},status:"rejected",reason:Bt},Jo())}finally{J.p=Z,ke!==null&&qe.types!==null&&(ke.types=qe.types),re.T=ke}}function rde(){}function kj(h,y,_,j){if(h.tag!==5)throw Error(r(476));var V=VU(h).queue;$U(h,V,y,ue,_===null?rde:function(){return GU(h),_(j)})}function VU(h){var y=h.memoizedState;if(y!==null)return y;y={memoizedState:ue,baseState:ue,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:xu,lastRenderedState:ue},next:null};var _={};return y.next={memoizedState:_,baseState:_,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:xu,lastRenderedState:_},next:null},h.memoizedState=y,h=h.alternate,h!==null&&(h.memoizedState=y),y}function GU(h){var y=VU(h);y.next===null&&(y=h.alternate.memoizedState),hv(h,y.next.queue,{},Jo())}function Nj(){return ds(jv)}function WU(){return ni().memoizedState}function KU(){return ni().memoizedState}function ade(h){for(var y=h.return;y!==null;){switch(y.tag){case 24:case 3:var _=Jo();h=Zm(_);var j=eh(y,h,_);j!==null&&(Po(j,y,_),lv(j,y,_)),y={cache:ej()},h.payload=y;return}y=y.return}}function ide(h,y,_){var j=Jo();_={lane:j,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null},S_(h)?YU(y,_):(_=qA(h,y,_,j),_!==null&&(Po(_,h,j),QU(_,y,j)))}function XU(h,y,_){var j=Jo();hv(h,y,_,j)}function hv(h,y,_,j){var V={lane:j,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null};if(S_(h))YU(y,V);else{var Z=h.alternate;if(h.lanes===0&&(Z===null||Z.lanes===0)&&(Z=y.lastRenderedReducer,Z!==null))try{var ke=y.lastRenderedState,qe=Z(ke,_);if(V.hasEagerState=!0,V.eagerState=qe,Si(qe,ke))return n_(h,y,V,0),oa===null&&t_(),!1}catch{}if(_=qA(h,y,V,j),_!==null)return Po(_,h,j),QU(_,y,j),!0}return!1}function Cj(h,y,_,j){if(j={lane:2,revertLane:aM(),gesture:null,action:j,hasEagerState:!1,eagerState:null,next:null},S_(h)){if(y)throw Error(r(479))}else y=qA(h,_,j,2),y!==null&&Po(y,h,2)}function S_(h){var y=h.alternate;return h===Kn||y!==null&&y===Kn}function YU(h,y){fb=f_=!0;var _=h.pending;_===null?y.next=y:(y.next=_.next,_.next=y),h.pending=y}function QU(h,y,_){if((_&4194048)!==0){var j=y.lanes;j&=h.pendingLanes,_|=j,y.lanes=_,Pt(h,_)}}var pv={readContext:ds,use:x_,useCallback:Ha,useContext:Ha,useEffect:Ha,useImperativeHandle:Ha,useLayoutEffect:Ha,useInsertionEffect:Ha,useMemo:Ha,useReducer:Ha,useRef:Ha,useState:Ha,useDebugValue:Ha,useDeferredValue:Ha,useTransition:Ha,useSyncExternalStore:Ha,useId:Ha,useHostTransitionStatus:Ha,useFormState:Ha,useActionState:Ha,useOptimistic:Ha,useMemoCache:Ha,useCacheRefresh:Ha};pv.useEffectEvent=Ha;var JU={readContext:ds,use:x_,useCallback:function(h,y){return Js().memoizedState=[h,y===void 0?null:y],h},useContext:ds,useEffect:RU,useImperativeHandle:function(h,y,_){_=_!=null?_.concat([h]):null,v_(4194308,4,zU.bind(null,y,h),_)},useLayoutEffect:function(h,y){return v_(4194308,4,h,y)},useInsertionEffect:function(h,y){v_(4,2,h,y)},useMemo:function(h,y){var _=Js();y=y===void 0?null:y;var j=h();if(uf){je(!0);try{h()}finally{je(!1)}}return _.memoizedState=[j,y],j},useReducer:function(h,y,_){var j=Js();if(_!==void 0){var V=_(y);if(uf){je(!0);try{_(y)}finally{je(!1)}}}else V=y;return j.memoizedState=j.baseState=V,h={pending:null,lanes:0,dispatch:null,lastRenderedReducer:h,lastRenderedState:V},j.queue=h,h=h.dispatch=ide.bind(null,Kn,h),[j.memoizedState,h]},useRef:function(h){var y=Js();return h={current:h},y.memoizedState=h},useState:function(h){h=yj(h);var y=h.queue,_=XU.bind(null,Kn,y);return y.dispatch=_,[h.memoizedState,_]},useDebugValue:Sj,useDeferredValue:function(h,y){var _=Js();return _j(_,h,y)},useTransition:function(){var h=yj(!1);return h=$U.bind(null,Kn,h.queue,!0,!1),Js().memoizedState=h,[!1,h]},useSyncExternalStore:function(h,y,_){var j=Kn,V=Js();if(wr){if(_===void 0)throw Error(r(407));_=_()}else{if(_=y(),oa===null)throw Error(r(349));(br&127)!==0||yU(j,y,_)}V.memoizedState=_;var Z={value:_,getSnapshot:y};return V.queue=Z,RU(wU.bind(null,j,Z,h),[h]),j.flags|=2048,bb(9,{destroy:void 0},vU.bind(null,j,Z,_,y),null),_},useId:function(){var h=Js(),y=oa.identifierPrefix;if(wr){var _=ud,j=dd;_=(j&~(1<<32-Ue(j)-1)).toString(32)+_,y="_"+y+"R_"+_,_=g_++,0<_&&(y+="H"+_.toString(32)),y+="_"}else _=Jce++,y="_"+y+"r_"+_.toString(32)+"_";return h.memoizedState=y},useHostTransitionStatus:Nj,useFormState:jU,useActionState:jU,useOptimistic:function(h){var y=Js();y.memoizedState=y.baseState=h;var _={pending:null,lanes:0,dispatch:null,lastRenderedReducer:null,lastRenderedState:null};return y.queue=_,y=Cj.bind(null,Kn,!0,_),_.dispatch=y,[h,y]},useMemoCache:gj,useCacheRefresh:function(){return Js().memoizedState=ade.bind(null,Kn)},useEffectEvent:function(h){var y=Js(),_={impl:h};return y.memoizedState=_,function(){if((Fr&2)!==0)throw Error(r(440));return _.impl.apply(void 0,arguments)}}},Pj={readContext:ds,use:x_,useCallback:BU,useContext:ds,useEffect:wj,useImperativeHandle:UU,useInsertionEffect:IU,useLayoutEffect:OU,useMemo:HU,useReducer:y_,useRef:FU,useState:function(){return y_(xu)},useDebugValue:Sj,useDeferredValue:function(h,y){var _=ni();return qU(_,Wr.memoizedState,h,y)},useTransition:function(){var h=y_(xu)[0],y=ni().memoizedState;return[typeof h=="boolean"?h:mv(h),y]},useSyncExternalStore:xU,useId:WU,useHostTransitionStatus:Nj,useFormState:MU,useActionState:MU,useOptimistic:function(h,y){var _=ni();return kU(_,Wr,h,y)},useMemoCache:gj,useCacheRefresh:KU};Pj.useEffectEvent=LU;var ZU={readContext:ds,use:x_,useCallback:BU,useContext:ds,useEffect:wj,useImperativeHandle:UU,useInsertionEffect:IU,useLayoutEffect:OU,useMemo:HU,useReducer:xj,useRef:FU,useState:function(){return xj(xu)},useDebugValue:Sj,useDeferredValue:function(h,y){var _=ni();return Wr===null?_j(_,h,y):qU(_,Wr.memoizedState,h,y)},useTransition:function(){var h=xj(xu)[0],y=ni().memoizedState;return[typeof h=="boolean"?h:mv(h),y]},useSyncExternalStore:xU,useId:WU,useHostTransitionStatus:Nj,useFormState:DU,useActionState:DU,useOptimistic:function(h,y){var _=ni();return Wr!==null?kU(_,Wr,h,y):(_.baseState=h,[h,_.queue.dispatch])},useMemoCache:gj,useCacheRefresh:KU};ZU.useEffectEvent=LU;function Tj(h,y,_,j){y=h.memoizedState,_=_(j,y),_=_==null?y:m({},y,_),h.memoizedState=_,h.lanes===0&&(h.updateQueue.baseState=_)}var Aj={enqueueSetState:function(h,y,_){h=h._reactInternals;var j=Jo(),V=Zm(j);V.payload=y,_!=null&&(V.callback=_),y=eh(h,V,j),y!==null&&(Po(y,h,j),lv(y,h,j))},enqueueReplaceState:function(h,y,_){h=h._reactInternals;var j=Jo(),V=Zm(j);V.tag=1,V.payload=y,_!=null&&(V.callback=_),y=eh(h,V,j),y!==null&&(Po(y,h,j),lv(y,h,j))},enqueueForceUpdate:function(h,y){h=h._reactInternals;var _=Jo(),j=Zm(_);j.tag=2,y!=null&&(j.callback=y),y=eh(h,j,_),y!==null&&(Po(y,h,_),lv(y,h,_))}};function eB(h,y,_,j,V,Z,ke){return h=h.stateNode,typeof h.shouldComponentUpdate=="function"?h.shouldComponentUpdate(j,Z,ke):y.prototype&&y.prototype.isPureReactComponent?!Sn(_,j)||!Sn(V,Z):!0}function tB(h,y,_,j){h=y.state,typeof y.componentWillReceiveProps=="function"&&y.componentWillReceiveProps(_,j),typeof y.UNSAFE_componentWillReceiveProps=="function"&&y.UNSAFE_componentWillReceiveProps(_,j),y.state!==h&&Aj.enqueueReplaceState(y,y.state,null)}function mf(h,y){var _=y;if("ref"in y){_={};for(var j in y)j!=="ref"&&(_[j]=y[j])}if(h=h.defaultProps){_===y&&(_=m({},_));for(var V in h)_[V]===void 0&&(_[V]=h[V])}return _}function nB(h){e_(h)}function rB(h){console.error(h)}function aB(h){e_(h)}function __(h,y){try{var _=h.onUncaughtError;_(y.value,{componentStack:y.stack})}catch(j){setTimeout(function(){throw j})}}function iB(h,y,_){try{var j=h.onCaughtError;j(_.value,{componentStack:_.stack,errorBoundary:y.tag===1?y.stateNode:null})}catch(V){setTimeout(function(){throw V})}}function jj(h,y,_){return _=Zm(_),_.tag=3,_.payload={element:null},_.callback=function(){__(h,y)},_}function sB(h){return h=Zm(h),h.tag=3,h}function oB(h,y,_,j){var V=_.type.getDerivedStateFromError;if(typeof V=="function"){var Z=j.value;h.payload=function(){return V(Z)},h.callback=function(){iB(y,_,j)}}var ke=_.stateNode;ke!==null&&typeof ke.componentDidCatch=="function"&&(h.callback=function(){iB(y,_,j),typeof V!="function"&&(sh===null?sh=new Set([this]):sh.add(this));var qe=j.stack;this.componentDidCatch(j.value,{componentStack:qe!==null?qe:""})})}function sde(h,y,_,j,V){if(_.flags|=32768,j!==null&&typeof j=="object"&&typeof j.then=="function"){if(y=_.alternate,y!==null&&cb(y,_,V,!0),_=Ko.current,_!==null){switch(_.tag){case 31:case 13:return jl===null?R_():_.alternate===null&&qa===0&&(qa=3),_.flags&=-257,_.flags|=65536,_.lanes=V,j===d_?_.flags|=16384:(y=_.updateQueue,y===null?_.updateQueue=new Set([j]):y.add(j),tM(h,j,V)),!1;case 22:return _.flags|=65536,j===d_?_.flags|=16384:(y=_.updateQueue,y===null?(y={transitions:null,markerInstances:null,retryQueue:new Set([j])},_.updateQueue=y):(_=y.retryQueue,_===null?y.retryQueue=new Set([j]):_.add(j)),tM(h,j,V)),!1}throw Error(r(435,_.tag))}return tM(h,j,V),R_(),!1}if(wr)return y=Ko.current,y!==null?((y.flags&65536)===0&&(y.flags|=256),y.flags|=65536,y.lanes=V,j!==XA&&(h=Error(r(422),{cause:j}),rv(Cl(h,_)))):(j!==XA&&(y=Error(r(423),{cause:j}),rv(Cl(y,_))),h=h.current.alternate,h.flags|=65536,V&=-V,h.lanes|=V,j=Cl(j,_),V=jj(h.stateNode,j,V),sj(h,V),qa!==4&&(qa=2)),!1;var Z=Error(r(520),{cause:j});if(Z=Cl(Z,_),Sv===null?Sv=[Z]:Sv.push(Z),qa!==4&&(qa=2),y===null)return!0;j=Cl(j,_),_=y;do{switch(_.tag){case 3:return _.flags|=65536,h=V&-V,_.lanes|=h,h=jj(_.stateNode,j,h),sj(_,h),!1;case 1:if(y=_.type,Z=_.stateNode,(_.flags&128)===0&&(typeof y.getDerivedStateFromError=="function"||Z!==null&&typeof Z.componentDidCatch=="function"&&(sh===null||!sh.has(Z))))return _.flags|=65536,V&=-V,_.lanes|=V,V=sB(V),oB(V,h,_,j),sj(_,V),!1}_=_.return}while(_!==null);return!1}var Mj=Error(r(461)),ci=!1;function us(h,y,_,j){y.child=h===null?uU(y,null,_,j):df(y,h.child,_,j)}function lB(h,y,_,j,V){_=_.render;var Z=y.ref;if("ref"in j){var ke={};for(var qe in j)qe!=="ref"&&(ke[qe]=j[qe])}else ke=j;return sf(y),j=mj(h,y,_,ke,Z,V),qe=hj(),h!==null&&!ci?(pj(h,y,V),yu(h,y,V)):(wr&&qe&&WA(y),y.flags|=1,us(h,y,j,V),y.child)}function cB(h,y,_,j,V){if(h===null){var Z=_.type;return typeof Z=="function"&&!$A(Z)&&Z.defaultProps===void 0&&_.compare===null?(y.tag=15,y.type=Z,dB(h,y,Z,j,V)):(h=a_(_.type,null,j,y,y.mode,V),h.ref=y.ref,h.return=y,y.child=h)}if(Z=h.child,!zj(h,V)){var ke=Z.memoizedProps;if(_=_.compare,_=_!==null?_:Sn,_(ke,j)&&h.ref===y.ref)return yu(h,y,V)}return y.flags|=1,h=hu(Z,j),h.ref=y.ref,h.return=y,y.child=h}function dB(h,y,_,j,V){if(h!==null){var Z=h.memoizedProps;if(Sn(Z,j)&&h.ref===y.ref)if(ci=!1,y.pendingProps=j=Z,zj(h,V))(h.flags&131072)!==0&&(ci=!0);else return y.lanes=h.lanes,yu(h,y,V)}return Ej(h,y,_,j,V)}function uB(h,y,_,j){var V=j.children,Z=h!==null?h.memoizedState:null;if(h===null&&y.stateNode===null&&(y.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),j.mode==="hidden"){if((y.flags&128)!==0){if(Z=Z!==null?Z.baseLanes|_:_,h!==null){for(j=y.child=h.child,V=0;j!==null;)V=V|j.lanes|j.childLanes,j=j.sibling;j=V&~Z}else j=0,y.child=null;return mB(h,y,Z,_,j)}if((_&536870912)!==0)y.memoizedState={baseLanes:0,cachePool:null},h!==null&&l_(y,Z!==null?Z.cachePool:null),Z!==null?pU(y,Z):lj(),fU(y);else return j=y.lanes=536870912,mB(h,y,Z!==null?Z.baseLanes|_:_,_,j)}else Z!==null?(l_(y,Z.cachePool),pU(y,Z),nh(),y.memoizedState=null):(h!==null&&l_(y,null),lj(),nh());return us(h,y,V,_),y.child}function fv(h,y){return h!==null&&h.tag===22||y.stateNode!==null||(y.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),y.sibling}function mB(h,y,_,j,V){var Z=nj();return Z=Z===null?null:{parent:oi._currentValue,pool:Z},y.memoizedState={baseLanes:_,cachePool:Z},h!==null&&l_(y,null),lj(),fU(y),h!==null&&cb(h,y,j,!0),y.childLanes=V,null}function k_(h,y){return y=C_({mode:y.mode,children:y.children},h.mode),y.ref=h.ref,h.child=y,y.return=h,y}function hB(h,y,_){return df(y,h.child,null,_),h=k_(y,y.pendingProps),h.flags|=2,Xo(y),y.memoizedState=null,h}function ode(h,y,_){var j=y.pendingProps,V=(y.flags&128)!==0;if(y.flags&=-129,h===null){if(wr){if(j.mode==="hidden")return h=k_(y,j),y.lanes=536870912,fv(null,h);if(dj(y),(h=ga)?(h=N8(h,Al),h=h!==null&&h.data==="&"?h:null,h!==null&&(y.memoizedState={dehydrated:h,treeContext:Km!==null?{id:dd,overflow:ud}:null,retryLane:536870912,hydrationErrors:null},_=Y5(h),_.return=y,y.child=_,cs=y,ga=null)):h=null,h===null)throw Ym(y);return y.lanes=536870912,null}return k_(y,j)}var Z=h.memoizedState;if(Z!==null){var ke=Z.dehydrated;if(dj(y),V)if(y.flags&256)y.flags&=-257,y=hB(h,y,_);else if(y.memoizedState!==null)y.child=h.child,y.flags|=128,y=null;else throw Error(r(558));else if(ci||cb(h,y,_,!1),V=(_&h.childLanes)!==0,ci||V){if(j=oa,j!==null&&(ke=qt(j,_),ke!==0&&ke!==Z.retryLane))throw Z.retryLane=ke,tf(h,ke),Po(j,h,ke),Mj;R_(),y=hB(h,y,_)}else h=Z.treeContext,ga=Ml(ke.nextSibling),cs=y,wr=!0,Xm=null,Al=!1,h!==null&&Z5(y,h),y=k_(y,j),y.flags|=4096;return y}return h=hu(h.child,{mode:j.mode,children:j.children}),h.ref=y.ref,y.child=h,h.return=y,h}function N_(h,y){var _=y.ref;if(_===null)h!==null&&h.ref!==null&&(y.flags|=4194816);else{if(typeof _!="function"&&typeof _!="object")throw Error(r(284));(h===null||h.ref!==_)&&(y.flags|=4194816)}}function Ej(h,y,_,j,V){return sf(y),_=mj(h,y,_,j,void 0,V),j=hj(),h!==null&&!ci?(pj(h,y,V),yu(h,y,V)):(wr&&j&&WA(y),y.flags|=1,us(h,y,_,V),y.child)}function pB(h,y,_,j,V,Z){return sf(y),y.updateQueue=null,_=bU(y,j,_,V),gU(h),j=hj(),h!==null&&!ci?(pj(h,y,Z),yu(h,y,Z)):(wr&&j&&WA(y),y.flags|=1,us(h,y,_,Z),y.child)}function fB(h,y,_,j,V){if(sf(y),y.stateNode===null){var Z=ib,ke=_.contextType;typeof ke=="object"&&ke!==null&&(Z=ds(ke)),Z=new _(j,Z),y.memoizedState=Z.state!==null&&Z.state!==void 0?Z.state:null,Z.updater=Aj,y.stateNode=Z,Z._reactInternals=y,Z=y.stateNode,Z.props=j,Z.state=y.memoizedState,Z.refs={},aj(y),ke=_.contextType,Z.context=typeof ke=="object"&&ke!==null?ds(ke):ib,Z.state=y.memoizedState,ke=_.getDerivedStateFromProps,typeof ke=="function"&&(Tj(y,_,ke,j),Z.state=y.memoizedState),typeof _.getDerivedStateFromProps=="function"||typeof Z.getSnapshotBeforeUpdate=="function"||typeof Z.UNSAFE_componentWillMount!="function"&&typeof Z.componentWillMount!="function"||(ke=Z.state,typeof Z.componentWillMount=="function"&&Z.componentWillMount(),typeof Z.UNSAFE_componentWillMount=="function"&&Z.UNSAFE_componentWillMount(),ke!==Z.state&&Aj.enqueueReplaceState(Z,Z.state,null),dv(y,j,Z,V),cv(),Z.state=y.memoizedState),typeof Z.componentDidMount=="function"&&(y.flags|=4194308),j=!0}else if(h===null){Z=y.stateNode;var qe=y.memoizedProps,ct=mf(_,qe);Z.props=ct;var wt=Z.context,Lt=_.contextType;ke=ib,typeof Lt=="object"&&Lt!==null&&(ke=ds(Lt));var Bt=_.getDerivedStateFromProps;Lt=typeof Bt=="function"||typeof Z.getSnapshotBeforeUpdate=="function",qe=y.pendingProps!==qe,Lt||typeof Z.UNSAFE_componentWillReceiveProps!="function"&&typeof Z.componentWillReceiveProps!="function"||(qe||wt!==ke)&&tB(y,Z,j,ke),Jm=!1;var Ct=y.memoizedState;Z.state=Ct,dv(y,j,Z,V),cv(),wt=y.memoizedState,qe||Ct!==wt||Jm?(typeof Bt=="function"&&(Tj(y,_,Bt,j),wt=y.memoizedState),(ct=Jm||eB(y,_,ct,j,Ct,wt,ke))?(Lt||typeof Z.UNSAFE_componentWillMount!="function"&&typeof Z.componentWillMount!="function"||(typeof Z.componentWillMount=="function"&&Z.componentWillMount(),typeof Z.UNSAFE_componentWillMount=="function"&&Z.UNSAFE_componentWillMount()),typeof Z.componentDidMount=="function"&&(y.flags|=4194308)):(typeof Z.componentDidMount=="function"&&(y.flags|=4194308),y.memoizedProps=j,y.memoizedState=wt),Z.props=j,Z.state=wt,Z.context=ke,j=ct):(typeof Z.componentDidMount=="function"&&(y.flags|=4194308),j=!1)}else{Z=y.stateNode,ij(h,y),ke=y.memoizedProps,Lt=mf(_,ke),Z.props=Lt,Bt=y.pendingProps,Ct=Z.context,wt=_.contextType,ct=ib,typeof wt=="object"&&wt!==null&&(ct=ds(wt)),qe=_.getDerivedStateFromProps,(wt=typeof qe=="function"||typeof Z.getSnapshotBeforeUpdate=="function")||typeof Z.UNSAFE_componentWillReceiveProps!="function"&&typeof Z.componentWillReceiveProps!="function"||(ke!==Bt||Ct!==ct)&&tB(y,Z,j,ct),Jm=!1,Ct=y.memoizedState,Z.state=Ct,dv(y,j,Z,V),cv();var Dt=y.memoizedState;ke!==Bt||Ct!==Dt||Jm||h!==null&&h.dependencies!==null&&s_(h.dependencies)?(typeof qe=="function"&&(Tj(y,_,qe,j),Dt=y.memoizedState),(Lt=Jm||eB(y,_,Lt,j,Ct,Dt,ct)||h!==null&&h.dependencies!==null&&s_(h.dependencies))?(wt||typeof Z.UNSAFE_componentWillUpdate!="function"&&typeof Z.componentWillUpdate!="function"||(typeof Z.componentWillUpdate=="function"&&Z.componentWillUpdate(j,Dt,ct),typeof Z.UNSAFE_componentWillUpdate=="function"&&Z.UNSAFE_componentWillUpdate(j,Dt,ct)),typeof Z.componentDidUpdate=="function"&&(y.flags|=4),typeof Z.getSnapshotBeforeUpdate=="function"&&(y.flags|=1024)):(typeof Z.componentDidUpdate!="function"||ke===h.memoizedProps&&Ct===h.memoizedState||(y.flags|=4),typeof Z.getSnapshotBeforeUpdate!="function"||ke===h.memoizedProps&&Ct===h.memoizedState||(y.flags|=1024),y.memoizedProps=j,y.memoizedState=Dt),Z.props=j,Z.state=Dt,Z.context=ct,j=Lt):(typeof Z.componentDidUpdate!="function"||ke===h.memoizedProps&&Ct===h.memoizedState||(y.flags|=4),typeof Z.getSnapshotBeforeUpdate!="function"||ke===h.memoizedProps&&Ct===h.memoizedState||(y.flags|=1024),j=!1)}return Z=j,N_(h,y),j=(y.flags&128)!==0,Z||j?(Z=y.stateNode,_=j&&typeof _.getDerivedStateFromError!="function"?null:Z.render(),y.flags|=1,h!==null&&j?(y.child=df(y,h.child,null,V),y.child=df(y,null,_,V)):us(h,y,_,V),y.memoizedState=Z.state,h=y.child):h=yu(h,y,V),h}function gB(h,y,_,j){return rf(),y.flags|=256,us(h,y,_,j),y.child}var Dj={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function Fj(h){return{baseLanes:h,cachePool:iU()}}function Rj(h,y,_){return h=h!==null?h.childLanes&~_:0,y&&(h|=Qo),h}function bB(h,y,_){var j=y.pendingProps,V=!1,Z=(y.flags&128)!==0,ke;if((ke=Z)||(ke=h!==null&&h.memoizedState===null?!1:(ti.current&2)!==0),ke&&(V=!0,y.flags&=-129),ke=(y.flags&32)!==0,y.flags&=-33,h===null){if(wr){if(V?th(y):nh(),(h=ga)?(h=N8(h,Al),h=h!==null&&h.data!=="&"?h:null,h!==null&&(y.memoizedState={dehydrated:h,treeContext:Km!==null?{id:dd,overflow:ud}:null,retryLane:536870912,hydrationErrors:null},_=Y5(h),_.return=y,y.child=_,cs=y,ga=null)):h=null,h===null)throw Ym(y);return bM(h)?y.lanes=32:y.lanes=536870912,null}var qe=j.children;return j=j.fallback,V?(nh(),V=y.mode,qe=C_({mode:"hidden",children:qe},V),j=nf(j,V,_,null),qe.return=y,j.return=y,qe.sibling=j,y.child=qe,j=y.child,j.memoizedState=Fj(_),j.childLanes=Rj(h,ke,_),y.memoizedState=Dj,fv(null,j)):(th(y),Lj(y,qe))}var ct=h.memoizedState;if(ct!==null&&(qe=ct.dehydrated,qe!==null)){if(Z)y.flags&256?(th(y),y.flags&=-257,y=Ij(h,y,_)):y.memoizedState!==null?(nh(),y.child=h.child,y.flags|=128,y=null):(nh(),qe=j.fallback,V=y.mode,j=C_({mode:"visible",children:j.children},V),qe=nf(qe,V,_,null),qe.flags|=2,j.return=y,qe.return=y,j.sibling=qe,y.child=j,df(y,h.child,null,_),j=y.child,j.memoizedState=Fj(_),j.childLanes=Rj(h,ke,_),y.memoizedState=Dj,y=fv(null,j));else if(th(y),bM(qe)){if(ke=qe.nextSibling&&qe.nextSibling.dataset,ke)var wt=ke.dgst;ke=wt,j=Error(r(419)),j.stack="",j.digest=ke,rv({value:j,source:null,stack:null}),y=Ij(h,y,_)}else if(ci||cb(h,y,_,!1),ke=(_&h.childLanes)!==0,ci||ke){if(ke=oa,ke!==null&&(j=qt(ke,_),j!==0&&j!==ct.retryLane))throw ct.retryLane=j,tf(h,j),Po(ke,h,j),Mj;gM(qe)||R_(),y=Ij(h,y,_)}else gM(qe)?(y.flags|=192,y.child=h.child,y=null):(h=ct.treeContext,ga=Ml(qe.nextSibling),cs=y,wr=!0,Xm=null,Al=!1,h!==null&&Z5(y,h),y=Lj(y,j.children),y.flags|=4096);return y}return V?(nh(),qe=j.fallback,V=y.mode,ct=h.child,wt=ct.sibling,j=hu(ct,{mode:"hidden",children:j.children}),j.subtreeFlags=ct.subtreeFlags&65011712,wt!==null?qe=hu(wt,qe):(qe=nf(qe,V,_,null),qe.flags|=2),qe.return=y,j.return=y,j.sibling=qe,y.child=j,fv(null,j),j=y.child,qe=h.child.memoizedState,qe===null?qe=Fj(_):(V=qe.cachePool,V!==null?(ct=oi._currentValue,V=V.parent!==ct?{parent:ct,pool:ct}:V):V=iU(),qe={baseLanes:qe.baseLanes|_,cachePool:V}),j.memoizedState=qe,j.childLanes=Rj(h,ke,_),y.memoizedState=Dj,fv(h.child,j)):(th(y),_=h.child,h=_.sibling,_=hu(_,{mode:"visible",children:j.children}),_.return=y,_.sibling=null,h!==null&&(ke=y.deletions,ke===null?(y.deletions=[h],y.flags|=16):ke.push(h)),y.child=_,y.memoizedState=null,_)}function Lj(h,y){return y=C_({mode:"visible",children:y},h.mode),y.return=h,h.child=y}function C_(h,y){return h=Wo(22,h,null,y),h.lanes=0,h}function Ij(h,y,_){return df(y,h.child,null,_),h=Lj(y,y.pendingProps.children),h.flags|=2,y.memoizedState=null,h}function xB(h,y,_){h.lanes|=y;var j=h.alternate;j!==null&&(j.lanes|=y),JA(h.return,y,_)}function Oj(h,y,_,j,V,Z){var ke=h.memoizedState;ke===null?h.memoizedState={isBackwards:y,rendering:null,renderingStartTime:0,last:j,tail:_,tailMode:V,treeForkCount:Z}:(ke.isBackwards=y,ke.rendering=null,ke.renderingStartTime=0,ke.last=j,ke.tail=_,ke.tailMode=V,ke.treeForkCount=Z)}function yB(h,y,_){var j=y.pendingProps,V=j.revealOrder,Z=j.tail;j=j.children;var ke=ti.current,qe=(ke&2)!==0;if(qe?(ke=ke&1|2,y.flags|=128):ke&=1,pe(ti,ke),us(h,y,j,_),j=wr?nv:0,!qe&&h!==null&&(h.flags&128)!==0)e:for(h=y.child;h!==null;){if(h.tag===13)h.memoizedState!==null&&xB(h,_,y);else if(h.tag===19)xB(h,_,y);else if(h.child!==null){h.child.return=h,h=h.child;continue}if(h===y)break e;for(;h.sibling===null;){if(h.return===null||h.return===y)break e;h=h.return}h.sibling.return=h.return,h=h.sibling}switch(V){case"forwards":for(_=y.child,V=null;_!==null;)h=_.alternate,h!==null&&p_(h)===null&&(V=_),_=_.sibling;_=V,_===null?(V=y.child,y.child=null):(V=_.sibling,_.sibling=null),Oj(y,!1,V,_,Z,j);break;case"backwards":case"unstable_legacy-backwards":for(_=null,V=y.child,y.child=null;V!==null;){if(h=V.alternate,h!==null&&p_(h)===null){y.child=V;break}h=V.sibling,V.sibling=_,_=V,V=h}Oj(y,!0,_,null,Z,j);break;case"together":Oj(y,!1,null,null,void 0,j);break;default:y.memoizedState=null}return y.child}function yu(h,y,_){if(h!==null&&(y.dependencies=h.dependencies),ih|=y.lanes,(_&y.childLanes)===0)if(h!==null){if(cb(h,y,_,!1),(_&y.childLanes)===0)return null}else return null;if(h!==null&&y.child!==h.child)throw Error(r(153));if(y.child!==null){for(h=y.child,_=hu(h,h.pendingProps),y.child=_,_.return=y;h.sibling!==null;)h=h.sibling,_=_.sibling=hu(h,h.pendingProps),_.return=y;_.sibling=null}return y.child}function zj(h,y){return(h.lanes&y)!==0?!0:(h=h.dependencies,!!(h!==null&&s_(h)))}function lde(h,y,_){switch(y.tag){case 3:F(y,y.stateNode.containerInfo),Qm(y,oi,h.memoizedState.cache),rf();break;case 27:case 5:U(y);break;case 4:F(y,y.stateNode.containerInfo);break;case 10:Qm(y,y.type,y.memoizedProps.value);break;case 31:if(y.memoizedState!==null)return y.flags|=128,dj(y),null;break;case 13:var j=y.memoizedState;if(j!==null)return j.dehydrated!==null?(th(y),y.flags|=128,null):(_&y.child.childLanes)!==0?bB(h,y,_):(th(y),h=yu(h,y,_),h!==null?h.sibling:null);th(y);break;case 19:var V=(h.flags&128)!==0;if(j=(_&y.childLanes)!==0,j||(cb(h,y,_,!1),j=(_&y.childLanes)!==0),V){if(j)return yB(h,y,_);y.flags|=128}if(V=y.memoizedState,V!==null&&(V.rendering=null,V.tail=null,V.lastEffect=null),pe(ti,ti.current),j)break;return null;case 22:return y.lanes=0,uB(h,y,_,y.pendingProps);case 24:Qm(y,oi,h.memoizedState.cache)}return yu(h,y,_)}function vB(h,y,_){if(h!==null)if(h.memoizedProps!==y.pendingProps)ci=!0;else{if(!zj(h,_)&&(y.flags&128)===0)return ci=!1,lde(h,y,_);ci=(h.flags&131072)!==0}else ci=!1,wr&&(y.flags&1048576)!==0&&J5(y,nv,y.index);switch(y.lanes=0,y.tag){case 16:e:{var j=y.pendingProps;if(h=lf(y.elementType),y.type=h,typeof h=="function")$A(h)?(j=mf(h,j),y.tag=1,y=fB(null,y,h,j,_)):(y.tag=0,y=Ej(null,y,h,j,_));else{if(h!=null){var V=h.$$typeof;if(V===P){y.tag=11,y=lB(null,y,h,j,_);break e}else if(V===T){y.tag=14,y=cB(null,y,h,j,_);break e}}throw y=L(h)||h,Error(r(306,y,""))}}return y;case 0:return Ej(h,y,y.type,y.pendingProps,_);case 1:return j=y.type,V=mf(j,y.pendingProps),fB(h,y,j,V,_);case 3:e:{if(F(y,y.stateNode.containerInfo),h===null)throw Error(r(387));j=y.pendingProps;var Z=y.memoizedState;V=Z.element,ij(h,y),dv(y,j,null,_);var ke=y.memoizedState;if(j=ke.cache,Qm(y,oi,j),j!==Z.cache&&ZA(y,[oi],_,!0),cv(),j=ke.element,Z.isDehydrated)if(Z={element:j,isDehydrated:!1,cache:ke.cache},y.updateQueue.baseState=Z,y.memoizedState=Z,y.flags&256){y=gB(h,y,j,_);break e}else if(j!==V){V=Cl(Error(r(424)),y),rv(V),y=gB(h,y,j,_);break e}else for(h=y.stateNode.containerInfo,h.nodeType===9?h=h.body:h=h.nodeName==="HTML"?h.ownerDocument.body:h,ga=Ml(h.firstChild),cs=y,wr=!0,Xm=null,Al=!0,_=uU(y,null,j,_),y.child=_;_;)_.flags=_.flags&-3|4096,_=_.sibling;else{if(rf(),j===V){y=yu(h,y,_);break e}us(h,y,j,_)}y=y.child}return y;case 26:return N_(h,y),h===null?(_=M8(y.type,null,y.pendingProps,null))?y.memoizedState=_:wr||(_=y.type,h=y.pendingProps,j=H_(B.current).createElement(_),j[At]=y,j[Kt]=h,ms(j,_,h),at(j),y.stateNode=j):y.memoizedState=M8(y.type,h.memoizedProps,y.pendingProps,h.memoizedState),null;case 27:return U(y),h===null&&wr&&(j=y.stateNode=T8(y.type,y.pendingProps,B.current),cs=y,Al=!0,V=ga,dh(y.type)?(xM=V,ga=Ml(j.firstChild)):ga=V),us(h,y,y.pendingProps.children,_),N_(h,y),h===null&&(y.flags|=4194304),y.child;case 5:return h===null&&wr&&((V=j=ga)&&(j=Ode(j,y.type,y.pendingProps,Al),j!==null?(y.stateNode=j,cs=y,ga=Ml(j.firstChild),Al=!1,V=!0):V=!1),V||Ym(y)),U(y),V=y.type,Z=y.pendingProps,ke=h!==null?h.memoizedProps:null,j=Z.children,hM(V,Z)?j=null:ke!==null&&hM(V,ke)&&(y.flags|=32),y.memoizedState!==null&&(V=mj(h,y,Zce,null,null,_),jv._currentValue=V),N_(h,y),us(h,y,j,_),y.child;case 6:return h===null&&wr&&((h=_=ga)&&(_=zde(_,y.pendingProps,Al),_!==null?(y.stateNode=_,cs=y,ga=null,h=!0):h=!1),h||Ym(y)),null;case 13:return bB(h,y,_);case 4:return F(y,y.stateNode.containerInfo),j=y.pendingProps,h===null?y.child=df(y,null,j,_):us(h,y,j,_),y.child;case 11:return lB(h,y,y.type,y.pendingProps,_);case 7:return us(h,y,y.pendingProps,_),y.child;case 8:return us(h,y,y.pendingProps.children,_),y.child;case 12:return us(h,y,y.pendingProps.children,_),y.child;case 10:return j=y.pendingProps,Qm(y,y.type,j.value),us(h,y,j.children,_),y.child;case 9:return V=y.type._context,j=y.pendingProps.children,sf(y),V=ds(V),j=j(V),y.flags|=1,us(h,y,j,_),y.child;case 14:return cB(h,y,y.type,y.pendingProps,_);case 15:return dB(h,y,y.type,y.pendingProps,_);case 19:return yB(h,y,_);case 31:return ode(h,y,_);case 22:return uB(h,y,_,y.pendingProps);case 24:return sf(y),j=ds(oi),h===null?(V=nj(),V===null&&(V=oa,Z=ej(),V.pooledCache=Z,Z.refCount++,Z!==null&&(V.pooledCacheLanes|=_),V=Z),y.memoizedState={parent:j,cache:V},aj(y),Qm(y,oi,V)):((h.lanes&_)!==0&&(ij(h,y),dv(y,null,null,_),cv()),V=h.memoizedState,Z=y.memoizedState,V.parent!==j?(V={parent:j,cache:j},y.memoizedState=V,y.lanes===0&&(y.memoizedState=y.updateQueue.baseState=V),Qm(y,oi,j)):(j=Z.cache,Qm(y,oi,j),j!==V.cache&&ZA(y,[oi],_,!0))),us(h,y,y.pendingProps.children,_),y.child;case 29:throw y.pendingProps}throw Error(r(156,y.tag))}function vu(h){h.flags|=4}function Uj(h,y,_,j,V){if((y=(h.mode&32)!==0)&&(y=!1),y){if(h.flags|=16777216,(V&335544128)===V)if(h.stateNode.complete)h.flags|=8192;else if(WB())h.flags|=8192;else throw cf=d_,rj}else h.flags&=-16777217}function wB(h,y){if(y.type!=="stylesheet"||(y.state.loading&4)!==0)h.flags&=-16777217;else if(h.flags|=16777216,!L8(y))if(WB())h.flags|=8192;else throw cf=d_,rj}function P_(h,y){y!==null&&(h.flags|=4),h.flags&16384&&(y=h.tag!==22?st():536870912,h.lanes|=y,wb|=y)}function gv(h,y){if(!wr)switch(h.tailMode){case"hidden":y=h.tail;for(var _=null;y!==null;)y.alternate!==null&&(_=y),y=y.sibling;_===null?h.tail=null:_.sibling=null;break;case"collapsed":_=h.tail;for(var j=null;_!==null;)_.alternate!==null&&(j=_),_=_.sibling;j===null?y||h.tail===null?h.tail=null:h.tail.sibling=null:j.sibling=null}}function ba(h){var y=h.alternate!==null&&h.alternate.child===h.child,_=0,j=0;if(y)for(var V=h.child;V!==null;)_|=V.lanes|V.childLanes,j|=V.subtreeFlags&65011712,j|=V.flags&65011712,V.return=h,V=V.sibling;else for(V=h.child;V!==null;)_|=V.lanes|V.childLanes,j|=V.subtreeFlags,j|=V.flags,V.return=h,V=V.sibling;return h.subtreeFlags|=j,h.childLanes=_,y}function cde(h,y,_){var j=y.pendingProps;switch(KA(y),y.tag){case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return ba(y),null;case 1:return ba(y),null;case 3:return _=y.stateNode,j=null,h!==null&&(j=h.memoizedState.cache),y.memoizedState.cache!==j&&(y.flags|=2048),gu(oi),D(),_.pendingContext&&(_.context=_.pendingContext,_.pendingContext=null),(h===null||h.child===null)&&(lb(y)?vu(y):h===null||h.memoizedState.isDehydrated&&(y.flags&256)===0||(y.flags|=1024,YA())),ba(y),null;case 26:var V=y.type,Z=y.memoizedState;return h===null?(vu(y),Z!==null?(ba(y),wB(y,Z)):(ba(y),Uj(y,V,null,j,_))):Z?Z!==h.memoizedState?(vu(y),ba(y),wB(y,Z)):(ba(y),y.flags&=-16777217):(h=h.memoizedProps,h!==j&&vu(y),ba(y),Uj(y,V,h,j,_)),null;case 27:if(Q(y),_=B.current,V=y.type,h!==null&&y.stateNode!=null)h.memoizedProps!==j&&vu(y);else{if(!j){if(y.stateNode===null)throw Error(r(166));return ba(y),null}h=xe.current,lb(y)?eU(y):(h=T8(V,j,_),y.stateNode=h,vu(y))}return ba(y),null;case 5:if(Q(y),V=y.type,h!==null&&y.stateNode!=null)h.memoizedProps!==j&&vu(y);else{if(!j){if(y.stateNode===null)throw Error(r(166));return ba(y),null}if(Z=xe.current,lb(y))eU(y);else{var ke=H_(B.current);switch(Z){case 1:Z=ke.createElementNS("http://www.w3.org/2000/svg",V);break;case 2:Z=ke.createElementNS("http://www.w3.org/1998/Math/MathML",V);break;default:switch(V){case"svg":Z=ke.createElementNS("http://www.w3.org/2000/svg",V);break;case"math":Z=ke.createElementNS("http://www.w3.org/1998/Math/MathML",V);break;case"script":Z=ke.createElement("div"),Z.innerHTML=" - + +