From 3fa0b621ddd40cd5a755fb2c125a4884eaa9918f Mon Sep 17 00:00:00 2001 From: maziggy Date: Tue, 28 Apr 2026 14:50:28 +0200 Subject: [PATCH] fix(slicer): correct multi-color slice output + UX polish across the slice pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The slicer CLI silently substitutes embedded defaults for any AMS slot the user didn't supply a profile for. When a multi-color project (e.g. a MakerWorld helmet with white shell + grey support filament configured project-wide) was sliced for a "single-color" plate, the CLI took the user's white pick for slot 1 and quietly filled slot 2 with the source 3MF's embedded grey support filament — producing a slice the user never asked for. Same silent-fallback class as the strip-removal bug. Backend `/filament-requirements` now returns the FULL project AMS slot list (from project_settings.config) with a `used_in_plate: bool` flag per entry. The flag comes from the cached preview slice for unsliced files; sliced files (where slice_info.config already pre-filters by used_g > 0) get used_in_plate=true on every entry. SliceModal renders one dropdown per project slot — slots flagged used_in_plate=true are editable, slots flagged false are auto-picked from project metadata via the existing colour-match scoring and disabled with a "-- not used by this plate" suffix. The wire format always carries a profile per project slot, so the CLI never falls back to embedded defaults. Adjacent UX fixes in the same session, since they all hit the same flow: - Persistent slice-progress toast: SliceJobTrackerProvider now opens a persistent loading toast per active job ("Slicing X -- 47s") with a 1Hz elapsed-time tick and replaces it with the existing transient success/error toast on terminal state. The previous start+finish toast pair left a UX dead zone where users couldn't tell whether a long slice was still running. - "Analyzing plate filaments..." spinner now shows elapsed seconds and, after 5s, a hint that the wait is a one-time preview slice (cached; re-opens are instant). Addresses "is anything happening?" on first open of an unsliced complex multi-color 3MF. - Pre-slice printer-mismatch warning + disabled Slice button: the plates response now exposes `source_printer_model` from project_settings.config; SliceModal compares against the picked printer profile name and surfaces an inline warning + disables Slice on mismatch. The CLI rejects cross-printer slices (rc=-16) and used to fall back to embedded settings, producing wrong-printer g-code that errored at print dispatch. - Sliced-archive card now reflects the actually-used filament list, not the source's project-wide AMS config: slice_and_persist_as_archive reads filament_type / filament_color from the sliced output's slice_info.config (which already gates on used_g > 0) instead of inheriting from the source archive. A 16+ swatch card on what was actually a 2-color print was the visible symptom. - MakerWorld URL-paste resolver enriches each instance with `compatibility` + `otherCompatibility` from design.instances[].extention.modelInfo. The /instances/hits payload omits this so every instance row used to look identical; users blindly picked the first one regardless of whether it matched their printer. Tests: 27 SliceModal tests (2 new for the disabled-row contract + 3 for printer-mismatch + 4 for multi-color rendering); 4 new SliceJobTrackerContext tests for the persistent-toast lifecycle; backend filament-requirements / slice-preview / threemf-tools suites green. i18n: new keys slice.queuedToast, slice.runningToast, slice.analyzingPlateFilamentsHint, slice.notUsedByPlate, slice.printerMismatch, makerworld.slicedFor, makerworld.alsoCompatible across all 8 UI languages (English + German fully translated, the six others seeded with English copies pending native translation, matching the project's existing flow for newly-added user-facing features). --- CHANGELOG.md | 6 + backend/app/api/routes/archives.py | 53 +- backend/app/api/routes/library.py | 66 +- .../__tests__/components/SliceModal.test.tsx | 137 +++ .../contexts/SliceJobTrackerContext.test.tsx | 264 ++++++ frontend/src/components/SliceModal.tsx | 109 ++- .../src/contexts/SliceJobTrackerContext.tsx | 81 +- frontend/src/i18n/locales/de.ts | 4 + frontend/src/i18n/locales/en.ts | 4 + frontend/src/i18n/locales/fr.ts | 4 + frontend/src/i18n/locales/it.ts | 4 + frontend/src/i18n/locales/ja.ts | 4 + frontend/src/i18n/locales/pt-BR.ts | 4 + frontend/src/i18n/locales/zh-CN.ts | 4 + frontend/src/i18n/locales/zh-TW.ts | 4 + frontend/src/types/plates.ts | 9 + .../{index-BZTbdNTm.js => index-B6Qs-684.js} | 798 +++++++++--------- static/index.html | 2 +- 18 files changed, 1058 insertions(+), 499 deletions(-) create mode 100644 frontend/src/__tests__/contexts/SliceJobTrackerContext.test.tsx rename static/assets/{index-BZTbdNTm.js => index-B6Qs-684.js} (87%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54dbabd9b..8abae31b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,12 @@ All notable changes to Bambuddy will be documented in this file. - **Slice modal had no warning when the picked printer profile didn't match the source 3MF's bound printer — silent wrong-printer output** — Both BambuStudio and OrcaSlicer CLIs reject `--load-settings` for a printer different from the one the source 3MF was originally bound to (`rc=-16` "current 3mf file not support the new printer") because the cross-printer "convert project" flow is desktop-Studio only; the slice would then fall back to embedded settings and produce a file sliced for the wrong printer that errored at print dispatch time with "File was sliced for A1, but printing on H2D". The plates response now exposes `source_printer_model` (read from `project_settings.config`'s `printer_model` field, with fallback to stripping the nozzle suffix off `printer_settings_id`); the SliceModal compares it against the picked printer profile name (substring match against the model prefix, e.g. `"Bambu Lab H2D 0.4 nozzle"` matches `"H2D"`) and surfaces an inline amber warning explaining the limitation, plus disables the Slice button while the warning is up so users can't dispatch a guaranteed-wrong slice. Cloud presets with arbitrary user-chosen names (e.g. `"My Custom X1C"`) and legacy 3MFs without `project_settings.printer_model` fall through to no-warning, which is a reasonable default — the user picked it knowingly. New `extract_source_printer_model_from_3mf` helper in `threemf_tools.py` with 6 unit tests covering missing/direct/nozzle-stripped/corrupt-JSON paths; 3 frontend tests in SliceModal pinning the warning + disabled-button on mismatch, no-warning on match, and no-warning when the source model is unknown. New i18n key `slice.printerMismatch` localised across all 8 UI languages. +- **Sliced output of a "single-color" plate had filaments the user never picked** — When a multi-color project (e.g. a MakerWorld Stormtrooper helmet with white shell + grey support filament configured project-wide) was sliced for plate 1 (which only paints with white), the resulting `.gcode.3mf`'s `slice_info.config` had two filaments — white (the user's pick) and grey (a colour the user never chose). Root cause: the SliceModal was sending only the slots the picked plate consumed, but the slicer CLI requires a profile per project AMS slot — when fewer were supplied, the CLI silently substituted the missing slots from the source 3MF's embedded filament metadata, leaking the original creator's grey support filament into the user's output. Same silent-fallback class as the strip-removal bug. Fix: backend's `/filament-requirements` endpoint now returns the FULL project AMS slot list with a `used_in_plate: bool` flag per entry (computed from the cached preview slice for unsliced files; always `true` for sliced files since `slice_info.config` already pre-filters by `used_g > 0`). The SliceModal renders one dropdown per project slot — slots flagged `used_in_plate=true` are editable as before, slots flagged `used_in_plate=false` are auto-picked from project metadata via the existing `(filament_type, filament_colour)` scoring path and disabled with a "— not used by this plate" suffix on the label, so the user only interacts with what matters for their plate while the wire format always carries a profile per project slot. 2 new frontend tests pin the disabled-row rendering and the full-list-on-submit invariant. New i18n key `slice.notUsedByPlate` localised across all 8 UI languages (English + German fully translated, the six others seeded with English copies pending native translation, matching the project's existing flow for newly-added user-facing features). + +- **"Analyzing plate filaments…" spinner gave no signal that anything was happening on the first Slice-modal open for an unsliced project file** — On a multi-color 3MF without slice_info data, the backend runs a preview slice via the sidecar to discover which AMS slots the picked plate actually consumes. That's the only source of truth: tried two heuristics — painted-face quadtree scan (silently missed extruders when `object_id` mapping between `model_settings.config` and `3D/3dmodel.model` diverged, surfaced as a single dropdown for a 4-color print) and project-wide AMS list (over-rendered every plate to the project's full slot count) — and both produced wrong counts on real-world multi-color projects. Reverted to preview-slice-as-source-of-truth. The result is cached per `(kind, source_id, plate_id, content_hash)` so re-opens of the same plate are instant, but the first open on a complex model is a real slice (multi-second to multi-minute). The inline spinner now shows elapsed seconds and, after 5s, a hint explaining that this is a one-time preview slice and re-opens will be instant — addresses the original "is anything happening?" complaint without sacrificing correctness. Project-wide `extract_project_filaments_from_3mf` remains as a final fallback when the sidecar isn't configured. New i18n key `slice.analyzingPlateFilamentsHint` localised across all 8 UI languages (English + German fully translated, the six others seeded with English copies pending native translation, matching the project's existing flow for newly-added user-facing features). + +- **No visual indicator while a slice job was running — users couldn't tell if a long slice was still progressing or had hung** — Previously SliceJobTrackerProvider emitted one transient toast on enqueue ("Slicing X in the background…") and one on completion ("Sliced X"), with nothing in between. For large multi-color models that take 30s–several minutes to slice, the start toast auto-dismissed after 3s and left a UX dead zone where users would ask "is it still slicing?". The tracker now opens a persistent `slice-job-{id}` toast with a spinner that updates every second showing elapsed time + phase ("Queued: X — 4s" → "Slicing X — 47s"), then is replaced by the existing transient success/error toast on terminal state. Polling cadence (1.5s) is unchanged — a separate 1Hz tick re-renders just the elapsed-time counter so the toast stays smooth even if the backend is slow to respond. Time format compresses gracefully past 60s ("1m 5s") and 60m ("1h 12m"). 4 new unit tests in `SliceJobTrackerContext.test.tsx` covering: persistent toast renders at t=0 (no wait for first tick), elapsed time updates each second while running, success completion replaces persistent with transient "Sliced X", failure replaces with transient error toast carrying the sidecar's `error_detail`. New i18n keys `slice.queuedToast` / `slice.runningToast` localised across all 8 UI languages (English + German fully translated, the six others seeded with English copies pending native translation, matching the project's existing flow for newly-added user-facing features). + - **MakerWorld URL-paste resolver listed plate instances without showing which printer each was sliced for** — MakerWorld's `/instances/hits` endpoint omits the per-instance compatibility info that lives on `design.instances[].extention.modelInfo` (`compatibility` = primary printer the instance was sliced for, `otherCompatibility` = additional printers the uploader marked it compatible with), so every instance row in the resolved-design preview looked identical and users blindly picked the first one regardless of whether it matched their printer — leading to "I downloaded the H2D version and got A1 g-code" complaints. The resolve route now joins both endpoint payloads by instance ID and forwards both fields onto each hit; the MakerWorld page renders "Sliced for {primaryPrinter}" + (when present) "Also marked compatible: ..." per instance row. Backend tests in `test_makerworld_routes.py::TestResolve` cover the merge happy path (compatibility lists land on the right hits) and the "missing modelInfo" fallback (older designs / hits without a matching design.instances entry don't crash the response, just lose the optional fields). New i18n keys `makerworld.slicedFor` / `makerworld.alsoCompatible` localised across all 8 UI languages. - **Moving a file to an external folder updated the DB row but never wrote the bytes to the mount** ([#1112](https://github.com/maziggy/bambuddy/issues/1112) follow-up — confirmed by @Carter3DP after testing 0.2.4b1) — Carter's report read "the file appears in Bambuddy but not physically on the external folder", which traced to `move_files` only updating `file.folder_id` in the DB while leaving the bytes in the internal `library_files_dir`. Direct upload to a writable external folder was already fixed in 0.2.4b1; the move path was not. Cross-boundary moves now physically relocate the bytes through a new `_move_file_bytes` helper. Same-boundary moves (managed → managed) keep the existing DB-only fast path because the file's on-disk location doesn't depend on which managed folder owns it. The helper handles four flows: managed → external (copy bytes to `/`, flip `is_external=True`, store the absolute path, unlink the managed source), external → managed (copy bytes into internal storage with a fresh UUID name, flip `is_external=False`, store the relative path, unlink the external source, recompute `file_hash` since scan-tracked rows historically carry `file_hash=None`), external → external (same as managed → external), and managed → managed (DB-only). Copy-then-unlink ordering means a partial copy followed by a failed unlink leaves both copies on disk rather than losing the source if the target write fails halfway through on a flaky NAS mount. Failed `shutil.copy2` cleans up partial dest before raising. Defence-in-depth checks block: source on a read-only external mount (move = delete-on-source which a RO mount can't fulfil — would copy-then-fail-to-unlink and silently duplicate the file), filename collisions on the target mount (won't silently overwrite a file the user already has on the NAS), traversal-style filenames after `Path.resolve()`, missing source on disk, and `os.access(W_OK)` on the target mount. Each skip carries a structured `{file_id, code, reason}` entry in a new `skipped_reasons` field on the response so the UI can surface "5 of 10 files skipped: 3 had filename collisions on the NAS, 2 are no longer on disk" instead of a blank "skipped: 5". The original `{moved, skipped}` numeric counters are preserved so existing frontend code that only reads those keeps working unchanged. Six new integration tests in `test_external_folders_api.py::TestCrossBoundaryMove` covering: managed → external relocates bytes (the actual #1112 fix — bytes land on mount, internal source removed, DB row matches reality), external → managed relocates bytes (symmetric path including hash recompute), name collision on target external mount skips with `code: "name_collision"` and leaves the pre-existing target file intact, source on read-only external mount skips with `code: "source_readonly"`, managed → managed stays DB-only (file_path doesn't change, no shutil.copy), and `skipped_reasons` is always present (empty list when nothing skipped) so frontend code can treat it as the source of truth without optional-chaining. diff --git a/backend/app/api/routes/archives.py b/backend/app/api/routes/archives.py index 2b2a3650f..d7e74b3e0 100644 --- a/backend/app/api/routes/archives.py +++ b/backend/app/api/routes/archives.py @@ -29,7 +29,6 @@ from backend.app.schemas.slicer import SliceRequest from backend.app.services.archive import ArchiveService from backend.app.utils.threemf_tools import ( extract_nozzle_mapping_from_3mf, - extract_plate_extruder_set_from_3mf, extract_project_filaments_from_3mf, extract_source_printer_model_from_3mf, ) @@ -3237,6 +3236,7 @@ async def get_filament_requirements( "used_grams": round(used_grams, 1), "used_meters": float(used_m) if used_m else 0, "tray_info_idx": tray_info_idx, + "used_in_plate": True, } ) break @@ -3267,41 +3267,32 @@ async def get_filament_requirements( "used_grams": round(used_grams, 1), "used_meters": float(used_m) if used_m else 0, "tray_info_idx": tray_info_idx, + "used_in_plate": True, } ) - # Unsliced project files: slice_info has nothing usable. Try a - # preview-slice via the sidecar — the slicer's own logic - # determines which filaments the plate actually consumes - # (Bambu Studio prunes painted regions whose extruder isn't - # used, etc.) — and parse the resulting slice_info. Cached so - # the modal opens fast on the second visit. Falls back to the - # painted-face heuristic if the sidecar isn't configured or - # the preview slice errors out. - if not filaments and plate_id is not None: - preview = await _try_preview_slice_filaments( - db, - kind="archive", - source_id=archive_id, - plate_id=plate_id, - file_path=file_path, - ) - if preview is not None: - filaments = preview - - # Last-resort fallback for unsliced files when preview slicing - # also can't help (no sidecar, slicer errored, etc.). See - # library.py for the matching block. + # Unsliced project files: see library.py for full rationale. + # Return the FULL project_settings.config slot list with a + # used_in_plate flag derived from the preview slice; the + # CLI needs every slot pre-filled to avoid silent default + # substitution. if not filaments: project_filaments = extract_project_filaments_from_3mf(zf) - if plate_id is not None and project_filaments: - used_slots = extract_plate_extruder_set_from_3mf(zf, plate_id) - if used_slots: - filaments = [f for f in project_filaments if f["slot_id"] in used_slots] - else: - filaments = project_filaments - elif project_filaments: - filaments = project_filaments + used_slot_ids: set[int] = set() + if project_filaments and plate_id is not None: + preview = await _try_preview_slice_filaments( + db, + kind="archive", + source_id=archive_id, + plate_id=plate_id, + file_path=file_path, + ) + if preview is not None: + used_slot_ids = {f["slot_id"] for f in preview} + fallback_all_used = not used_slot_ids + for f in project_filaments: + f["used_in_plate"] = fallback_all_used or f["slot_id"] in used_slot_ids + filaments = project_filaments # Sort by slot ID filaments.sort(key=lambda x: x["slot_id"]) diff --git a/backend/app/api/routes/library.py b/backend/app/api/routes/library.py index fbb5b9d25..d34bcaeb3 100644 --- a/backend/app/api/routes/library.py +++ b/backend/app/api/routes/library.py @@ -63,7 +63,6 @@ from backend.app.services.archive import ThreeMFParser from backend.app.services.stl_thumbnail import generate_stl_thumbnail from backend.app.utils.threemf_tools import ( extract_nozzle_mapping_from_3mf, - extract_plate_extruder_set_from_3mf, extract_project_filaments_from_3mf, extract_source_printer_model_from_3mf, ) @@ -2475,6 +2474,11 @@ async def get_library_file_filament_requirements( "used_grams": round(used_grams, 1), "used_meters": float(used_m) if used_m else 0, "tray_info_idx": tray_info_idx, + # Sliced output already pre-filtered by used_g>0, + # so every entry that survives is in fact used by + # this plate. Print-dispatch consumers ignore the + # flag; SliceModal uses it to enable/disable rows. + "used_in_plate": True, } ) break @@ -2503,40 +2507,42 @@ async def get_library_file_filament_requirements( "used_grams": round(used_grams, 1), "used_meters": float(used_m) if used_m else 0, "tray_info_idx": tray_info_idx, + "used_in_plate": True, } ) - # Unsliced project files: slice_info has nothing usable. Try a - # preview-slice via the sidecar — see archives.py for full - # rationale. Cached per (kind, id, plate, content_hash). - if not filaments and plate_id is not None: - preview = await _try_preview_slice_filaments( - db, - kind="library_file", - source_id=file_id, - plate_id=plate_id, - file_path=file_path, - ) - if preview is not None: - filaments = preview - - # Last-resort fallback when preview slicing also can't help (no - # sidecar configured, slicer errored, etc.). project_settings - # gives us the full AMS slot config; the plate-extruder filter - # narrows it to the slots the painted faces reference. + # Unsliced project files: slice_info had no per-plate data. + # Return the FULL project_settings.config AMS slot list so + # the slicer CLI receives a profile for every project slot + # (otherwise it silently fills the gap from embedded + # defaults — surfaces as "I picked white but the print has + # grey" because the source's grey support filament leaks + # into the output). Use the preview slice to mark which + # slots the picked plate actually consumes; the SliceModal + # disables the unused rows so the user only interacts with + # the dropdowns that matter, while the backend still has + # the complete list to pass to the CLI. if not filaments: project_filaments = extract_project_filaments_from_3mf(zf) - if plate_id is not None and project_filaments: - used_slots = extract_plate_extruder_set_from_3mf(zf, plate_id) - if used_slots: - filaments = [f for f in project_filaments if f["slot_id"] in used_slots] - else: - # No extruder metadata anywhere — return the full - # project list rather than zero so the user still - # gets to pick (over-rendering > under-rendering). - filaments = project_filaments - elif project_filaments: - filaments = project_filaments + used_slot_ids: set[int] = set() + if project_filaments and plate_id is not None: + preview = await _try_preview_slice_filaments( + db, + kind="library_file", + source_id=file_id, + plate_id=plate_id, + file_path=file_path, + ) + if preview is not None: + used_slot_ids = {f["slot_id"] for f in preview} + # Default to "every slot is used" when preview-slice + # didn't produce data: better to over-enable dropdowns + # than under-enable and have the user unable to pick a + # filament the plate actually uses. + fallback_all_used = not used_slot_ids + for f in project_filaments: + f["used_in_plate"] = fallback_all_used or f["slot_id"] in used_slot_ids + filaments = project_filaments # Sort by slot ID filaments.sort(key=lambda x: x["slot_id"]) diff --git a/frontend/src/__tests__/components/SliceModal.test.tsx b/frontend/src/__tests__/components/SliceModal.test.tsx index 854ce65aa..4384870a9 100644 --- a/frontend/src/__tests__/components/SliceModal.test.tsx +++ b/frontend/src/__tests__/components/SliceModal.test.tsx @@ -819,4 +819,141 @@ describe('SliceModal', () => { const sliceButton = screen.getByRole('button', { name: /^Slice$/ }) as HTMLButtonElement; expect(sliceButton.disabled).toBe(false); }); + + // The `used_in_plate` flag tells the modal which AMS slots are + // actually consumed by the picked plate. Slots flagged as unused + // are still rendered (the slicer CLI needs a profile per project + // slot, otherwise it silently fills the gap from embedded defaults + // and unwanted colours leak into the output) but disabled in the UI + // so the user only interacts with the dropdowns that matter. + it('disables filament dropdowns for slots not used by the picked plate', async () => { + mockApi.getLibraryFilePlates.mockResolvedValue({ + file_id: 100, + filename: 'Helmet.3mf', + is_multi_plate: false, + plates: [ + { + index: 1, + name: 'Plate 1', + objects: ['Helmet'], + has_thumbnail: false, + thumbnail_url: null, + print_time_seconds: 1200, + filament_used_grams: 80, + filaments: [], + }, + ], + }); + // Project has 2 AMS slots configured (white + grey support), but + // plate 1 only paints with white (slot 1). The backend now returns + // BOTH slots with used_in_plate flagging the difference. + mockApi.getLibraryFileFilamentRequirements.mockResolvedValue({ + file_id: 100, + filename: 'Helmet.3mf', + plate_id: 1, + filaments: [ + { slot_id: 1, type: 'PLA', color: '#FFFFFF', used_grams: 80, used_meters: 27, used_in_plate: true }, + { slot_id: 2, type: 'PLA', color: '#808080', used_grams: 0, used_meters: 0, used_in_plate: false }, + ], + }); + mockApi.getSlicerPresets.mockResolvedValue({ + cloud: { + printer: [{ id: 'P1', name: 'X1C', source: 'cloud' }], + process: [{ id: 'PR1', name: '0.20mm', source: 'cloud' }], + filament: [ + { id: 'F-WHITE', name: 'Cloud PLA White', source: 'cloud', filament_type: 'PLA', filament_colour: '#FFFFFF' }, + { id: 'F-GREY', name: 'Cloud PLA Grey', source: 'cloud', filament_type: 'PLA', filament_colour: '#808080' }, + ], + }, + local: { printer: [], process: [], filament: [] }, + standard: { printer: [], process: [], filament: [] }, + cloud_status: 'ok', + }); + + renderWithTracker({ + source: { kind: 'libraryFile', id: 100, filename: 'Helmet.3mf' }, + onClose: vi.fn(), + }); + + await waitFor(() => expect(screen.getByText('X1C')).toBeDefined()); + + // Both filament rows render — 1 printer + 1 process + 2 filament = 4. + const selects = screen.getAllByRole('combobox') as HTMLSelectElement[]; + expect(selects).toHaveLength(4); + // Slot 1 (used) is editable, slot 2 (not used) is disabled. + expect(selects[2].disabled).toBe(false); + expect(selects[3].disabled).toBe(true); + // The disabled row's label calls out why it's disabled. + expect(screen.getByText(/not used by this plate/i)).toBeDefined(); + }); + + it('still sends both filaments to the backend even when one slot is disabled', async () => { + // The auto-pick scoring fills the disabled slot from project + // metadata — the slicer CLI requires a profile for every project + // slot, otherwise it silently fills the gap. The disabled UI is + // purely cosmetic; the wire format must include the full list. + mockApi.getLibraryFilePlates.mockResolvedValue({ + file_id: 100, + filename: 'Helmet.3mf', + is_multi_plate: false, + plates: [ + { + index: 1, + name: 'Plate 1', + objects: ['Helmet'], + has_thumbnail: false, + thumbnail_url: null, + print_time_seconds: 1200, + filament_used_grams: 80, + filaments: [], + }, + ], + }); + mockApi.getLibraryFileFilamentRequirements.mockResolvedValue({ + file_id: 100, + filename: 'Helmet.3mf', + plate_id: 1, + filaments: [ + { slot_id: 1, type: 'PLA', color: '#FFFFFF', used_grams: 80, used_meters: 27, used_in_plate: true }, + { slot_id: 2, type: 'PLA', color: '#808080', used_grams: 0, used_meters: 0, used_in_plate: false }, + ], + }); + mockApi.getSlicerPresets.mockResolvedValue({ + cloud: { + printer: [{ id: 'P1', name: 'X1C', source: 'cloud' }], + process: [{ id: 'PR1', name: '0.20mm', source: 'cloud' }], + filament: [ + { id: 'F-WHITE', name: 'Cloud PLA White', source: 'cloud', filament_type: 'PLA', filament_colour: '#FFFFFF' }, + { id: 'F-GREY', name: 'Cloud PLA Grey', source: 'cloud', filament_type: 'PLA', filament_colour: '#808080' }, + ], + }, + local: { printer: [], process: [], filament: [] }, + standard: { printer: [], process: [], filament: [] }, + cloud_status: 'ok', + }); + mockApi.sliceLibraryFile.mockResolvedValue({ + job_id: 50, + status: 'pending', + status_url: '/api/v1/slice-jobs/50', + }); + + renderWithTracker({ + source: { kind: 'libraryFile', id: 100, filename: 'Helmet.3mf' }, + onClose: vi.fn(), + }); + + await waitFor(() => expect(screen.getByText('X1C')).toBeDefined()); + + const user = userEvent.setup(); + await user.click(screen.getByRole('button', { name: /^Slice$/ })); + + await waitFor(() => { + const [, body] = mockApi.sliceLibraryFile.mock.calls[0]; + // Both slots populated: slot 1 with the user's white pick, slot + // 2 auto-picked with grey from the colour-match scoring. + expect(body.filament_presets).toHaveLength(2); + expect(body.filament_presets[0]).toEqual({ source: 'cloud', id: 'F-WHITE' }); + expect(body.filament_presets[1]).toEqual({ source: 'cloud', id: 'F-GREY' }); + }); + }); }); diff --git a/frontend/src/__tests__/contexts/SliceJobTrackerContext.test.tsx b/frontend/src/__tests__/contexts/SliceJobTrackerContext.test.tsx new file mode 100644 index 000000000..2a2016253 --- /dev/null +++ b/frontend/src/__tests__/contexts/SliceJobTrackerContext.test.tsx @@ -0,0 +1,264 @@ +/** + * Tests for SliceJobTrackerProvider's persistent progress toast. + * + * The tracker shows a persistent loading toast (`slice-job-{id}`) that + * updates every second with elapsed time + phase label, then is replaced + * by a transient success/error toast on terminal state. Without the + * persistent toast, long slices on large models produce a "is it still + * running?" UX gap between the start toast and the completion toast. + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { act, render, screen } from '@testing-library/react'; +import { type ReactNode } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ToastProvider } from '../../contexts/ToastContext'; +import { SliceJobTrackerProvider, useSliceJobTracker } from '../../contexts/SliceJobTrackerContext'; +import { api } from '../../api/client'; + +vi.mock('../../api/client', () => ({ + api: { + getSliceJob: vi.fn(), + }, +})); + +const mockApi = api as unknown as { getSliceJob: ReturnType }; + +function Wrapper({ children }: { children: ReactNode }) { + // A fresh QueryClient per test so invalidateQueries calls don't leak + // between tests. + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return ( + + + {children} + + + ); +} + +function TrackTrigger({ id, name }: { id: number; name: string }) { + const { trackJob } = useSliceJobTracker(); + return ( + + ); +} + +describe('SliceJobTrackerProvider — persistent progress toast', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('renders a persistent toast immediately when a job is tracked', () => { + // Job stays running indefinitely — completion path is its own test. + mockApi.getSliceJob.mockResolvedValue({ + job_id: 1, + status: 'running', + kind: 'library_file', + source_id: 100, + source_name: 'BigCube.stl', + created_at: new Date().toISOString(), + started_at: new Date().toISOString(), + completed_at: null, + }); + + render( + + + , + ); + + act(() => { + screen.getByText('track-1').click(); + }); + + // Initial frame: the "Queued" phase before the first poll lands. The + // toast must be on screen at t=0 without waiting for any tick. + expect(screen.getByText(/BigCube\.stl/)).toBeDefined(); + expect(screen.getByText(/0s/)).toBeDefined(); + }); + + it('updates elapsed time each second while the job is running', async () => { + let resolveFirstPoll: () => void = () => {}; + const firstPollLanded = new Promise((resolve) => { + resolveFirstPoll = resolve; + }); + + mockApi.getSliceJob.mockImplementation(async () => { + resolveFirstPoll(); + return { + job_id: 2, + status: 'running', + kind: 'library_file', + source_id: 101, + source_name: 'TallTower.stl', + created_at: new Date().toISOString(), + started_at: new Date().toISOString(), + completed_at: null, + }; + }); + + render( + + + , + ); + + act(() => { + screen.getByText('track-2').click(); + }); + + // Advance fake time past the 1.5s poll so the phase flips + // pending→running before we check the message. + await act(async () => { + vi.advanceTimersByTime(1500); + }); + await firstPollLanded; + // Let any pending promise microtasks drain on the test loop. + await act(async () => { + await Promise.resolve(); + }); + + // Tick another 5 seconds — elapsed should now be ~6s total since + // the start, and the toast must reflect it. + act(() => { + vi.advanceTimersByTime(5000); + }); + + // 1500ms (poll) + 5000ms (tick) = 6500ms ≈ 6s rounded down. + expect(screen.getByText(/6s/)).toBeDefined(); + expect(screen.getByText(/TallTower\.stl/)).toBeDefined(); + }); + + it('replaces the persistent toast with a transient success toast on completion', async () => { + let pollCount = 0; + mockApi.getSliceJob.mockImplementation(async () => { + pollCount += 1; + // First poll: running. Second poll: completed. + if (pollCount === 1) { + return { + job_id: 3, + status: 'running', + kind: 'library_file', + source_id: 102, + source_name: 'Done.stl', + created_at: new Date().toISOString(), + started_at: new Date().toISOString(), + completed_at: null, + }; + } + return { + job_id: 3, + status: 'completed', + kind: 'library_file', + source_id: 102, + source_name: 'Done.stl', + created_at: new Date().toISOString(), + started_at: new Date().toISOString(), + completed_at: new Date().toISOString(), + }; + }); + + render( + + + , + ); + + act(() => { + screen.getByText('track-3').click(); + }); + + // Drive both polls (each 1.5s). After the second, completeJob should + // dismiss the persistent toast and show the transient success toast. + await act(async () => { + vi.advanceTimersByTime(1500); + }); + await act(async () => { + await Promise.resolve(); + }); + await act(async () => { + vi.advanceTimersByTime(1500); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + // The progress toast text "Slicing Done.stl —" / "Queued: Done.stl" + // should be gone; the success toast "Sliced Done.stl" should be up. + expect(screen.queryByText(/Slicing Done\.stl —/)).toBeNull(); + expect(screen.queryByText(/Queued: Done\.stl/)).toBeNull(); + expect(screen.getByText(/Sliced Done\.stl/)).toBeDefined(); + }); + + it('replaces the persistent toast with a transient error toast on failure', async () => { + let pollCount = 0; + mockApi.getSliceJob.mockImplementation(async () => { + pollCount += 1; + if (pollCount === 1) { + return { + job_id: 4, + status: 'running', + kind: 'library_file', + source_id: 103, + source_name: 'Broken.stl', + created_at: new Date().toISOString(), + started_at: new Date().toISOString(), + completed_at: null, + }; + } + return { + job_id: 4, + status: 'failed', + kind: 'library_file', + source_id: 103, + source_name: 'Broken.stl', + created_at: new Date().toISOString(), + started_at: new Date().toISOString(), + completed_at: new Date().toISOString(), + error_status: 500, + error_detail: 'sidecar segfault', + }; + }); + + render( + + + , + ); + + act(() => { + screen.getByText('track-4').click(); + }); + + await act(async () => { + vi.advanceTimersByTime(1500); + }); + await act(async () => { + await Promise.resolve(); + }); + await act(async () => { + vi.advanceTimersByTime(1500); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(screen.queryByText(/Slicing Broken\.stl —/)).toBeNull(); + // The failure detail must surface in the toast — generic "failed" + // strings stripped of the sidecar reason were the original UX + // complaint that motivated `_format_sidecar_error` on the backend. + expect(screen.getByText(/sidecar segfault/)).toBeDefined(); + }); +}); diff --git a/frontend/src/components/SliceModal.tsx b/frontend/src/components/SliceModal.tsx index 1f689eaac..767fd5601 100644 --- a/frontend/src/components/SliceModal.tsx +++ b/frontend/src/components/SliceModal.tsx @@ -103,6 +103,41 @@ function fromRefValue(raw: string): PresetRef | null { return { source, id }; } +// Inline spinner for the filament-requirements query. The backend runs a +// preview slice on first open of an unsliced project file (cached after); +// on a complex multi-color model that's a real slice — multi-second to +// multi-minute. The static "Analyzing plate filaments…" string left +// users wondering whether anything was happening, so the spinner now +// shows elapsed seconds and a hint that explains the wait. After ~5s it +// also surfaces a "this is a one-time slice — repeat opens are instant" +// note so users don't worry it'll be slow forever. +function FilamentAnalysisSpinner() { + const { t } = useTranslation(); + const [elapsed, setElapsed] = useState(0); + useEffect(() => { + const startedAt = Date.now(); + const id = setInterval(() => setElapsed(Math.floor((Date.now() - startedAt) / 1000)), 1000); + return () => clearInterval(id); + }, []); + return ( +
+
+ + {t('slice.analyzingPlateFilaments', 'Analyzing plate filaments…')} + {elapsed}s +
+ {elapsed >= 5 && ( +
+ {t( + 'slice.analyzingPlateFilamentsHint', + 'Running a preview slice to discover which AMS slots this plate uses. Cached after — re-opening is instant.', + )} +
+ )} +
+ ); +} + export function SliceModal({ source, onClose }: SliceModalProps) { const { t } = useTranslation(); const { trackJob } = useSliceJobTracker(); @@ -371,39 +406,49 @@ export function SliceModal({ source, onClose }: SliceModalProps) { scoped spinner so the user sees the printer/process dropdowns instead of an opaque "Loading presets…" wait. */} {filamentReqsQuery.isLoading ? ( -
- - {t('slice.analyzingPlateFilaments', 'Analyzing plate filaments…')} -
+ ) : ( - filamentSlots.map((slot, idx) => ( - 1 - ? t('slice.filamentSlot', { - index: idx + 1, - type: slot.type, - defaultValue: `Filament ${idx + 1} (${slot.type || ''})`, - }) - : t('slice.filament', 'Filament profile') - } - slot="filament" - data={presetsQuery.data} - value={filamentPresets[idx] ?? null} - onChange={(ref) => - setFilamentPresets((current) => { - const next = current.length === filamentSlots.length - ? [...current] - : filamentSlots.map((_, i) => current[i] ?? null); - next[idx] = ref; - return next; - }) - } - disabled={isEnqueuing} - swatchColor={filamentSlots.length > 1 ? slot.color : undefined} - /> - )) + filamentSlots.map((slot, idx) => { + // Slots flagged by the backend as not used by the + // picked plate are auto-picked from project metadata + // and disabled — the slicer CLI still needs a + // profile per project slot, but the user shouldn't + // have to think about slots their plate doesn't + // paint with. used_in_plate defaults to true when + // missing (sliced 3MFs and the no-flag legacy path). + const isUsed = slot.used_in_plate !== false; + const baseLabel = + filamentSlots.length > 1 + ? t('slice.filamentSlot', { + index: idx + 1, + type: slot.type, + defaultValue: `Filament ${idx + 1} (${slot.type || ''})`, + }) + : t('slice.filament', 'Filament profile'); + const label = isUsed + ? baseLabel + : `${baseLabel} ${t('slice.notUsedByPlate', '— not used by this plate')}`; + return ( + + setFilamentPresets((current) => { + const next = current.length === filamentSlots.length + ? [...current] + : filamentSlots.map((_, i) => current[i] ?? null); + next[idx] = ref; + return next; + }) + } + disabled={isEnqueuing || !isUsed} + swatchColor={filamentSlots.length > 1 ? slot.color : undefined} + /> + ); + }) )} )} diff --git a/frontend/src/contexts/SliceJobTrackerContext.tsx b/frontend/src/contexts/SliceJobTrackerContext.tsx index 52a3dd0f4..1c3c1235a 100644 --- a/frontend/src/contexts/SliceJobTrackerContext.tsx +++ b/frontend/src/contexts/SliceJobTrackerContext.tsx @@ -6,11 +6,16 @@ * shows toasts on terminal state. Lives at app level so polling continues * across navigation — slice can run in the background while the user does * other things. + * + * Each tracked job also gets a persistent toast (`slice-job-{id}`) with a + * spinner + elapsed-time counter that updates every second so the user has + * a continuous visual indicator while a long slice is running. The toast + * is replaced by a transient success/error toast on terminal state. */ import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { useQueryClient } from '@tanstack/react-query'; -import { api, type SliceJobState } from '../api/client'; +import { api, type SliceJobState, type SliceJobStatus } from '../api/client'; import { useToast } from './ToastContext'; interface TrackedJob { @@ -27,10 +32,24 @@ interface SliceJobTrackerContextValue { const SliceJobTrackerContext = createContext(null); const POLL_INTERVAL_MS = 1500; +const TICK_INTERVAL_MS = 1000; + +const toastIdFor = (jobId: number) => `slice-job-${jobId}`; + +function formatElapsed(seconds: number): string { + const s = Math.max(0, Math.floor(seconds)); + if (s < 60) return `${s}s`; + const m = Math.floor(s / 60); + const remS = s % 60; + if (m < 60) return `${m}m ${remS}s`; + const h = Math.floor(m / 60); + const remM = m % 60; + return `${h}h ${remM}m`; +} export function SliceJobTrackerProvider({ children }: { children: ReactNode }) { const { t } = useTranslation(); - const { showToast } = useToast(); + const { showToast, showPersistentToast, dismissToast } = useToast(); const queryClient = useQueryClient(); const [activeJobs, setActiveJobs] = useState([]); @@ -39,17 +58,52 @@ export function SliceJobTrackerProvider({ children }: { children: ReactNode }) { const activeJobsRef = useRef([]); activeJobsRef.current = activeJobs; + // Per-job start time + latest phase, kept in refs so the 1s tick + // doesn't need to re-render on every update. Keyed by job id. + const startedAtRef = useRef>(new Map()); + const phaseRef = useRef>(new Map()); + + const renderProgressToast = useCallback( + (job: TrackedJob) => { + const startedAt = startedAtRef.current.get(job.id); + if (startedAt == null) return; + const elapsedSecs = (Date.now() - startedAt) / 1000; + const phase = phaseRef.current.get(job.id) ?? 'pending'; + const messageKey = phase === 'pending' ? 'slice.queuedToast' : 'slice.runningToast'; + const fallback = + phase === 'pending' + ? 'Queued: {{name}} — {{elapsed}}' + : 'Slicing {{name}} — {{elapsed}}'; + showPersistentToast( + toastIdFor(job.id), + t(messageKey, fallback, { name: job.sourceName, elapsed: formatElapsed(elapsedSecs) }), + 'loading', + ); + }, + [showPersistentToast, t], + ); + const trackJob = useCallback( (id: number, kind: 'libraryFile' | 'archive', sourceName: string) => { setActiveJobs((prev) => (prev.some((j) => j.id === id) ? prev : [...prev, { id, kind, sourceName }])); - showToast(t('slice.startedToast', 'Slicing {{name}} in the background…', { name: sourceName }), 'info'); + startedAtRef.current.set(id, Date.now()); + phaseRef.current.set(id, 'pending'); + // Render the initial frame immediately so the user sees the toast + // before the first tick lands (~1s delay otherwise). + renderProgressToast({ id, kind, sourceName }); }, - [showToast, t], + [renderProgressToast], ); const completeJob = useCallback( (job: TrackedJob, state: SliceJobState) => { setActiveJobs((prev) => prev.filter((j) => j.id !== job.id)); + startedAtRef.current.delete(job.id); + phaseRef.current.delete(job.id); + + // Replace the persistent progress toast with a transient + // success/error toast (auto-dismisses after 3s, same as showToast). + dismissToast(toastIdFor(job.id)); if (state.status === 'completed') { // `used_embedded_settings` still comes back on the result for tests @@ -70,19 +124,21 @@ export function SliceJobTrackerProvider({ children }: { children: ReactNode }) { queryClient.invalidateQueries({ queryKey: ['library-files'] }); queryClient.invalidateQueries({ queryKey: ['archives'] }); }, - [queryClient, showToast, t], + [dismissToast, queryClient, showToast, t], ); + // Status polling. Updates phase on each successful poll and triggers + // completeJob on terminal states. useEffect(() => { if (activeJobs.length === 0) return; let cancelled = false; const interval = setInterval(async () => { if (cancelled) return; - // Snapshot the current list so concurrent updates don't surprise us. const snapshot = [...activeJobsRef.current]; for (const job of snapshot) { try { const state = await api.getSliceJob(job.id); + phaseRef.current.set(job.id, state.status); if (state.status === 'completed' || state.status === 'failed') { completeJob(job, state); } @@ -97,6 +153,19 @@ export function SliceJobTrackerProvider({ children }: { children: ReactNode }) { }; }, [activeJobs.length, completeJob]); + // 1Hz tick that re-renders each persistent progress toast with the + // current elapsed time. Independent of the status poll so the counter + // stays smooth even while the backend is slow to respond. + useEffect(() => { + if (activeJobs.length === 0) return; + const tick = setInterval(() => { + for (const job of activeJobsRef.current) { + renderProgressToast(job); + } + }, TICK_INTERVAL_MS); + return () => clearInterval(tick); + }, [activeJobs.length, renderProgressToast]); + return ( {children} diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index ec55c9864..373524d26 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -3254,6 +3254,8 @@ export default { selectPreset: '— Profil auswählen —', loadingPresets: 'Profile werden geladen…', analyzingPlateFilaments: 'Plattenfilamente werden analysiert…', + analyzingPlateFilamentsHint: 'Es wird ein Probeschnitt ausgeführt, um die belegten AMS-Slots dieser Platte zu ermitteln. Wird zwischengespeichert — erneutes Öffnen ist sofort.', + notUsedByPlate: '— wird von dieser Platte nicht verwendet', printerMismatch: 'Dieses 3MF wurde für {{source}} gesliced, du hast aber {{target}} ausgewählt. Der Slicer-CLI kann ein 3MF nicht für einen anderen Drucker neu slicen — öffne die Quelle in Bambu Studio, ändere den Drucker und exportiere neu.', noPresetsForSlot: 'Keine Profile verfügbar', presetsLoadFailed: 'Profile konnten nicht geladen werden. Importiere sie zuerst unter Einstellungen → Profile.', @@ -3262,6 +3264,8 @@ export default { queued: 'In Warteschlange…', failed: 'Slicen fehlgeschlagen. Logs des Slicer-Sidecars prüfen.', startedToast: '{{name}} wird im Hintergrund gesliced…', + queuedToast: 'Warteschlange: {{name}} — {{elapsed}}', + runningToast: '{{name}} wird gesliced — {{elapsed}}', completedToast: '{{name}} wurde gesliced', failedToast: 'Slicen von {{name}} fehlgeschlagen: {{detail}}', tier: { diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 7d262e805..092286075 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -3257,6 +3257,8 @@ export default { selectPreset: '— Select a preset —', loadingPresets: 'Loading presets…', analyzingPlateFilaments: 'Analyzing plate filaments…', + analyzingPlateFilamentsHint: 'Running a preview slice to discover which AMS slots this plate uses. Cached after — re-opening is instant.', + notUsedByPlate: '— not used by this plate', printerMismatch: 'This 3MF was sliced for {{source}}, but you picked {{target}}. The slicer CLI cannot re-slice a 3MF for a different printer — open the source in Bambu Studio, change the printer, and re-export.', noPresetsForSlot: 'No presets available', presetsLoadFailed: 'Failed to load presets. Open Settings → Profiles to import them first.', @@ -3265,6 +3267,8 @@ export default { queued: 'Queued…', failed: 'Slicing failed. Check the slicer sidecar logs.', startedToast: 'Slicing {{name}} in the background…', + queuedToast: 'Queued: {{name}} — {{elapsed}}', + runningToast: 'Slicing {{name}} — {{elapsed}}', completedToast: 'Sliced {{name}}', failedToast: 'Slicing {{name}} failed: {{detail}}', tier: { diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index fd90fc512..9087db467 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -3176,6 +3176,8 @@ export default { selectPreset: '— Select a preset —', loadingPresets: 'Loading presets…', analyzingPlateFilaments: 'Analyzing plate filaments…', + analyzingPlateFilamentsHint: 'Running a preview slice to discover which AMS slots this plate uses. Cached after — re-opening is instant.', + notUsedByPlate: '— not used by this plate', printerMismatch: 'This 3MF was sliced for {{source}}, but you picked {{target}}. The slicer CLI cannot re-slice a 3MF for a different printer — open the source in Bambu Studio, change the printer, and re-export.', noPresetsForSlot: 'No presets available', presetsLoadFailed: 'Failed to load presets. Open Settings → Profiles to import them first.', @@ -3184,6 +3186,8 @@ export default { queued: 'Queued…', failed: 'Slicing failed. Check the slicer sidecar logs.', startedToast: 'Slicing {{name}} in the background…', + queuedToast: 'Queued: {{name}} — {{elapsed}}', + runningToast: 'Slicing {{name}} — {{elapsed}}', completedToast: 'Sliced {{name}}', failedToast: 'Slicing {{name}} failed: {{detail}}', tier: { diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index b7bb1d49e..d79b0be20 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -3175,6 +3175,8 @@ export default { selectPreset: '— Select a preset —', loadingPresets: 'Loading presets…', analyzingPlateFilaments: 'Analyzing plate filaments…', + analyzingPlateFilamentsHint: 'Running a preview slice to discover which AMS slots this plate uses. Cached after — re-opening is instant.', + notUsedByPlate: '— not used by this plate', printerMismatch: 'This 3MF was sliced for {{source}}, but you picked {{target}}. The slicer CLI cannot re-slice a 3MF for a different printer — open the source in Bambu Studio, change the printer, and re-export.', noPresetsForSlot: 'No presets available', presetsLoadFailed: 'Failed to load presets. Open Settings → Profiles to import them first.', @@ -3183,6 +3185,8 @@ export default { queued: 'Queued…', failed: 'Slicing failed. Check the slicer sidecar logs.', startedToast: 'Slicing {{name}} in the background…', + queuedToast: 'Queued: {{name}} — {{elapsed}}', + runningToast: 'Slicing {{name}} — {{elapsed}}', completedToast: 'Sliced {{name}}', failedToast: 'Slicing {{name}} failed: {{detail}}', tier: { diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 8cbed8632..9ed0e3d80 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -3214,6 +3214,8 @@ export default { selectPreset: '— Select a preset —', loadingPresets: 'Loading presets…', analyzingPlateFilaments: 'Analyzing plate filaments…', + analyzingPlateFilamentsHint: 'Running a preview slice to discover which AMS slots this plate uses. Cached after — re-opening is instant.', + notUsedByPlate: '— not used by this plate', printerMismatch: 'This 3MF was sliced for {{source}}, but you picked {{target}}. The slicer CLI cannot re-slice a 3MF for a different printer — open the source in Bambu Studio, change the printer, and re-export.', noPresetsForSlot: 'No presets available', presetsLoadFailed: 'Failed to load presets. Open Settings → Profiles to import them first.', @@ -3222,6 +3224,8 @@ export default { queued: 'Queued…', failed: 'Slicing failed. Check the slicer sidecar logs.', startedToast: 'Slicing {{name}} in the background…', + queuedToast: 'Queued: {{name}} — {{elapsed}}', + runningToast: 'Slicing {{name}} — {{elapsed}}', completedToast: 'Sliced {{name}}', failedToast: 'Slicing {{name}} failed: {{detail}}', tier: { diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index c44706bef..69c2d3204 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/frontend/src/i18n/locales/pt-BR.ts @@ -3189,6 +3189,8 @@ export default { selectPreset: '— Select a preset —', loadingPresets: 'Loading presets…', analyzingPlateFilaments: 'Analyzing plate filaments…', + analyzingPlateFilamentsHint: 'Running a preview slice to discover which AMS slots this plate uses. Cached after — re-opening is instant.', + notUsedByPlate: '— not used by this plate', printerMismatch: 'This 3MF was sliced for {{source}}, but you picked {{target}}. The slicer CLI cannot re-slice a 3MF for a different printer — open the source in Bambu Studio, change the printer, and re-export.', noPresetsForSlot: 'No presets available', presetsLoadFailed: 'Failed to load presets. Open Settings → Profiles to import them first.', @@ -3197,6 +3199,8 @@ export default { queued: 'Queued…', failed: 'Slicing failed. Check the slicer sidecar logs.', startedToast: 'Slicing {{name}} in the background…', + queuedToast: 'Queued: {{name}} — {{elapsed}}', + runningToast: 'Slicing {{name}} — {{elapsed}}', completedToast: 'Sliced {{name}}', failedToast: 'Slicing {{name}} failed: {{detail}}', tier: { diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index e2ee3be88..f9b150a9b 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -3241,6 +3241,8 @@ export default { selectPreset: '— Select a preset —', loadingPresets: 'Loading presets…', analyzingPlateFilaments: 'Analyzing plate filaments…', + analyzingPlateFilamentsHint: 'Running a preview slice to discover which AMS slots this plate uses. Cached after — re-opening is instant.', + notUsedByPlate: '— not used by this plate', printerMismatch: 'This 3MF was sliced for {{source}}, but you picked {{target}}. The slicer CLI cannot re-slice a 3MF for a different printer — open the source in Bambu Studio, change the printer, and re-export.', noPresetsForSlot: 'No presets available', presetsLoadFailed: 'Failed to load presets. Open Settings → Profiles to import them first.', @@ -3249,6 +3251,8 @@ export default { queued: 'Queued…', failed: 'Slicing failed. Check the slicer sidecar logs.', startedToast: 'Slicing {{name}} in the background…', + queuedToast: 'Queued: {{name}} — {{elapsed}}', + runningToast: 'Slicing {{name}} — {{elapsed}}', completedToast: 'Sliced {{name}}', failedToast: 'Slicing {{name}} failed: {{detail}}', tier: { diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts index b091c0fbb..977b7e1ed 100644 --- a/frontend/src/i18n/locales/zh-TW.ts +++ b/frontend/src/i18n/locales/zh-TW.ts @@ -3241,6 +3241,8 @@ export default { selectPreset: '— Select a preset —', loadingPresets: 'Loading presets…', analyzingPlateFilaments: 'Analyzing plate filaments…', + analyzingPlateFilamentsHint: 'Running a preview slice to discover which AMS slots this plate uses. Cached after — re-opening is instant.', + notUsedByPlate: '— not used by this plate', printerMismatch: 'This 3MF was sliced for {{source}}, but you picked {{target}}. The slicer CLI cannot re-slice a 3MF for a different printer — open the source in Bambu Studio, change the printer, and re-export.', noPresetsForSlot: 'No presets available', presetsLoadFailed: 'Failed to load presets. Open Settings → Profiles to import them first.', @@ -3249,6 +3251,8 @@ export default { queued: 'Queued…', failed: 'Slicing failed. Check the slicer sidecar logs.', startedToast: 'Slicing {{name}} in the background…', + queuedToast: 'Queued: {{name}} — {{elapsed}}', + runningToast: 'Slicing {{name}} — {{elapsed}}', completedToast: 'Sliced {{name}}', failedToast: 'Slicing {{name}} failed: {{detail}}', tier: { diff --git a/frontend/src/types/plates.ts b/frontend/src/types/plates.ts index d530d93a0..dd689a2af 100644 --- a/frontend/src/types/plates.ts +++ b/frontend/src/types/plates.ts @@ -4,6 +4,15 @@ export interface PlateFilament { color: string; used_grams: number; used_meters: number; + // True when this AMS slot is consumed by the picked plate. False + // means the slot is configured project-wide but the picked plate + // doesn't paint with it. Sliced 3MFs (.gcode.3mf) report only used + // filaments — the field is true for every entry. Unsliced project + // files report ALL project slots; SliceModal disables the unused + // rows so the user only interacts with the dropdowns that matter, + // while the backend still passes the complete list to the slicer + // CLI to prevent silent fallback to embedded defaults. + used_in_plate?: boolean; } export interface PlateMetadata { diff --git a/static/assets/index-BZTbdNTm.js b/static/assets/index-B6Qs-684.js similarity index 87% rename from static/assets/index-BZTbdNTm.js rename to static/assets/index-B6Qs-684.js index 457ae0aeb..1f017c7c6 100644 --- a/static/assets/index-BZTbdNTm.js +++ b/static/assets/index-B6Qs-684.js @@ -1,42 +1,42 @@ -function Jde(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 Z_=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function gc(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var SM={exports:{}},Ev={};var H8;function Zde(){if(H8)return Ev;H8=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 Ev.Fragment=e,Ev.jsx=n,Ev.jsxs=n,Ev}var q8;function eue(){return q8||(q8=1,SM.exports=Zde()),SM.exports}var a=eue(),_M={exports:{}},Wn={};var $8;function tue(){if($8)return Wn;$8=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,b={};function g(K,ae,me){this.props=K,this.context=ae,this.refs=b,this.updater=me||x}g.prototype.isReactComponent={},g.prototype.setState=function(K,ae){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,ae,"setState")},g.prototype.forceUpdate=function(K){this.updater.enqueueForceUpdate(this,K,"forceUpdate")};function S(){}S.prototype=g.prototype;function N(K,ae,me){this.props=K,this.context=ae,this.refs=b,this.updater=me||x}var P=N.prototype=new S;P.constructor=N,v(P,g.prototype),P.isPureReactComponent=!0;var C=Array.isArray;function A(){}var T={H:null,A:null,T:null,S:null},F=Object.prototype.hasOwnProperty;function k(K,ae,me){var xe=me.ref;return{$$typeof:t,type:K,key:ae,ref:xe!==void 0?xe:null,props:me}}function M(K,ae){return k(K.type,ae,K.props)}function U(K){return typeof K=="object"&&K!==null&&K.$$typeof===t}function z(K){var ae={"=":"=0",":":"=2"};return"$"+K.replace(/[=:]/g,function(me){return ae[me]})}var X=/\/+/g;function L(K,ae){return typeof K=="object"&&K!==null&&K.key!=null?z(""+K.key):ae.toString(36)}function ne(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(ae){K.status==="pending"&&(K.status="fulfilled",K.value=ae)},function(ae){K.status==="pending"&&(K.status="rejected",K.reason=ae)})),K.status){case"fulfilled":return K.value;case"rejected":throw K.reason}}throw K}function re(K,ae,me,xe,be){var H=typeof K;(H==="undefined"||H==="boolean")&&(K=null);var Q=!1;if(K===null)Q=!0;else switch(H){case"bigint":case"string":case"number":Q=!0;break;case"object":switch(K.$$typeof){case t:case e:Q=!0;break;case u:return Q=K._init,re(Q(K._payload),ae,me,xe,be)}}if(Q)return be=be(K),Q=xe===""?"."+L(K,0):xe,C(be)?(me="",Q!=null&&(me=Q.replace(X,"$&/")+"/"),re(be,ae,me,"",function(B){return B})):be!=null&&(U(be)&&(be=M(be,me+(be.key==null||K&&K.key===be.key?"":(""+be.key).replace(X,"$&/")+"/")+Q)),ae.push(be)),1;Q=0;var D=xe===""?".":xe+":";if(C(K))for(var E=0;E>>1,oe=re[se];if(0>>1;sei(me,ue))xei(be,me)?(re[se]=be,re[xe]=ue,se=xe):(re[se]=me,re[ae]=ue,se=ae);else if(xei(be,ue))re[se]=be,re[xe]=ue,se=xe;else break e}}return ee}function i(re,ee){var ue=re.sortIndex-ee.sortIndex;return ue!==0?ue:re.id-ee.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,b=!1,g=typeof setTimeout=="function"?setTimeout:null,S=typeof clearTimeout=="function"?clearTimeout:null,N=typeof setImmediate<"u"?setImmediate:null;function P(re){for(var ee=n(d);ee!==null;){if(ee.callback===null)r(d);else if(ee.startTime<=re)r(d),ee.sortIndex=ee.expirationTime,e(c,ee);else break;ee=n(d)}}function C(re){if(v=!1,P(re),!x)if(n(c)!==null)x=!0,A||(A=!0,z());else{var ee=n(d);ee!==null&&ne(C,ee.startTime-re)}}var A=!1,T=-1,F=5,k=-1;function M(){return b?!0:!(t.unstable_now()-kre&&M());){var se=m.callback;if(typeof se=="function"){m.callback=null,p=m.priorityLevel;var oe=se(m.expirationTime<=re);if(re=t.unstable_now(),typeof oe=="function"){m.callback=oe,P(re),ee=!0;break t}m===n(c)&&r(c),P(re)}else r(c);m=n(c)}if(m!==null)ee=!0;else{var K=n(d);K!==null&&ne(C,K.startTime-re),ee=!1}}break e}finally{m=null,p=ue,f=!1}ee=void 0}}finally{ee?z():A=!1}}}var z;if(typeof N=="function")z=function(){N(U)};else if(typeof MessageChannel<"u"){var X=new MessageChannel,L=X.port2;X.port1.onmessage=U,z=function(){L.postMessage(null)}}else z=function(){g(U,0)};function ne(re,ee){T=g(function(){re(t.unstable_now())},ee)}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,ne(C,ue-se))):(re.sortIndex=oe,e(c,re),x||f||(x=!0,A||(A=!0,z()))),re},t.unstable_shouldYield=M,t.unstable_wrapCallback=function(re){var ee=p;return function(){var ue=p;p=ee;try{return re.apply(this,arguments)}finally{p=ue}}}})(CM)),CM}var W8;function aue(){return W8||(W8=1,NM.exports=rue()),NM.exports}var PM={exports:{}},Es={};var K8;function iue(){if(K8)return Es;K8=1;var t=Ug();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(),PM.exports=iue(),PM.exports}var Y8;function sue(){if(Y8)return Dv;Y8=1;var t=aue(),e=Ug(),n=rQ();function r(h){var y="https://react.dev/errors/"+h;if(1oe||(h.current=se[oe],se[oe]=null,oe--)}function me(h,y){oe++,se[oe]=h.current,h.current=y}var xe=K(null),be=K(null),H=K(null),Q=K(null);function D(h,y){switch(me(H,y),me(be,h),me(xe,null),y.nodeType){case 9:case 11:h=(h=y.documentElement)&&(h=h.namespaceURI)?u8(h):0;break;default:if(h=y.tagName,y=y.namespaceURI)y=u8(y),h=m8(y,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}ae(xe),me(xe,h)}function E(){ae(xe),ae(be),ae(H)}function B(h){h.memoizedState!==null&&me(Q,h);var y=xe.current,_=m8(y,h.type);y!==_&&(me(be,h),me(xe,_))}function Y(h){be.current===h&&(ae(xe),ae(be)),Q.current===h&&(ae(Q),Tv._currentValue=ue)}var I,ce;function O(h){if(I===void 0)try{throw Error()}catch(_){var y=_.stack.trim().match(/\n( *(at )?)/);I=y&&y[1]||"",ce=-1<_.stack.indexOf(` +function Zde(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 Z_=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function gc(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var SM={exports:{}},Ev={};var H8;function eue(){if(H8)return Ev;H8=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 Ev.Fragment=e,Ev.jsx=n,Ev.jsxs=n,Ev}var q8;function tue(){return q8||(q8=1,SM.exports=eue()),SM.exports}var a=tue(),_M={exports:{}},Wn={};var $8;function nue(){if($8)return Wn;$8=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,b={};function g(K,ae,me){this.props=K,this.context=ae,this.refs=b,this.updater=me||x}g.prototype.isReactComponent={},g.prototype.setState=function(K,ae){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,ae,"setState")},g.prototype.forceUpdate=function(K){this.updater.enqueueForceUpdate(this,K,"forceUpdate")};function S(){}S.prototype=g.prototype;function N(K,ae,me){this.props=K,this.context=ae,this.refs=b,this.updater=me||x}var P=N.prototype=new S;P.constructor=N,v(P,g.prototype),P.isPureReactComponent=!0;var C=Array.isArray;function A(){}var T={H:null,A:null,T:null,S:null},F=Object.prototype.hasOwnProperty;function k(K,ae,me){var xe=me.ref;return{$$typeof:t,type:K,key:ae,ref:xe!==void 0?xe:null,props:me}}function M(K,ae){return k(K.type,ae,K.props)}function U(K){return typeof K=="object"&&K!==null&&K.$$typeof===t}function z(K){var ae={"=":"=0",":":"=2"};return"$"+K.replace(/[=:]/g,function(me){return ae[me]})}var X=/\/+/g;function L(K,ae){return typeof K=="object"&&K!==null&&K.key!=null?z(""+K.key):ae.toString(36)}function ne(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(ae){K.status==="pending"&&(K.status="fulfilled",K.value=ae)},function(ae){K.status==="pending"&&(K.status="rejected",K.reason=ae)})),K.status){case"fulfilled":return K.value;case"rejected":throw K.reason}}throw K}function re(K,ae,me,xe,be){var H=typeof K;(H==="undefined"||H==="boolean")&&(K=null);var Q=!1;if(K===null)Q=!0;else switch(H){case"bigint":case"string":case"number":Q=!0;break;case"object":switch(K.$$typeof){case t:case e:Q=!0;break;case u:return Q=K._init,re(Q(K._payload),ae,me,xe,be)}}if(Q)return be=be(K),Q=xe===""?"."+L(K,0):xe,C(be)?(me="",Q!=null&&(me=Q.replace(X,"$&/")+"/"),re(be,ae,me,"",function(B){return B})):be!=null&&(U(be)&&(be=M(be,me+(be.key==null||K&&K.key===be.key?"":(""+be.key).replace(X,"$&/")+"/")+Q)),ae.push(be)),1;Q=0;var D=xe===""?".":xe+":";if(C(K))for(var E=0;E>>1,oe=re[se];if(0>>1;sei(me,ue))xei(be,me)?(re[se]=be,re[xe]=ue,se=xe):(re[se]=me,re[ae]=ue,se=ae);else if(xei(be,ue))re[se]=be,re[xe]=ue,se=xe;else break e}}return ee}function i(re,ee){var ue=re.sortIndex-ee.sortIndex;return ue!==0?ue:re.id-ee.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,b=!1,g=typeof setTimeout=="function"?setTimeout:null,S=typeof clearTimeout=="function"?clearTimeout:null,N=typeof setImmediate<"u"?setImmediate:null;function P(re){for(var ee=n(d);ee!==null;){if(ee.callback===null)r(d);else if(ee.startTime<=re)r(d),ee.sortIndex=ee.expirationTime,e(c,ee);else break;ee=n(d)}}function C(re){if(v=!1,P(re),!x)if(n(c)!==null)x=!0,A||(A=!0,z());else{var ee=n(d);ee!==null&&ne(C,ee.startTime-re)}}var A=!1,T=-1,F=5,k=-1;function M(){return b?!0:!(t.unstable_now()-kre&&M());){var se=m.callback;if(typeof se=="function"){m.callback=null,p=m.priorityLevel;var oe=se(m.expirationTime<=re);if(re=t.unstable_now(),typeof oe=="function"){m.callback=oe,P(re),ee=!0;break t}m===n(c)&&r(c),P(re)}else r(c);m=n(c)}if(m!==null)ee=!0;else{var K=n(d);K!==null&&ne(C,K.startTime-re),ee=!1}}break e}finally{m=null,p=ue,f=!1}ee=void 0}}finally{ee?z():A=!1}}}var z;if(typeof N=="function")z=function(){N(U)};else if(typeof MessageChannel<"u"){var X=new MessageChannel,L=X.port2;X.port1.onmessage=U,z=function(){L.postMessage(null)}}else z=function(){g(U,0)};function ne(re,ee){T=g(function(){re(t.unstable_now())},ee)}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,ne(C,ue-se))):(re.sortIndex=oe,e(c,re),x||f||(x=!0,A||(A=!0,z()))),re},t.unstable_shouldYield=M,t.unstable_wrapCallback=function(re){var ee=p;return function(){var ue=p;p=ee;try{return re.apply(this,arguments)}finally{p=ue}}}})(CM)),CM}var W8;function iue(){return W8||(W8=1,NM.exports=aue()),NM.exports}var PM={exports:{}},Es={};var K8;function sue(){if(K8)return Es;K8=1;var t=Ug();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(),PM.exports=sue(),PM.exports}var Y8;function oue(){if(Y8)return Dv;Y8=1;var t=iue(),e=Ug(),n=aQ();function r(h){var y="https://react.dev/errors/"+h;if(1oe||(h.current=se[oe],se[oe]=null,oe--)}function me(h,y){oe++,se[oe]=h.current,h.current=y}var xe=K(null),be=K(null),H=K(null),Q=K(null);function D(h,y){switch(me(H,y),me(be,h),me(xe,null),y.nodeType){case 9:case 11:h=(h=y.documentElement)&&(h=h.namespaceURI)?u8(h):0;break;default:if(h=y.tagName,y=y.namespaceURI)y=u8(y),h=m8(y,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}ae(xe),me(xe,h)}function E(){ae(xe),ae(be),ae(H)}function B(h){h.memoizedState!==null&&me(Q,h);var y=xe.current,_=m8(y,h.type);y!==_&&(me(be,h),me(xe,_))}function Y(h){be.current===h&&(ae(xe),ae(be)),Q.current===h&&(ae(Q),Tv._currentValue=ue)}var I,ce;function O(h){if(I===void 0)try{throw Error()}catch(_){var y=_.stack.trim().match(/\n( *(at )?)/);I=y&&y[1]||"",ce=-1<_.stack.indexOf(` at`)?" ()":-1<_.stack.indexOf("@")?"@unknown:0:0":""}return` `+I+h+ce}var W=!1;function V(h,y){if(!h||W)return"";W=!0;var _=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var j={DetermineComponentFrameRoot:function(){try{if(y){var Ut=function(){throw Error()};if(Object.defineProperty(Ut.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(Ut,[])}catch(At){var St=At}Reflect.construct(h,[],Ut)}else{try{Ut.call()}catch(At){St=At}h.call(Ut.prototype)}}else{try{throw Error()}catch(At){St=At}(Ut=h())&&typeof Ut.catch=="function"&&Ut.catch(function(){})}}catch(At){if(At&&St&&typeof At.stack=="string")return[At.stack,St.stack]}return[null,null]}};j.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var G=Object.getOwnPropertyDescriptor(j.DetermineComponentFrameRoot,"name");G&&G.configurable&&Object.defineProperty(j.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var Z=j.DetermineComponentFrameRoot(),Ne=Z[0],qe=Z[1];if(Ne&&qe){var lt=Ne.split(` `),vt=qe.split(` `);for(G=j=0;jG||lt[j]!==vt[G]){var Rt=` `+lt[j].replace(" at new "," at ");return h.displayName&&Rt.includes("")&&(Rt=Rt.replace("",h.displayName)),Rt}while(1<=j&&0<=G);break}}}finally{W=!1,Error.prepareStackTrace=_}return(_=h?h.displayName||h.name:"")?O(_):""}function q(h,y){switch(h.tag){case 26:case 27:case 5:return O(h.type);case 16:return O("Lazy");case 13:return h.child!==y&&y!==null?O("Suspense Fallback"):O("Suspense");case 19:return O("SuspenseList");case 0:case 15:return V(h.type,!1);case 11:return V(h.type.render,!1);case 1:return V(h.type,!0);case 31:return O("Activity");default:return""}}function J(h){try{var y="",_=null;do y+=q(h,_),_=h,h=h.return;while(h);return y}catch(j){return` Error generating stack: `+j.message+` -`+j.stack}}var ie=Object.prototype.hasOwnProperty,ge=t.unstable_scheduleCallback,pe=t.unstable_cancelCallback,he=t.unstable_shouldYield,$=t.unstable_requestPaint,R=t.unstable_now,te=t.unstable_getCurrentPriorityLevel,fe=t.unstable_ImmediatePriority,ke=t.unstable_UserBlockingPriority,je=t.unstable_NormalPriority,Ae=t.unstable_LowPriority,ye=t.unstable_IdlePriority,Re=t.log,Be=t.unstable_setDisableYieldValue,Ce=null,ze=null;function Me(h){if(typeof Re=="function"&&Be(h),ze&&typeof ze.setStrictMode=="function")try{ze.setStrictMode(Ce,h)}catch{}}var Ie=Math.clz32?Math.clz32:ve,Ge=Math.log,et=Math.LN2;function ve(h){return h>>>=0,h===0?32:31-(Ge(h)/et|0)|0}var Xe=256,Pe=262144,Ve=4194304;function we(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 de(h,y,_){var j=h.pendingLanes;if(j===0)return 0;var G=0,Z=h.suspendedLanes,Ne=h.pingedLanes;h=h.warmLanes;var qe=j&134217727;return qe!==0?(j=qe&~Z,j!==0?G=we(j):(Ne&=qe,Ne!==0?G=we(Ne):_||(_=qe&~h,_!==0&&(G=we(_))))):(qe=j&~Z,qe!==0?G=we(qe):Ne!==0?G=we(Ne):_||(_=j&~h,_!==0&&(G=we(_)))),G===0?0:y!==0&&y!==G&&(y&Z)===0&&(Z=G&-G,_=y&-y,Z>=_||Z===32&&(_&4194048)!==0)?y:G}function Se(h,y){return(h.pendingLanes&~(h.suspendedLanes&~h.pingedLanes)&y)===0}function $e(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=Ve;return Ve<<=1,(Ve&62914560)===0&&(Ve=4194304),h}function He(h){for(var y=[],_=0;31>_;_++)y.push(h);return y}function rt(h,y){h.pendingLanes|=y,y!==268435456&&(h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0)}function ht(h,y,_,j,G,Z){var Ne=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,lt=h.expirationTimes,vt=h.hiddenUpdates;for(_=Ne&~_;0<_;){var Rt=31-Ie(_),Ut=1<"u")return null;try{return h.activeElement||h.body}catch{return h.body}}var Pr=/[\n"\\]/g;function In(h){return h.replace(Pr,function(y){return"\\"+y.charCodeAt(0).toString(16)+" "})}function Da(h,y,_,j,G,Z,Ne,qe){h.name="",Ne!=null&&typeof Ne!="function"&&typeof Ne!="symbol"&&typeof Ne!="boolean"?h.type=Ne:h.removeAttribute("type"),y!=null?Ne==="number"?(y===0&&h.value===""||h.value!=y)&&(h.value=""+cn(y)):h.value!==""+cn(y)&&(h.value=""+cn(y)):Ne!=="submit"&&Ne!=="reset"||h.removeAttribute("value"),y!=null?Br(h,Ne,cn(y)):_!=null?Br(h,Ne,cn(_)):j!=null&&h.removeAttribute("value"),G==null&&Z!=null&&(h.defaultChecked=!!Z),G!=null&&(h.checked=G&&typeof G!="function"&&typeof G!="symbol"),qe!=null&&typeof qe!="function"&&typeof qe!="symbol"&&typeof qe!="boolean"?h.name=""+cn(qe):h.removeAttribute("name")}function $n(h,y,_,j,G,Z,Ne,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)){Sn(h);return}_=_!=null?""+cn(_):"",y=y!=null?""+cn(y):_,qe||y===h.value||(h.value=y),h.defaultValue=y}j=j??G,j=typeof j!="function"&&typeof j!="symbol"&&!!j,h.checked=qe?h.checked:!!j,h.defaultChecked=!!j,Ne!=null&&typeof Ne!="function"&&typeof Ne!="symbol"&&typeof Ne!="boolean"&&(h.name=Ne),Sn(h)}function Br(h,y,_){y==="number"&&br(h.ownerDocument)===h||h.defaultValue===""+_||(h.defaultValue=""+_)}function Kr(h,y,_,j){if(h=h.options,y){y={};for(var G=0;G<_.length;G++)y["$"+_[G]]=!0;for(_=0;_"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),zi=!1;if($o)try{var wl={};Object.defineProperty(wl,"passive",{get:function(){zi=!0}}),window.addEventListener("test",wl,wl),window.removeEventListener("test",wl,wl)}catch{zi=!1}var wo=null,ha=null,vc=null;function Om(){if(vc)return vc;var h,y=ha,_=y.length,j,G="value"in wo?wo.value:wo.textContent,Z=G.length;for(h=0;h<_&&y[h]===G[h];h++);var Ne=_-h;for(j=1;j<=Ne&&y[_-j]===G[Z-j];j++);return vc=G.slice(h,1=it),ct=" ",Mn=!1;function xr(h,y){switch(h){case"keyup":return Fn.indexOf(y.keyCode)!==-1;case"keydown":return y.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function lr(h){return h=h.detail,typeof h=="object"&&"data"in h?h.data:null}var Ir=!1;function Fa(h,y){switch(h){case"compositionend":return lr(y);case"keypress":return y.which!==32?null:(Mn=!0,ct);case"textInput":return h=y.data,h===ct&&Mn?null:h;default:return null}}function un(h,y){if(Ir)return h==="compositionend"||!_n&&xr(h,y)?(h=Om(),vc=ha=wo=null,Ir=!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}_=Yp(_)}}function Jy(h,y){return h&&y?h===y?!0:h&&h.nodeType===3?!1:y&&y.nodeType===3?Jy(h,y.parentNode):"contains"in h?h.contains(y):h.compareDocumentPosition?!!(h.compareDocumentPosition(y)&16):!1:!1}function YS(h){h=h!=null&&h.ownerDocument!=null&&h.ownerDocument.defaultView!=null?h.ownerDocument.defaultView:window;for(var y=br(h.document);y instanceof h.HTMLIFrameElement;){try{var _=typeof y.contentWindow.location.href=="string"}catch{_=!1}if(_)h=y.contentWindow;else break;y=br(h.document)}return y}function MA(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 Mce=$o&&"documentMode"in document&&11>=document.documentMode,eb=null,EA=null,Zy=null,DA=!1;function D5(h,y,_){var j=_.window===_?_.document:_.nodeType===9?_:_.ownerDocument;DA||eb==null||eb!==br(j)||(j=eb,"selectionStart"in j&&MA(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}),Zy&&Vm(Zy,j)||(Zy=j,j=I_(EA,"onSelect"),0>=Ne,G-=Ne,ld=1<<32-Ie(y)+G|_<Qn?(vr=vn,vn=null):vr=vn.sibling;var Mr=St(gt,vn,yt[Qn],It);if(Mr===null){vn===null&&(vn=vr);break}h&&vn&&Mr.alternate===null&&y(gt,vn),pt=Z(Mr,pt,Qn),jr===null?An=Mr:jr.sibling=Mr,jr=Mr,vn=vr}if(Qn===yt.length)return _(gt,vn),Sr&&cu(gt,Qn),An;if(vn===null){for(;QnQn?(vr=vn,vn=null):vr=vn.sibling;var hh=St(gt,vn,Mr.value,It);if(hh===null){vn===null&&(vn=vr);break}h&&vn&&hh.alternate===null&&y(gt,vn),pt=Z(hh,pt,Qn),jr===null?An=hh:jr.sibling=hh,jr=hh,vn=vr}if(Mr.done)return _(gt,vn),Sr&&cu(gt,Qn),An;if(vn===null){for(;!Mr.done;Qn++,Mr=yt.next())Mr=Ut(gt,Mr.value,It),Mr!==null&&(pt=Z(Mr,pt,Qn),jr===null?An=Mr:jr.sibling=Mr,jr=Mr);return Sr&&cu(gt,Qn),An}for(vn=j(vn);!Mr.done;Qn++,Mr=yt.next())Mr=At(vn,gt,Qn,Mr.value,It),Mr!==null&&(h&&Mr.alternate!==null&&vn.delete(Mr.key===null?Qn:Mr.key),pt=Z(Mr,pt,Qn),jr===null?An=Mr:jr.sibling=Mr,jr=Mr);return h&&vn.forEach(function(Qde){return y(gt,Qde)}),Sr&&cu(gt,Qn),An}function Qr(gt,pt,yt,It){if(typeof yt=="object"&&yt!==null&&yt.type===v&&yt.key===null&&(yt=yt.props.children),typeof yt=="object"&&yt!==null){switch(yt.$$typeof){case f:e:{for(var An=yt.key;pt!==null;){if(pt.key===An){if(An=yt.type,An===v){if(pt.tag===7){_(gt,pt.sibling),It=G(pt,yt.props.children),It.return=gt,gt=It;break e}}else if(pt.elementType===An||typeof An=="object"&&An!==null&&An.$$typeof===F&&sf(An)===pt.type){_(gt,pt.sibling),It=G(pt,yt.props),iv(It,yt),It.return=gt,gt=It;break e}_(gt,pt);break}else y(gt,pt);pt=pt.sibling}yt.type===v?(It=ef(yt.props.children,gt.mode,It,yt.key),It.return=gt,gt=It):(It=t_(yt.type,yt.key,yt.props,null,gt.mode,It),iv(It,yt),It.return=gt,gt=It)}return Ne(gt);case x:e:{for(An=yt.key;pt!==null;){if(pt.key===An)if(pt.tag===4&&pt.stateNode.containerInfo===yt.containerInfo&&pt.stateNode.implementation===yt.implementation){_(gt,pt.sibling),It=G(pt,yt.children||[]),It.return=gt,gt=It;break e}else{_(gt,pt);break}else y(gt,pt);pt=pt.sibling}It=UA(yt,gt.mode,It),It.return=gt,gt=It}return Ne(gt);case F:return yt=sf(yt),Qr(gt,pt,yt,It)}if(ne(yt))return gn(gt,pt,yt,It);if(z(yt)){if(An=z(yt),typeof An!="function")throw Error(r(150));return yt=An.call(yt),Ln(gt,pt,yt,It)}if(typeof yt.then=="function")return Qr(gt,pt,l_(yt),It);if(yt.$$typeof===N)return Qr(gt,pt,a_(gt,yt),It);c_(gt,yt)}return typeof yt=="string"&&yt!==""||typeof yt=="number"||typeof yt=="bigint"?(yt=""+yt,pt!==null&&pt.tag===6?(_(gt,pt.sibling),It=G(pt,yt),It.return=gt,gt=It):(_(gt,pt),It=zA(yt,gt.mode,It),It.return=gt,gt=It),Ne(gt)):_(gt,pt)}return function(gt,pt,yt,It){try{av=0;var An=Qr(gt,pt,yt,It);return ub=null,An}catch(vn){if(vn===db||vn===s_)throw vn;var jr=Ko(29,vn,null,gt.mode);return jr.lanes=It,jr.return=gt,jr}}}var lf=nU(!0),rU=nU(!1),Ym=!1;function JA(h){h.updateQueue={baseState:h.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function ZA(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 Qm(h){return{lane:h,tag:0,payload:null,callback:null,next:null}}function Jm(h,y,_){var j=h.updateQueue;if(j===null)return null;if(j=j.shared,(Lr&2)!==0){var G=j.pending;return G===null?y.next=y:(y.next=G.next,G.next=y),j.pending=y,y=e_(h),U5(h,null,_),y}return ZS(h,j,y,_),e_(h)}function sv(h,y,_){if(y=y.updateQueue,y!==null&&(y=y.shared,(_&4194048)!==0)){var j=y.lanes;j&=h.pendingLanes,_|=j,y.lanes=_,qt(h,_)}}function ej(h,y){var _=h.updateQueue,j=h.alternate;if(j!==null&&(j=j.updateQueue,_===j)){var G=null,Z=null;if(_=_.firstBaseUpdate,_!==null){do{var Ne={lane:_.lane,tag:_.tag,payload:_.payload,callback:null,next:null};Z===null?G=Z=Ne:Z=Z.next=Ne,_=_.next}while(_!==null);Z===null?G=Z=y:Z=Z.next=y}else G=Z=y;_={baseState:j.baseState,firstBaseUpdate:G,lastBaseUpdate:Z,shared:j.shared,callbacks:j.callbacks},h.updateQueue=_;return}h=_.lastBaseUpdate,h===null?_.firstBaseUpdate=y:h.next=y,_.lastBaseUpdate=y}var tj=!1;function ov(){if(tj){var h=cb;if(h!==null)throw h}}function lv(h,y,_,j){tj=!1;var G=h.updateQueue;Ym=!1;var Z=G.firstBaseUpdate,Ne=G.lastBaseUpdate,qe=G.shared.pending;if(qe!==null){G.shared.pending=null;var lt=qe,vt=lt.next;lt.next=null,Ne===null?Z=vt:Ne.next=vt,Ne=lt;var Rt=h.alternate;Rt!==null&&(Rt=Rt.updateQueue,qe=Rt.lastBaseUpdate,qe!==Ne&&(qe===null?Rt.firstBaseUpdate=vt:qe.next=vt,Rt.lastBaseUpdate=lt))}if(Z!==null){var Ut=G.baseState;Ne=0,Rt=vt=lt=null,qe=Z;do{var St=qe.lane&-536870913,At=St!==qe.lane;if(At?(yr&St)===St:(j&St)===St){St!==0&&St===lb&&(tj=!0),Rt!==null&&(Rt=Rt.next={lane:0,tag:qe.tag,payload:qe.payload,callback:null,next:null});e:{var gn=h,Ln=qe;St=y;var Qr=_;switch(Ln.tag){case 1:if(gn=Ln.payload,typeof gn=="function"){Ut=gn.call(Qr,Ut,St);break e}Ut=gn;break e;case 3:gn.flags=gn.flags&-65537|128;case 0:if(gn=Ln.payload,St=typeof gn=="function"?gn.call(Qr,Ut,St):gn,St==null)break e;Ut=m({},Ut,St);break e;case 2:Ym=!0}}St=qe.callback,St!==null&&(h.flags|=64,At&&(h.flags|=8192),At=G.callbacks,At===null?G.callbacks=[St]:At.push(St))}else At={lane:St,tag:qe.tag,payload:qe.payload,callback:qe.callback,next:null},Rt===null?(vt=Rt=At,lt=Ut):Rt=Rt.next=At,Ne|=St;if(qe=qe.next,qe===null){if(qe=G.shared.pending,qe===null)break;At=qe,qe=At.next,At.next=null,G.lastBaseUpdate=At,G.shared.pending=null}}while(!0);Rt===null&&(lt=Ut),G.baseState=lt,G.firstBaseUpdate=vt,G.lastBaseUpdate=Rt,Z===null&&(G.shared.lanes=0),rh|=Ne,h.lanes=Ne,h.memoizedState=Ut}}function aU(h,y){if(typeof h!="function")throw Error(r(191,h));h.call(y)}function iU(h,y){var _=h.callbacks;if(_!==null)for(h.callbacks=null,h=0;h<_.length;h++)aU(_[h],y)}var mb=K(null),d_=K(0);function sU(h,y){h=vu,me(d_,h),me(mb,y),vu=h|y.baseLanes}function nj(){me(d_,vu),me(mb,mb.current)}function rj(){vu=d_.current,ae(mb),ae(d_)}var Xo=K(null),Dl=null;function Zm(h){var y=h.alternate;me(ai,ai.current&1),me(Xo,h),Dl===null&&(y===null||mb.current!==null||y.memoizedState!==null)&&(Dl=h)}function aj(h){me(ai,ai.current),me(Xo,h),Dl===null&&(Dl=h)}function oU(h){h.tag===22?(me(ai,ai.current),me(Xo,h),Dl===null&&(Dl=h)):eh()}function eh(){me(ai,ai.current),me(Xo,Xo.current)}function Yo(h){ae(Xo),Dl===h&&(Dl=null),ae(ai)}var ai=K(0);function u_(h){for(var y=h;y!==null;){if(y.tag===13){var _=y.memoizedState;if(_!==null&&(_=_.dehydrated,_===null||dM(_)||uM(_)))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 mu=0,Xn=null,Xr=null,mi=null,m_=!1,hb=!1,cf=!1,h_=0,cv=0,pb=null,Bce=0;function $a(){throw Error(r(321))}function ij(h,y){if(y===null)return!1;for(var _=0;_Z?Z:8;var Ne=re.T,qe={};re.T=qe,vj(h,!1,y,_);try{var lt=G(),vt=re.S;if(vt!==null&&vt(qe,lt),lt!==null&&typeof lt=="object"&&typeof lt.then=="function"){var Rt=Uce(lt,j);uv(h,y,Rt,Zo(h))}else uv(h,y,j,Zo(h))}catch(Ut){uv(h,y,{then:function(){},status:"rejected",reason:Ut},Zo())}finally{ee.p=Z,Ne!==null&&qe.types!==null&&(Ne.types=qe.types),re.T=Ne}}function Gce(){}function xj(h,y,_,j){if(h.tag!==5)throw Error(r(476));var G=OU(h).queue;LU(h,G,y,ue,_===null?Gce:function(){return IU(h),_(j)})}function OU(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:hu,lastRenderedState:ue},next:null};var _={};return y.next={memoizedState:_,baseState:_,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:hu,lastRenderedState:_},next:null},h.memoizedState=y,h=h.alternate,h!==null&&(h.memoizedState=y),y}function IU(h){var y=OU(h);y.next===null&&(y=h.alternate.memoizedState),uv(h,y.next.queue,{},Zo())}function yj(){return ds(Tv)}function zU(){return ii().memoizedState}function UU(){return ii().memoizedState}function Wce(h){for(var y=h.return;y!==null;){switch(y.tag){case 24:case 3:var _=Zo();h=Qm(_);var j=Jm(y,h,_);j!==null&&(To(j,y,_),sv(j,y,_)),y={cache:KA()},h.payload=y;return}y=y.return}}function Kce(h,y,_){var j=Zo();_={lane:j,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null},y_(h)?HU(y,_):(_=OA(h,y,_,j),_!==null&&(To(_,h,j),qU(_,y,j)))}function BU(h,y,_){var j=Zo();uv(h,y,_,j)}function uv(h,y,_,j){var G={lane:j,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null};if(y_(h))HU(y,G);else{var Z=h.alternate;if(h.lanes===0&&(Z===null||Z.lanes===0)&&(Z=y.lastRenderedReducer,Z!==null))try{var Ne=y.lastRenderedState,qe=Z(Ne,_);if(G.hasEagerState=!0,G.eagerState=qe,_o(qe,Ne))return ZS(h,y,G,0),oa===null&&JS(),!1}catch{}if(_=OA(h,y,G,j),_!==null)return To(_,h,j),qU(_,y,j),!0}return!1}function vj(h,y,_,j){if(j={lane:2,revertLane:Jj(),gesture:null,action:j,hasEagerState:!1,eagerState:null,next:null},y_(h)){if(y)throw Error(r(479))}else y=OA(h,_,j,2),y!==null&&To(y,h,2)}function y_(h){var y=h.alternate;return h===Xn||y!==null&&y===Xn}function HU(h,y){hb=m_=!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=_,qt(h,_)}}var mv={readContext:ds,use:f_,useCallback:$a,useContext:$a,useEffect:$a,useImperativeHandle:$a,useLayoutEffect:$a,useInsertionEffect:$a,useMemo:$a,useReducer:$a,useRef:$a,useState:$a,useDebugValue:$a,useDeferredValue:$a,useTransition:$a,useSyncExternalStore:$a,useId:$a,useHostTransitionStatus:$a,useFormState:$a,useActionState:$a,useOptimistic:$a,useMemoCache:$a,useCacheRefresh:$a};mv.useEffectEvent=$a;var $U={readContext:ds,use:f_,useCallback:function(h,y){return Zs().memoizedState=[h,y===void 0?null:y],h},useContext:ds,useEffect:PU,useImperativeHandle:function(h,y,_){_=_!=null?_.concat([h]):null,b_(4194308,4,MU.bind(null,y,h),_)},useLayoutEffect:function(h,y){return b_(4194308,4,h,y)},useInsertionEffect:function(h,y){b_(4,2,h,y)},useMemo:function(h,y){var _=Zs();y=y===void 0?null:y;var j=h();if(cf){Me(!0);try{h()}finally{Me(!1)}}return _.memoizedState=[j,y],j},useReducer:function(h,y,_){var j=Zs();if(_!==void 0){var G=_(y);if(cf){Me(!0);try{_(y)}finally{Me(!1)}}}else G=y;return j.memoizedState=j.baseState=G,h={pending:null,lanes:0,dispatch:null,lastRenderedReducer:h,lastRenderedState:G},j.queue=h,h=h.dispatch=Kce.bind(null,Xn,h),[j.memoizedState,h]},useRef:function(h){var y=Zs();return h={current:h},y.memoizedState=h},useState:function(h){h=hj(h);var y=h.queue,_=BU.bind(null,Xn,y);return y.dispatch=_,[h.memoizedState,_]},useDebugValue:gj,useDeferredValue:function(h,y){var _=Zs();return bj(_,h,y)},useTransition:function(){var h=hj(!1);return h=LU.bind(null,Xn,h.queue,!0,!1),Zs().memoizedState=h,[!1,h]},useSyncExternalStore:function(h,y,_){var j=Xn,G=Zs();if(Sr){if(_===void 0)throw Error(r(407));_=_()}else{if(_=y(),oa===null)throw Error(r(349));(yr&127)!==0||uU(j,y,_)}G.memoizedState=_;var Z={value:_,getSnapshot:y};return G.queue=Z,PU(hU.bind(null,j,Z,h),[h]),j.flags|=2048,fb(9,{destroy:void 0},mU.bind(null,j,Z,_,y),null),_},useId:function(){var h=Zs(),y=oa.identifierPrefix;if(Sr){var _=cd,j=ld;_=(j&~(1<<32-Ie(j)-1)).toString(32)+_,y="_"+y+"R_"+_,_=h_++,0<_&&(y+="H"+_.toString(32)),y+="_"}else _=Bce++,y="_"+y+"r_"+_.toString(32)+"_";return h.memoizedState=y},useHostTransitionStatus:yj,useFormState:SU,useActionState:SU,useOptimistic:function(h){var y=Zs();y.memoizedState=y.baseState=h;var _={pending:null,lanes:0,dispatch:null,lastRenderedReducer:null,lastRenderedState:null};return y.queue=_,y=vj.bind(null,Xn,!0,_),_.dispatch=y,[h,y]},useMemoCache:dj,useCacheRefresh:function(){return Zs().memoizedState=Wce.bind(null,Xn)},useEffectEvent:function(h){var y=Zs(),_={impl:h};return y.memoizedState=_,function(){if((Lr&2)!==0)throw Error(r(440));return _.impl.apply(void 0,arguments)}}},wj={readContext:ds,use:f_,useCallback:DU,useContext:ds,useEffect:fj,useImperativeHandle:EU,useInsertionEffect:AU,useLayoutEffect:jU,useMemo:FU,useReducer:g_,useRef:CU,useState:function(){return g_(hu)},useDebugValue:gj,useDeferredValue:function(h,y){var _=ii();return RU(_,Xr.memoizedState,h,y)},useTransition:function(){var h=g_(hu)[0],y=ii().memoizedState;return[typeof h=="boolean"?h:dv(h),y]},useSyncExternalStore:dU,useId:zU,useHostTransitionStatus:yj,useFormState:_U,useActionState:_U,useOptimistic:function(h,y){var _=ii();return gU(_,Xr,h,y)},useMemoCache:dj,useCacheRefresh:UU};wj.useEffectEvent=TU;var VU={readContext:ds,use:f_,useCallback:DU,useContext:ds,useEffect:fj,useImperativeHandle:EU,useInsertionEffect:AU,useLayoutEffect:jU,useMemo:FU,useReducer:mj,useRef:CU,useState:function(){return mj(hu)},useDebugValue:gj,useDeferredValue:function(h,y){var _=ii();return Xr===null?bj(_,h,y):RU(_,Xr.memoizedState,h,y)},useTransition:function(){var h=mj(hu)[0],y=ii().memoizedState;return[typeof h=="boolean"?h:dv(h),y]},useSyncExternalStore:dU,useId:zU,useHostTransitionStatus:yj,useFormState:NU,useActionState:NU,useOptimistic:function(h,y){var _=ii();return Xr!==null?gU(_,Xr,h,y):(_.baseState=h,[h,_.queue.dispatch])},useMemoCache:dj,useCacheRefresh:UU};VU.useEffectEvent=TU;function Sj(h,y,_,j){y=h.memoizedState,_=_(j,y),_=_==null?y:m({},y,_),h.memoizedState=_,h.lanes===0&&(h.updateQueue.baseState=_)}var _j={enqueueSetState:function(h,y,_){h=h._reactInternals;var j=Zo(),G=Qm(j);G.payload=y,_!=null&&(G.callback=_),y=Jm(h,G,j),y!==null&&(To(y,h,j),sv(y,h,j))},enqueueReplaceState:function(h,y,_){h=h._reactInternals;var j=Zo(),G=Qm(j);G.tag=1,G.payload=y,_!=null&&(G.callback=_),y=Jm(h,G,j),y!==null&&(To(y,h,j),sv(y,h,j))},enqueueForceUpdate:function(h,y){h=h._reactInternals;var _=Zo(),j=Qm(_);j.tag=2,y!=null&&(j.callback=y),y=Jm(h,j,_),y!==null&&(To(y,h,_),sv(y,h,_))}};function GU(h,y,_,j,G,Z,Ne){return h=h.stateNode,typeof h.shouldComponentUpdate=="function"?h.shouldComponentUpdate(j,Z,Ne):y.prototype&&y.prototype.isPureReactComponent?!Vm(_,j)||!Vm(G,Z):!0}function WU(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&&_j.enqueueReplaceState(y,y.state,null)}function df(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 G in h)_[G]===void 0&&(_[G]=h[G])}return _}function KU(h){QS(h)}function XU(h){console.error(h)}function YU(h){QS(h)}function v_(h,y){try{var _=h.onUncaughtError;_(y.value,{componentStack:y.stack})}catch(j){setTimeout(function(){throw j})}}function QU(h,y,_){try{var j=h.onCaughtError;j(_.value,{componentStack:_.stack,errorBoundary:y.tag===1?y.stateNode:null})}catch(G){setTimeout(function(){throw G})}}function kj(h,y,_){return _=Qm(_),_.tag=3,_.payload={element:null},_.callback=function(){v_(h,y)},_}function JU(h){return h=Qm(h),h.tag=3,h}function ZU(h,y,_,j){var G=_.type.getDerivedStateFromError;if(typeof G=="function"){var Z=j.value;h.payload=function(){return G(Z)},h.callback=function(){QU(y,_,j)}}var Ne=_.stateNode;Ne!==null&&typeof Ne.componentDidCatch=="function"&&(h.callback=function(){QU(y,_,j),typeof G!="function"&&(ah===null?ah=new Set([this]):ah.add(this));var qe=j.stack;this.componentDidCatch(j.value,{componentStack:qe!==null?qe:""})})}function Xce(h,y,_,j,G){if(_.flags|=32768,j!==null&&typeof j=="object"&&typeof j.then=="function"){if(y=_.alternate,y!==null&&ob(y,_,G,!0),_=Xo.current,_!==null){switch(_.tag){case 31:case 13:return Dl===null?E_():_.alternate===null&&Va===0&&(Va=3),_.flags&=-257,_.flags|=65536,_.lanes=G,j===o_?_.flags|=16384:(y=_.updateQueue,y===null?_.updateQueue=new Set([j]):y.add(j),Xj(h,j,G)),!1;case 22:return _.flags|=65536,j===o_?_.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)),Xj(h,j,G)),!1}throw Error(r(435,_.tag))}return Xj(h,j,G),E_(),!1}if(Sr)return y=Xo.current,y!==null?((y.flags&65536)===0&&(y.flags|=256),y.flags|=65536,y.lanes=G,j!==qA&&(h=Error(r(422),{cause:j}),tv(Al(h,_)))):(j!==qA&&(y=Error(r(423),{cause:j}),tv(Al(y,_))),h=h.current.alternate,h.flags|=65536,G&=-G,h.lanes|=G,j=Al(j,_),G=kj(h.stateNode,j,G),ej(h,G),Va!==4&&(Va=2)),!1;var Z=Error(r(520),{cause:j});if(Z=Al(Z,_),vv===null?vv=[Z]:vv.push(Z),Va!==4&&(Va=2),y===null)return!0;j=Al(j,_),_=y;do{switch(_.tag){case 3:return _.flags|=65536,h=G&-G,_.lanes|=h,h=kj(_.stateNode,j,h),ej(_,h),!1;case 1:if(y=_.type,Z=_.stateNode,(_.flags&128)===0&&(typeof y.getDerivedStateFromError=="function"||Z!==null&&typeof Z.componentDidCatch=="function"&&(ah===null||!ah.has(Z))))return _.flags|=65536,G&=-G,_.lanes|=G,G=JU(G),ZU(G,h,_,j),ej(_,G),!1}_=_.return}while(_!==null);return!1}var Nj=Error(r(461)),hi=!1;function us(h,y,_,j){y.child=h===null?rU(y,null,_,j):lf(y,h.child,_,j)}function eB(h,y,_,j,G){_=_.render;var Z=y.ref;if("ref"in j){var Ne={};for(var qe in j)qe!=="ref"&&(Ne[qe]=j[qe])}else Ne=j;return rf(y),j=sj(h,y,_,Ne,Z,G),qe=oj(),h!==null&&!hi?(lj(h,y,G),pu(h,y,G)):(Sr&&qe&&BA(y),y.flags|=1,us(h,y,j,G),y.child)}function tB(h,y,_,j,G){if(h===null){var Z=_.type;return typeof Z=="function"&&!IA(Z)&&Z.defaultProps===void 0&&_.compare===null?(y.tag=15,y.type=Z,nB(h,y,Z,j,G)):(h=t_(_.type,null,j,y,y.mode,G),h.ref=y.ref,h.return=y,y.child=h)}if(Z=h.child,!Dj(h,G)){var Ne=Z.memoizedProps;if(_=_.compare,_=_!==null?_:Vm,_(Ne,j)&&h.ref===y.ref)return pu(h,y,G)}return y.flags|=1,h=lu(Z,j),h.ref=y.ref,h.return=y,y.child=h}function nB(h,y,_,j,G){if(h!==null){var Z=h.memoizedProps;if(Vm(Z,j)&&h.ref===y.ref)if(hi=!1,y.pendingProps=j=Z,Dj(h,G))(h.flags&131072)!==0&&(hi=!0);else return y.lanes=h.lanes,pu(h,y,G)}return Cj(h,y,_,j,G)}function rB(h,y,_,j){var G=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,G=0;j!==null;)G=G|j.lanes|j.childLanes,j=j.sibling;j=G&~Z}else j=0,y.child=null;return aB(h,y,Z,_,j)}if((_&536870912)!==0)y.memoizedState={baseLanes:0,cachePool:null},h!==null&&i_(y,Z!==null?Z.cachePool:null),Z!==null?sU(y,Z):nj(),oU(y);else return j=y.lanes=536870912,aB(h,y,Z!==null?Z.baseLanes|_:_,_,j)}else Z!==null?(i_(y,Z.cachePool),sU(y,Z),eh(),y.memoizedState=null):(h!==null&&i_(y,null),nj(),eh());return us(h,y,G,_),y.child}function hv(h,y){return h!==null&&h.tag===22||y.stateNode!==null||(y.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),y.sibling}function aB(h,y,_,j,G){var Z=YA();return Z=Z===null?null:{parent:ui._currentValue,pool:Z},y.memoizedState={baseLanes:_,cachePool:Z},h!==null&&i_(y,null),nj(),oU(y),h!==null&&ob(h,y,j,!0),y.childLanes=G,null}function w_(h,y){return y=__({mode:y.mode,children:y.children},h.mode),y.ref=h.ref,h.child=y,y.return=h,y}function iB(h,y,_){return lf(y,h.child,null,_),h=w_(y,y.pendingProps),h.flags|=2,Yo(y),y.memoizedState=null,h}function Yce(h,y,_){var j=y.pendingProps,G=(y.flags&128)!==0;if(y.flags&=-129,h===null){if(Sr){if(j.mode==="hidden")return h=w_(y,j),y.lanes=536870912,hv(null,h);if(aj(y),(h=xa)?(h=b8(h,El),h=h!==null&&h.data==="&"?h:null,h!==null&&(y.memoizedState={dehydrated:h,treeContext:Gm!==null?{id:ld,overflow:cd}:null,retryLane:536870912,hydrationErrors:null},_=H5(h),_.return=y,y.child=_,cs=y,xa=null)):h=null,h===null)throw Km(y);return y.lanes=536870912,null}return w_(y,j)}var Z=h.memoizedState;if(Z!==null){var Ne=Z.dehydrated;if(aj(y),G)if(y.flags&256)y.flags&=-257,y=iB(h,y,_);else if(y.memoizedState!==null)y.child=h.child,y.flags|=128,y=null;else throw Error(r(558));else if(hi||ob(h,y,_,!1),G=(_&h.childLanes)!==0,hi||G){if(j=oa,j!==null&&(Ne=Vt(j,_),Ne!==0&&Ne!==Z.retryLane))throw Z.retryLane=Ne,Zp(h,Ne),To(j,h,Ne),Nj;E_(),y=iB(h,y,_)}else h=Z.treeContext,xa=Fl(Ne.nextSibling),cs=y,Sr=!0,Wm=null,El=!1,h!==null&&V5(y,h),y=w_(y,j),y.flags|=4096;return y}return h=lu(h.child,{mode:j.mode,children:j.children}),h.ref=y.ref,y.child=h,h.return=y,h}function S_(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 Cj(h,y,_,j,G){return rf(y),_=sj(h,y,_,j,void 0,G),j=oj(),h!==null&&!hi?(lj(h,y,G),pu(h,y,G)):(Sr&&j&&BA(y),y.flags|=1,us(h,y,_,G),y.child)}function sB(h,y,_,j,G,Z){return rf(y),y.updateQueue=null,_=cU(y,j,_,G),lU(h),j=oj(),h!==null&&!hi?(lj(h,y,Z),pu(h,y,Z)):(Sr&&j&&BA(y),y.flags|=1,us(h,y,_,Z),y.child)}function oB(h,y,_,j,G){if(rf(y),y.stateNode===null){var Z=rb,Ne=_.contextType;typeof Ne=="object"&&Ne!==null&&(Z=ds(Ne)),Z=new _(j,Z),y.memoizedState=Z.state!==null&&Z.state!==void 0?Z.state:null,Z.updater=_j,y.stateNode=Z,Z._reactInternals=y,Z=y.stateNode,Z.props=j,Z.state=y.memoizedState,Z.refs={},JA(y),Ne=_.contextType,Z.context=typeof Ne=="object"&&Ne!==null?ds(Ne):rb,Z.state=y.memoizedState,Ne=_.getDerivedStateFromProps,typeof Ne=="function"&&(Sj(y,_,Ne,j),Z.state=y.memoizedState),typeof _.getDerivedStateFromProps=="function"||typeof Z.getSnapshotBeforeUpdate=="function"||typeof Z.UNSAFE_componentWillMount!="function"&&typeof Z.componentWillMount!="function"||(Ne=Z.state,typeof Z.componentWillMount=="function"&&Z.componentWillMount(),typeof Z.UNSAFE_componentWillMount=="function"&&Z.UNSAFE_componentWillMount(),Ne!==Z.state&&_j.enqueueReplaceState(Z,Z.state,null),lv(y,j,Z,G),ov(),Z.state=y.memoizedState),typeof Z.componentDidMount=="function"&&(y.flags|=4194308),j=!0}else if(h===null){Z=y.stateNode;var qe=y.memoizedProps,lt=df(_,qe);Z.props=lt;var vt=Z.context,Rt=_.contextType;Ne=rb,typeof Rt=="object"&&Rt!==null&&(Ne=ds(Rt));var Ut=_.getDerivedStateFromProps;Rt=typeof Ut=="function"||typeof Z.getSnapshotBeforeUpdate=="function",qe=y.pendingProps!==qe,Rt||typeof Z.UNSAFE_componentWillReceiveProps!="function"&&typeof Z.componentWillReceiveProps!="function"||(qe||vt!==Ne)&&WU(y,Z,j,Ne),Ym=!1;var St=y.memoizedState;Z.state=St,lv(y,j,Z,G),ov(),vt=y.memoizedState,qe||St!==vt||Ym?(typeof Ut=="function"&&(Sj(y,_,Ut,j),vt=y.memoizedState),(lt=Ym||GU(y,_,lt,j,St,vt,Ne))?(Rt||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=vt),Z.props=j,Z.state=vt,Z.context=Ne,j=lt):(typeof Z.componentDidMount=="function"&&(y.flags|=4194308),j=!1)}else{Z=y.stateNode,ZA(h,y),Ne=y.memoizedProps,Rt=df(_,Ne),Z.props=Rt,Ut=y.pendingProps,St=Z.context,vt=_.contextType,lt=rb,typeof vt=="object"&&vt!==null&&(lt=ds(vt)),qe=_.getDerivedStateFromProps,(vt=typeof qe=="function"||typeof Z.getSnapshotBeforeUpdate=="function")||typeof Z.UNSAFE_componentWillReceiveProps!="function"&&typeof Z.componentWillReceiveProps!="function"||(Ne!==Ut||St!==lt)&&WU(y,Z,j,lt),Ym=!1,St=y.memoizedState,Z.state=St,lv(y,j,Z,G),ov();var At=y.memoizedState;Ne!==Ut||St!==At||Ym||h!==null&&h.dependencies!==null&&r_(h.dependencies)?(typeof qe=="function"&&(Sj(y,_,qe,j),At=y.memoizedState),(Rt=Ym||GU(y,_,Rt,j,St,At,lt)||h!==null&&h.dependencies!==null&&r_(h.dependencies))?(vt||typeof Z.UNSAFE_componentWillUpdate!="function"&&typeof Z.componentWillUpdate!="function"||(typeof Z.componentWillUpdate=="function"&&Z.componentWillUpdate(j,At,lt),typeof Z.UNSAFE_componentWillUpdate=="function"&&Z.UNSAFE_componentWillUpdate(j,At,lt)),typeof Z.componentDidUpdate=="function"&&(y.flags|=4),typeof Z.getSnapshotBeforeUpdate=="function"&&(y.flags|=1024)):(typeof Z.componentDidUpdate!="function"||Ne===h.memoizedProps&&St===h.memoizedState||(y.flags|=4),typeof Z.getSnapshotBeforeUpdate!="function"||Ne===h.memoizedProps&&St===h.memoizedState||(y.flags|=1024),y.memoizedProps=j,y.memoizedState=At),Z.props=j,Z.state=At,Z.context=lt,j=Rt):(typeof Z.componentDidUpdate!="function"||Ne===h.memoizedProps&&St===h.memoizedState||(y.flags|=4),typeof Z.getSnapshotBeforeUpdate!="function"||Ne===h.memoizedProps&&St===h.memoizedState||(y.flags|=1024),j=!1)}return Z=j,S_(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=lf(y,h.child,null,G),y.child=lf(y,null,_,G)):us(h,y,_,G),y.memoizedState=Z.state,h=y.child):h=pu(h,y,G),h}function lB(h,y,_,j){return tf(),y.flags|=256,us(h,y,_,j),y.child}var Pj={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function Tj(h){return{baseLanes:h,cachePool:Q5()}}function Aj(h,y,_){return h=h!==null?h.childLanes&~_:0,y&&(h|=Jo),h}function cB(h,y,_){var j=y.pendingProps,G=!1,Z=(y.flags&128)!==0,Ne;if((Ne=Z)||(Ne=h!==null&&h.memoizedState===null?!1:(ai.current&2)!==0),Ne&&(G=!0,y.flags&=-129),Ne=(y.flags&32)!==0,y.flags&=-33,h===null){if(Sr){if(G?Zm(y):eh(),(h=xa)?(h=b8(h,El),h=h!==null&&h.data!=="&"?h:null,h!==null&&(y.memoizedState={dehydrated:h,treeContext:Gm!==null?{id:ld,overflow:cd}:null,retryLane:536870912,hydrationErrors:null},_=H5(h),_.return=y,y.child=_,cs=y,xa=null)):h=null,h===null)throw Km(y);return uM(h)?y.lanes=32:y.lanes=536870912,null}var qe=j.children;return j=j.fallback,G?(eh(),G=y.mode,qe=__({mode:"hidden",children:qe},G),j=ef(j,G,_,null),qe.return=y,j.return=y,qe.sibling=j,y.child=qe,j=y.child,j.memoizedState=Tj(_),j.childLanes=Aj(h,Ne,_),y.memoizedState=Pj,hv(null,j)):(Zm(y),jj(y,qe))}var lt=h.memoizedState;if(lt!==null&&(qe=lt.dehydrated,qe!==null)){if(Z)y.flags&256?(Zm(y),y.flags&=-257,y=Mj(h,y,_)):y.memoizedState!==null?(eh(),y.child=h.child,y.flags|=128,y=null):(eh(),qe=j.fallback,G=y.mode,j=__({mode:"visible",children:j.children},G),qe=ef(qe,G,_,null),qe.flags|=2,j.return=y,qe.return=y,j.sibling=qe,y.child=j,lf(y,h.child,null,_),j=y.child,j.memoizedState=Tj(_),j.childLanes=Aj(h,Ne,_),y.memoizedState=Pj,y=hv(null,j));else if(Zm(y),uM(qe)){if(Ne=qe.nextSibling&&qe.nextSibling.dataset,Ne)var vt=Ne.dgst;Ne=vt,j=Error(r(419)),j.stack="",j.digest=Ne,tv({value:j,source:null,stack:null}),y=Mj(h,y,_)}else if(hi||ob(h,y,_,!1),Ne=(_&h.childLanes)!==0,hi||Ne){if(Ne=oa,Ne!==null&&(j=Vt(Ne,_),j!==0&&j!==lt.retryLane))throw lt.retryLane=j,Zp(h,j),To(Ne,h,j),Nj;dM(qe)||E_(),y=Mj(h,y,_)}else dM(qe)?(y.flags|=192,y.child=h.child,y=null):(h=lt.treeContext,xa=Fl(qe.nextSibling),cs=y,Sr=!0,Wm=null,El=!1,h!==null&&V5(y,h),y=jj(y,j.children),y.flags|=4096);return y}return G?(eh(),qe=j.fallback,G=y.mode,lt=h.child,vt=lt.sibling,j=lu(lt,{mode:"hidden",children:j.children}),j.subtreeFlags=lt.subtreeFlags&65011712,vt!==null?qe=lu(vt,qe):(qe=ef(qe,G,_,null),qe.flags|=2),qe.return=y,j.return=y,j.sibling=qe,y.child=j,hv(null,j),j=y.child,qe=h.child.memoizedState,qe===null?qe=Tj(_):(G=qe.cachePool,G!==null?(lt=ui._currentValue,G=G.parent!==lt?{parent:lt,pool:lt}:G):G=Q5(),qe={baseLanes:qe.baseLanes|_,cachePool:G}),j.memoizedState=qe,j.childLanes=Aj(h,Ne,_),y.memoizedState=Pj,hv(h.child,j)):(Zm(y),_=h.child,h=_.sibling,_=lu(_,{mode:"visible",children:j.children}),_.return=y,_.sibling=null,h!==null&&(Ne=y.deletions,Ne===null?(y.deletions=[h],y.flags|=16):Ne.push(h)),y.child=_,y.memoizedState=null,_)}function jj(h,y){return y=__({mode:"visible",children:y},h.mode),y.return=h,h.child=y}function __(h,y){return h=Ko(22,h,null,y),h.lanes=0,h}function Mj(h,y,_){return lf(y,h.child,null,_),h=jj(y,y.pendingProps.children),h.flags|=2,y.memoizedState=null,h}function dB(h,y,_){h.lanes|=y;var j=h.alternate;j!==null&&(j.lanes|=y),GA(h.return,y,_)}function Ej(h,y,_,j,G,Z){var Ne=h.memoizedState;Ne===null?h.memoizedState={isBackwards:y,rendering:null,renderingStartTime:0,last:j,tail:_,tailMode:G,treeForkCount:Z}:(Ne.isBackwards=y,Ne.rendering=null,Ne.renderingStartTime=0,Ne.last=j,Ne.tail=_,Ne.tailMode=G,Ne.treeForkCount=Z)}function uB(h,y,_){var j=y.pendingProps,G=j.revealOrder,Z=j.tail;j=j.children;var Ne=ai.current,qe=(Ne&2)!==0;if(qe?(Ne=Ne&1|2,y.flags|=128):Ne&=1,me(ai,Ne),us(h,y,j,_),j=Sr?ev:0,!qe&&h!==null&&(h.flags&128)!==0)e:for(h=y.child;h!==null;){if(h.tag===13)h.memoizedState!==null&&dB(h,_,y);else if(h.tag===19)dB(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(G){case"forwards":for(_=y.child,G=null;_!==null;)h=_.alternate,h!==null&&u_(h)===null&&(G=_),_=_.sibling;_=G,_===null?(G=y.child,y.child=null):(G=_.sibling,_.sibling=null),Ej(y,!1,G,_,Z,j);break;case"backwards":case"unstable_legacy-backwards":for(_=null,G=y.child,y.child=null;G!==null;){if(h=G.alternate,h!==null&&u_(h)===null){y.child=G;break}h=G.sibling,G.sibling=_,_=G,G=h}Ej(y,!0,_,null,Z,j);break;case"together":Ej(y,!1,null,null,void 0,j);break;default:y.memoizedState=null}return y.child}function pu(h,y,_){if(h!==null&&(y.dependencies=h.dependencies),rh|=y.lanes,(_&y.childLanes)===0)if(h!==null){if(ob(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,_=lu(h,h.pendingProps),y.child=_,_.return=y;h.sibling!==null;)h=h.sibling,_=_.sibling=lu(h,h.pendingProps),_.return=y;_.sibling=null}return y.child}function Dj(h,y){return(h.lanes&y)!==0?!0:(h=h.dependencies,!!(h!==null&&r_(h)))}function Qce(h,y,_){switch(y.tag){case 3:D(y,y.stateNode.containerInfo),Xm(y,ui,h.memoizedState.cache),tf();break;case 27:case 5:B(y);break;case 4:D(y,y.stateNode.containerInfo);break;case 10:Xm(y,y.type,y.memoizedProps.value);break;case 31:if(y.memoizedState!==null)return y.flags|=128,aj(y),null;break;case 13:var j=y.memoizedState;if(j!==null)return j.dehydrated!==null?(Zm(y),y.flags|=128,null):(_&y.child.childLanes)!==0?cB(h,y,_):(Zm(y),h=pu(h,y,_),h!==null?h.sibling:null);Zm(y);break;case 19:var G=(h.flags&128)!==0;if(j=(_&y.childLanes)!==0,j||(ob(h,y,_,!1),j=(_&y.childLanes)!==0),G){if(j)return uB(h,y,_);y.flags|=128}if(G=y.memoizedState,G!==null&&(G.rendering=null,G.tail=null,G.lastEffect=null),me(ai,ai.current),j)break;return null;case 22:return y.lanes=0,rB(h,y,_,y.pendingProps);case 24:Xm(y,ui,h.memoizedState.cache)}return pu(h,y,_)}function mB(h,y,_){if(h!==null)if(h.memoizedProps!==y.pendingProps)hi=!0;else{if(!Dj(h,_)&&(y.flags&128)===0)return hi=!1,Qce(h,y,_);hi=(h.flags&131072)!==0}else hi=!1,Sr&&(y.flags&1048576)!==0&&$5(y,ev,y.index);switch(y.lanes=0,y.tag){case 16:e:{var j=y.pendingProps;if(h=sf(y.elementType),y.type=h,typeof h=="function")IA(h)?(j=df(h,j),y.tag=1,y=oB(null,y,h,j,_)):(y.tag=0,y=Cj(null,y,h,j,_));else{if(h!=null){var G=h.$$typeof;if(G===P){y.tag=11,y=eB(null,y,h,j,_);break e}else if(G===T){y.tag=14,y=tB(null,y,h,j,_);break e}}throw y=L(h)||h,Error(r(306,y,""))}}return y;case 0:return Cj(h,y,y.type,y.pendingProps,_);case 1:return j=y.type,G=df(j,y.pendingProps),oB(h,y,j,G,_);case 3:e:{if(D(y,y.stateNode.containerInfo),h===null)throw Error(r(387));j=y.pendingProps;var Z=y.memoizedState;G=Z.element,ZA(h,y),lv(y,j,null,_);var Ne=y.memoizedState;if(j=Ne.cache,Xm(y,ui,j),j!==Z.cache&&WA(y,[ui],_,!0),ov(),j=Ne.element,Z.isDehydrated)if(Z={element:j,isDehydrated:!1,cache:Ne.cache},y.updateQueue.baseState=Z,y.memoizedState=Z,y.flags&256){y=lB(h,y,j,_);break e}else if(j!==G){G=Al(Error(r(424)),y),tv(G),y=lB(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,xa=Fl(h.firstChild),cs=y,Sr=!0,Wm=null,El=!0,_=rU(y,null,j,_),y.child=_;_;)_.flags=_.flags&-3|4096,_=_.sibling;else{if(tf(),j===G){y=pu(h,y,_);break e}us(h,y,j,_)}y=y.child}return y;case 26:return S_(h,y),h===null?(_=_8(y.type,null,y.pendingProps,null))?y.memoizedState=_:Sr||(_=y.type,h=y.pendingProps,j=z_(H.current).createElement(_),j[jt]=y,j[Jt]=h,ms(j,_,h),nt(j),y.stateNode=j):y.memoizedState=_8(y.type,h.memoizedProps,y.pendingProps,h.memoizedState),null;case 27:return B(y),h===null&&Sr&&(j=y.stateNode=v8(y.type,y.pendingProps,H.current),cs=y,El=!0,G=xa,lh(y.type)?(mM=G,xa=Fl(j.firstChild)):xa=G),us(h,y,y.pendingProps.children,_),S_(h,y),h===null&&(y.flags|=4194304),y.child;case 5:return h===null&&Sr&&((G=j=xa)&&(j=Pde(j,y.type,y.pendingProps,El),j!==null?(y.stateNode=j,cs=y,xa=Fl(j.firstChild),El=!1,G=!0):G=!1),G||Km(y)),B(y),G=y.type,Z=y.pendingProps,Ne=h!==null?h.memoizedProps:null,j=Z.children,oM(G,Z)?j=null:Ne!==null&&oM(G,Ne)&&(y.flags|=32),y.memoizedState!==null&&(G=sj(h,y,Hce,null,null,_),Tv._currentValue=G),S_(h,y),us(h,y,j,_),y.child;case 6:return h===null&&Sr&&((h=_=xa)&&(_=Tde(_,y.pendingProps,El),_!==null?(y.stateNode=_,cs=y,xa=null,h=!0):h=!1),h||Km(y)),null;case 13:return cB(h,y,_);case 4:return D(y,y.stateNode.containerInfo),j=y.pendingProps,h===null?y.child=lf(y,null,j,_):us(h,y,j,_),y.child;case 11:return eB(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,Xm(y,y.type,j.value),us(h,y,j.children,_),y.child;case 9:return G=y.type._context,j=y.pendingProps.children,rf(y),G=ds(G),j=j(G),y.flags|=1,us(h,y,j,_),y.child;case 14:return tB(h,y,y.type,y.pendingProps,_);case 15:return nB(h,y,y.type,y.pendingProps,_);case 19:return uB(h,y,_);case 31:return Yce(h,y,_);case 22:return rB(h,y,_,y.pendingProps);case 24:return rf(y),j=ds(ui),h===null?(G=YA(),G===null&&(G=oa,Z=KA(),G.pooledCache=Z,Z.refCount++,Z!==null&&(G.pooledCacheLanes|=_),G=Z),y.memoizedState={parent:j,cache:G},JA(y),Xm(y,ui,G)):((h.lanes&_)!==0&&(ZA(h,y),lv(y,null,null,_),ov()),G=h.memoizedState,Z=y.memoizedState,G.parent!==j?(G={parent:j,cache:j},y.memoizedState=G,y.lanes===0&&(y.memoizedState=y.updateQueue.baseState=G),Xm(y,ui,j)):(j=Z.cache,Xm(y,ui,j),j!==G.cache&&WA(y,[ui],_,!0))),us(h,y,y.pendingProps.children,_),y.child;case 29:throw y.pendingProps}throw Error(r(156,y.tag))}function fu(h){h.flags|=4}function Fj(h,y,_,j,G){if((y=(h.mode&32)!==0)&&(y=!1),y){if(h.flags|=16777216,(G&335544128)===G)if(h.stateNode.complete)h.flags|=8192;else if(zB())h.flags|=8192;else throw of=o_,QA}else h.flags&=-16777217}function hB(h,y){if(y.type!=="stylesheet"||(y.state.loading&4)!==0)h.flags&=-16777217;else if(h.flags|=16777216,!T8(y))if(zB())h.flags|=8192;else throw of=o_,QA}function k_(h,y){y!==null&&(h.flags|=4),h.flags&16384&&(y=h.tag!==22?st():536870912,h.lanes|=y,yb|=y)}function pv(h,y){if(!Sr)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 ya(h){var y=h.alternate!==null&&h.alternate.child===h.child,_=0,j=0;if(y)for(var G=h.child;G!==null;)_|=G.lanes|G.childLanes,j|=G.subtreeFlags&65011712,j|=G.flags&65011712,G.return=h,G=G.sibling;else for(G=h.child;G!==null;)_|=G.lanes|G.childLanes,j|=G.subtreeFlags,j|=G.flags,G.return=h,G=G.sibling;return h.subtreeFlags|=j,h.childLanes=_,y}function Jce(h,y,_){var j=y.pendingProps;switch(HA(y),y.tag){case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return ya(y),null;case 1:return ya(y),null;case 3:return _=y.stateNode,j=null,h!==null&&(j=h.memoizedState.cache),y.memoizedState.cache!==j&&(y.flags|=2048),uu(ui),E(),_.pendingContext&&(_.context=_.pendingContext,_.pendingContext=null),(h===null||h.child===null)&&(sb(y)?fu(y):h===null||h.memoizedState.isDehydrated&&(y.flags&256)===0||(y.flags|=1024,$A())),ya(y),null;case 26:var G=y.type,Z=y.memoizedState;return h===null?(fu(y),Z!==null?(ya(y),hB(y,Z)):(ya(y),Fj(y,G,null,j,_))):Z?Z!==h.memoizedState?(fu(y),ya(y),hB(y,Z)):(ya(y),y.flags&=-16777217):(h=h.memoizedProps,h!==j&&fu(y),ya(y),Fj(y,G,h,j,_)),null;case 27:if(Y(y),_=H.current,G=y.type,h!==null&&y.stateNode!=null)h.memoizedProps!==j&&fu(y);else{if(!j){if(y.stateNode===null)throw Error(r(166));return ya(y),null}h=xe.current,sb(y)?G5(y):(h=v8(G,j,_),y.stateNode=h,fu(y))}return ya(y),null;case 5:if(Y(y),G=y.type,h!==null&&y.stateNode!=null)h.memoizedProps!==j&&fu(y);else{if(!j){if(y.stateNode===null)throw Error(r(166));return ya(y),null}if(Z=xe.current,sb(y))G5(y);else{var Ne=z_(H.current);switch(Z){case 1:Z=Ne.createElementNS("http://www.w3.org/2000/svg",G);break;case 2:Z=Ne.createElementNS("http://www.w3.org/1998/Math/MathML",G);break;default:switch(G){case"svg":Z=Ne.createElementNS("http://www.w3.org/2000/svg",G);break;case"math":Z=Ne.createElementNS("http://www.w3.org/1998/Math/MathML",G);break;case"script":Z=Ne.createElement("div"),Z.innerHTML=" +