From c4951f5fab37f135d559c17a66cc30510294b9c0 Mon Sep 17 00:00:00 2001 From: 4R5T Date: Sun, 15 Feb 2026 22:00:07 -0600 Subject: [PATCH 01/13] init --- backend/app.py | 430 +- frontend/package-lock.json | 685 +- frontend/package.json | 3 + .../src/components/DimensionEditDropdown.css | 179 + .../src/components/DimensionEditDropdown.jsx | 155 + frontend/src/components/DimensionSelector.css | 331 + frontend/src/components/DimensionSelector.jsx | 215 + .../src/components/DimensionSelectorModal.css | 350 + .../src/components/DimensionSelectorModal.jsx | 214 + .../src/components/DimensionSelectorPanel.css | 507 ++ .../src/components/DimensionSelectorPanel.jsx | 383 + frontend/src/components/Evaluation3D.jsx | 1038 +++ frontend/src/components/IdeaCard.css | 17 + frontend/src/components/IdeaCard.jsx | 1003 ++- .../src/components/TreePlotVisualization.jsx | 7914 ++++++++++++----- .../components/UserActionTrackingPanel.jsx | 246 + .../src/components/useUserActionTracking.js | 98 + frontend/src/utils/contentParser.js | 176 + frontend/src/utils/plotStatusTracker.js | 114 + frontend/src/utils/userActionTracker.js | 795 ++ tiny_scientist/data.py | 3 + tiny_scientist/prompts/thinker_prompt.yaml | 321 +- tiny_scientist/thinker.py | 459 +- 23 files changed, 12795 insertions(+), 2841 deletions(-) create mode 100644 frontend/src/components/DimensionEditDropdown.css create mode 100644 frontend/src/components/DimensionEditDropdown.jsx create mode 100644 frontend/src/components/DimensionSelector.css create mode 100644 frontend/src/components/DimensionSelector.jsx create mode 100644 frontend/src/components/DimensionSelectorModal.css create mode 100644 frontend/src/components/DimensionSelectorModal.jsx create mode 100644 frontend/src/components/DimensionSelectorPanel.css create mode 100644 frontend/src/components/DimensionSelectorPanel.jsx create mode 100644 frontend/src/components/Evaluation3D.jsx create mode 100644 frontend/src/components/IdeaCard.css create mode 100644 frontend/src/components/UserActionTrackingPanel.jsx create mode 100644 frontend/src/components/useUserActionTracking.js create mode 100644 frontend/src/utils/contentParser.js create mode 100644 frontend/src/utils/plotStatusTracker.js create mode 100644 frontend/src/utils/userActionTracker.js diff --git a/backend/app.py b/backend/app.py index d6c6d373..0949a2b5 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2,6 +2,7 @@ import io import os import sys +import uuid from typing import Any, Dict, Optional, Union from flask import Flask, Response, jsonify, request, send_file, session @@ -141,6 +142,48 @@ def patch_module_print() -> None: reviewer: Optional[Reviewer] = None global_cost_tracker: Optional[BudgetChecker] = None +global_idea_storage: Dict[str, Any] = {} + + +def _store_or_update_idea(idea: Dict[str, Any]) -> None: + iid = idea.get("id") + if not iid: + return + existing = global_idea_storage.get(iid, {}) + merged = {**existing, **idea} + global_idea_storage[iid] = merged + + +def _list_stored_ideas() -> list[Dict[str, Any]]: + return list(global_idea_storage.values()) + + +def _bulk_update_scores(scored_payloads: list[Dict[str, Any]]) -> None: + for s in scored_payloads: + iid = s.get("id") + if not iid or iid not in global_idea_storage: + continue + + update_dict = {} + + if s.get("scores"): + update_dict["scores"] = s.get("scores") + update_dict["Dimension1Score"] = s.get("dimension1Score") + update_dict["Dimension2Score"] = s.get("dimension2Score") + update_dict["Dimension3Score"] = s.get("dimension3Score") + update_dict["Dimension1Reason"] = s.get("Dimension1Reason", "") + update_dict["Dimension2Reason"] = s.get("Dimension2Reason", "") + update_dict["Dimension3Reason"] = s.get("Dimension3Reason", "") + + update_dict["NoveltyScore"] = s.get("noveltyScore") + update_dict["FeasibilityScore"] = s.get("feasibilityScore") + update_dict["ImpactScore"] = s.get("impactScore") + update_dict["NoveltyReason"] = s.get("NoveltyReason", "") + update_dict["FeasibilityReason"] = s.get("FeasibilityReason", "") + update_dict["ImpactReason"] = s.get("ImpactReason", "") + + global_idea_storage[iid].update(update_dict) + def format_name_for_display(name: Optional[str]) -> str: """Formats a name""" @@ -386,6 +429,71 @@ def generate_children() -> Union[Response, tuple[Response, int]]: return jsonify(response) +@app.route("/api/suggest-dimensions", methods=["POST"]) +def suggest_dimensions() -> Union[Response, tuple[Response, int]]: + """Suggest dimension pairs for evaluating ideas based on research intent.""" + data = request.json + if data is None: + return jsonify({"error": "No JSON data provided"}), 400 + if thinker is None: + return jsonify({"error": "Thinker not configured"}), 400 + + intent = data.get("intent") + if not intent: + return jsonify({"error": "Intent is required"}), 400 + + dimension_pairs = thinker.suggest_dimensions(intent=intent) + return jsonify({"dimension_pairs": dimension_pairs}) + + +@app.route("/api/evaluate-dimension", methods=["POST"]) +def evaluate_single_dimension() -> Union[Response, tuple[Response, int]]: + """Evaluate ideas on a single dimension pair only.""" + data = request.json + if data is None: + return jsonify({"error": "No JSON data provided"}), 400 + if thinker is None: + return jsonify({"error": "Thinker not configured"}), 400 + + ideas = data.get("ideas", []) + intent = data.get("intent") + dimension_pair = data.get("dimension_pair") + dimension_index = data.get("dimension_index", 0) + + print("[DEBUG] Single dimension evaluation request:") + print(f" - Ideas count: {len(ideas)}") + print(f" - Dimension pair: {dimension_pair}") + print(f" - Dimension index: {dimension_index}") + + if not dimension_pair: + return jsonify({"error": "dimension_pair is required"}), 400 + + try: + scores = thinker.rank_single_dimension( + ideas=ideas, + intent=intent, + dimension_pair=dimension_pair, + dimension_index=dimension_index, + ) + print(f"[DEBUG] Single dimension evaluation returned {len(scores)} scores") + return jsonify( + { + "scores": scores, + "dimension_pair": dimension_pair, + "dimension_index": dimension_index, + } + ) + except Exception as e: + print(f"[ERROR] Single dimension evaluation failed: {str(e)}") + import traceback + + traceback.print_exc() + return ( + jsonify({"error": f"Error during single dimension evaluation: {str(e)}"}), + 500, + ) + + @app.route("/api/modify", methods=["POST"]) def modify_idea() -> Union[Response, tuple[Response, int]]: """Modify an idea (modifyIdeaBasedOnModifications)""" @@ -457,43 +565,313 @@ def merge_ideas() -> Union[Response, tuple[Response, int]]: @app.route("/api/evaluate", methods=["POST"]) def evaluate_ideas() -> Union[Response, tuple[Response, int]]: - """Evaluate ideas (evaluateIdeas)""" data = request.json if data is None: return jsonify({"error": "No JSON data provided"}), 400 if thinker is None: return jsonify({"error": "Thinker not configured"}), 400 - ideas = data.get("ideas") + + incoming_ideas = data.get("ideas", []) intent = data.get("intent") + dimension_pairs = data.get("dimension_pairs", []) + user_score_corrections = data.get("userScoreCorrections", []) + mode = data.get("mode", "incremental") + explicit_target_ids = set(data.get("targetIds", []) or []) + + print("[DEBUG] Evaluation request received:") + print(f" - Mode: {mode}") + print(f" - Incoming ideas count: {len(incoming_ideas)}") + print(f" - Incoming idea IDs: {[i.get('id', 'NO_ID') for i in incoming_ideas]}") + print(f" - Intent: {intent[:50] if intent else 'None'}...") + print(f" - Target IDs: {list(explicit_target_ids)}") + print(f" - Dimension pairs: {len(dimension_pairs)}") + + existing_ids = {i["id"] for i in _list_stored_ideas()} + new_ids = [] + for inc in incoming_ideas: + inc_id = inc.get("id") or str(uuid.uuid4()) + inc["id"] = inc_id + if inc_id not in existing_ids: + new_ids.append(inc_id) + _store_or_update_idea({**inc}) + + stored_list = _list_stored_ideas() + + if mode == "full": + target_ids_local = {idea.get("id") for idea in stored_list} + else: + target_ids_local = set(new_ids) | explicit_target_ids + if not explicit_target_ids: + for idea in stored_list: + if not ( + idea.get("NoveltyScore") is not None + and idea.get("FeasibilityScore") is not None + and idea.get("ImpactScore") is not None + ): + target_ids_local.add(idea.get("id")) + + if mode == "full": + evaluation_input = stored_list + else: + evaluation_input = [] + for idea in stored_list: + iid = idea.get("id") + has_scores = ( + idea.get("NoveltyScore") is not None + and idea.get("FeasibilityScore") is not None + and idea.get("ImpactScore") is not None + ) + idea_copy = idea.copy() + if iid in target_ids_local: + idea_copy.pop("AlreadyScored", None) + evaluation_input.append(idea_copy) + else: + if has_scores: + idea_copy["AlreadyScored"] = True + evaluation_input.append(idea_copy) + + print("[DEBUG] About to call thinker.rank with:") + print(f" - Evaluation input count: {len(evaluation_input)}") + print(f" - Evaluation input IDs: {[i.get('id') for i in evaluation_input]}") + print(f" - Intent length: {len(intent) if intent else 0}") + print(f" - Partial mode: {mode != 'full'}") - # Use original data directly (no conversion needed) - thinker_ideas = ideas + try: + scored_ideas = thinker.rank( + ideas=evaluation_input, + intent=intent, + dimension_pairs=dimension_pairs if dimension_pairs else None, + user_score_corrections=user_score_corrections, + partial=(mode != "full"), + ) + print( + f"[DEBUG] thinker.rank returned {len(scored_ideas) if scored_ideas else 0} scored ideas" + ) + if scored_ideas: + print( + f"[DEBUG] First scored idea: {scored_ideas[0].get('Title', 'NO_TITLE')}" + ) + except Exception as e: + print(f"[ERROR] thinker.rank failed: {str(e)}") + print(f"[ERROR] Exception type: {type(e).__name__}") + import traceback - # Store original IDs in order - LLM doesn't preserve titles, use index mapping - original_ids = [idea.get("id") for idea in ideas] + print(f"[ERROR] Traceback: {traceback.format_exc()}") + scored_ideas = [] + + title_maps = [{}, {}, {}] + for idea in stored_list: + iid = idea.get("id") + if not iid: + continue + + for key in [idea.get("Title"), idea.get("Name"), idea.get("title")]: + if key and key.strip(): + title_maps[0][key.strip()] = iid + + for raw_name in [idea.get("Name"), idea.get("Title")]: + if raw_name and raw_name.strip(): + formatted = format_name_for_display(raw_name) + title_maps[1][formatted] = iid + + for key in [idea.get("Title"), idea.get("Name")]: + if key and key.strip(): + variations = [ + key.strip().lower(), + key.strip().replace(" ", "_").lower(), + key.strip().replace("_", " ").lower(), + ] + for variation in variations: + title_maps[2][variation] = iid + + print("[DEBUG] Title maps built:") + print(f" - Map 0 (original): {list(title_maps[0].keys())}") + print(f" - Map 1 (formatted): {list(title_maps[1].keys())}") + print(f" - Map 2 (variations): {list(title_maps[2].keys())}") + + def find_idea_id(scored_idea): + scored_titles = [ + scored_idea.get("Title", "").strip(), + scored_idea.get("Name", "").strip(), + scored_idea.get("title", "").strip(), + ] - # Rank ideas - scored_ideas = thinker.rank(ideas=thinker_ideas, intent=intent) + print(f"[DEBUG] Finding ID for scored idea with titles: {scored_titles}") + + for title in scored_titles: + if not title: + continue + + for i, title_map in enumerate(title_maps): + if title in title_map: + print( + f"[DEBUG] Found exact match in map {i}: '{title}' -> {title_map[title]}" + ) + return title_map[title] + + title_lower = title.lower() + for i, title_map in enumerate(title_maps): + for stored_title, stored_id in title_map.items(): + if stored_title.lower() == title_lower: + print( + f"[DEBUG] Found case-insensitive match in map {i}: '{title}' -> {stored_id}" + ) + return stored_id + + print(f"[DEBUG] No match found for titles: {scored_titles}") + return None + + payloads = [] + unmatched_ideas = [] + matched_ids = set() + + pair_count = len(dimension_pairs) if dimension_pairs else 0 + use_dimension_pairs = pair_count >= 2 + + for i, scored in enumerate(scored_ideas): + iid = find_idea_id(scored) + + if not iid and mode == "incremental" and i < len(new_ids): + potential_id = new_ids[i] + if potential_id in target_ids_local and potential_id not in matched_ids: + iid = potential_id + print(f"[DEBUG] Fallback ID match: scored[{i}] -> {iid}") + + if not iid: + unmatched_ideas.append( + { + "title": scored.get("Title", scored.get("Name", "Unknown")), + "scored_keys": list(scored.keys()), + "index": i, + } + ) + continue + + if mode == "incremental" and iid not in target_ids_local: + continue + + matched_ids.add(iid) + if iid and ("-CUSTOM-" in iid or iid.startswith("C-")): + print(f"[DEBUG] Custom idea {iid} reasoning fields:") + if use_dimension_pairs: + print(f" Dimension1Reason: {scored.get('Dimension1Reason', '')}") + print(f" Dimension2Reason: {scored.get('Dimension2Reason', '')}") + if pair_count >= 3: + print(f" Dimension3Reason: {scored.get('Dimension3Reason', '')}") + else: + print(f" NoveltyReason: {scored.get('NoveltyReason', '')}") + print(f" FeasibilityReason: {scored.get('FeasibilityReason', '')}") + print(f" ImpactReason: {scored.get('ImpactReason', '')}") + print(f" All scored keys: {list(scored.keys())}") + + if use_dimension_pairs: + payload = { + "id": iid, + "scores": scored.get("scores", {}), + } + payload["dimension1Score"] = scored.get("Dimension1Score") + payload["dimension2Score"] = scored.get("Dimension2Score") + payload["Dimension1Reason"] = scored.get("Dimension1Reason", "") + payload["Dimension2Reason"] = scored.get("Dimension2Reason", "") + if pair_count >= 3: + payload["dimension3Score"] = scored.get("Dimension3Score") + payload["Dimension3Reason"] = scored.get("Dimension3Reason", "") + payloads.append(payload) + else: + payloads.append( + { + "id": iid, + "noveltyScore": scored.get("NoveltyScore"), + "feasibilityScore": scored.get("FeasibilityScore"), + "impactScore": scored.get("ImpactScore"), + "NoveltyReason": scored.get("NoveltyReason", ""), + "FeasibilityReason": scored.get("FeasibilityReason", ""), + "ImpactReason": scored.get("ImpactReason", ""), + } + ) - # Return in the format expected by TreePlot - # Use index-based mapping since LLM changes titles but preserves order - response = [] - for i, idea in enumerate(scored_ideas): - # Use index to map back to original ID - original_id = original_ids[i] if i < len(original_ids) else f"idea_{i}" + print("[DEBUG] Evaluation matching results:") + print(f" - Mode: {mode}") + print(f" - Stored ideas: {len(stored_list)}") + print(f" - Scored ideas from LLM: {len(scored_ideas)}") + print(f" - Target IDs: {list(target_ids_local)}") + print(f" - New IDs: {new_ids}") + print(f" - Successful matches: {len(payloads)}") + print(f" - Failed matches: {len(unmatched_ideas)}") + + if unmatched_ideas: + print(f"[WARNING] Could not match {len(unmatched_ideas)} ideas:") + for um in unmatched_ideas[:3]: + print( + f" - [{um.get('index', '?')}] Title: '{um['title']}', Keys: {um['scored_keys']}" + ) + print(f"[DEBUG] Available stored titles: {list(title_maps[0].keys())}") + + if payloads: + print("[DEBUG] Matched payloads:") + for p in payloads[:2]: + print( + f" - ID: {p['id']}, Scores: N={p.get('noveltyScore')}, F={p.get('feasibilityScore')}, I={p.get('impactScore')}, " + f"D1={p.get('dimension1Score')}, D2={p.get('dimension2Score')}, D3={p.get('dimension3Score')}" + ) + if p["id"] and ("-CUSTOM-" in p["id"] or p["id"].startswith("C-")): + print( + f" Custom idea reasoning: N={p.get('NoveltyReason', 'MISSING')[:50]}..., F={p.get('FeasibilityReason', 'MISSING')[:50]}..., I={p.get('ImpactReason', 'MISSING')[:50]}..." + ) - response.append( - { - "id": original_id, # Use index-based mapping - "noveltyScore": idea.get("NoveltyScore"), - "noveltyReason": idea.get("NoveltyReason", ""), - "feasibilityScore": idea.get("FeasibilityScore"), - "feasibilityReason": idea.get("FeasibilityReason", ""), - "impactScore": idea.get("ImpactScore"), - "impactReason": idea.get("ImpactReason", ""), - } - ) - return jsonify(response) + _bulk_update_scores(payloads) + + updated_all = _list_stored_ideas() + + client_return = [] + for i in updated_all: + item = { + "id": i.get("id"), + "title": format_name_for_display(i.get("Name") or i.get("Title")), + "content": format_idea_content(i), + "originalData": i, + } + + if i.get("scores"): + item["scores"] = i.get("scores") + item["dimension1Score"] = i.get("Dimension1Score") + item["dimension2Score"] = i.get("Dimension2Score") + item["dimension3Score"] = i.get("Dimension3Score") + item["Dimension1Reason"] = i.get("Dimension1Reason", "") + item["Dimension2Reason"] = i.get("Dimension2Reason", "") + item["Dimension3Reason"] = i.get("Dimension3Reason", "") + + item["noveltyScore"] = i.get("NoveltyScore") + item["feasibilityScore"] = i.get("FeasibilityScore") + item["impactScore"] = i.get("ImpactScore") + item["NoveltyReason"] = i.get("NoveltyReason", "") + item["FeasibilityReason"] = i.get("FeasibilityReason", "") + item["ImpactReason"] = i.get("ImpactReason", "") + + client_return.append(item) + + custom_ideas_in_return = [ + item + for item in client_return + if item["id"] and ("-CUSTOM-" in item["id"] or item["id"].startswith("C-")) + ] + if custom_ideas_in_return: + print("[DEBUG] Custom ideas in client return:") + for item in custom_ideas_in_return: + print(f" - ID: {item['id']}") + print(f" NoveltyReason: {item.get('NoveltyReason', 'MISSING')[:50]}...") + print( + f" FeasibilityReason: {item.get('FeasibilityReason', 'MISSING')[:50]}..." + ) + print(f" ImpactReason: {item.get('ImpactReason', 'MISSING')[:50]}...") + + meta = { + "mode": mode, + "scoredCount": len(payloads), + "totalIdeas": len(updated_all), + "targets": list(target_ids_local), + } + return jsonify({"ideas": client_return, "meta": meta}) def format_idea_content(idea: Union[Dict[str, Any], str]) -> str: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bbdba2a3..2dd0332e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,8 @@ "version": "0.1.0", "dependencies": { "@monaco-editor/react": "^4.7.0", + "@react-three/drei": "^9.122.0", + "@react-three/fiber": "^8.18.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -24,6 +26,7 @@ "recharts": "^2.13.3", "remark-gfm": "^4.0.0", "socket.io-client": "^4.8.1", + "three": "^0.181.2", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -2241,6 +2244,12 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -2829,6 +2838,12 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==" }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "license": "Apache-2.0" + }, "node_modules/@monaco-editor/loader": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", @@ -2852,6 +2867,18 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", + "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -2968,6 +2995,195 @@ } } }, + "node_modules/@react-spring/animated": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", + "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", + "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", + "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", + "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/three": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz", + "integrity": "sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/core": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "@react-three/fiber": ">=6.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "three": ">=0.126" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", + "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", + "license": "MIT" + }, + "node_modules/@react-three/drei": { + "version": "9.122.0", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.122.0.tgz", + "integrity": "sha512-SEO/F/rBCTjlLez7WAlpys+iGe9hty4rNgjZvgkQeXFSiwqD4Hbk/wNHMAbdd8vprO2Aj81mihv4dF5bC7D0CA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@react-spring/three": "~9.7.5", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^2.9.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "react-composer": "^5.0.3", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.7.8", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.0", + "tunnel-rat": "^0.1.2", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^8", + "react": "^18", + "react-dom": "^18", + "three": ">=0.137" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz", + "integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/react-reconciler": "^0.26.7", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^1.0.6", + "react-reconciler": "^0.27.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.21.0", + "suspend-react": "^0.1.3", + "zustand": "^3.7.1" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=18 <19", + "react-dom": ">=18 <19", + "react-native": ">=0.64", + "three": ">=0.133" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber/node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/@react-three/fiber/node_modules/zustand": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "license": "MIT", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3614,6 +3830,12 @@ "node": ">=10.13.0" } }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -3753,6 +3975,12 @@ "@types/ms": "*" } }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "8.56.12", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", @@ -4114,6 +4342,12 @@ "@types/node": "*" } }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -4161,6 +4395,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-reconciler": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz", + "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -4219,6 +4462,12 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.9", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", @@ -4227,6 +4476,21 @@ "@types/jest": "*" } }, + "node_modules/@types/three": { + "version": "0.182.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz", + "integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==", + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.22.0" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -4238,6 +4502,12 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.5.13", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", @@ -4482,6 +4752,24 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -4613,6 +4901,12 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@webgpu/types": { + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", + "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", + "license": "BSD-3-Clause" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -5411,7 +5705,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5447,6 +5740,15 @@ "node": ">= 8.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -5611,7 +5913,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, "funding": [ { "type": "github", @@ -5709,6 +6010,15 @@ "node": ">= 6" } }, + "node_modules/camera-controls": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.1.tgz", + "integrity": "sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.126.1" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -6266,6 +6576,24 @@ "node": ">=10" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7287,6 +7615,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "license": "MIT", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -7508,6 +7845,12 @@ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -8730,6 +9073,12 @@ "node": ">= 8" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -9370,6 +9719,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -9537,6 +9892,12 @@ "he": "bin/he" } }, + "node_modules/hls.js": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", + "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==", + "license": "Apache-2.0" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -9843,7 +10204,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -10344,6 +10704,12 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -10634,6 +11000,27 @@ "node": ">= 0.4" } }, + "node_modules/its-fine": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", + "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.0" + }, + "peerDependencies": { + "react": ">=18.0" + } + }, + "node_modules/its-fine/node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -11972,6 +12359,16 @@ "lz-string": "bin/bin.js" } }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -12347,6 +12744,21 @@ "node": ">= 8" } }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz", + "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==", + "license": "MIT" + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -15113,6 +15525,12 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -15178,6 +15596,16 @@ "asap": "~2.0.6" } }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -15380,6 +15808,18 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "node_modules/react-composer": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz", + "integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.6.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -15528,6 +15968,31 @@ "react": ">=18" } }, + "node_modules/react-reconciler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz", + "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.21.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/react-reconciler/node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -15637,6 +16102,21 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -16884,6 +17364,32 @@ "node": ">= 0.8.0" } }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "license": "MIT", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "license": "MIT" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -17321,6 +17827,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, "node_modules/svg-parser": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", @@ -17679,6 +18194,45 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.181.2", + "resolved": "https://registry.npmjs.org/three/-/three-0.181.2.tgz", + "integrity": "sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ==", + "license": "MIT" + }, + "node_modules/three-mesh-bvh": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.8.tgz", + "integrity": "sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==", + "deprecated": "Deprecated due to three.js version incompatibility. Please use v0.8.0, instead.", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.151.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.1", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", + "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", + "license": "MIT", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, "node_modules/throat": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", @@ -17761,6 +18315,36 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/troika-three-text": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", + "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.4", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", + "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", + "license": "MIT" + }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -17835,6 +18419,43 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -18213,6 +18834,15 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -18237,6 +18867,15 @@ "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -18398,6 +19037,17 @@ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==" }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -19240,6 +19890,35 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index bd5d8fa2..a3791513 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,6 +5,8 @@ "proxy": "http://localhost:5000", "dependencies": { "@monaco-editor/react": "^4.7.0", + "@react-three/drei": "^9.122.0", + "@react-three/fiber": "^8.18.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -20,6 +22,7 @@ "recharts": "^2.13.3", "remark-gfm": "^4.0.0", "socket.io-client": "^4.8.1", + "three": "^0.181.2", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/frontend/src/components/DimensionEditDropdown.css b/frontend/src/components/DimensionEditDropdown.css new file mode 100644 index 00000000..e8728293 --- /dev/null +++ b/frontend/src/components/DimensionEditDropdown.css @@ -0,0 +1,179 @@ +.dimension-edit-dropdown { + position: absolute; + background: white; + border: 1px solid #e5e7eb; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1000; + width: 380px; /* Increased width */ + display: flex; + flex-direction: column; + animation: fadeIn 0.15s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-5px); } + to { opacity: 1; transform: translateY(0); } +} + +.edit-dropdown-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid #f3f4f6; + background-color: #f9fafb; + border-radius: 8px 8px 0 0; +} + +.edit-dropdown-title { + font-size: 14px; + font-weight: 600; + color: #374151; +} + +.edit-close-btn { + background: none; + border: none; + font-size: 20px; + color: #9ca3af; + cursor: pointer; + padding: 0; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 4px; +} + +.edit-close-btn:hover { + background-color: #e5e7eb; + color: #4b5563; +} + +.edit-dropdown-body { + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.edit-field-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.edit-field-group label { + font-size: 12px; + font-weight: 600; + color: #6b7280; + margin-left: 2px; +} + +.edit-dimension-input { + width: 100%; + padding: 8px 10px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + box-sizing: border-box; +} + +.edit-dimension-input:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); +} + +.edit-dimension-desc { + width: 100%; + padding: 8px 10px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 12px; + color: #4b5563; + background-color: white; + resize: vertical; + box-sizing: border-box; + font-family: inherit; +} + +.edit-dimension-desc:focus { + outline: none; + border-color: #3b82f6; + background-color: white; +} + +.edit-vs-separator-row { + display: flex; + justify-content: center; + align-items: center; + padding: 4px 0; +} + +.edit-vs-separator { + color: #9ca3af; + font-size: 12px; + font-weight: 600; + background-color: #f3f4f6; + padding: 2px 8px; + border-radius: 10px; +} + +/* Colors for inputs */ +.dimension-a { + /* border-left: 3px solid #3b82f6; */ +} + +.dimension-b { + /* border-left: 3px solid #f59e0b; */ +} + +.edit-dropdown-footer { + padding: 12px 16px; + border-top: 1px solid #f3f4f6; + display: flex; + justify-content: flex-end; + gap: 8px; + background-color: #f9fafb; + border-radius: 0 0 8px 8px; +} + +.edit-btn-cancel { + padding: 8px 12px; + background: white; + border: 1px solid #d1d5db; + border-radius: 6px; + color: #374151; + font-size: 13px; + font-weight: 500; + cursor: pointer; +} + +.edit-btn-cancel:hover { + background: #f3f4f6; +} + +.edit-btn-confirm { + padding: 8px 12px; + background: #3b82f6; + border: 1px solid transparent; + border-radius: 6px; + color: white; + font-size: 13px; + font-weight: 500; + cursor: pointer; +} + +.edit-btn-confirm:hover { + background: #2563eb; +} + +.edit-btn-confirm:disabled { + background: #93c5fd; + cursor: not-allowed; +} diff --git a/frontend/src/components/DimensionEditDropdown.jsx b/frontend/src/components/DimensionEditDropdown.jsx new file mode 100644 index 00000000..60ddbd20 --- /dev/null +++ b/frontend/src/components/DimensionEditDropdown.jsx @@ -0,0 +1,155 @@ +import React, { useState, useEffect, useRef } from 'react'; +import './DimensionEditDropdown.css'; + +const DimensionEditDropdown = ({ + isOpen, + anchorPosition, // { top, left } 定位 + currentPair, // { dimensionA, dimensionB, descriptionA, descriptionB } + pairIndex, // 0/1/2 - 维度索引 + onClose, + onConfirm, // (newPair, pairIndex) => void + isLoading = false, +}) => { + const [editedA, setEditedA] = useState(''); + const [editedB, setEditedB] = useState(''); + const [editedDescA, setEditedDescA] = useState(''); + const [editedDescB, setEditedDescB] = useState(''); + const dropdownRef = useRef(null); + + // 当currentPair变化时,更新输入框 + useEffect(() => { + if (currentPair) { + setEditedA(currentPair.dimensionA || ''); + setEditedB(currentPair.dimensionB || ''); + setEditedDescA(currentPair.descriptionA || ''); + setEditedDescB(currentPair.descriptionB || ''); + } + }, [currentPair]); + + // 点击外部关闭 + useEffect(() => { + const handleClickOutside = (e) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { + onClose(); + } + }; + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [isOpen, onClose]); + + // ESC键关闭 + useEffect(() => { + const handleKeyDown = (e) => { + if (e.key === 'Escape') { + onClose(); + } + }; + if (isOpen) { + document.addEventListener('keydown', handleKeyDown); + } + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose]); + + if (!isOpen) return null; + + const axisLabel = pairIndex === 0 ? 'X-axis' : pairIndex === 1 ? 'Y-axis' : 'Z-axis'; + const hasChanges = editedA !== (currentPair?.dimensionA || '') || + editedB !== (currentPair?.dimensionB || '') || + editedDescA !== (currentPair?.descriptionA || '') || + editedDescB !== (currentPair?.descriptionB || ''); + + const isValid = editedA.trim() && editedB.trim(); + + const handleConfirm = () => { + if (!isValid || isLoading) return; + onConfirm({ + ...currentPair, + dimensionA: editedA.trim(), + dimensionB: editedB.trim(), + descriptionA: editedDescA.trim(), + descriptionB: editedDescB.trim(), + }, pairIndex); + }; + + return ( +
+ {/* Header */} +
+ + Dimension {pairIndex + 1} ({axisLabel}) + + +
+ + {/* Input fields */} +
+
+ + setEditedA(e.target.value)} + placeholder="Dimension A Name" + autoFocus + /> +