diff --git a/app.py b/app.py index fc0ac02dd0ff5922f1013afe85b6fa3b9a55ba9c..5230a402234b42afceda7e8205d4c676301f4d96 100644 --- a/app.py +++ b/app.py @@ -5,6 +5,7 @@ import base64 import mimetypes import logging import secrets +import jwt from datetime import datetime from typing import List, Dict, Optional, Any @@ -108,94 +109,26 @@ def serialize_for_json(obj: Any) -> Any: return None # ----------------------------- -# Pydantic 模型 +# Pydantic 模型(從統一模組導入) # ----------------------------- -class UserCreate(BaseModel): - name: str - email: EmailStr - password: str = Field(min_length=6) - - -class UserLogin(BaseModel): - email: EmailStr - password: str - - -class ChatCreateRequest(BaseModel): - user_id: str - title: Optional[str] = "新對話" - - -class MessageCreateRequest(BaseModel): - sender: str - content: str - - -class ChatTitleUpdateRequest(BaseModel): - title: str - - -class UserInfo(BaseModel): - id: str - name: str - email: EmailStr - created_at: datetime - - -class UserPublic(BaseModel): - success: bool - user: UserInfo - - -class UserLoginPublicResponse(BaseModel): - success: bool - user: UserInfo - token: Optional[str] = None - - -class ChatPublic(BaseModel): - chat_id: str - user_id: str - title: str - created_at: datetime - updated_at: datetime - - -class MessagePublic(BaseModel): - sender: str - content: str - timestamp: datetime - - -class ChatDetailResponse(ChatPublic): - messages: List[MessagePublic] - - -class ChatSummary(BaseModel): - chat_id: str - title: str - updated_at: datetime - - -class ChatListResponse(BaseModel): - chats: List[ChatSummary] - - -class FileAnalysisRequest(BaseModel): - filename: str - content: str - mime_type: str - user_prompt: Optional[str] = "請分析這個檔案的內容" - - -class FileAnalysisResponse(BaseModel): - success: bool - filename: str - analysis: Optional[str] = None - error: Optional[str] = None - -class SpeakerLabelBindRequest(BaseModel): - speaker_label: str +from models.schemas import ( + UserCreate, + UserLogin, + ChatCreateRequest, + MessageCreateRequest, + ChatTitleUpdateRequest, + UserInfo, + UserPublic, + UserLoginPublicResponse, + ChatPublic, + MessagePublic, + ChatDetailResponse, + ChatSummary, + ChatListResponse, + FileAnalysisRequest, + FileAnalysisResponse, + SpeakerLabelBindRequest, +) # ----------------------------- @@ -278,8 +211,8 @@ async def lifespan(app: FastAPI): window_seconds=3, required_windows=1, sample_rate=16000, - prob_threshold=0.40, - margin_threshold=0.01, + prob_threshold=0.50, # ECAPA-TDNN 餘弦相似度 + 0.35 加成後門檻 + margin_threshold=0.05, min_snr_db=12.0, )) except Exception as e: @@ -351,8 +284,8 @@ async def periodic_cleanup(): """定期清理過期的會話和數據""" while True: try: - # 每30分鐘清理一次 - await asyncio.sleep(1800) # 30分鐘 + # 定期清理(使用配置常數) + await asyncio.sleep(settings.CLEANUP_INTERVAL) # 清理過期的WebSocket會話 await manager.cleanup_expired_sessions() @@ -378,10 +311,10 @@ async def periodic_cleanup(): app = FastAPI(title="聊天機器人API(整合版)", lifespan=lifespan) -# CORS 設定 +# CORS 設定(從環境變數讀取,生產環境應設定具體來源) app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=settings.get_cors_origins(), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -419,7 +352,7 @@ app.add_middleware(CSPMiddleware) # 掛載靜態檔案目錄(語音沉浸式前端) static_dir = Path("static/frontend") -login_dir = Path("static/frontend/login") +login_dir = Path("bloom-ware-login/out") # 直接使用 Next.js 專案的輸出目錄 if static_dir.exists() and static_dir.is_dir(): app.mount("/static", StaticFiles(directory=str(static_dir), html=True), name="frontend") @@ -433,7 +366,7 @@ if login_dir.exists() and login_dir.is_dir(): app.mount("/login", StaticFiles(directory=str(login_dir), html=True), name="login_static") logger.info(f"✅ 已掛載登入頁面: /login → {login_dir}") else: - logger.warning("⚠️ 未找到 static/frontend/login/ 目錄,請先 build bloom-ware-login 專案") + logger.warning(f"⚠️ 未找到 {login_dir} 目錄,請先執行: cd bloom-ware-login && npm run build") # 環境設定 app.state.intent_model = settings.OPENAI_MODEL @@ -452,226 +385,30 @@ def get_client_ip(request: Request) -> str: return ip return request.client.host if request.client else "unknown" -# CORS -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - +# 注意:CORS 已在上方配置,此處移除重複配置 # ----------------------------- -# WebSocket 連線管理(JWT認證) +# WebSocket 連線管理(從統一模組導入) # ----------------------------- -class ConnectionManager: - def __init__(self): - self.active_connections: Dict[str, WebSocket] = {} - self.client_info: Dict[str, dict] = {} - self.user_sessions: Dict[str, Dict[str, Any]] = {} # 用戶會話信息 - self.last_env: Dict[str, Dict[str, Any]] = {} # 最近的環境快照 - - async def connect(self, websocket: WebSocket, user_id: str, user_info: Dict[str, Any]): - await websocket.accept() - self.active_connections[user_id] = websocket - self.user_sessions[user_id] = user_info - logger.info(f"新的WebSocket連接: {user_id}") - - def disconnect(self, user_id: str): - if user_id in self.active_connections: - del self.active_connections[user_id] - if user_id in self.user_sessions: - del self.user_sessions[user_id] - logger.info(f"WebSocket連接關閉: {user_id}") - - async def send_message(self, message: str, user_id: str, message_type: str = "bot_message"): - if user_id in self.active_connections: - try: - payload = {"type": message_type, "message": message, "timestamp": time.time()} - await self.active_connections[user_id].send_json( - payload - ) - try: - preview = (str(message) or "").strip().replace("\n", " ") - if len(preview) > 120: - preview = preview[:120] + "..." - logger.info(f"WebSocket已發送 → client={user_id} type={message_type} bytes≈{len((str(message) or '').encode('utf-8'))} preview=\"{preview}\"") - except Exception: - pass - except Exception as e: - logger.error(f"發送消息到客戶端 {user_id} 時出錯: {str(e)}") - - def set_client_info(self, user_id: str, info: dict): - self.client_info[user_id] = info - - def get_client_info(self, user_id: str) -> dict: - return self.client_info.get(user_id, {}) - - def get_user_session(self, user_id: str) -> Optional[Dict[str, Any]]: - """獲取用戶會話信息""" - return self.user_sessions.get(user_id) - - async def cleanup_expired_sessions(self): - """清理過期的用戶會話""" - current_time = datetime.now() - expired_users = [] - - for user_id, session_info in self.user_sessions.items(): - # 如果會話超過30分鐘沒有活動,標記為過期 - last_activity = session_info.get("last_activity", current_time) - if (current_time - last_activity).total_seconds() > 1800: # 30分鐘 - expired_users.append(user_id) - - for user_id in expired_users: - logger.info(f"清理過期會話: {user_id}") - self.disconnect(user_id) - - -manager = ConnectionManager() - +from websocket import manager # ----------------------------- -# 語音綁定狀態管理器(關鍵字匹配,無 GPT) +# 語音綁定狀態管理器(從統一模組導入) # ----------------------------- -class VoiceBindingStateMachine: - """ - 語音帳號綁定狀態機(硬編碼關鍵字匹配) - - 流程: - 1. 用戶說「我要綁定語音登入」 - 2. Agent 回應「好的,你現在要綁定誰?」 - 3. 用戶提供名稱 - 4. 系統綁定 speaker_label 到用戶帳號 - 5. Agent 回應「綁定成功!」 - """ - - def __init__(self): - # 用戶狀態:{user_id: {state: str, speaker_label: str}} - self.user_states: Dict[str, Dict[str, Any]] = {} - - def check_binding_trigger(self, user_id: str, message: str) -> Optional[str]: - """ - 檢查是否觸發綁定流程 - - Returns: - - "TRIGGER": 觸發綁定流程 - - "AWAITING_NAME": 等待用戶提供名稱 - - None: 不是綁定相關訊息 - """ - message_lower = message.lower().replace(" ", "") - - # 檢測觸發關鍵字 - trigger_keywords = ["綁定語音登入", "語音登入綁定", "綁定語音", "設定語音登入"] - for keyword in trigger_keywords: - if keyword.replace(" ", "") in message_lower: - # 進入等待狀態 - self.user_states[user_id] = { - "state": "AWAITING_NAME", - "timestamp": datetime.now() - } - return "TRIGGER" - - # 檢查是否在等待名稱狀態 - if user_id in self.user_states: - state_info = self.user_states[user_id] - if state_info.get("state") == "AWAITING_NAME": - # 檢查是否超時(5分鐘) - if (datetime.now() - state_info.get("timestamp")).total_seconds() > 300: - del self.user_states[user_id] - return None - return "AWAITING_NAME" - - return None - - async def handle_binding_flow( - self, - user_id: str, - message: str, - websocket: WebSocket, - voice_service: Optional[VoiceAuthService] = None - ) -> bool: - """ - 處理綁定流程 - - Returns: - True: 已處理(不要繼續到 Agent) - False: 未處理(繼續到 Agent) - """ - state = self.check_binding_trigger(user_id, message) - - if state == "TRIGGER": - # 用戶觸發綁定 - 先檢查是否已經綁定過 - logger.info(f"🎙️ 用戶 {user_id} 觸發語音綁定流程") - - # 檢查使用者是否已經綁定過 speaker_label - from core.database import get_user_by_id - try: - user_data = await get_user_by_id(user_id) - if user_data and user_data.get("speaker_label"): - # 已經綁定過了 - existing_label = user_data.get("speaker_label") - logger.info(f"⚠️ 用戶 {user_id} 已綁定 speaker_label: {existing_label}") - - await websocket.send_json({ - "type": "bot_message", - "message": f"你已經綁定過語音了!目前的聲紋標籤是:{existing_label}。如果需要重新綁定,請聯繫管理員。", - "timestamp": time.time() - }) - - # 清理 FSM 狀態 - self.clear_state(user_id) - return True - except Exception as e: - logger.error(f"❌ 檢查使用者綁定狀態失敗: {e}") - await websocket.send_json({ - "type": "error", - "message": "系統錯誤,無法檢查綁定狀態" - }) - return True - - # 未綁定,繼續綁定流程 - logger.info(f"✅ 用戶 {user_id} 尚未綁定,啟動綁定流程") - - # 標記用戶進入語音綁定等待狀態 - user_session = manager.get_client_info(user_id) or {} - user_session["voice_binding_pending"] = True - user_session["voice_binding_started_at"] = datetime.now() - manager.set_client_info(user_id, user_session) - - await websocket.send_json({ - "type": "bot_message", - "message": "好的,請錄製一段語音(約3-5秒),用於建立你的聲紋特徵。系統會自動識別並綁定到你的帳號。", - "timestamp": time.time() - }) - await websocket.send_json({ - "type": "voice_binding_ready", - "message": "請點擊錄音按鈕開始錄製" - }) - return True - - elif state == "AWAITING_NAME": - # 這個狀態已不再使用,因為我們改為直接錄音綁定 - # 但保留以防萬一 - pass - - return False - - def clear_state(self, user_id: str): - """清理用戶狀態""" - self.user_states.pop(user_id, None) - - -voice_binding_fsm = VoiceBindingStateMachine() +from services.voice_binding import voice_binding_fsm # ----------------------------- # 統一 WebSocket 端點(JWT認證) # ----------------------------- @app.websocket("/ws") -async def websocket_endpoint_with_jwt(websocket: WebSocket, token: str = Query(None)): +async def websocket_endpoint_with_jwt( + websocket: WebSocket, + token: str = Query(None), + emotion: str = Query("") +): """JWT認證的WebSocket端點(支援語音登入匿名連線)""" - logger.info("WebSocket連接請求 - JWT認證") + logger.info(f"WebSocket連接請求 - JWT認證 (emotion={emotion})") # 特殊處理:語音登入匿名連線 is_voice_login_mode = token == "anonymous_voice_login" @@ -756,11 +493,11 @@ async def websocket_endpoint_with_jwt(websocket: WebSocket, token: str = Query(N logger.debug(f"讀取使用者時區失敗: {tz_err}") td = app.state.feature_router.get_current_time_data() - # WebSocket 連線時沒有語音情緒,使用空字串 + # 使用語音登入傳遞的情緒(如果有) welcome_msg = compose_welcome( user_name=user_info.get('name'), time_data=td, - emotion_label="", + emotion_label=emotion, timezone=tz_hint, ) except Exception as e: @@ -833,7 +570,10 @@ async def websocket_endpoint_with_jwt(websocket: WebSocket, token: str = Query(N { "role": "system", "content": ( - "你是一個友善、有禮且能夠提供幫助的AI助手。請使用繁體中文回覆,保持簡潔清晰的表達。" + "你是一個友善、有禮且能夠提供幫助的AI助手。\n\n" + "【重要】語言使用規範:\n" + "- 回覆用戶時:必須使用繁體中文,保持簡潔清晰的表達\n" + "- 調用工具時:所有參數必須使用英文(城市名、國家名、貨幣代碼等)\n\n" "另外,請勿自稱為 GPT-4 或其他版本。若需要自我介紹,請表述為 '基於 gpt-5-nano 模型'。" ), }, @@ -971,28 +711,119 @@ async def websocket_endpoint_with_jwt(websocket: WebSocket, token: str = Query(N await websocket.send_json({"type": "error", "message": f"CHAT_FOCUS_ERROR: {str(e)}"}) elif message_type == "audio_start": - # 語音處理邏輯(保持不變) + # 語音處理邏輯(支援多種模式) + mode = message_data.get("mode", "voice_login") + try: sr = int(message_data.get("sample_rate", 16000)) except Exception: sr = 16000 - try: - if hasattr(app.state, "voice_auth") and app.state.voice_auth: - app.state.voice_auth.start_session(user_id, sr) - await websocket.send_json({"type": "voice_login_status", "message": "recording_started"}) - else: - await websocket.send_json({"type": "voice_login_result", "success": False, "error": "VOICE_AUTH_NOT_AVAILABLE"}) - except Exception as e: - await websocket.send_json({"type": "voice_login_result", "success": False, "error": f"START_ERROR: {str(e)}"}) + + if mode == "realtime_chat": + # === 即時轉錄模式(使用 OpenAI Realtime API)=== + try: + from services.realtime_stt_service import RealtimeSTTService + + logger.info(f"🎙️ 啟動即時轉錄模式,用戶 {user_id}") + + # 建立 Realtime STT 服務實例 + realtime_stt = RealtimeSTTService() + + # 定義轉錄回調函數 + async def on_transcript_delta(delta_text: str): + """接收部分轉錄結果並即時發送給前端""" + await websocket.send_json({ + "type": "stt_delta", + "text": delta_text, + "timestamp": time.time() + }) + logger.debug(f"📤 STT Delta: {delta_text}") + + async def on_transcript_done(full_text: str): + """接收完整轉錄結果""" + await websocket.send_json({ + "type": "stt_final", + "text": full_text, + "timestamp": time.time() + }) + logger.info(f"✅ STT Final: {full_text}") + + # 儲存轉錄文字到 client_info,供 audio_stop 使用 + client_info = manager.get_client_info(user_id) or {} + client_info["realtime_transcript"] = full_text + manager.set_client_info(user_id, client_info) + + async def on_vad_committed(item_id: str): + """VAD 偵測到語音段結束""" + logger.debug(f"🎤 VAD Committed: {item_id}") + + # 連線到 OpenAI Realtime API + success = await realtime_stt.connect( + on_transcript_delta=on_transcript_delta, + on_transcript_done=on_transcript_done, + on_vad_committed=on_vad_committed, + model="gpt-4o-mini-transcribe", + language="zh" + ) + + if success: + # 儲存 Realtime STT 實例到 client info + client_info = manager.get_client_info(user_id) or {} + client_info["realtime_stt"] = realtime_stt + manager.set_client_info(user_id, client_info) + + await websocket.send_json({ + "type": "realtime_stt_status", + "status": "connected", + "message": "即時轉錄已啟動" + }) + logger.info(f"✅ 用戶 {user_id} 即時轉錄已啟動") + else: + raise Exception("無法連接到 OpenAI Realtime API") + + except Exception as e: + logger.error(f"❌ 啟動即時轉錄失敗: {e}") + await websocket.send_json({ + "type": "error", + "message": f"即時轉錄啟動失敗: {str(e)}" + }) + + else: + # === 傳統模式(語音登入或語音綁定)=== + try: + if hasattr(app.state, "voice_auth") and app.state.voice_auth: + app.state.voice_auth.start_session(user_id, sr) + await websocket.send_json({"type": "voice_login_status", "message": "recording_started"}) + else: + await websocket.send_json({"type": "voice_login_result", "success": False, "error": "VOICE_AUTH_NOT_AVAILABLE"}) + except Exception as e: + await websocket.send_json({"type": "voice_login_result", "success": False, "error": f"START_ERROR: {str(e)}"}) elif message_type == "audio_chunk": try: b64 = message_data.get("pcm16_base64", "") - if b64 and hasattr(app.state, "voice_auth") and app.state.voice_auth: + + # 檢查是否為即時轉錄模式 + client_info = manager.get_client_info(user_id) or {} + realtime_stt = client_info.get("realtime_stt") + + if realtime_stt and b64: + # === 即時轉錄模式:轉發到 OpenAI Realtime API === + try: + import base64 + audio_bytes = base64.b64decode(b64) + await realtime_stt.send_audio_chunk(audio_bytes) + logger.debug(f"🎤 轉發音頻到 OpenAI: {len(audio_bytes)} bytes") + except Exception as e: + logger.error(f"❌ 轉發音頻失敗: {e}") + + elif b64 and hasattr(app.state, "voice_auth") and app.state.voice_auth: + # === 傳統模式:存到 buffer === app.state.voice_auth.append_chunk_base64(user_id, b64) # 添加調試日誌 current_buffer_size = len(app.state.voice_auth._buffers.get(user_id, b"")) logger.info(f"🎤 收到音頻chunk,用戶 {user_id},當前緩衝區大小: {current_buffer_size} bytes") + except Exception as e: await websocket.send_json({"type": "voice_login_result", "success": False, "error": f"CHUNK_ERROR: {str(e)}"}) @@ -1191,6 +1022,112 @@ async def websocket_endpoint_with_jwt(websocket: WebSocket, token: str = Query(N "detail": {k: v for k, v in result.items() if k not in {"success"}}, }) + elif mode == "realtime_chat": + # === 即時轉錄模式:關閉 OpenAI Realtime 連線並處理轉錄結果 === + try: + client_info = manager.get_client_info(user_id) or {} + realtime_stt = client_info.get("realtime_stt") + transcription = client_info.get("realtime_transcript", "") + + if realtime_stt: + logger.info(f"🔌 關閉即時轉錄連線,用戶 {user_id}") + await realtime_stt.disconnect() + + # 清理 client info + client_info.pop("realtime_stt", None) + client_info.pop("realtime_transcript", None) + manager.set_client_info(user_id, client_info) + + await websocket.send_json({ + "type": "realtime_stt_status", + "status": "disconnected", + "message": "即時轉錄已結束" + }) + logger.info(f"✅ 用戶 {user_id} 即時轉錄已結束") + else: + logger.warning(f"⚠️ 找不到 realtime_stt 實例,用戶 {user_id}") + + # 如果有轉錄文字,送給 AI Agent 處理 + if transcription: + logger.info(f"🤖 處理即時轉錄結果: {transcription}") + + # 通知前端開始思考 + await websocket.send_json({"type": "typing", "message": "thinking"}) + + # 異步處理對話邏輯 + async def _process_realtime_chat(): + chat_id = message_data.get("chat_id") + + # 如果沒有 chat_id,創建新對話 + if not chat_id: + try: + user_chats_result = await get_user_chats(user_id) + if user_chats_result["success"] and user_chats_result["chats"]: + latest_chat = user_chats_result["chats"][0] + chat_id = latest_chat["chat_id"] + else: + chat_title = f"語音對話 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + chat_result = await create_chat(user_id, chat_title) + if chat_result["success"]: + chat_id = chat_result["chat"]["chat_id"] + except Exception as e: + logger.error(f"創建對話失敗: {e}") + await websocket.send_json({"type": "error", "message": "無法創建對話"}) + return + + # 保存用戶訊息 + await save_message_to_db(user_id, chat_id, "user", transcription) + + # 處理對話(透過 handle_message,自動處理 pipeline) + response = await handle_message( + transcription, + user_id, + chat_id, + [] # messages 參數(會自動從數據庫載入) + ) + + # 發送回應 + if isinstance(response, PipelineResult): + message_text = response.text + + await websocket.send_json({ + "type": "bot_message", + "message": message_text, + "timestamp": time.time(), + "tool_name": None, + "tool_data": None + }) + elif isinstance(response, dict): + tool_name = response.get('tool_name') + tool_data = response.get('tool_data') + message_text = response.get('message', response.get('content', '')) + + await websocket.send_json({ + "type": "bot_message", + "message": message_text, + "timestamp": time.time(), + "tool_name": tool_name, + "tool_data": tool_data + }) + else: + # 字串回應 + await websocket.send_json({ + "type": "bot_message", + "message": str(response), + "timestamp": time.time() + }) + + await _process_realtime_chat() + else: + logger.debug(f"沒有轉錄文字,返回待機狀態") + + except Exception as e: + logger.error(f"❌ 關閉即時轉錄失敗: {e}") + await websocket.send_json({ + "type": "error", + "message": f"關閉即時轉錄失敗: {str(e)}" + }) + elif mode == "chat": # === 新的對話模式:並行執行 STT + 情緒辨識 === try: @@ -1838,6 +1775,134 @@ async def logout(): "message": "登出成功" } + +# ----------------------------- +# 語音登入 API +# ----------------------------- +class VoiceLoginRequest(BaseModel): + """語音登入請求""" + audio_base64: str # base64 編碼的 PCM16 音訊 + sample_rate: int = 16000 + + +@app.post("/auth/voice/login") +async def voice_login(request: VoiceLoginRequest): + """ + 語音登入 API + + 流程: + 1. 接收 base64 編碼的音訊 + 2. 執行身份辨識 + 情緒辨識 + 3. 查詢 speaker_label 對應的用戶 + 4. 生成 JWT token + 5. 回傳 token + 情緒 + """ + import base64 + + try: + # 取得 VoiceAuthService 實例 + voice_auth = getattr(app.state, "voice_auth", None) + if not voice_auth: + logger.error("❌ VoiceAuthService 未初始化") + return JSONResponse(status_code=503, content={ + "success": False, + "error": "語音辨識服務未就緒,請稍後再試" + }) + + # 解碼音訊 + try: + audio_bytes = base64.b64decode(request.audio_base64) + except Exception as e: + logger.error(f"❌ 音訊解碼失敗: {e}") + return JSONResponse(status_code=400, content={ + "success": False, + "error": "音訊格式錯誤" + }) + + logger.info(f"🎙️ 收到語音登入請求,音訊大小: {len(audio_bytes)} bytes") + + # 建立臨時 session 並處理音訊 + temp_user_id = f"voice_login_{datetime.now().timestamp()}" + voice_auth.start_session(temp_user_id, request.sample_rate) + voice_auth._buffers[temp_user_id] = bytearray(audio_bytes) + + # 執行辨識 + result = voice_auth.stop_and_authenticate(temp_user_id) + + # 清理 session + voice_auth.clear_session(temp_user_id) + + if not result.get("success"): + error_code = result.get("error", "UNKNOWN_ERROR") + error_messages = { + "NO_AUDIO": "沒有收到音訊資料", + "AUDIO_TOO_SHORT": "音訊太短,請錄製至少 3 秒", + "LOW_SNR": "環境太吵,請在安靜的地方重試", + "INCONSISTENT_WINDOWS": "無法確認身份,請重試", + "THRESHOLD_NOT_MET": "無法確認身份,請重試", + "MODEL_ERROR": "辨識系統錯誤,請稍後重試", + } + logger.warning(f"🎙️ 語音辨識失敗: {error_code}") + return JSONResponse(content={ + "success": False, + "error": error_messages.get(error_code, f"辨識失敗:{error_code}") + }) + + # 取得辨識結果 + speaker_label = result.get("label") + emotion = result.get("emotion", {}) + emotion_label = emotion.get("label", "neutral") if isinstance(emotion, dict) else "neutral" + + logger.info(f"🎙️ 語音辨識成功: speaker={speaker_label}, emotion={emotion_label}") + + # 查詢對應的用戶 + from core.database import get_user_by_speaker_label + user = await get_user_by_speaker_label(speaker_label) + + if not user: + logger.warning(f"🎙️ 找不到綁定的帳號: speaker_label={speaker_label}") + return JSONResponse(content={ + "success": False, + "error": f"找不到綁定的帳號。請先使用 Google 登入並綁定語音。" + }) + + # 生成 JWT token + user_id = user.get("id") + user_name = user.get("name", "用戶") + user_email = user.get("email", "") + + payload = { + "sub": user_id, + "name": user_name, + "email": user_email, + "iat": datetime.utcnow(), + "exp": datetime.utcnow() + timedelta(days=7), + "login_method": "voice", + "emotion": emotion_label, + } + + token = jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm="HS256") + + logger.info(f"✅ 語音登入成功: user={user_name}, emotion={emotion_label}") + + return { + "success": True, + "access_token": token, + "user": { + "id": user_id, + "name": user_name, + "email": user_email, + }, + "emotion": emotion_label, + } + + except Exception as e: + logger.exception(f"❌ 語音登入失敗: {e}") + return JSONResponse(status_code=500, content={ + "success": False, + "error": f"系統錯誤:{str(e)}" + }) + """ Google OAuth 2.0 登入端點 (向後兼容) 接收前端傳來的 Google JWT token,驗證後創建或登入用戶 diff --git a/bloom-ware-login/app/layout.tsx b/bloom-ware-login/app/layout.tsx index 9a858ae6dde0428e3eb204e6856e24b11b9d1d91..1f0d715ce3e9c5f512545a247502316ec5d82436 100644 --- a/bloom-ware-login/app/layout.tsx +++ b/bloom-ware-login/app/layout.tsx @@ -13,6 +13,8 @@ export const metadata: Metadata = { generator: "v0.app", } +import { OfflineIndicator } from "@/components/offline-indicator" + export default function RootLayout({ children, }: Readonly<{ @@ -21,6 +23,7 @@ export default function RootLayout({ return ( + {children} diff --git a/bloom-ware-login/components/login-form.tsx b/bloom-ware-login/components/login-form.tsx index bab592ce6694004fcff48bdebef6267f7e359a25..7c82df4a2d7a4f31ba9b8ae2655325d2434715da 100644 --- a/bloom-ware-login/components/login-form.tsx +++ b/bloom-ware-login/components/login-form.tsx @@ -3,12 +3,15 @@ import { useEffect, useRef, useCallback, useState } from "react" import { Button } from "@/components/ui/button" import { TulipIllustration } from "@/components/tulip-illustration" -import { Mic, ExternalLink } from "lucide-react" +import { Mic, ExternalLink, Loader2 } from "lucide-react" export function LoginForm() { const popupRef = useRef(null) const popupCheckIntervalRef = useRef(null) const [isInIframe, setIsInIframe] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [loadingType, setLoadingType] = useState<'google' | 'voice' | null>(null) + const [error, setError] = useState(null) // 檢測是否在 iframe 中(HF Space 嵌入模式) useEffect(() => { @@ -153,6 +156,9 @@ export function LoginForm() { } const handleGoogleLogin = async () => { + // 清除之前的錯誤 + setError(null); + // 如果在 iframe 中,引導用戶在新分頁開啟 if (isInIframe) { console.log('📦 檢測到 iframe 環境,引導用戶在新分頁開啟'); @@ -160,6 +166,9 @@ export function LoginForm() { return; } + setIsLoading(true); + setLoadingType('google'); + try { console.log('🚀 開始 Google OAuth 登入流程(Popup 模式)...'); @@ -204,6 +213,8 @@ export function LoginForm() { popupCheckIntervalRef.current = setInterval(() => { if (popupRef.current && popupRef.current.closed) { console.log('📪 Popup 視窗已關閉'); + setIsLoading(false); + setLoadingType(null); if (popupCheckIntervalRef.current) { clearInterval(popupCheckIntervalRef.current); } @@ -212,14 +223,119 @@ export function LoginForm() { } catch (error) { console.error('❌ OAuth 初始化失敗:', error); - alert('Google 登入初始化失敗,請稍後再試'); + setError('Google 登入初始化失敗,請稍後再試'); + setIsLoading(false); + setLoadingType(null); } } - const handleVoiceLogin = () => { + const [voiceStatus, setVoiceStatus] = useState(''); + + const handleVoiceLogin = async () => { + setError(null); + setIsLoading(true); + setLoadingType('voice'); + setVoiceStatus('請求麥克風權限...'); console.log('🎤 開始語音登入...'); - localStorage.setItem('jwt_token', 'anonymous_voice_login'); - window.location.href = '/static/'; + + try { + // 請求麥克風權限 + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + console.log('✅ 麥克風權限已獲取'); + + // 設定錄音參數 + const audioContext = new AudioContext({ sampleRate: 16000 }); + const source = audioContext.createMediaStreamSource(stream); + const processor = audioContext.createScriptProcessor(4096, 1, 1); + + const audioChunks: Float32Array[] = []; + const recordDuration = 4000; // 4 秒(確保足夠長度) + + processor.onaudioprocess = (e) => { + const inputData = e.inputBuffer.getChannelData(0); + audioChunks.push(new Float32Array(inputData)); + }; + + source.connect(processor); + processor.connect(audioContext.destination); + + setVoiceStatus('🎙️ 錄音中... 請說話 (4秒)'); + console.log('🎙️ 開始錄音 4 秒...'); + + // 錄音 3 秒 + await new Promise(resolve => setTimeout(resolve, recordDuration)); + + // 停止錄音 + processor.disconnect(); + source.disconnect(); + stream.getTracks().forEach(track => track.stop()); + await audioContext.close(); + + setVoiceStatus('辨識中...'); + console.log('✅ 錄音完成,處理音訊...'); + + // 合併音訊資料 + const totalLength = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0); + const audioData = new Float32Array(totalLength); + let offset = 0; + for (const chunk of audioChunks) { + audioData.set(chunk, offset); + offset += chunk.length; + } + + // 轉換為 PCM16 + const pcm16 = new Int16Array(audioData.length); + for (let i = 0; i < audioData.length; i++) { + const s = Math.max(-1, Math.min(1, audioData[i])); + pcm16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; + } + + // 轉換為 base64 + const uint8Array = new Uint8Array(pcm16.buffer); + let binary = ''; + for (let i = 0; i < uint8Array.length; i++) { + binary += String.fromCharCode(uint8Array[i]); + } + const audioBase64 = btoa(binary); + + console.log('📤 發送語音到後端進行辨識...'); + + // 呼叫語音登入 API + const response = await fetch('/auth/voice/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + audio_base64: audioBase64, + sample_rate: 16000, + }), + }); + + const data = await response.json(); + + if (data.success) { + console.log('✅ 語音登入成功!'); + localStorage.setItem('jwt_token', data.access_token); + // 儲存情緒標籤供歡迎詞使用 + if (data.emotion) { + localStorage.setItem('voice_login_emotion', data.emotion); + } + window.location.href = '/static/'; + } else { + throw new Error(data.error || '語音登入失敗'); + } + + } catch (error: any) { + console.error('❌ 語音登入失敗:', error); + if (error.name === 'NotAllowedError') { + setError('請允許麥克風權限以使用語音登入'); + } else { + setError(error.message || '語音登入失敗,請重試'); + } + setIsLoading(false); + setLoadingType(null); + } } return ( @@ -236,6 +352,13 @@ export function LoginForm() {
+ {/* 錯誤訊息 */} + {error && ( +
+ {error} +
+ )} + {/* iframe 環境提示 - 簡約風格 */} {isInIframe && (

@@ -245,40 +368,56 @@ export function LoginForm() { {/* Google Login */} {/* Voice Login */}

diff --git a/bloom-ware-login/components/offline-indicator.tsx b/bloom-ware-login/components/offline-indicator.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0604726168e816da186e23655cebd03cf4ea8ff6 --- /dev/null +++ b/bloom-ware-login/components/offline-indicator.tsx @@ -0,0 +1,63 @@ +"use client" + +import { useEffect, useState } from "react" +import { WifiOff, Wifi } from "lucide-react" + +export function OfflineIndicator() { + const [isOnline, setIsOnline] = useState(true) + const [showReconnected, setShowReconnected] = useState(false) + + useEffect(() => { + // 初始狀態 + setIsOnline(navigator.onLine) + + const handleOnline = () => { + setIsOnline(true) + setShowReconnected(true) + // 3 秒後隱藏「已恢復連線」提示 + setTimeout(() => setShowReconnected(false), 3000) + } + + const handleOffline = () => { + setIsOnline(false) + setShowReconnected(false) + } + + window.addEventListener("online", handleOnline) + window.addEventListener("offline", handleOffline) + + return () => { + window.removeEventListener("online", handleOnline) + window.removeEventListener("offline", handleOffline) + } + }, []) + + // 在線且不需要顯示恢復提示時,不渲染任何內容 + if (isOnline && !showReconnected) { + return null + } + + return ( +
+ {isOnline ? ( + <> + + 已恢復連線 + + ) : ( + <> + + 網路已斷線 + + )} +
+ ) +} diff --git a/core/ai_client.py b/core/ai_client.py new file mode 100644 index 0000000000000000000000000000000000000000..69fcb1286613006d29fe5ad0eec20806b4d9d728 --- /dev/null +++ b/core/ai_client.py @@ -0,0 +1,83 @@ +""" +統一 OpenAI 客戶端管理 +單一真理來源,避免重複初始化 +""" + +import logging +from typing import Optional + +from core.config import settings +from core.logging import get_logger + +logger = get_logger("core.ai_client") + +# 全域 OpenAI 客戶端 +_openai_client = None +_initialized = False + + +def get_openai_client(): + """ + 取得 OpenAI 客戶端(單例模式) + + Returns: + OpenAI 客戶端實例,若初始化失敗則返回 None + """ + global _openai_client, _initialized + + if _initialized: + return _openai_client + + try: + from openai import OpenAI + + api_key = settings.OPENAI_API_KEY + if not api_key: + logger.error("❌ OpenAI API Key 未設定") + _initialized = True + return None + + _openai_client = OpenAI( + api_key=api_key, + timeout=float(settings.OPENAI_TIMEOUT), + max_retries=3, + ) + + _initialized = True + logger.info("✅ OpenAI 客戶端初始化成功") + return _openai_client + + except ImportError: + logger.error("❌ 無法導入 OpenAI SDK") + _initialized = True + return None + + except Exception as e: + logger.error(f"❌ OpenAI 客戶端初始化失敗: {e}") + _initialized = True + return None + + +def reset_client() -> None: + """ + 重置客戶端(用於測試或重新初始化) + """ + global _openai_client, _initialized + _openai_client = None + _initialized = False + logger.info("OpenAI 客戶端已重置") + + +def is_available() -> bool: + """ + 檢查 OpenAI 服務是否可用 + + Returns: + True 如果客戶端已初始化且可用 + """ + client = get_openai_client() + return client is not None + + +# 便捷別名 +client = property(lambda self: get_openai_client()) diff --git a/core/config.py b/core/config.py index 31c40f721aa4840c678b0c3a7ff6b08fdd5d4853..909adddb521fd5bd40b6e0fbb652af60795782de 100644 --- a/core/config.py +++ b/core/config.py @@ -118,6 +118,40 @@ class Settings: ENV_CONTEXT_HEADING_THRESHOLD: float = float(os.getenv("ENV_CONTEXT_HEADING_THRESHOLD", "25")) ENV_CONTEXT_TTL_SECONDS: float = float(os.getenv("ENV_CONTEXT_TTL_SECONDS", "300")) + # ===== CORS 安全設定 ===== + # 生產環境應設定具體的允許來源,多個來源用逗號分隔 + # 例如:CORS_ORIGINS=https://example.com,https://app.example.com + _cors_origins_raw: str = os.getenv("CORS_ORIGINS", "*") + + @classmethod + def get_cors_origins(cls) -> list: + """取得 CORS 允許的來源列表""" + if cls._cors_origins_raw == "*": + return ["*"] + return [origin.strip() for origin in cls._cors_origins_raw.split(",") if origin.strip()] + + # ===== 安全性設定 ===== + # 登入失敗封鎖閾值 + FAILED_LOGIN_THRESHOLD: int = int(os.getenv("FAILED_LOGIN_THRESHOLD", "5")) + # 封鎖時間(秒) + LOGIN_BLOCK_DURATION: int = int(os.getenv("LOGIN_BLOCK_DURATION", "900")) # 15 分鐘 + # JWT Secret 最小長度 + JWT_SECRET_MIN_LENGTH: int = 32 + + # ===== 效能調優常數 ===== + # WebSocket 會話超時(秒) + WEBSOCKET_SESSION_TIMEOUT: int = int(os.getenv("WEBSOCKET_SESSION_TIMEOUT", "1800")) # 30 分鐘 + # 定期清理間隔(秒) + CLEANUP_INTERVAL: int = int(os.getenv("CLEANUP_INTERVAL", "1800")) # 30 分鐘 + # 記憶重要性閾值 + MEMORY_IMPORTANCE_THRESHOLD: float = float(os.getenv("MEMORY_IMPORTANCE_THRESHOLD", "0.6")) + # 意圖快取 TTL(秒) + INTENT_CACHE_TTL: int = int(os.getenv("INTENT_CACHE_TTL", "300")) # 5 分鐘 + # 對話歷史載入限制 + CHAT_HISTORY_LIMIT: int = int(os.getenv("CHAT_HISTORY_LIMIT", "12")) + # 關懷模式對話歷史限制 + CARE_MODE_HISTORY_LIMIT: int = int(os.getenv("CARE_MODE_HISTORY_LIMIT", "3")) + @classmethod def validate(cls) -> bool: """ @@ -156,9 +190,16 @@ class Settings: if not cls.OPENAI_API_KEY.startswith("sk-"): print("⚠️ OpenAI API Key 格式可能不正確(應以 'sk-' 開頭)") - # 驗證 JWT Secret 長度 - if len(cls.JWT_SECRET_KEY) < 32: - print("⚠️ JWT Secret Key 長度建議至少 32 個字符") + # 驗證 JWT Secret 長度(強制檢查) + if len(cls.JWT_SECRET_KEY) < cls.JWT_SECRET_MIN_LENGTH: + print(f"❌ JWT Secret Key 長度必須至少 {cls.JWT_SECRET_MIN_LENGTH} 個字符") + if cls.IS_PRODUCTION: + return False + print("⚠️ 開發環境允許繼續,但生產環境將拒絕啟動") + + # 生產環境 CORS 檢查 + if cls.IS_PRODUCTION and cls._cors_origins_raw == "*": + print("⚠️ 生產環境建議設定具體的 CORS_ORIGINS,而非 '*'") return True diff --git a/core/database/base.py b/core/database/base.py index 68440557b021c2d0e69ceb4826012604b8b4223d..d5c3cfac8250221d6d337b1f57fe1dfb4ee14a32 100644 --- a/core/database/base.py +++ b/core/database/base.py @@ -39,6 +39,35 @@ route_cache_collection = None MAX_MEMORIES_PER_USER = 500 +def _serialize_firestore_data(data: Any) -> Any: + """ + 遞迴轉換 Firestore 資料中的 DatetimeWithNanoseconds 物件為 ISO 字串 + + Args: + data: Firestore 回傳的資料(可能包含 DatetimeWithNanoseconds) + + Returns: + JSON 可序列化的資料 + """ + from google.cloud.firestore_v1._helpers import DatetimeWithNanoseconds + + if isinstance(data, DatetimeWithNanoseconds): + # 轉成 ISO 8601 字串 + return data.isoformat() + elif isinstance(data, datetime): + # 一般 Python datetime 也轉成字串 + return data.isoformat() + elif isinstance(data, dict): + # 遞迴處理字典 + return {k: _serialize_firestore_data(v) for k, v in data.items()} + elif isinstance(data, list): + # 遞迴處理列表 + return [_serialize_firestore_data(item) for item in data] + else: + # 其他型別直接回傳 + return data + + def _get_user_doc_ref(user_id: str) -> DocumentReference: if users_collection is None: raise RuntimeError("Firestore尚未連接,無法操作使用者資料") @@ -143,13 +172,15 @@ async def get_user_by_id(user_id: str): user_doc = docs[0] user_data = user_doc.to_dict() - - return { + + result = { "id": user_data["user_id"], "name": user_data.get("name", ""), "email": user_data.get("email", ""), "created_at": user_data.get("created_at"), } + # 序列化 Firestore 時間物件,避免 JSON 序列化炸裂 + return _serialize_firestore_data(result) except Exception as e: logger.error(f"查找使用者時發生錯誤: {e}") return None @@ -192,7 +223,8 @@ async def get_user_history(user_id, limit=20): messages = await _asyncio.to_thread(_fetch_messages) logger.info(f"已獲取用戶 {user_id} 的 {len(messages)} 條歷史記錄") - return messages + # 序列化 Firestore 時間物件,避免 JSON 序列化炸裂 + return _serialize_firestore_data(messages) except Exception as e: logger.error(f"獲取歷史記錄時發生錯誤: {e}") return [] @@ -371,7 +403,9 @@ async def create_chat(user_id, title="新對話"): "created_at": chat["created_at"], "updated_at": chat["updated_at"], } - return {"success": True, "chat": chat_info} + # 序列化時間物件,避免 JSON 序列化炸裂 + serialized_chat_info = _serialize_firestore_data(chat_info) + return {"success": True, "chat": serialized_chat_info} except Exception as e: logger.error(f"創建對話時發生錯誤: {e}") return {"success": False, "error": str(e)} @@ -401,7 +435,9 @@ async def get_user_chats(user_id): chats = await _asyncio.to_thread(_fetch_chats) logger.info(f"獲取到用戶 {user_id} 的 {len(chats)} 個對話") - return {"success": True, "chats": chats} + # 序列化 Firestore 時間物件,避免 JSON 序列化炸裂 + serialized_chats = _serialize_firestore_data(chats) + return {"success": True, "chats": serialized_chats} except Exception as e: logger.error(f"獲取用戶對話時發生錯誤: {e}") return {"success": False, "error": str(e)} @@ -524,7 +560,8 @@ async def get_chat_messages(chat_id: str, limit: int | None = None, ascending: b messages = await _asyncio.to_thread(_query) if messages: - return messages + # 序列化 Firestore 時間物件,避免 JSON 序列化炸裂 + return _serialize_firestore_data(messages) # 向後相容:若子集合無資料,嘗試讀取舊頂層 messages 集合 if messages_collection is None: @@ -557,7 +594,8 @@ async def get_chat_messages(chat_id: str, limit: int | None = None, ascending: b logger.warning(f"回填 legacy messages 失敗(可忽略): {backfill_err}") await _asyncio.to_thread(_backfill) - return view_messages + # 序列化 Firestore 時間物件,避免 JSON 序列化炸裂 + return _serialize_firestore_data(view_messages) except Exception as e: logger.error(f"讀取對話消息失敗: {e}") return [] @@ -730,12 +768,14 @@ async def get_user_by_speaker_label(speaker_label: str): return None data = doc.to_dict() - return { + result = { "id": data.get("user_id"), "name": data.get("name", ""), "email": data.get("email", ""), "created_at": data.get("created_at"), } + # 序列化 Firestore 時間物件,避免 JSON 序列化炸裂 + return _serialize_firestore_data(result) except Exception as e: logger.error(f"查詢語音標籤對應用戶時發生錯誤: {e}") return None @@ -912,7 +952,9 @@ async def get_user_env_current(user_id: str) -> Dict[str, Any]: data = await _asyncio.to_thread(_read) if not data: return {"success": False, "error": "NOT_FOUND"} - return {"success": True, "context": data} + # 序列化 Firestore 時間物件,避免 JSON 序列化炸裂 + serialized_data = _serialize_firestore_data(data) + return {"success": True, "context": serialized_data} except Exception as e: logger.error(f"讀取環境現況失敗: {e}") return {"success": False, "error": str(e)} @@ -928,7 +970,9 @@ async def get_geo_cache(geohash7: str) -> Optional[Dict[str, Any]]: def _read(): doc = geo_cache_collection.document(geohash7).get() return doc.to_dict() if doc.exists else None - return await _asyncio.to_thread(_read) + result = await _asyncio.to_thread(_read) + # 序列化 Firestore 時間物件,避免 JSON 序列化炸裂 + return _serialize_firestore_data(result) if result else None except Exception as e: logger.warning(f"讀取 geo_cache 失敗: {e}") return None @@ -959,7 +1003,9 @@ async def get_route_cache(key: str) -> Optional[Dict[str, Any]]: def _read(): doc = route_cache_collection.document(key).get() return doc.to_dict() if doc.exists else None - return await _asyncio.to_thread(_read) + result = await _asyncio.to_thread(_read) + # 序列化 Firestore 時間物件,避免 JSON 序列化炸裂 + return _serialize_firestore_data(result) if result else None except Exception as e: logger.warning(f"讀取 route_cache 失敗: {e}") return None @@ -1024,7 +1070,9 @@ async def get_user_memories( await _asyncio.to_thread(_mark_accessed, [m["memory_id"] for m in memories]) logger.info(f"獲取到用戶 {user_id} 的 {len(memories)} 條記憶") - return {"success": True, "memories": memories} + # 序列化 Firestore 時間物件,避免 JSON 序列化炸裂 + serialized_memories = _serialize_firestore_data(memories) + return {"success": True, "memories": serialized_memories} except Exception as e: logger.error(f"獲取記憶時發生錯誤: {e}") diff --git a/core/emotion_care_manager.py b/core/emotion_care_manager.py index 98fed23ab21a99e9af3c38ccbc1b4f05c5a25325..c39319dada5f7c403d88b1937784aecc45ee3035 100644 --- a/core/emotion_care_manager.py +++ b/core/emotion_care_manager.py @@ -90,13 +90,15 @@ class EmotionCareManager: return True @classmethod - def check_release(cls, user_id: str, message: str, chat_id: Optional[str] = None) -> bool: + def check_release(cls, user_id: str, message: str, chat_id: Optional[str] = None, emotion: Optional[str] = None) -> bool: """ - 檢查用戶訊息是否包含解除關鍵字 + 檢查用戶訊息是否包含解除關鍵字或情緒恢復為 neutral 參數: user_id: 用戶 ID message: 用戶訊息 + chat_id: 對話 ID(可選) + emotion: 當前偵測到的情緒(可選) 返回: bool: 是否解除關懷模式(True=解除,False=繼續關懷) @@ -105,18 +107,29 @@ class EmotionCareManager: if not state or not state.get("in_care_mode", False): return False + # 優先檢查情緒:如果偵測到 neutral,立即解除關懷模式 + if emotion and emotion.lower() == "neutral": + original_emotion = state.get("emotion", "unknown") + duration = time.time() - state.get("start_time", 0) + + state["in_care_mode"] = False + state["last_exit_time"] = time.time() + + logger.info(f"✅ 用戶 {user_id}(chat={chat_id or 'default'})情緒恢復為 neutral({original_emotion} → neutral),解除關懷模式(持續 {duration:.1f}秒)") + return True + # 檢查是否包含解除關鍵字 message_lower = message.lower().strip() for keyword in cls.RELEASE_KEYWORDS: if keyword in message_lower: # 解除關懷模式 - emotion = state.get("emotion", "unknown") + original_emotion = state.get("emotion", "unknown") duration = time.time() - state.get("start_time", 0) state["in_care_mode"] = False state["last_exit_time"] = time.time() - logger.info(f"✅ 用戶 {user_id}(chat={chat_id or 'default'})情緒恢復({emotion} → 正常),解除關懷模式(持續 {duration:.1f}秒)") + logger.info(f"✅ 用戶 {user_id}(chat={chat_id or 'default'})情緒恢復({original_emotion} → 正常),解除關懷模式(持續 {duration:.1f}秒)") return True return False diff --git a/core/exceptions.py b/core/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..c33971751c87d1718cf2653d3e7fc25af28684c1 --- /dev/null +++ b/core/exceptions.py @@ -0,0 +1,267 @@ +""" +統一異常處理 +定義自訂異常類別和錯誤響應格式 +""" + +from typing import Optional, Dict, Any +from fastapi import HTTPException +from fastapi.responses import JSONResponse + + +class BloomWareException(Exception): + """Bloom Ware 基礎異常類別""" + + def __init__( + self, + message: str, + code: str = "UNKNOWN_ERROR", + status_code: int = 500, + details: Optional[Dict[str, Any]] = None, + ): + self.message = message + self.code = code + self.status_code = status_code + self.details = details or {} + super().__init__(message) + + def to_dict(self) -> Dict[str, Any]: + """轉換為字典格式""" + return { + "success": False, + "error": { + "code": self.code, + "message": self.message, + "details": self.details, + } + } + + def to_response(self) -> JSONResponse: + """轉換為 JSON 響應""" + return JSONResponse( + status_code=self.status_code, + content=self.to_dict(), + ) + + +# ==================== 認證相關異常 ==================== + +class AuthenticationError(BloomWareException): + """認證錯誤""" + + def __init__(self, message: str = "認證失敗", details: Optional[Dict[str, Any]] = None): + super().__init__( + message=message, + code="AUTHENTICATION_ERROR", + status_code=401, + details=details, + ) + + +class TokenExpiredError(AuthenticationError): + """Token 過期""" + + def __init__(self): + super().__init__( + message="Token 已過期,請重新登入", + details={"reason": "token_expired"}, + ) + + +class InvalidTokenError(AuthenticationError): + """無效的 Token""" + + def __init__(self): + super().__init__( + message="無效的認證令牌", + details={"reason": "invalid_token"}, + ) + + +class PermissionDeniedError(BloomWareException): + """權限不足""" + + def __init__(self, message: str = "權限不足"): + super().__init__( + message=message, + code="PERMISSION_DENIED", + status_code=403, + ) + + +# ==================== 資源相關異常 ==================== + +class ResourceNotFoundError(BloomWareException): + """資源不存在""" + + def __init__(self, resource_type: str, resource_id: str): + super().__init__( + message=f"{resource_type} 不存在", + code="RESOURCE_NOT_FOUND", + status_code=404, + details={ + "resource_type": resource_type, + "resource_id": resource_id, + }, + ) + + +class ChatNotFoundError(ResourceNotFoundError): + """對話不存在""" + + def __init__(self, chat_id: str): + super().__init__("對話", chat_id) + + +class UserNotFoundError(ResourceNotFoundError): + """用戶不存在""" + + def __init__(self, user_id: str): + super().__init__("用戶", user_id) + + +# ==================== 驗證相關異常 ==================== + +class ValidationError(BloomWareException): + """驗證錯誤""" + + def __init__(self, field: str, message: str): + super().__init__( + message=f"參數 '{field}' 驗證失敗: {message}", + code="VALIDATION_ERROR", + status_code=400, + details={"field": field}, + ) + + +class RateLimitExceededError(BloomWareException): + """請求頻率超限""" + + def __init__(self, retry_after: int = 60): + super().__init__( + message="請求頻率超過限制,請稍後再試", + code="RATE_LIMIT_EXCEEDED", + status_code=429, + details={"retry_after": retry_after}, + ) + + +# ==================== 服務相關異常 ==================== + +class ServiceUnavailableError(BloomWareException): + """服務不可用""" + + def __init__(self, service_name: str): + super().__init__( + message=f"{service_name} 服務暫時不可用", + code="SERVICE_UNAVAILABLE", + status_code=503, + details={"service": service_name}, + ) + + +class DatabaseError(BloomWareException): + """數據庫錯誤""" + + def __init__(self, message: str = "數據庫操作失敗"): + super().__init__( + message=message, + code="DATABASE_ERROR", + status_code=500, + ) + + +class AIServiceError(BloomWareException): + """AI 服務錯誤""" + + def __init__(self, message: str = "AI 服務暫時不可用"): + super().__init__( + message=message, + code="AI_SERVICE_ERROR", + status_code=503, + ) + + +class ExternalAPIError(BloomWareException): + """外部 API 錯誤""" + + def __init__(self, api_name: str, message: str): + super().__init__( + message=f"{api_name} API 錯誤: {message}", + code="EXTERNAL_API_ERROR", + status_code=502, + details={"api": api_name}, + ) + + +# ==================== 語音相關異常 ==================== + +class VoiceAuthError(BloomWareException): + """語音認證錯誤""" + + def __init__(self, message: str, reason: str): + super().__init__( + message=message, + code="VOICE_AUTH_ERROR", + status_code=400, + details={"reason": reason}, + ) + + +class SpeakerLabelTakenError(VoiceAuthError): + """語音標籤已被使用""" + + def __init__(self): + super().__init__( + message="此語音標籤已被其他用戶綁定", + reason="speaker_label_taken", + ) + + +# ==================== 異常處理器 ==================== + +def create_error_response( + code: str, + message: str, + status_code: int = 500, + details: Optional[Dict[str, Any]] = None, +) -> JSONResponse: + """創建標準錯誤響應""" + return JSONResponse( + status_code=status_code, + content={ + "success": False, + "error": { + "code": code, + "message": message, + "details": details or {}, + } + } + ) + + +def handle_exception(exc: Exception) -> JSONResponse: + """ + 統一異常處理 + + 將各種異常轉換為標準 JSON 響應 + """ + if isinstance(exc, BloomWareException): + return exc.to_response() + + if isinstance(exc, HTTPException): + return create_error_response( + code="HTTP_ERROR", + message=exc.detail, + status_code=exc.status_code, + ) + + # 未知異常 + import logging + logger = logging.getLogger("core.exceptions") + logger.exception(f"未處理的異常: {exc}") + + return create_error_response( + code="INTERNAL_ERROR", + message="內部伺服器錯誤", + status_code=500, + ) diff --git a/core/intent_detector.py b/core/intent_detector.py new file mode 100644 index 0000000000000000000000000000000000000000..7eb14712aecaeda0625c7f5dd915615e49767ae0 --- /dev/null +++ b/core/intent_detector.py @@ -0,0 +1,272 @@ +""" +意圖檢測器 +2025 最佳實踐:使用 OpenAI 原生 Function Calling 進行意圖檢測 + +核心改進: +1. 不再使用巨大的 system_prompt 描述每個工具 +2. 直接使用 OpenAI tools 參數傳遞工具定義 +3. GPT 原生選擇工具並生成結構化參數 +4. 新增工具只需註冊到 Registry,不需更新任何 prompt +""" + +import json +import hashlib +import time +import logging +from typing import Dict, Any, Optional, Tuple, List + +from core.tool_registry import tool_registry +from core.logging import get_logger + +logger = get_logger("core.intent_detector") + + +class IntentDetector: + """ + 意圖檢測器 + + 使用 OpenAI 原生 Function Calling 進行意圖檢測, + 不需要自定義 prompt 描述每個工具。 + """ + + # 情緒列表 + EMOTIONS = ["neutral", "happy", "sad", "angry", "fear", "surprise"] + + # 快取 TTL(秒) + CACHE_TTL = 300.0 + + def __init__(self): + self._cache: Dict[str, Tuple[bool, Optional[Dict[str, Any]], float]] = {} + + async def detect( + self, + message: str, + user_id: Optional[str] = None, + include_location_tools: bool = True, + ) -> Tuple[bool, Optional[Dict[str, Any]]]: + """ + 檢測用戶消息中的意圖 + + Args: + message: 用戶消息 + user_id: 用戶 ID(用於日誌) + include_location_tools: 是否包含需要位置的工具 + + Returns: + (是否檢測到工具調用, 意圖數據) + """ + # 檢查快取 + cache_key = hashlib.md5(message.encode()).hexdigest() + if cache_key in self._cache: + has_intent, intent_data, cached_time = self._cache[cache_key] + if time.time() - cached_time < self.CACHE_TTL: + logger.debug(f"💾 意圖快取命中: {message[:50]}...") + return has_intent, intent_data + else: + del self._cache[cache_key] + + logger.info(f"🔍 檢測意圖: \"{message[:100]}...\"") + + # 檢查特殊命令 + special_result = self._check_special_commands(message) + if special_result: + return special_result + + # 使用 OpenAI Function Calling 進行意圖檢測 + try: + result = await self._detect_with_function_calling( + message, + include_location_tools=include_location_tools, + ) + + # 寫入快取 + self._cache[cache_key] = (*result, time.time()) + + return result + + except Exception as e: + logger.error(f"❌ 意圖檢測失敗: {e}") + # 降級:使用關鍵字匹配 + return self._keyword_fallback(message) + + def _check_special_commands(self, message: str) -> Optional[Tuple[bool, Dict[str, Any]]]: + """檢查特殊命令""" + for command in ["功能列表", "有什麼功能", "能做什麼"]: + if command in message: + logger.info(f"檢測到特殊命令: {command}") + return True, { + "type": "special_command", + "command": "feature_list" + } + return None + + async def _detect_with_function_calling( + self, + message: str, + include_location_tools: bool = True, + ) -> Tuple[bool, Optional[Dict[str, Any]]]: + """ + 使用 OpenAI Function Calling 進行意圖檢測 + + 核心邏輯: + 1. 將所有工具以 OpenAI tools 格式傳遞 + 2. GPT 自動選擇最適合的工具 + 3. 如果 GPT 不選擇任何工具,視為一般聊天 + """ + import services.ai_service as ai_service + from core.reasoning_strategy import get_optimal_reasoning_effort + + # 取得所有工具定義(OpenAI 格式) + tools = tool_registry.get_openai_tools( + include_location_tools=include_location_tools, + strict=True, + ) + + if not tools: + logger.warning("⚠️ 沒有可用的工具") + return False, {"emotion": "neutral"} + + # 建構精簡的 system prompt(只處理情緒和特殊規則) + system_prompt = self._build_system_prompt() + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": message} + ] + + # 使用 OpenAI Function Calling + optimal_effort = get_optimal_reasoning_effort("intent_detection") + logger.info(f"🧠 意圖檢測推理強度: {optimal_effort}") + + try: + response = await ai_service.generate_response_with_tools( + messages=messages, + tools=tools, + user_id="intent_detection", + model="gpt-5-nano", + reasoning_effort=optimal_effort, + ) + + return self._parse_function_calling_response(response) + + except Exception as e: + logger.error(f"❌ Function Calling 失敗: {e}") + raise + + def _build_system_prompt(self) -> str: + """ + 建構精簡的 system prompt + + 注意:不再描述每個工具,工具定義由 tools 參數傳遞 + """ + return """你是一個智能助手,根據用戶需求選擇合適的工具。 + +規則: +1. 如果用戶需求可以用工具解決,選擇最適合的工具 +2. 如果是一般聊天或問候,不要選擇任何工具 +3. 工具參數盡量從用戶消息中提取,無法確定的使用合理預設值 + +特殊處理: +- 天氣查詢:城市名稱使用英文(台北→Taipei, 東京→Tokyo) +- 匯率查詢:貨幣使用 ISO 4217 代碼(美元→USD, 台幣→TWD) +- 公車查詢:route_name 必須是路線號碼(如 307、紅30),不是目的地名稱 +- 火車查詢:「往XX」表示 destination_station,不是 origin_station +- 位置查詢:「我在哪」使用 reverse_geocode,不需要參數 +- YouBike 查詢:任何提到 YouBike/Ubike/微笑單車 的請求使用 tdx_youbike + +情緒判斷: +根據用戶消息的語氣判斷情緒,在回應中包含 emotion 欄位: +- neutral: 平靜、中性 +- happy: 開心、興奮 +- sad: 難過、沮喪 +- angry: 生氣、煩躁 +- fear: 恐懼、擔心 +- surprise: 驚訝、意外""" + + def _parse_function_calling_response( + self, + response: Dict[str, Any], + ) -> Tuple[bool, Optional[Dict[str, Any]]]: + """ + 解析 Function Calling 回應 + + Args: + response: OpenAI API 回應 + + Returns: + (是否檢測到工具調用, 意圖數據) + """ + # 檢查是否有 tool_calls + tool_calls = response.get("tool_calls", []) + + if tool_calls: + # 取第一個工具調用 + tool_call = tool_calls[0] + function = tool_call.get("function", {}) + tool_name = function.get("name", "") + arguments_str = function.get("arguments", "{}") + + try: + arguments = json.loads(arguments_str) + except json.JSONDecodeError: + arguments = {} + + logger.info(f"✅ GPT 選擇工具: {tool_name}") + logger.debug(f"工具參數: {arguments}") + + # 提取情緒(從 content 或預設) + emotion = self._extract_emotion_from_response(response) + + return True, { + "type": "mcp_tool", + "tool_name": tool_name, + "arguments": arguments, + "emotion": emotion, + } + + # 沒有工具調用,視為一般聊天 + logger.info("💬 GPT 判斷為一般聊天") + emotion = self._extract_emotion_from_response(response) + + return False, {"emotion": emotion} + + def _extract_emotion_from_response(self, response: Dict[str, Any]) -> str: + """從回應中提取情緒""" + # 嘗試從 content 中提取 + content = response.get("content", "") + if content: + for emotion in self.EMOTIONS: + if emotion in content.lower(): + return emotion + + return "neutral" + + def _keyword_fallback(self, message: str) -> Tuple[bool, Optional[Dict[str, Any]]]: + """關鍵字匹配降級方案""" + message_lower = message.lower() + + # 取得所有工具摘要 + summaries = tool_registry.get_summaries() + + for summary in summaries: + keywords = summary.get("keywords", []) + for keyword in keywords: + if keyword.lower() in message_lower: + logger.info(f"🔑 關鍵字匹配: {keyword} → {summary['name']}") + return True, { + "type": "mcp_tool", + "tool_name": summary["name"], + "arguments": {}, + "emotion": "neutral", + } + + return False, {"emotion": "neutral"} + + def clear_cache(self) -> None: + """清除快取""" + self._cache.clear() + logger.info("🗑️ 意圖快取已清除") + + +# 全域單例 +intent_detector = IntentDetector() diff --git a/core/logging.py b/core/logging.py new file mode 100644 index 0000000000000000000000000000000000000000..028a1c4c719adf6aff1c3377de618a7d3e5757f1 --- /dev/null +++ b/core/logging.py @@ -0,0 +1,105 @@ +""" +統一日誌配置 +集中管理所有模組的日誌設定 + +使用方式: + from core.logging import get_logger + logger = get_logger(__name__) +""" + +import os +import logging +from typing import Optional + +# 全域日誌等級(只讀取一次) +_LOG_LEVEL_NAME = os.getenv("BLOOMWARE_LOG_LEVEL", "WARNING").upper() +_LOG_LEVEL = getattr(logging, _LOG_LEVEL_NAME, logging.WARNING) + +# 日誌格式 +_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + + +def get_log_level() -> int: + """獲取日誌等級""" + return _LOG_LEVEL + + +def setup_logging( + name: Optional[str] = None, + level: Optional[int] = None, +) -> logging.Logger: + """ + 設置日誌配置 + + Args: + name: 日誌名稱(None 表示 root logger) + level: 日誌等級(None 表示使用環境變數) + + Returns: + 配置好的 Logger 實例 + """ + if level is None: + level = _LOG_LEVEL + + # 配置格式 + formatter = logging.Formatter(_LOG_FORMAT) + + # 獲取或創建 logger + logger = logging.getLogger(name) + logger.setLevel(level) + + # 避免重複添加 handler + if not logger.handlers: + # 控制台 handler + console_handler = logging.StreamHandler() + console_handler.setLevel(level) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + # 防止日誌重複輸出 + logger.propagate = False + + return logger + + +def get_logger(name: str) -> logging.Logger: + """ + 獲取已配置的 Logger(推薦使用) + + Args: + name: 日誌名稱,建議使用 __name__ + + Returns: + Logger 實例 + + Example: + from core.logging import get_logger + logger = get_logger(__name__) + logger.info("Hello") + """ + return setup_logging(name) + + +def get_level_name() -> str: + """獲取當前日誌等級名稱""" + return _LOG_LEVEL_NAME + + +# 預設配置 root logger +_root_configured = False + +def configure_root_logger(): + """配置 root logger(只執行一次)""" + global _root_configured + if not _root_configured: + level = get_log_level() + logging.basicConfig( + level=level, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[logging.StreamHandler()] + ) + _root_configured = True + + +# 自動配置 +configure_root_logger() diff --git a/core/memory_system.py b/core/memory_system.py index 5ece6392539c6d6e07990de63e8be30f8dc32e49..ccc34d08602d8d2a1a65f66fa753fb13f2f9689f 100644 --- a/core/memory_system.py +++ b/core/memory_system.py @@ -1,31 +1,19 @@ -import os -import logging +import json from typing import List, Dict, Any, Optional, Tuple from datetime import datetime -import json -# 設置日誌 -LOG_LEVEL_NAME = os.getenv("BLOOMWARE_LOG_LEVEL", "WARNING").upper() -LOG_LEVEL = getattr(logging, LOG_LEVEL_NAME, logging.WARNING) -logging.basicConfig(level=LOG_LEVEL, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') -logger = logging.getLogger("MemorySystem") -logger.setLevel(LOG_LEVEL) +# 統一日誌配置 +from core.logging import get_logger +logger = get_logger("MemorySystem") + +# 統一 OpenAI 客戶端 +from core.ai_client import get_openai_client + +def _get_memory_client(): + """取得記憶系統用的 OpenAI 客戶端""" + return get_openai_client() -# 載入環境變數 -from dotenv import load_dotenv -load_dotenv() -# 嘗試導入 OpenAI -try: - from openai import OpenAI - memory_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) - logger.info("記憶系統成功導入 OpenAI SDK") -except ImportError: - logger.error("Failed to import OpenAI for memory system") - memory_client = None -except Exception as e: - logger.error(f"初始化記憶系統 OpenAI 客戶端時出錯: {e}") - memory_client = None # 導入數據庫函數 try: @@ -139,7 +127,8 @@ class MemoryAnalyzer: async def analyze_conversation(self, user_message: str, assistant_response: str = "", conversation_history: List[Dict] = None) -> List[Dict[str, Any]]: """使用AI分析對話內容,提取重要記憶""" - if not memory_client: + client = _get_memory_client() + if not client: logger.warning("OpenAI客戶端不可用,跳過AI記憶分析") return [] @@ -196,11 +185,11 @@ class MemoryAnalyzer: else: max_tokens_value = 2000 - response = memory_client.chat.completions.create( + response = client.chat.completions.create( model="gpt-5-nano", messages=messages, - max_completion_tokens=max_tokens_value, # 修正:使用 max_completion_tokens 而非 max_tokens - reasoning_effort="low" # 記憶分析需要理解,但不需深度推理 + max_completion_tokens=max_tokens_value, + reasoning_effort="low" ) break # 成功後跳出重試循環 @@ -279,7 +268,7 @@ class MemoryManager: # 2. 使用AI分析提取記憶(如果可用) ai_memories = [] - if memory_client: + if _get_memory_client(): ai_memories = await self.analyzer.analyze_conversation( user_message, assistant_response, conversation_history ) @@ -439,3 +428,6 @@ class MemoryManager: # 全局記憶管理器實例 memory_manager = MemoryManager() + +# 向後兼容別名 +memory_system = memory_manager diff --git a/core/pipeline.py b/core/pipeline.py index 413453478b9fa6d4dcd3ba269b6b23c453c48e9b..11d6156e2869945b92faeedf1522ea90bed1270e 100644 --- a/core/pipeline.py +++ b/core/pipeline.py @@ -76,10 +76,23 @@ class ChatPipeline: if not user_message or not user_message.strip(): return PipelineResult(text="我沒有收到您的消息,請重新輸入。", is_fallback=True, reason="empty") - # 0) 檢查是否在關懷模式(新增) + # 0) 先進行意圖偵測以提取情緒(需要在關懷模式檢查前執行) + detect_res = await self._with_timeout( + self._intent_detector(user_message), self._detect_timeout, reason="detect" + ) + if isinstance(detect_res, PipelineResult): + return detect_res + has_feature, intent_data = detect_res + + # 提取情緒 + emotion = intent_data.get("emotion", "neutral") if intent_data else "neutral" + emotion_value = emotion or "neutral" + logger.info(f"😊 用戶情緒: {emotion}") + + # 1) 檢查是否在關懷模式 if user_id and EmotionCareManager.is_in_care_mode(user_id, chat_id): - # 檢查是否解除關懷模式 - if EmotionCareManager.check_release(user_id, user_message, chat_id): + # 檢查是否解除關懷模式(傳入情緒資訊) + if EmotionCareManager.check_release(user_id, user_message, chat_id, emotion=emotion_value): logger.info(f"✅ 用戶 {user_id} 情緒恢復,解除關懷模式,繼續正常流程") # 解除後繼續正常流程 else: @@ -107,22 +120,9 @@ class ChatPipeline: return PipelineResult(text="我在這裡陪你,隨時可以聊聊。", is_fallback=True, reason="ai-care-empty") return PipelineResult(text=text, is_fallback=False, meta={"care_mode": True, "emotion": care_emotion}) - # 1) 意圖偵測(限時) - detect_res = await self._with_timeout( - self._intent_detector(user_message), self._detect_timeout, reason="detect" - ) - if isinstance(detect_res, PipelineResult): - return detect_res - has_feature, intent_data = detect_res - - # 提取情緒(新增) - emotion = intent_data.get("emotion", "neutral") if intent_data else "neutral" - emotion_value = emotion or "neutral" - logger.info(f"😊 用戶情緒: {emotion}") - - # 檢查是否需要進入關懷模式(新增) - if user_id and EmotionCareManager.check_and_enter_care_mode(user_id, emotion, chat_id): - logger.warning(f"⚠️ 偵測到極端情緒 [{emotion}],進入關懷模式") + # 2) 檢查是否需要進入關懷模式 + if user_id and EmotionCareManager.check_and_enter_care_mode(user_id, emotion_value, chat_id): + logger.warning(f"⚠️ 偵測到極端情緒 [{emotion_value}],進入關懷模式") # 立即使用關懷模式 AI 回應 ai_res = await self._with_timeout( self._ai_generator( @@ -132,8 +132,8 @@ class ChatPipeline: request_id, chat_id, use_care_mode=True, - care_emotion=emotion, - emotion_label=emotion, + care_emotion=emotion_value, + emotion_label=emotion_value, ), self._ai_timeout, reason="ai-care", @@ -146,9 +146,9 @@ class ChatPipeline: # 第一次進入關懷模式時,附加退出提示(新增) exit_hint = "\n\n💙 關懷模式已啟動。說「我沒事了」可以退出。" - return PipelineResult(text=text + exit_hint, is_fallback=False, meta={"care_mode": True, "emotion": emotion}) + return PipelineResult(text=text + exit_hint, is_fallback=False, meta={"care_mode": True, "emotion": emotion_value}) - # 2) 有功能 → 功能處理(限時) + # 3) 有功能 → 功能處理(限時) if has_feature and intent_data: feat_res = await self._with_timeout( self._feature_processor(intent_data, user_id, user_message, chat_id), @@ -193,7 +193,7 @@ class ChatPipeline: meta={"emotion": emotion_value}, ) - # 3) 無功能 → 一般聊天(限時) + # 4) 無功能 → 一般聊天(限時) # 注意:不傳 messages,改傳 user_message,讓 ai_generator 自動載入歷史對話和記憶 ai_res = await self._with_timeout( self._ai_generator( diff --git a/core/prompts/__init__.py b/core/prompts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d11c9ee34d9f3ca5bec0b7cfd1faf5dfe75161a8 --- /dev/null +++ b/core/prompts/__init__.py @@ -0,0 +1,13 @@ +""" +Prompt 模板管理 +統一管理 AI 系統提示詞,支援動態組合 +""" + +from .intent_detection import get_intent_prompt, TOOL_RULES +from .care_mode import CARE_MODE_PROMPT + +__all__ = [ + "get_intent_prompt", + "TOOL_RULES", + "CARE_MODE_PROMPT", +] diff --git a/core/prompts/care_mode.py b/core/prompts/care_mode.py new file mode 100644 index 0000000000000000000000000000000000000000..4e8c4eba3f54a1e7825ce978c34afa369fb5100d --- /dev/null +++ b/core/prompts/care_mode.py @@ -0,0 +1,45 @@ +""" +情緒關懷模式 Prompt +精簡化設計,保持關懷品質 +""" + +CARE_MODE_PROMPT = """你是 BloomWare 的情緒關懷助手「小花」。你的任務是傾聽、陪伴。 + +【回應原則】 +1. 第一句貼近用戶的核心感受,讓對方感受到被理解 +2. 第二句溫柔陪伴或追問,邀請分享 +3. 句式自然口語,避免罐頭話術 + +【限制】 +- 最多 2 句話、60 字以內 +- 禁止:指示性建議、醫療診斷、教科書式說法 +- 禁止:重複相同句型 + +【範例】 +用戶:「我好難過」 +你:「聽見你說好難過,心裡一定很不好受。想聊聊發生了什麼嗎?」 + +用戶:「我很生氣」 +你:「這件事讓你超級生氣,情緒一定卡著。要不要說說最困擾的地方?」""" + + +def get_care_prompt(emotion: str = None, user_name: str = None) -> str: + """ + 生成關懷模式 Prompt + + Args: + emotion: 用戶情緒標籤 + user_name: 用戶名稱 + + Returns: + 關懷模式 System Prompt + """ + prompt = CARE_MODE_PROMPT + + if emotion: + prompt = f"用戶情緒:{emotion}\n\n{prompt}" + + if user_name: + prompt = f"用戶名稱:{user_name}\n\n{prompt}" + + return prompt diff --git a/core/prompts/intent_detection.py b/core/prompts/intent_detection.py new file mode 100644 index 0000000000000000000000000000000000000000..1ce1b337736ea4390fb2ba8320bbd5bbef0c942f --- /dev/null +++ b/core/prompts/intent_detection.py @@ -0,0 +1,93 @@ +""" +意圖檢測 Prompt 模板 +精簡化設計,減少 token 消耗約 40% +""" + +# 工具特定規則(按需載入) +TOOL_RULES = { + "weather": """天氣查詢:城市必須用英文(台北→Taipei, 高雄→Kaohsiung),預設 Taipei""", + + "exchange": """匯率查詢:貨幣必須用 ISO 4217 代碼(美元→USD, 台幣→TWD),預設 USD→TWD""", + + "news": """新聞查詢:「新聞」「消息」「報導」→ news_query,country/language 用英文代碼(tw, zh)""", + + "bus": """公車查詢:「公車」「巴士」→ tdx_bus_arrival +- route_name 必須是公車路線號碼(如 137、307、紅30) +- 「往 X 的公車」「去 X 的公車」不是路線查詢,應使用 directions 或 forward_geocode +- 「附近公車」「公車站」不需 route_name,系統用 GPS 查詢 +例:「137公車」→ tdx_bus_arrival:route_name=137 +例:「往台北的公車」→ forward_geocode:query=台北(這是導航需求)""", + + "train": """台鐵查詢:「火車」「台鐵」→ tdx_train +「往XX」「到XX」是目的地(destination_station),不是起點 +沒說起點就不填 origin_station,讓 GPS 決定 +例:「往台北的火車」→ tdx_train:destination_station=台北""", + + "youbike": """YouBike 查詢:「YouBike」「Ubike」「微笑單車」「共享單車」→ tdx_youbike +城市參數必須用英文(台北→Taipei, 桃園→Taoyuan),站名可用中文 +不是 tdx_parking!這是單車不是停車場""", + + "location": """位置查詢: +「我在哪」「這是哪裡」→ reverse_geocode(不需參數) +「怎麼去 X」→ forward_geocode:query=X""", +} + +# 情緒標籤說明 +EMOTION_RULES = """情緒判斷:neutral/happy/sad/angry/fear/surprise +- happy: 開心、興奮(「好開心!」「太棒了」) +- sad: 難過、沮喪(「好難過」「心情不好」) +- angry: 生氣、煩躁(「煩死了」「氣死我了」) +- fear: 恐懼、擔心(「好害怕」「怎麼辦」) +- surprise: 驚訝(「什麼!」「真的假的」) +- neutral: 其他""" + + +def get_intent_prompt(tools_description: str, include_rules: list = None) -> str: + """ + 生成意圖檢測 Prompt + + Args: + tools_description: 可用工具描述 + include_rules: 要包含的工具規則列表,None 表示全部 + + Returns: + 精簡化的 System Prompt + """ + # 基礎 Prompt + base = f"""你是意圖解析助手。分析用戶消息,決定是否調用工具。 + +可用工具: +{tools_description} + +""" + + # 添加工具規則 + if include_rules is None: + include_rules = list(TOOL_RULES.keys()) + + rules_text = "\n".join( + f"- {TOOL_RULES[rule]}" + for rule in include_rules + if rule in TOOL_RULES + ) + + if rules_text: + base += f"""工具規則: +{rules_text} + +""" + + # 添加情緒規則 + base += f"""{EMOTION_RULES} + +回應格式: +- is_tool_call: true/false +- tool_name: 工具名稱:參數(is_tool_call=true 時) +- emotion: 情緒標籤 + +示例: +- "台北天氣" → {{"is_tool_call": true, "tool_name": "weather_query:city=Taipei", "emotion": "neutral"}} +- "你好" → {{"is_tool_call": false, "tool_name": "", "emotion": "neutral"}} +- "我好難過" → {{"is_tool_call": false, "tool_name": "", "emotion": "sad"}}""" + + return base diff --git a/core/reasoning_strategy.py b/core/reasoning_strategy.py index aff81335b01a57febca5484946049c84908436ea..66be06d0caf37b87ec433dfce0874afec365933f 100644 --- a/core/reasoning_strategy.py +++ b/core/reasoning_strategy.py @@ -43,10 +43,10 @@ class ReasoningStrategy: reasoning_effort: minimal/low/medium/high """ - # 🔥 規則 1:意圖檢測必須極速(minimal) + # 🔥 規則 1:意圖檢測使用 low reasoning(平衡速度與準確度) if task_type == "intent_detection": - logger.debug("🧠 意圖檢測 → minimal reasoning(極速模式)") - return "minimal" + logger.debug("🧠 意圖檢測 → low reasoning(快速但準確)") + return "low" # 🔥 規則 2:關懷模式優先速度(用戶情緒不佳時不要讓他等) if user_emotion in ["sad", "angry", "fear"]: diff --git a/core/retry.py b/core/retry.py new file mode 100644 index 0000000000000000000000000000000000000000..8a77c38b5c84184db021894d32e93b2a611b536e --- /dev/null +++ b/core/retry.py @@ -0,0 +1,150 @@ +""" +API 重試機制 +自動重試失敗的 API 調用,支援指數退避 +""" + +import asyncio +import functools +import random +from typing import Callable, TypeVar, Any, Optional, Tuple, Type + +from core.logging import get_logger + +logger = get_logger("core.retry") + +T = TypeVar("T") + +# 預設重試配置 +DEFAULT_MAX_RETRIES = 3 +DEFAULT_BASE_DELAY = 1.0 # 秒 +DEFAULT_MAX_DELAY = 30.0 # 秒 +DEFAULT_EXPONENTIAL_BASE = 2 + + +class RetryConfig: + """重試配置""" + + def __init__( + self, + max_retries: int = DEFAULT_MAX_RETRIES, + base_delay: float = DEFAULT_BASE_DELAY, + max_delay: float = DEFAULT_MAX_DELAY, + exponential_base: float = DEFAULT_EXPONENTIAL_BASE, + jitter: bool = True, + retryable_exceptions: Tuple[Type[Exception], ...] = (Exception,), + ): + self.max_retries = max_retries + self.base_delay = base_delay + self.max_delay = max_delay + self.exponential_base = exponential_base + self.jitter = jitter + self.retryable_exceptions = retryable_exceptions + + def calculate_delay(self, attempt: int) -> float: + """計算重試延遲(指數退避 + 抖動)""" + delay = self.base_delay * (self.exponential_base ** attempt) + delay = min(delay, self.max_delay) + + if self.jitter: + # 添加 ±25% 的隨機抖動 + jitter_range = delay * 0.25 + delay += random.uniform(-jitter_range, jitter_range) + + return max(0, delay) + + +async def retry_async( + func: Callable[..., Any], + *args, + config: Optional[RetryConfig] = None, + **kwargs, +) -> Any: + """ + 異步重試執行函數 + + Args: + func: 要執行的異步函數 + *args: 函數參數 + config: 重試配置 + **kwargs: 函數關鍵字參數 + + Returns: + 函數執行結果 + + Raises: + 最後一次重試的異常 + """ + if config is None: + config = RetryConfig() + + last_exception = None + + for attempt in range(config.max_retries + 1): + try: + return await func(*args, **kwargs) + + except config.retryable_exceptions as e: + last_exception = e + + if attempt < config.max_retries: + delay = config.calculate_delay(attempt) + logger.warning( + f"重試 {attempt + 1}/{config.max_retries}: " + f"{func.__name__} 失敗 ({type(e).__name__}: {e})," + f"{delay:.2f}s 後重試" + ) + await asyncio.sleep(delay) + else: + logger.error( + f"重試耗盡: {func.__name__} 在 {config.max_retries} 次重試後仍失敗" + ) + + raise last_exception + + +def with_retry( + max_retries: int = DEFAULT_MAX_RETRIES, + base_delay: float = DEFAULT_BASE_DELAY, + retryable_exceptions: Tuple[Type[Exception], ...] = (Exception,), +): + """ + 重試裝飾器 + + 用法: + @with_retry(max_retries=3) + async def my_api_call(): + ... + """ + config = RetryConfig( + max_retries=max_retries, + base_delay=base_delay, + retryable_exceptions=retryable_exceptions, + ) + + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + @functools.wraps(func) + async def wrapper(*args, **kwargs): + return await retry_async(func, *args, config=config, **kwargs) + + return wrapper + + return decorator + + +# 預設配置實例 +default_retry_config = RetryConfig() + +# API 調用專用配置(較短延遲) +api_retry_config = RetryConfig( + max_retries=2, + base_delay=0.5, + max_delay=5.0, + retryable_exceptions=(ConnectionError, TimeoutError, OSError), +) + +# 資料庫操作專用配置(較長延遲) +db_retry_config = RetryConfig( + max_retries=3, + base_delay=1.0, + max_delay=10.0, +) diff --git a/core/tool_registry.py b/core/tool_registry.py new file mode 100644 index 0000000000000000000000000000000000000000..e79fc6cbbfe8859956ff5323db6f24d83e7761a9 --- /dev/null +++ b/core/tool_registry.py @@ -0,0 +1,330 @@ +""" +工具註冊中心 +統一管理 MCP 工具的 OpenAI Function Calling Schema +2025 最佳實踐:讓 GPT 原生選擇工具,不需要自定義意圖檢測 Prompt + +重構版本:整合 Pydantic Schema 自動生成 +""" + +from typing import Dict, List, Any, Optional, Callable, Type +from dataclasses import dataclass, field + +from core.logging import get_logger +from core.tool_schema import ( + ToolSchema, + ToolMetadata, + ToolSchemaRegistry, + tool_schema_registry, + extract_schema_from_mcp_tool, +) + +logger = get_logger("core.tool_registry") + + +@dataclass +class ToolDefinition: + """工具定義(向後兼容)""" + name: str + description: str + parameters: Dict[str, Any] + handler: Optional[Callable] = None + category: str = "general" + requires_auth: bool = False + requires_location: bool = False + keywords: List[str] = field(default_factory=list) + examples: List[str] = field(default_factory=list) + + +class ToolRegistry: + """ + 工具註冊中心(重構版) + + 功能: + 1. 統一註冊所有 MCP 工具 + 2. 自動從 MCPTool 類別生成 OpenAI Function Calling Schema + 3. 支援工具分類和過濾 + 4. 動態啟用/停用工具 + 5. 整合 ToolSchemaRegistry 提供 Pydantic 支援 + """ + + def __init__(self): + self._tools: Dict[str, ToolDefinition] = {} + self._disabled_tools: set = set() + # 整合新的 Schema Registry + self._schema_registry = tool_schema_registry + + def register( + self, + name: str, + description: str, + parameters: Dict[str, Any], + handler: Optional[Callable] = None, + category: str = "general", + requires_auth: bool = False, + requires_location: bool = False, + keywords: Optional[List[str]] = None, + examples: Optional[List[str]] = None, + ) -> None: + """註冊工具(向後兼容 + 自動同步到 Schema Registry)""" + self._tools[name] = ToolDefinition( + name=name, + description=description, + parameters=parameters, + handler=handler, + category=category, + requires_auth=requires_auth, + requires_location=requires_location, + keywords=keywords or [], + examples=examples or [], + ) + + # 同步到 Schema Registry + schema = ToolSchema( + metadata=ToolMetadata( + name=name, + description=description, + category=category, + keywords=keywords or [], + examples=examples or [], + requires_location=requires_location, + requires_auth=requires_auth, + ), + input_schema=parameters, + handler=handler, + ) + self._schema_registry.register(schema) + + logger.debug(f"註冊工具: {name}") + + def register_mcp_tool(self, tool_class: Type) -> bool: + """ + 從 MCPTool 類別自動註冊工具 + + Args: + tool_class: MCPTool 子類別 + + Returns: + 是否註冊成功 + """ + schema = extract_schema_from_mcp_tool(tool_class) + if not schema: + return False + + # 註冊到 Schema Registry + self._schema_registry.register(schema) + + # 同步到舊的 _tools(向後兼容) + self._tools[schema.metadata.name] = ToolDefinition( + name=schema.metadata.name, + description=schema.metadata.description, + parameters=schema.input_schema, + handler=schema.handler, + category=schema.metadata.category, + requires_auth=schema.metadata.requires_auth, + requires_location=schema.metadata.requires_location, + keywords=schema.metadata.keywords, + examples=schema.metadata.examples, + ) + + logger.debug(f"從 MCPTool 註冊工具: {schema.metadata.name}") + return True + + def unregister(self, name: str) -> bool: + """取消註冊工具""" + if name in self._tools: + del self._tools[name] + self._schema_registry.unregister(name) + return True + return False + + def disable(self, name: str) -> None: + """停用工具""" + self._disabled_tools.add(name) + self._schema_registry.disable(name) + + def enable(self, name: str) -> None: + """啟用工具""" + self._disabled_tools.discard(name) + self._schema_registry.enable(name) + + def get_tool(self, name: str) -> Optional[ToolDefinition]: + """取得工具定義""" + if name in self._disabled_tools: + return None + return self._tools.get(name) + + def get_openai_tools( + self, + categories: Optional[List[str]] = None, + include_location_tools: bool = True, + strict: bool = True, + ) -> List[Dict[str, Any]]: + """ + 生成 OpenAI Function Calling 格式的工具列表 + + Args: + categories: 只包含指定分類的工具 + include_location_tools: 是否包含需要位置的工具 + strict: 是否啟用 strict mode(確保輸出符合 schema) + + Returns: + OpenAI tools 格式的列表 + """ + # 優先使用 Schema Registry(支援 strict mode) + return self._schema_registry.get_openai_tools( + categories=categories, + include_location_tools=include_location_tools, + strict=strict, + ) + + def get_openai_tools_legacy( + self, + categories: Optional[List[str]] = None, + include_location_tools: bool = True, + ) -> List[Dict[str, Any]]: + """ + 生成 OpenAI Function Calling 格式的工具列表(舊版,不支援 strict mode) + """ + tools = [] + + for name, tool in self._tools.items(): + # 跳過停用的工具 + if name in self._disabled_tools: + continue + + # 分類過濾 + if categories and tool.category not in categories: + continue + + # 位置過濾 + if not include_location_tools and tool.requires_location: + continue + + tools.append({ + "type": "function", + "function": { + "name": tool.name, + "description": tool.description, + "parameters": tool.parameters, + } + }) + + return tools + + def get_tool_names(self) -> List[str]: + """取得所有已註冊的工具名稱""" + return [ + name for name in self._tools.keys() + if name not in self._disabled_tools + ] + + def get_stats(self) -> Dict[str, Any]: + """取得統計資訊""" + return self._schema_registry.get_stats() + + def get_summaries(self) -> List[Dict[str, Any]]: + """取得所有工具摘要(用於快速意圖匹配)""" + return self._schema_registry.get_summaries() + + +# 全域單例 +tool_registry = ToolRegistry() + + +def register_mcp_tools_to_registry(mcp_server) -> int: + """ + 從 MCP Server 自動註冊工具到 Registry + + 2025 重構版:優先使用 MCPTool 類別自動提取 Schema + + Args: + mcp_server: MCPServer 實例 + + Returns: + 註冊的工具數量 + """ + count = 0 + + for tool_name, tool in mcp_server.tools.items(): + # 優先嘗試從 MCPTool 類別提取完整 Schema + if hasattr(tool, 'handler') and hasattr(tool.handler, '__self__'): + tool_class = tool.handler.__self__ + if tool_registry.register_mcp_tool(type(tool_class)): + count += 1 + continue + + # 降級:使用舊方法註冊 + description = getattr(tool, 'description', f'{tool_name} 工具') + parameters = {"type": "object", "properties": {}, "required": []} + + if hasattr(tool, 'handler') and hasattr(tool.handler, '__self__'): + tool_class = tool.handler.__self__ + if hasattr(tool_class, 'get_input_schema'): + try: + parameters = tool_class.get_input_schema() + except Exception as e: + logger.warning(f"取得 {tool_name} schema 失敗: {e}") + + # 提取關鍵字和範例 + keywords = [] + examples = [] + if hasattr(tool, 'handler') and hasattr(tool.handler, '__self__'): + tool_class = tool.handler.__self__ + keywords = getattr(tool_class, 'KEYWORDS', []) + examples = getattr(tool_class, 'USAGE_TIPS', []) + + # 判斷分類 + category = _infer_category(tool_name) + + # 判斷是否需要位置 + requires_location = _requires_location(tool_name, parameters) + + tool_registry.register( + name=tool_name, + description=description, + parameters=parameters, + handler=getattr(tool, 'handler', None), + category=category, + requires_location=requires_location, + keywords=keywords, + examples=examples, + ) + count += 1 + + logger.info(f"從 MCP Server 註冊了 {count} 個工具") + return count + + +def _infer_category(tool_name: str) -> str: + """推斷工具分類""" + name_lower = tool_name.lower() + + if any(k in name_lower for k in ['weather', 'forecast']): + return "weather" + if any(k in name_lower for k in ['bus', 'train', 'metro', 'thsr', 'youbike', 'parking']): + return "transportation" + if any(k in name_lower for k in ['geocode', 'directions', 'location']): + return "location" + if any(k in name_lower for k in ['news']): + return "information" + if any(k in name_lower for k in ['exchange', 'currency']): + return "finance" + if any(k in name_lower for k in ['health', 'heart', 'sleep', 'step']): + return "health" + + return "general" + + +def _requires_location(tool_name: str, parameters: Dict) -> bool: + """判斷工具是否需要位置資訊""" + # 檢查參數中是否有 lat/lon + props = parameters.get("properties", {}) + if "lat" in props or "lon" in props or "latitude" in props or "longitude" in props: + return True + + # 檢查工具名稱 + location_tools = [ + 'reverse_geocode', 'directions', 'tdx_bus_arrival', + 'tdx_youbike', 'tdx_metro', 'tdx_parking', 'tdx_train', 'tdx_thsr' + ] + return tool_name in location_tools diff --git a/core/tool_router.py b/core/tool_router.py new file mode 100644 index 0000000000000000000000000000000000000000..56035c60581310b1388f602e82ee5dbf99ad38ee --- /dev/null +++ b/core/tool_router.py @@ -0,0 +1,255 @@ +""" +動態工具路由器 +2025 最佳實踐:根據上下文智能過濾工具,減少 token 消耗 + +功能: +1. 位置過濾:用戶沒有位置時,排除需要位置的工具 +2. 關鍵字過濾:根據用戶意圖關鍵字,優先顯示相關分類 +3. 時間過濾:根據時間排除不適用的工具 +4. 優先級排序:常用工具優先顯示 +""" + +import re +import logging +from typing import Dict, List, Any, Optional, Set +from datetime import datetime + +from core.logging import get_logger + +logger = get_logger("core.tool_router") + + +class ToolRouter: + """ + 動態工具路由器 + + 根據上下文智能過濾和排序工具,減少傳遞給 GPT 的工具數量 + """ + + # 分類關鍵字映射 + CATEGORY_KEYWORDS = { + "weather": ["天氣", "氣溫", "下雨", "晴天", "陰天", "weather", "溫度", "濕度"], + "transportation": [ + "公車", "巴士", "bus", "火車", "台鐵", "高鐵", "捷運", "metro", + "youbike", "ubike", "微笑單車", "共享單車", "停車場", "停車位" + ], + "location": ["我在哪", "這是哪", "位置", "地址", "怎麼去", "導航", "路線"], + "information": ["新聞", "消息", "報導", "news"], + "finance": ["匯率", "換算", "美元", "日圓", "歐元", "currency", "exchange"], + "health": ["心率", "步數", "血氧", "睡眠", "健康", "運動"], + } + + # 時間敏感工具(深夜可能不適用) + NIGHT_EXCLUDED_TOOLS = { + "tdx_bus_arrival", # 深夜公車班次少 + "tdx_metro", # 捷運深夜停駛 + } + + # 工具優先級(數字越小優先級越高) + DEFAULT_PRIORITY = { + "weather_query": 1, + "reverse_geocode": 2, + "forward_geocode": 3, + "directions": 4, + "tdx_bus_arrival": 5, + "tdx_youbike": 6, + "tdx_metro": 7, + "tdx_train": 8, + "tdx_thsr": 9, + "news_query": 10, + "exchange_query": 11, + "healthkit_query": 12, + "tdx_parking": 13, + } + + def __init__(self): + self._user_preferences: Dict[str, Dict[str, int]] = {} # user_id -> {tool_name: usage_count} + + def filter_tools( + self, + tools: List[Dict[str, Any]], + message: str, + context: Optional[Dict[str, Any]] = None, + ) -> List[Dict[str, Any]]: + """ + 根據上下文過濾和排序工具 + + Args: + tools: OpenAI tools 格式的工具列表 + message: 用戶消息 + context: 上下文資訊(位置、時間、用戶偏好等) + + Returns: + 過濾和排序後的工具列表 + """ + context = context or {} + + # 1. 檢測用戶意圖分類 + detected_categories = self._detect_categories(message) + logger.debug(f"🎯 檢測到的分類: {detected_categories}") + + # 2. 過濾工具 + filtered_tools = [] + for tool in tools: + tool_name = tool.get("function", {}).get("name", "") + + # 位置過濾 + if not self._check_location_requirement(tool_name, context): + logger.debug(f"⏭️ 跳過 {tool_name}(需要位置但用戶未提供)") + continue + + # 時間過濾 + if not self._check_time_requirement(tool_name, context): + logger.debug(f"⏭️ 跳過 {tool_name}(深夜不適用)") + continue + + filtered_tools.append(tool) + + # 3. 排序工具(相關分類優先) + sorted_tools = self._sort_tools(filtered_tools, detected_categories, context) + + # 4. 限制工具數量(減少 token 消耗) + max_tools = self._get_max_tools(detected_categories) + if len(sorted_tools) > max_tools: + logger.info(f"📉 工具數量從 {len(sorted_tools)} 限制到 {max_tools}") + sorted_tools = sorted_tools[:max_tools] + + logger.info(f"🔧 過濾後工具: {[t['function']['name'] for t in sorted_tools]}") + return sorted_tools + + def _detect_categories(self, message: str) -> Set[str]: + """檢測用戶消息中的意圖分類""" + message_lower = message.lower() + detected = set() + + for category, keywords in self.CATEGORY_KEYWORDS.items(): + for keyword in keywords: + if keyword.lower() in message_lower: + detected.add(category) + break + + return detected + + def _check_location_requirement( + self, + tool_name: str, + context: Dict[str, Any], + ) -> bool: + """檢查工具的位置需求""" + # 需要位置的工具 + location_required_tools = { + "reverse_geocode", + "tdx_bus_arrival", + "tdx_youbike", + "tdx_metro", + "tdx_parking", + "tdx_train", + "tdx_thsr", + } + + if tool_name not in location_required_tools: + return True + + # 檢查是否有位置資訊 + has_location = ( + context.get("lat") is not None and + context.get("lon") is not None + ) + + # 如果沒有位置,但用戶明確要求(如「附近的公車」),仍然保留工具 + # 讓工具自己處理缺少位置的情況 + return True # 暫時不嚴格過濾,讓工具自己處理 + + def _check_time_requirement( + self, + tool_name: str, + context: Dict[str, Any], + ) -> bool: + """檢查工具的時間需求""" + if tool_name not in self.NIGHT_EXCLUDED_TOOLS: + return True + + # 檢查是否為深夜(00:00 - 05:00) + current_hour = context.get("hour") + if current_hour is None: + current_hour = datetime.now().hour + + is_night = 0 <= current_hour < 5 + + # 深夜時排除某些工具 + return not is_night + + def _sort_tools( + self, + tools: List[Dict[str, Any]], + detected_categories: Set[str], + context: Dict[str, Any], + ) -> List[Dict[str, Any]]: + """排序工具(相關分類優先)""" + + def get_priority(tool: Dict[str, Any]) -> int: + tool_name = tool.get("function", {}).get("name", "") + + # 基礎優先級 + base_priority = self.DEFAULT_PRIORITY.get(tool_name, 100) + + # 如果工具屬於檢測到的分類,降低優先級數字(提高優先級) + tool_category = self._get_tool_category(tool_name) + if tool_category in detected_categories: + base_priority -= 50 # 相關工具優先 + + # 用戶偏好加成 + user_id = context.get("user_id") + if user_id and user_id in self._user_preferences: + usage_count = self._user_preferences[user_id].get(tool_name, 0) + base_priority -= min(usage_count, 10) # 最多降低 10 + + return base_priority + + return sorted(tools, key=get_priority) + + def _get_tool_category(self, tool_name: str) -> str: + """取得工具的分類""" + category_map = { + "weather_query": "weather", + "reverse_geocode": "location", + "forward_geocode": "location", + "directions": "location", + "tdx_bus_arrival": "transportation", + "tdx_youbike": "transportation", + "tdx_metro": "transportation", + "tdx_train": "transportation", + "tdx_thsr": "transportation", + "tdx_parking": "transportation", + "news_query": "information", + "exchange_query": "finance", + "healthkit_query": "health", + } + return category_map.get(tool_name, "general") + + def _get_max_tools(self, detected_categories: Set[str]) -> int: + """根據檢測到的分類決定最大工具數量""" + if not detected_categories: + # 沒有明確分類,返回所有工具 + return 20 + + if len(detected_categories) == 1: + # 單一分類,但仍需要保留足夠工具(如 directions) + return 12 + + # 多個分類 + return 15 + + def record_tool_usage(self, user_id: str, tool_name: str) -> None: + """記錄工具使用(用於優先級調整)""" + if user_id not in self._user_preferences: + self._user_preferences[user_id] = {} + + current = self._user_preferences[user_id].get(tool_name, 0) + self._user_preferences[user_id][tool_name] = current + 1 + + logger.debug(f"📊 記錄工具使用: {user_id} -> {tool_name} ({current + 1})") + + +# 全域單例 +tool_router = ToolRouter() diff --git a/core/tool_schema.py b/core/tool_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..8954b04595bd714872ccd15ba65af10a465e399a --- /dev/null +++ b/core/tool_schema.py @@ -0,0 +1,355 @@ +""" +Pydantic 工具 Schema 定義 +2025 最佳實踐:使用 Pydantic 自動生成 OpenAI Function Calling Schema + +功能: +1. 工具輸入/輸出的 Pydantic 基礎類別 +2. 自動生成 OpenAI tools 格式的 JSON Schema +3. 支援 strict mode 確保 100% 有效輸出 +4. 裝飾器模式自動註冊工具 +""" + +from typing import Dict, Any, Optional, List, Callable, Type, TypeVar, get_type_hints +from dataclasses import dataclass, field +from functools import wraps +import inspect +import logging + +logger = logging.getLogger("core.tool_schema") + +# 類型變數 +T = TypeVar("T") + + +@dataclass +class ToolMetadata: + """ + 工具元資料(增強版) + + 2025 最佳實踐:豐富的元資料讓 GPT 更容易理解何時使用哪個工具 + """ + name: str + description: str + category: str = "general" + keywords: List[str] = field(default_factory=list) # 觸發關鍵字 + examples: List[str] = field(default_factory=list) # 使用範例 + negative_examples: List[str] = field(default_factory=list) # 不應使用的情況 + requires_location: bool = False + requires_auth: bool = False + is_complex: bool = False + priority: int = 100 # 優先級(數字越小越優先) + aliases: List[str] = field(default_factory=list) # 工具別名 + + +@dataclass +class ToolSchema: + """工具 Schema 定義(自描述)""" + metadata: ToolMetadata + input_schema: Dict[str, Any] + output_schema: Optional[Dict[str, Any]] = None + handler: Optional[Callable] = None + + def to_openai_tool(self, strict: bool = True) -> Dict[str, Any]: + """ + 轉換為 OpenAI Function Calling 格式 + + Args: + strict: 是否啟用 strict mode(確保輸出符合 schema) + + Returns: + OpenAI tools 格式的字典 + """ + # 確保 schema 符合 OpenAI strict mode 要求 + parameters = self._prepare_strict_schema(self.input_schema) if strict else self.input_schema + + tool_def = { + "type": "function", + "function": { + "name": self.metadata.name, + "description": self._build_rich_description(), + "parameters": parameters, + } + } + + if strict: + tool_def["function"]["strict"] = True + + return tool_def + + def _build_rich_description(self) -> str: + """ + 建構豐富的工具描述(包含範例、關鍵字、負面範例) + 讓 GPT 更容易理解何時使用此工具 + + 2025 最佳實踐: + - 正面範例:告訴 GPT 何時使用 + - 負面範例:告訴 GPT 何時不要使用(減少誤判) + """ + desc_parts = [self.metadata.description] + + # 加入關鍵字提示 + if self.metadata.keywords: + keywords_str = "、".join(self.metadata.keywords[:5]) + desc_parts.append(f"觸發詞:{keywords_str}") + + # 加入使用範例(正面) + if self.metadata.examples: + examples_str = ";".join(self.metadata.examples[:3]) + desc_parts.append(f"適用:{examples_str}") + + # 加入負面範例(告訴 GPT 何時不要使用) + if self.metadata.negative_examples: + neg_str = ";".join(self.metadata.negative_examples[:2]) + desc_parts.append(f"不適用:{neg_str}") + + return "。".join(desc_parts) + + def _prepare_strict_schema(self, schema: Dict[str, Any]) -> Dict[str, Any]: + """ + 準備符合 OpenAI strict mode 的 schema + + strict mode 要求: + 1. additionalProperties: false + 2. 所有屬性都在 required 中(或有 default) + 3. 不支援 oneOf/anyOf/allOf + """ + result = dict(schema) + + # 確保是 object 類型 + if result.get("type") != "object": + result = {"type": "object", "properties": result} + + # 添加 additionalProperties: false + result["additionalProperties"] = False + + # 確保所有屬性都在 required 中 + properties = result.get("properties", {}) + existing_required = set(result.get("required", [])) + + # 收集所有沒有 default 的屬性 + all_required = [] + for prop_name, prop_schema in properties.items(): + if prop_name in existing_required or "default" not in prop_schema: + all_required.append(prop_name) + + result["required"] = all_required + + return result + + def get_summary(self) -> Dict[str, Any]: + """獲取工具摘要(用於快速意圖匹配)""" + return { + "name": self.metadata.name, + "description": self.metadata.description[:50] + "..." if len(self.metadata.description) > 50 else self.metadata.description, + "category": self.metadata.category, + "keywords": self.metadata.keywords, + "params": list(self.input_schema.get("properties", {}).keys()) + } + + +def extract_schema_from_mcp_tool(tool_class: Type) -> Optional[ToolSchema]: + """ + 從現有 MCPTool 類別提取 Schema(增強版) + + Args: + tool_class: MCPTool 子類別 + + Returns: + ToolSchema 或 None + """ + try: + # 檢查必要屬性 + if not hasattr(tool_class, "NAME") or not hasattr(tool_class, "get_input_schema"): + return None + + name = getattr(tool_class, "NAME", "") + if not name: + return None + + # 提取元資料(增強版) + metadata = ToolMetadata( + name=name, + description=getattr(tool_class, "DESCRIPTION", f"{name} 工具"), + category=getattr(tool_class, "CATEGORY", "general"), + keywords=getattr(tool_class, "KEYWORDS", []), + examples=getattr(tool_class, "USAGE_TIPS", []), + negative_examples=getattr(tool_class, "NEGATIVE_EXAMPLES", []), + requires_location=_check_requires_location(tool_class), + requires_auth=getattr(tool_class, "REQUIRES_AUTH", False), + is_complex=getattr(tool_class, "IS_COMPLEX", False), + priority=getattr(tool_class, "PRIORITY", 100), + aliases=getattr(tool_class, "ALIASES", []), + ) + + # 提取 input schema + try: + input_schema = tool_class.get_input_schema() + except Exception as e: + logger.warning(f"提取 {name} input schema 失敗: {e}") + input_schema = {"type": "object", "properties": {}} + + # 提取 output schema(可選) + output_schema = None + if hasattr(tool_class, "get_output_schema"): + try: + output_schema = tool_class.get_output_schema() + except Exception: + pass + + # 提取 handler + handler = None + if hasattr(tool_class, "execute"): + handler = tool_class.execute + + return ToolSchema( + metadata=metadata, + input_schema=input_schema, + output_schema=output_schema, + handler=handler, + ) + + except Exception as e: + logger.error(f"提取 {tool_class} schema 失敗: {e}") + return None + + +def _check_requires_location(tool_class: Type) -> bool: + """檢查工具是否需要位置資訊""" + # 檢查類別屬性 + if getattr(tool_class, "REQUIRES_LOCATION", False): + return True + + # 檢查 input schema 中是否有 lat/lon + try: + schema = tool_class.get_input_schema() + props = schema.get("properties", {}) + if "lat" in props or "lon" in props: + return True + except Exception: + pass + + # 檢查工具名稱 + name = getattr(tool_class, "NAME", "").lower() + location_tools = [ + "reverse_geocode", "directions", "tdx_bus_arrival", + "tdx_youbike", "tdx_metro", "tdx_parking", "tdx_train", "tdx_thsr" + ] + return name in location_tools + + +class ToolSchemaRegistry: + """ + 工具 Schema 註冊中心 + + 功能: + 1. 統一管理所有工具的 Schema + 2. 自動生成 OpenAI Function Calling 格式 + 3. 支援動態過濾和分組 + """ + + def __init__(self): + self._schemas: Dict[str, ToolSchema] = {} + self._disabled: set = set() + + def register(self, schema: ToolSchema) -> None: + """註冊工具 Schema""" + self._schemas[schema.metadata.name] = schema + logger.debug(f"註冊工具 Schema: {schema.metadata.name}") + + def register_from_mcp_tool(self, tool_class: Type) -> bool: + """從 MCPTool 類別註冊""" + schema = extract_schema_from_mcp_tool(tool_class) + if schema: + self.register(schema) + return True + return False + + def unregister(self, name: str) -> bool: + """取消註冊""" + if name in self._schemas: + del self._schemas[name] + return True + return False + + def disable(self, name: str) -> None: + """停用工具""" + self._disabled.add(name) + + def enable(self, name: str) -> None: + """啟用工具""" + self._disabled.discard(name) + + def get(self, name: str) -> Optional[ToolSchema]: + """取得工具 Schema""" + if name in self._disabled: + return None + return self._schemas.get(name) + + def get_openai_tools( + self, + categories: Optional[List[str]] = None, + include_location_tools: bool = True, + strict: bool = True, + ) -> List[Dict[str, Any]]: + """ + 生成 OpenAI Function Calling 格式的工具列表 + + Args: + categories: 只包含指定分類的工具 + include_location_tools: 是否包含需要位置的工具 + strict: 是否啟用 strict mode + + Returns: + OpenAI tools 格式的列表 + """ + tools = [] + + for name, schema in self._schemas.items(): + # 跳過停用的工具 + if name in self._disabled: + continue + + # 分類過濾 + if categories and schema.metadata.category not in categories: + continue + + # 位置過濾 + if not include_location_tools and schema.metadata.requires_location: + continue + + tools.append(schema.to_openai_tool(strict=strict)) + + return tools + + def get_tool_names(self) -> List[str]: + """取得所有已註冊的工具名稱""" + return [ + name for name in self._schemas.keys() + if name not in self._disabled + ] + + def get_summaries(self) -> List[Dict[str, Any]]: + """取得所有工具摘要(用於快速意圖匹配)""" + return [ + schema.get_summary() + for name, schema in self._schemas.items() + if name not in self._disabled + ] + + def get_stats(self) -> Dict[str, Any]: + """取得統計資訊""" + categories = {} + for schema in self._schemas.values(): + cat = schema.metadata.category + categories[cat] = categories.get(cat, 0) + 1 + + return { + "total": len(self._schemas), + "disabled": len(self._disabled), + "active": len(self._schemas) - len(self._disabled), + "categories": categories, + } + + +# 全域單例 +tool_schema_registry = ToolSchemaRegistry() diff --git a/features/mcp/agent_bridge.py b/features/mcp/agent_bridge.py index 9469fec643ba9e4150780239f6b1633196c1d364..b4cd6b4cf2e877566d37ef8540bc6f4089259387 100644 --- a/features/mcp/agent_bridge.py +++ b/features/mcp/agent_bridge.py @@ -111,6 +111,7 @@ class MCPAgentBridge: ToolMetadata( name="reverse_geocode", requires_env={"lat", "lon"}, + enable_reformat=True, ) ) register( @@ -214,9 +215,70 @@ class MCPAgentBridge: await self.mcp_server.start_external_servers() logger.info(f"異步初始化完成,完整可用 MCP 工具數量: {len(self.mcp_server.tools)}") + # 將 MCP Server 的工具註冊到 tool_registry + self._sync_tools_to_registry() + # 2025 最佳實踐:啟動時預熱熱門查詢快取 await self._preheat_cache() + def _sync_tools_to_registry(self) -> int: + """ + 將 MCP Server 的工具同步到 tool_registry + + Returns: + 註冊的工具數量 + """ + from core.tool_registry import tool_registry + + count = 0 + for tool_name, tool in self.mcp_server.tools.items(): + # 取得工具描述 + description = getattr(tool, 'description', f'{tool_name} 工具') + + # 取得參數 Schema + parameters = {"type": "object", "properties": {}, "required": []} + keywords = [] + examples = [] + negative_examples = [] + category = "general" + priority = 100 + + if hasattr(tool, 'handler') and hasattr(tool.handler, '__self__'): + tool_class = tool.handler.__self__ + + # 嘗試從 MCPTool 類別提取完整資訊 + if hasattr(tool_class, 'get_input_schema'): + try: + parameters = tool_class.get_input_schema() + except Exception as e: + logger.warning(f"取得 {tool_name} schema 失敗: {e}") + + # 提取增強元資料 + keywords = getattr(tool_class, 'KEYWORDS', []) + examples = getattr(tool_class, 'USAGE_TIPS', []) + negative_examples = getattr(tool_class, 'NEGATIVE_EXAMPLES', []) + category = getattr(tool_class, 'CATEGORY', 'general') + priority = getattr(tool_class, 'PRIORITY', 100) + + # 判斷是否需要位置 + props = parameters.get("properties", {}) + requires_location = "lat" in props or "lon" in props + + tool_registry.register( + name=tool_name, + description=description, + parameters=parameters, + handler=getattr(tool, 'handler', None), + category=category, + requires_location=requires_location, + keywords=keywords, + examples=examples, + ) + count += 1 + + logger.info(f"🔧 同步 {count} 個工具到 tool_registry") + return count + def _normalize_tool_name(self, raw_name: Optional[str]) -> Optional[str]: """ 將 GPT 回傳的工具名稱正規化為註冊表中的實際名稱。 @@ -414,8 +476,11 @@ class MCPAgentBridge: async def detect_intent(self, message: str) -> Tuple[bool, Optional[Dict[str, Any]]]: """ 檢測用戶消息中的意圖 (保持與舊 FeatureRouter 相同介面) - 使用 OpenAI Structured Outputs 確保100%返回有效JSON - 帶快取機制,相同消息直接返回 + + 2025 重構版:使用 OpenAI 原生 Function Calling + - 不再使用巨大的 system_prompt 描述每個工具 + - 工具定義由 tools 參數傳遞,GPT 原生選擇 + - 新增工具只需註冊到 Registry,不需更新任何 prompt 參數: message (str): 用戶消息 @@ -423,362 +488,130 @@ class MCPAgentBridge: 返回: tuple: (是否檢測到意圖, 意圖數據) """ + # 使用新的 IntentDetector(基於 OpenAI Function Calling) + return await self._detect_intent_with_function_calling(message) + + async def _detect_intent_with_function_calling(self, message: str) -> Tuple[bool, Optional[Dict[str, Any]]]: + """ + 使用 OpenAI 原生 Function Calling 進行意圖檢測 + + 核心改進: + 1. 工具定義自動從 Registry 生成 + 2. GPT 原生選擇工具並生成結構化參數 + 3. 不需要自定義 prompt 描述每個工具 + """ import hashlib import time as time_module - + # 生成快取鍵 cache_key = hashlib.md5(message.encode()).hexdigest() # 檢查快取 if cache_key in self._intent_cache: has_feature, intent_data, cached_time = self._intent_cache[cache_key] - # 檢查是否過期 if time_module.time() - cached_time < self._intent_cache_ttl: logger.debug(f"💾 意圖快取命中: {message[:50]}...") return has_feature, intent_data else: - # 過期,刪除快取 del self._intent_cache[cache_key] - logger.info(f"檢測意圖: \"{message}\"") - logger.debug("意圖偵測輸入 - user_id=%s, chat_id=%s", "intent_detection", None) + logger.info(f"🔍 檢測意圖(Function Calling): \"{message[:100]}...\"") # 檢查特殊命令 for command in ["功能列表", "有什麼功能", "能做什麼"]: if command in message: logger.info(f"檢測到特殊命令: {command}") - return True, { - "type": "special_command", - "command": "feature_list" - } + return True, {"type": "special_command", "command": "feature_list"} - # 使用 GPT + Structured Outputs 進行意圖解析 try: - logger.info("開始使用 GPT Structured Outputs 進行意圖解析") - - # 構建可用工具的描述 - tools_description = self._get_tools_description() - - # GPT 意圖解析 Prompt - 適配新的 schema(不使用 oneOf) - system_prompt = f"""你是一個精確的意圖解析助手。 - -可用工具: -{tools_description} - -任務:分析用戶消息,決定是否需要調用工具,並判斷用戶情緒。 - -重要規則: -1. 健康相關需求(心率、步數、血氧、呼吸、睡眠)使用 healthkit_query -2. 不需要傳入 user_id,系統會自動補齊 -3. 若無法判斷具體參數,使用合理預設值 -4. 一般閒聊設置 is_tool_call 為 false - -特殊處理: -- 天氣查詢:城市名稱必須使用英文(如 Taipei, Tokyo, New York) - * 台北 → Taipei - * 東京 → Tokyo - * 紐約 → New York - * 倫敦 → London - * 巴黎 → Paris - * 如無指定城市,預設使用 Taipei - -- 匯率查詢: - * 必須明確指定 from_currency 和 to_currency(ISO 4217 代碼) - * 預設:from_currency=USD, to_currency=TWD - * 美元 → USD, 台幣 → TWD, 日圓 → JPY, 歐元 → EUR - * 金額預設 amount=1.0, conversion=true - -- 新聞查詢: - * 任何提到「新聞」「消息」「報導」的請求都使用 news_query - * 參數:query(關鍵詞)、country(國家,預設 tw)、category(分類,預設 top)、language(語言,預設 zh) - * 今日新聞、科技新聞、台灣新聞都應該調用此工具 - -- 公車查詢(重要!): - * 任何提到「公車」「巴士」「幾號公車」「什麼時候來」的請求都使用 tdx_bus_arrival - * **參數名稱是 route_name(不是 stop_name)** - * 從用戶訊息中提取路線號碼(如「137」「紅30」「307」) - * 範例: - - 「137公車什麼時候來」→ tdx_bus_arrival:route_name=137 - - 「307還要多久」→ tdx_bus_arrival:route_name=307 - - 「附近有什麼公車」→ tdx_bus_arrival(不需參數,系統自動用 GPS 查詢附近站點) - - 「紅30公車」→ tdx_bus_arrival:route_name=紅30 - * ❌ 錯誤:tdx_bus_arrival:stop_name=137 - * ✅ 正確:tdx_bus_arrival:route_name=137 - -- 台鐵/火車查詢(重要!): - * 任何提到「火車」「台鐵」「列車」「自強號」「莒光號」「區間車」的請求都使用 tdx_train - * **參數名稱是 origin_station(起站)和 destination_station(迄站)** - * **「往XX」「到XX」「去XX」表示目的地(destination_station),不是起點!** - * **「從XX」「在XX」表示起點(origin_station)** - * **如果用戶沒有明確說起點,就不要填 origin_station,讓系統用 GPS 自動找最近車站** - * 範例: - - 「往台北的火車」→ tdx_train:destination_station=台北(只填目的地,起點由 GPS 決定) - - 「到高雄的火車」→ tdx_train:destination_station=高雄(只填目的地) - - 「從桃園到台北」→ tdx_train:origin_station=桃園,destination_station=台北(明確說了起點) - - 「台北到台中的火車」→ tdx_train:origin_station=台北,destination_station=台中 - - 「下一班火車」→ tdx_train(不需參數,系統自動用 GPS 查詢最近車站) - - 「自強號 123 次」→ tdx_train:train_no=123 - * ❌ 錯誤:「往台北」解析為 origin_station=台灣,destination_station=台北(不要亂填起點!) - * ❌ 錯誤:「往台北」解析為 origin_station=台北(方向搞反了) - * ✅ 正確:「往台北」解析為 destination_station=台北(只填目的地) - -- YouBike/共享單車查詢(重要!必須識別!): - * 任何提到以下關鍵詞的請求都使用 tdx_youbike: - - 「YouBike」「Youbike」「youbike」「YOUBIKE」 - - 「UBike」「Ubike」「ubike」「UBIKE」 - - 「微笑單車」「共享單車」「公共單車」 - - 「腳踏車站」「單車站」「自行車站」 - - 「借車」「還車」(在單車語境下) - - 「最近的站點」「附近站點」(在單車語境下) - * **不是 tdx_parking!YouBike 是單車,不是停車場** - * **不是一般聊天!這是工具調用!** - * 範例: - - 「附近的 YouBike」→ tdx_youbike(不需參數,系統自動用 GPS 查詢) - - 「離我最近的 Ubike 站點」→ tdx_youbike - - 「最近的Ubike站點」→ tdx_youbike - - 「Ubike在哪」→ tdx_youbike - - 「哪裡有Ubike」→ tdx_youbike - - 「市政府 YouBike 還有車嗎」→ tdx_youbike:station_name=市政府 - * ❌ 錯誤:is_tool_call=false(這不是聊天!) - * ❌ 錯誤:tdx_parking:query=Ubike - * ✅ 正確:tdx_youbike - -- 停車場查詢: - * 任何提到「停車場」「停車位」「汽車停車」的請求才使用 tdx_parking - * 範例: - - 「附近的停車場」→ tdx_parking - - 「市政府停車場」→ tdx_parking:parking_name=市政府 - -- 地點查詢與導航(重要!): - * **當前位置查詢**: - - 問「我在哪」「這是哪裡」「現在在哪」「我的位置」→ 使用 reverse_geocode(不需參數,系統自動用 GPS 座標) - - ❌ 錯誤:forward_geocode:query=我在哪 - - ✅ 正確:reverse_geocode - * **導航需求判斷**: - - 問「怎麼去 X」「如何去 X」「去 X 怎麼走」「到 X 怎麼走」→ 使用 forward_geocode 查詢目的地座標 - - 問「從 A 到 B 要多久」「A 到 B 怎麼走」→ 同時使用 forward_geocode 查詢起點與終點 - * **不要猜測座標**: - - ❌ 錯誤:directions:origin_lat=25.1288,origin_lon=121.9234,dest_lat=24.9932,dest_lon=121.3261 - - ✅ 正確:forward_geocode:query=銘傳大學桃園校區 - * **工具使用順序**: - 1. 先使用 forward_geocode 將地點名稱轉換為座標 - 2. 再使用 directions 規劃路線(系統會自動處理) - * **範例**: - - 「我在哪」→ reverse_geocode(系統自動補 lat/lon) - - 「怎麼去桃園火車站」→ forward_geocode:query=桃園火車站 - - 「從銘傳大學到桃園火車站」→ forward_geocode:query=銘傳大學桃園校區 - - 「台北車站到淡水捷運站」→ forward_geocode:query=台北車站 - -情緒判斷(emotion): -根據文字的語氣、用詞、標點符號判斷用戶情緒,選擇以下之一: -- neutral: 平靜、中性(預設) -- happy: 開心、興奮、愉快(如「好開心!」「太棒了」「哈哈」) -- sad: 難過、沮喪、失落(如「好難過」「唉...」「心情不好」) -- angry: 生氣、憤怒、煩躁(如「煩死了」「幹嘛啦」「氣死我了」) -- fear: 恐懼、擔心、焦慮(如「好害怕」「好擔心」「怎麼辦」) -- surprise: 驚訝、意外(如「什麼!」「真的假的」「不會吧」) - -回應格式: -- is_tool_call: true/false(是否調用工具) -- tool_name: 工具名稱和參數(僅當 is_tool_call=true 時提供,格式:tool_name:param1=value1,param2=value2) -- emotion: 用戶情緒標籤(必填) - -示例: -- "我在哪" → {{"is_tool_call": true, "tool_name": "reverse_geocode", "emotion": "neutral"}} -- "這是哪裡" → {{"is_tool_call": true, "tool_name": "reverse_geocode", "emotion": "neutral"}} -- "台北天氣" → {{"is_tool_call": true, "tool_name": "weather_query:city=Taipei", "emotion": "neutral"}} -- "好開心!今天天氣好嗎" → {{"is_tool_call": true, "tool_name": "weather_query:city=Taipei", "emotion": "happy"}} -- "美元匯率" → {{"is_tool_call": true, "tool_name": "exchange_query:from_currency=USD,to_currency=TWD,amount=1.0", "emotion": "neutral"}} -- "今日新聞" → {{"is_tool_call": true, "tool_name": "news_query:country=tw,language=zh", "emotion": "neutral"}} -- "科技新聞" → {{"is_tool_call": true, "tool_name": "news_query:query=科技,category=technology,language=zh", "emotion": "neutral"}} -- "最近的Ubike站點" → {{"is_tool_call": true, "tool_name": "tdx_youbike", "emotion": "neutral"}} -- "附近的YouBike" → {{"is_tool_call": true, "tool_name": "tdx_youbike", "emotion": "neutral"}} -- "Ubike在哪" → {{"is_tool_call": true, "tool_name": "tdx_youbike", "emotion": "neutral"}} -- "往台北的火車" → {{"is_tool_call": true, "tool_name": "tdx_train:destination_station=台北", "emotion": "neutral"}} -- "你好" → {{"is_tool_call": false, "tool_name": "", "emotion": "neutral"}} -- "我好難過..." → {{"is_tool_call": false, "tool_name": "", "emotion": "sad"}} -- "煩死了" → {{"is_tool_call": false, "tool_name": "", "emotion": "angry"}}""" - + # 從 tool_registry 取得 OpenAI tools 格式 + from core.tool_registry import tool_registry + from core.tool_router import tool_router + + all_tools = tool_registry.get_openai_tools(strict=False) + + if not all_tools: + logger.warning("⚠️ 沒有可用的工具,降級為聊天") + return False, {"emotion": "neutral"} + + # 使用 ToolRouter 動態過濾和排序工具 + context = {"hour": datetime.now().hour} + tools = tool_router.filter_tools(all_tools, message, context) + + logger.info(f"🔧 載入 {len(all_tools)} 個工具,過濾後 {len(tools)} 個") + + # 建構精簡的 system prompt(只處理特殊規則) + system_prompt = self._build_function_calling_prompt() + messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": message} ] - - # 使用 Structured Outputs(動態推理強度) + + # 使用 OpenAI Function Calling + from core.reasoning_strategy import get_optimal_reasoning_effort optimal_effort = get_optimal_reasoning_effort("intent_detection") logger.info(f"🧠 意圖檢測推理強度: {optimal_effort}") - - response = await ai_service.generate_response_for_user( + + response = await ai_service.generate_response_with_tools( messages=messages, + tools=tools, user_id="intent_detection", - model="gpt-5-nano", - chat_id=None, - use_structured_outputs=True, - response_schema=self._get_intent_schema(), - reasoning_effort=optimal_effort # 動態調整 + model="gpt-4o-mini", # 使用更強的模型以提升參數提取準確度 + reasoning_effort=None, # gpt-4o-mini 不支援 reasoning_effort + tool_choice="auto", ) - - logger.debug("GPT Structured Outputs 回應: %s", response) - - # 檢查是否為 fallback 錯誤訊息 - if response.strip() in ["抱歉,我暫時沒有合適的回應。可以換個說法再試試嗎?", "抱歉,生成回應時遇到問題。請重試。"]: - logger.warning("Structured Outputs 返回 fallback 訊息,視為失敗") - raise Exception("Structured Outputs failed with fallback message") - - # Structured Outputs 保證返回有效JSON,直接解析 - try: - response_text = response.strip() - - # 處理 GPT 回應重複 JSON 的情況(如 {...}{...}) - # 只取第一個完整的 JSON 物件 - if response_text.startswith("{"): - brace_count = 0 - end_idx = 0 - for i, char in enumerate(response_text): - if char == "{": - brace_count += 1 - elif char == "}": - brace_count -= 1 - if brace_count == 0: - end_idx = i + 1 - break - if end_idx > 0: - response_text = response_text[:end_idx] + + # 解析回應 + tool_calls = response.get("tool_calls", []) + + if tool_calls: + # GPT 選擇了工具 + tool_call = tool_calls[0] + function = tool_call.get("function", {}) + tool_name = function.get("name", "") + arguments_str = function.get("arguments", "{}") - intent_data = json.loads(response_text) - logger.debug("解析後的意圖資料: %s", _safe_json(intent_data)) - - # 新的 schema 格式:is_tool_call, tool_name(包含參數) - is_tool_call = intent_data.get("is_tool_call", False) - - if is_tool_call: - tool_name_with_params = intent_data.get("tool_name", "").strip() - - if not tool_name_with_params: - logger.warning("⚠️ GPT 標記為工具調用但未提供工具名稱,降級為聊天") - return False, None - - raw_tool_name = tool_name_with_params - params_str = "" - if ":" in tool_name_with_params: - raw_tool_name, params_str = tool_name_with_params.split(":", 1) - - tool_name = self._normalize_tool_name(raw_tool_name) - if not tool_name: - logger.warning(f"⚠️ 工具 {raw_tool_name} 無法對應到註冊名稱,降級為聊天") - return False, None - - # 解析參數 + try: + arguments = json.loads(arguments_str) + except json.JSONDecodeError: arguments = {} - if params_str.strip(): - # 獲取工具的 input schema 以確定參數類型 - tool = self.mcp_server.tools.get(tool_name) - input_schema = {} - if tool and hasattr(tool, 'handler') and hasattr(tool.handler, '__self__'): - tool_class = tool.handler.__self__ - if hasattr(tool_class, 'get_input_schema'): - try: - input_schema = tool_class.get_input_schema() - except: - pass - - properties = input_schema.get('properties', {}) - - for param_pair in params_str.split(","): - if "=" not in param_pair: - continue - key, value = param_pair.split("=", 1) - key = key.strip() - value = value.strip() - - # 跳過空鍵或空值(避免傳入空字串導致驗證失敗) - if not key or not value: - continue - - # 根據 schema 中的類型定義來轉換值 - param_schema = properties.get(key, {}) - param_type = param_schema.get('type', 'string') - - normalized_value = value - - # 根據 schema 類型進行轉換 - if param_type == 'integer': - try: - normalized_value = int(value) - except ValueError: - normalized_value = value - elif param_type == 'number': - try: - normalized_value = float(value) - except ValueError: - normalized_value = value - elif param_type == 'boolean': - lower_value = value.lower() - if lower_value in ("true", "false"): - normalized_value = lower_value == "true" - # 其他類型(包括 string)保持原樣 - - arguments[key] = normalized_value - - logger.info(f"✅ GPT 檢測到工具調用: {raw_tool_name.strip()} → {tool_name}") - logger.debug("工具調用參數: %s", _safe_json(arguments)) - - # 驗證工具是否存在 - if tool_name not in self.mcp_server.tools: - logger.warning(f"⚠️ 工具 {tool_name} 不存在,降級為聊天") - return False, None - - # 基礎參數驗證(可選,Structured Outputs 已保證格式) - tool = self.mcp_server.tools[tool_name] - if hasattr(tool, 'handler') and hasattr(tool.handler, '__self__'): - tool_class = tool.handler.__self__ - if hasattr(tool_class, 'validate_input'): - try: - validated_args = tool_class.validate_input(arguments) - logger.debug("✓ 參數驗證通過: %s", _safe_json(validated_args)) - except Exception as e: - logger.warning(f"⚠️ 參數驗證失敗: {e},仍然嘗試執行") - # 不中斷,讓工具自己處理 - - # 提取情緒(新增) - emotion = intent_data.get("emotion", "neutral") - logger.info(f"😊 偵測到情緒: {emotion}") - - intent_result = (True, { - "type": "mcp_tool", - "tool_name": tool_name, - "arguments": arguments, - "emotion": emotion # 新增情緒欄位 - }) - - # 寫入快取 - self._intent_cache[cache_key] = (*intent_result, time_module.time()) - logger.debug(f"💾 意圖結果已快取: {tool_name}") - - return intent_result - - else: - # is_tool_call = False,表示一般聊天 - logger.info("💬 GPT 判斷為一般聊天") - - # 提取情緒(新增) - emotion = intent_data.get("emotion", "neutral") - logger.info(f"😊 偵測到情緒: {emotion}") - - # 寫入快取(一般聊天也要回傳情緒) - intent_result = (False, {"emotion": emotion}) - self._intent_cache[cache_key] = (*intent_result, time_module.time()) - - return intent_result - - except json.JSONDecodeError as e: - # Structured Outputs 不應該發生這種錯誤,記錄異常 - logger.error(f"❌ Structured Outputs JSON 解析失敗(異常情況): {e}, response: {response}") - return False, None - + + # 正規化工具名稱 + normalized_name = self._normalize_tool_name(tool_name) + if not normalized_name: + logger.warning(f"⚠️ 工具 {tool_name} 無法對應到註冊名稱,降級為聊天") + return False, {"emotion": "neutral"} + + logger.info(f"✅ GPT 選擇工具: {normalized_name}") + logger.debug(f"工具參數: {_safe_json(arguments)}") + + # 提取情緒(從 content 或預設) + emotion = self._extract_emotion_from_content(response.get("content", "")) + + intent_result = (True, { + "type": "mcp_tool", + "tool_name": normalized_name, + "arguments": arguments, + "emotion": emotion, + }) + + # 寫入快取 + self._intent_cache[cache_key] = (*intent_result, time_module.time()) + return intent_result + + else: + # GPT 未選擇工具,視為一般聊天 + logger.info("💬 GPT 判斷為一般聊天") + emotion = self._extract_emotion_from_content(response.get("content", "")) + + intent_result = (False, {"emotion": emotion}) + self._intent_cache[cache_key] = (*intent_result, time_module.time()) + return intent_result + except Exception as e: - logger.error(f"❌ GPT 意圖解析發生錯誤: {e}") - # 降級處理:使用關鍵詞匹配 + logger.error(f"❌ Function Calling 意圖檢測失敗: {e}") + # 降級:使用關鍵詞匹配 logger.info("🔄 嘗試使用關鍵詞匹配作為降級方案") try: fallback_result = self._keyword_intent_detection(message) @@ -787,10 +620,177 @@ class MCPAgentBridge: return fallback_result except Exception as fallback_error: logger.error(f"❌ 關鍵詞匹配也失敗: {fallback_error}") - + # 最終降級:視為一般聊天 logger.info("💬 降級為一般聊天") - return False, None + return False, {"emotion": "neutral"} + + def _build_function_calling_prompt(self) -> str: + """ + 建構精簡的 Function Calling system prompt + + 注意:不再描述每個工具,工具定義由 tools 參數傳遞 + 只處理特殊規則和情緒判斷 + """ + return """你是一個智能助手,根據用戶需求選擇合適的工具。 + +規則: +1. 如果用戶需求可以用工具解決,選擇最適合的工具 +2. 如果是一般聊天或問候,不要選擇任何工具 +3. 工具參數盡量從用戶消息中提取,無法確定的使用合理預設值 + +【重要】語言使用規範: +- 調用工具時:所有參數必須使用英文(城市名、國家名、貨幣代碼等) +- 回覆用戶時:必須使用繁體中文 +- 範例:用戶說「台北天氣」→ 參數 {"city": "Taipei"},回覆「台北目前...」 + +參數語言轉換規則: +- 城市名稱:台北→Taipei, 新北→NewTaipei, 桃園→Taoyuan, 台中→Taichung, 台南→Tainan, 高雄→Kaohsiung, 新竹→Hsinchu +- 國家名稱:台灣→Taiwan, 美國→USA, 日本→Japan, 英國→UK +- 貨幣代碼:美元→USD, 台幣→TWD, 日圓→JPY, 歐元→EUR, 英鎊→GBP + +【重要】城市參數提取原則: +- 只有在用戶明確提到城市名稱時才填 city 參數 +- 「附近」「這裡」「我這邊」等詞 → 不填 city 參數,系統會自動從 GPS 判斷 +- 「台北的XX」「桃園XX」→ 填對應的英文城市名 +- 範例:「附近的 YouBike」→ {},「桃園的 YouBike」→ {"city": "Taoyuan"} + +匯率查詢(重要!參數提取規則): +當用戶詢問匯率資訊時,你必須從消息中提取貨幣代碼並填入參數。 + +參數提取規則: +1. 句型「[貨幣A]轉[貨幣B]」「[貨幣A]換[貨幣B]」「[貨幣A]兌[貨幣B]」→ {"from_currency": "代碼A", "to_currency": "代碼B"} +2. 句型「[數字][貨幣A]是多少[貨幣B]」→ {"from_currency": "代碼A", "to_currency": "代碼B", "amount": 數字} +3. 句型「匯率」「美金」「日幣」→ 提取提到的貨幣 +4. 貨幣代碼必須用 ISO 4217 標準(3個大寫字母) + +常見貨幣代碼對照: +- 美元/美金 → USD +- 台幣/新台幣 → TWD +- 日圓/日幣 → JPY +- 歐元 → EUR +- 英鎊 → GBP +- 人民幣 → CNY +- 港幣 → HKD +- 韓元 → KRW + +實際範例: +- 「美元轉日幣的匯率」→ {"from_currency": "USD", "to_currency": "JPY"} +- 「台幣換美金」→ {"from_currency": "TWD", "to_currency": "USD"} +- 「100美元是多少台幣」→ {"from_currency": "USD", "to_currency": "TWD", "amount": 100} +- 「歐元兌日圓」→ {"from_currency": "EUR", "to_currency": "JPY"} +- 「匯率」→ {"from_currency": "USD", "to_currency": "TWD"}(預設) + +重要:必須提取貨幣代碼!不要返回空參數! + +公車查詢(重要!參數提取規則): +當用戶詢問公車資訊時,你必須從消息中提取路線號碼並填入參數。 + +tdx_bus_arrival 適用場景: +- 查詢「已知路線號碼」的到站時間 +- 查詢附近公車站點(不需 route_name) + +參數提取規則: +1. 句型「[數字]公車」「[數字]號公車」→ {"route_name": "數字"} +2. 句型「[顏色][數字]」(如「紅30」)→ {"route_name": "顏色數字"} +3. 句型「[數字]還要多久」「[數字]什麼時候到」→ {"route_name": "數字"} +4. 句型「[路線名]公車到站」→ {"route_name": "路線名"} +5. 「附近公車」「公車站」「有什麼公車」→ {}(系統自動從 GPS 判斷城市) +6. 城市參數:只在用戶明確提到城市時才填,否則留空讓系統自動判斷 + +實際範例: +- 「261公車什麼時候到」→ {"route_name": "261"}(不填 city) +- 「307還要多久」→ {"route_name": "307"}(不填 city) +- 「台北261公車」→ {"route_name": "261", "city": "Taipei"}(明確提到台北) +- 「桃園紅30公車」→ {"route_name": "紅30", "city": "Taoyuan"}(明確提到桃園) +- 「附近有什麼公車」→ {}(完全空參數,系統自動判斷) + +不適用場景(應使用 directions): +- 「從A到B的公車」「往XX的公車」→ 這是路線規劃,不是查詢特定路線 +- 「去台北的公車」→ 台北是目的地,不是路線號碼 + +重要:如果提到路線號碼,必須提取!城市參數必須用英文! + +火車查詢(重要!參數提取規則): +當用戶詢問火車資訊時,你必須從消息中提取站名並填入參數。 + +參數提取規則(適用於任何地名): +1. 句型「從 [地名A] 往/到 [地名B]」→ {"origin_station": "地名A", "destination_station": "地名B"} +2. 句型「[地名A] 到/往 [地名B]」→ {"origin_station": "地名A", "destination_station": "地名B"} +3. 句型「往/去 [地名]」→ {"destination_station": "地名"} +4. 句型「[車種][數字]次」→ {"train_no": "數字"} +5. 包含時間 → 提取為 departure_time(HH:MM 格式) + +實際範例: +- 「從彰化往台北的火車」→ {"origin_station": "彰化", "destination_station": "台北"} +- 「台中到高雄」→ {"origin_station": "台中", "destination_station": "高雄"} +- 「往新竹的火車」→ {"destination_station": "新竹"} +- 「自強號123次」→ {"train_no": "123"} +- 「早上8點台南到台北」→ {"origin_station": "台南", "destination_station": "台北", "departure_time": "08:00"} + +重要:絕對不要返回空的 {} 參數!必須從用戶消息中提取站名! + +位置查詢: +- 「我在哪」使用 reverse_geocode,不需要參數 +- 「怎麼去XX」使用 forward_geocode 或 directions + +YouBike 查詢(重要!參數提取規則): +當用戶詢問 YouBike/Ubike/微笑單車時,你必須調用 tdx_youbike 工具。 + +參數提取規則: +1. 「附近的 YouBike」「Ubike 在哪」→ {}(不填 city,系統自動從 GPS 判斷) +2. 「市政府 YouBike」「台北車站 Ubike」→ {"station_name": "市政府"}(不填 city) +3. 「XX站還有車嗎」→ {"station_name": "XX站"}(不填 city) +4. 「台北的 YouBike」「桃園 YouBike」→ 填對應英文城市名 +5. 站名可用中文,城市必須用英文 + +實際範例: +- 「附近的 YouBike」→ {}(完全空參數,系統自動判斷城市) +- 「市政府 YouBike 還有車嗎」→ {"station_name": "市政府"}(不填 city) +- 「台北車站 Ubike」→ {"station_name": "台北車站"}(不填 city) +- 「台北的 YouBike」→ {"city": "Taipei"}(明確提到台北) +- 「桃園 YouBike」→ {"city": "Taoyuan"}(明確提到桃園) + +重要:只在用戶明確提到城市時才填 city 參數!站名可保持中文! + +【情緒偵測】(重要!): +- 分析用戶的情緒狀態(根據用詞、語氣、標點符號、表情符號) +- 在回應的最後一行加上情緒標籤:[EMOTION:情緒] +- 情緒類型:neutral(平靜)、happy(開心)、sad(難過)、angry(生氣)、fear(害怕)、surprise(驚訝) +- 範例: + * 用戶說「我現在覺得很生氣」→ 回應最後加上 [EMOTION:angry] + * 用戶說「好開心啊!」→ 回應最後加上 [EMOTION:happy] + * 用戶說「我好難過...」→ 回應最後加上 [EMOTION:sad] + * 用戶說「好可怕」→ 回應最後加上 [EMOTION:fear] + * 用戶說「哇!」→ 回應最後加上 [EMOTION:surprise] + * 一般對話 → 回應最後加上 [EMOTION:neutral] +""" + + def _extract_emotion_from_content(self, content: str) -> str: + """從回應內容中提取情緒標籤 [EMOTION:xxx]""" + if not content: + return "neutral" + + # 優先從標籤提取 + import re + emotion_match = re.search(r'\[EMOTION:(neutral|happy|sad|angry|fear|surprise)\]', content, re.IGNORECASE) + if emotion_match: + emotion = emotion_match.group(1).lower() + logger.info(f"😊 從標籤提取情緒: {emotion}") + return emotion + + # 降級:從內容搜尋英文關鍵字 + content_lower = content.lower() + emotions = ["happy", "sad", "angry", "fear", "surprise"] + + for emotion in emotions: + if emotion in content_lower: + logger.debug(f"從內容搜尋到情緒關鍵字: {emotion}") + return emotion + + return "neutral" + + # 舊版 _detect_intent_legacy 已移除,改用 _detect_intent_with_function_calling def _get_intent_schema(self) -> Dict[str, Any]: """ @@ -1138,23 +1138,33 @@ class MCPAgentBridge: "你是一個友善、健談的AI助手。\n" "用戶剛剛問了一個問題,我已經用工具查詢到資料了。\n" "請用自然、口語化的方式回答用戶,就像朋友聊天一樣。\n\n" - "要求:\n" + "【核心原則】\n" + "⭐ 只回答使用者問的問題,不要把所有數據都說出來\n" + "⭐ 分析使用者的核心意圖(問溫度?天氣?時間?地點?數量?)\n" + "⭐ 從工具數據中只提取相關資訊,無關資訊一律省略\n\n" + "【回應要求】\n" "1. 使用口語化、親切的語氣(可以用「喔」「呢」「哦」等語氣詞)\n" "2. 不要列表式的羅列數據,而是用對話方式描述\n" - "3. 突出最重要的資訊(2-3句話)\n" + "3. 只說使用者問的內容(2-3句話)\n" "4. 適當使用 emoji 增加親和力\n" - "5. 如果數據很多,只說重點\n" + "5. 如有額外有用資訊,可簡短補充(不超過一句話)\n" "6. 保持簡短(50字以內最好)\n\n" - "範例:\n" - "❌ 不好:「當前溫度23.88°C,體感溫度24.02°C,天氣狀況多雲...」\n" - "✅ 良好:「台北現在23度左右,有點多雲呢!體感還蠻舒服的~」\n\n" - "記住:你是在聊天,不是在報告數據!" + "【範例】\n" + "用戶問:「台北現在幾度?」\n" + "工具返回:溫度23.88°C、濕度65%、風速3m/s、氣壓1013hPa...\n" + "❌ 錯誤:「台北現在23度,濕度65%,風速3m/s...」(說太多)\n" + "✅ 正確:「台北現在23度左右喔!」(只回答溫度)\n" + "✅ 可接受:「台北現在23度,體感蠻舒服的~」(簡短補充)\n\n" + "記住:精準回答使用者的問題,不要喧賓奪主!" ) user_prompt = ( - f"用戶問:「{original_message}」\n\n" - f"我用 {tool_name} 查到的資料:\n{content}\n\n" - f"請用自然對話的方式回答用戶(簡短、親切、口語化):" + f"【使用者的核心問題】\n" + f"「{original_message}」\n\n" + f"【工具 {tool_name} 返回的數據】\n" + f"{content}\n\n" + f"【任務】\n" + f"請只回答使用者問的問題(簡短、親切、口語化):" ) messages = [ diff --git a/features/mcp/tools/exchange_tool.py b/features/mcp/tools/exchange_tool.py index 25a6faffdaf5173d25e7cb72526c0515dd48ce07..2f305b708a542910b8c43ab78e59f0b164378d8e 100644 --- a/features/mcp/tools/exchange_tool.py +++ b/features/mcp/tools/exchange_tool.py @@ -101,12 +101,14 @@ class ExchangeTool(MCPTool): return cls.create_success_response( content=formatted_text, data={ - "rate": rate, - "from_currency": from_currency, - "to_currency": to_currency, - "amount": amount, - "converted_amount": amount * rate if conversion else None, - "raw_data": rate_data + "raw_data": { + "rate": rate, + "from_currency": from_currency, + "to_currency": to_currency, + "amount": amount, + "converted_amount": amount * rate if conversion else None, + "rate_data": rate_data + } } ) else: diff --git a/features/mcp/tools/geocode_tool.py b/features/mcp/tools/geocode_tool.py index 267b51184ba4ec42fb614d89977cdb89431fb0dd..d5b6ed093704f8899bed0c16fbe37e480c24e27b 100644 --- a/features/mcp/tools/geocode_tool.py +++ b/features/mcp/tools/geocode_tool.py @@ -168,7 +168,7 @@ class ReverseGeocodeTool(MCPTool): # 地點名稱(優先使用繁中) name = data.get("name") or "" - namedetails = data.get("namedetails", {}) + namedetails = data.get("namedetails") or {} name_zh = namedetails.get("name:zh") or namedetails.get("name:zh-TW") or name display_name = data.get("display_name") or "" diff --git a/features/mcp/tools/healthkit_tool.py b/features/mcp/tools/healthkit_tool.py index c0e93cf91e10d27974ad3afb4cbe4b8b4456ddea..2fdb444f44ee322d95e801925b3750e1ff6f7485 100644 --- a/features/mcp/tools/healthkit_tool.py +++ b/features/mcp/tools/healthkit_tool.py @@ -110,8 +110,8 @@ class HealthKitTool(MCPTool): logger.debug("✅ 使用 Firestore 數據庫連接") - # 解析參數 - user_id = arguments.get('user_id') + # 解析參數(_user_id 由 coordinator 注入) + user_id = arguments.get('_user_id') metric_type = arguments.get('metric_type', 'all') days = arguments.get('days', 7) latest_only = arguments.get('latest_only', False) @@ -120,6 +120,7 @@ class HealthKitTool(MCPTool): # 如果沒有提供 user_id,返回錯誤 if not user_id: return cls.create_error_response( + error="需要提供用戶 ID", code="USER_ID_REQUIRED" ) @@ -197,18 +198,27 @@ class HealthKitTool(MCPTool): except Exception: pass # 設備資訊是可選的 + # 限制返回給前端的數據量(避免傳輸過多數據) + # 前端工具卡片只顯示最新的 20 筆,但 count 顯示總數 + display_limit = 20 + display_data = data[:display_limit] if len(data) > display_limit else data + return cls.create_success_response( content=summary, data={ - "health_data": data, - "count": len(data), - "query": { - "metric_type": metric_type, - "days": days, - "latest_only": latest_only, - "aggregation": aggregation - }, - "device_info": device_info + "raw_data": { + "health_data": display_data, + "count": len(data), + "total_records": len(data), + "displayed_records": len(display_data), + "query": { + "metric_type": metric_type, + "days": days, + "latest_only": latest_only, + "aggregation": aggregation + }, + "device_info": device_info + } } ) @@ -325,7 +335,8 @@ class HealthKitTool(MCPTool): if values: avg_value = sum(values) / len(values) latest_value = values[0] if values else 0 - summary += f"• {name}: 最新 {latest_value:.1f},平均 {avg_value:.1f} ({len(values)} 筆記錄)\n" + # 不顯示記錄數,避免使用者困惑 + summary += f"• {name}: 最新 {latest_value:.1f},平均 {avg_value:.1f}\n" else: name = metric_names.get(metric_type, metric_type) if len(data) == 1: @@ -336,6 +347,7 @@ class HealthKitTool(MCPTool): values = [p["value"] for p in data] avg_value = sum(values) / len(values) latest_value = values[0] if values else 0 - summary = f"{name}數據:最新 {latest_value:.1f} {data[0]['unit']},平均 {avg_value:.1f},共 {len(data)} 筆記錄" + # 不顯示記錄數,避免使用者困惑(AI 會根據需要提取重點) + summary = f"{name}數據:最新 {latest_value:.1f} {data[0]['unit']},平均 {avg_value:.1f}" return summary diff --git a/features/mcp/tools/news_tool.py b/features/mcp/tools/news_tool.py index ef82a028fb0d950d37a95a04e91a4bb37fe29880..d4d8463e48ac7bda4a6d2128b15010fe804b7f4d 100644 --- a/features/mcp/tools/news_tool.py +++ b/features/mcp/tools/news_tool.py @@ -98,7 +98,14 @@ class NewsTool(MCPTool): "content": {"type": "string"}, "url": {"type": "string"}, "published_at": {"type": "string"}, - "source": {"type": "string"}, + "source": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "id": {"type": "string"}, + "url": {"type": "string"} + } + }, "category": {"type": "array"}, "language": {"type": "string"}, "sentiment": {"type": "string"} @@ -151,9 +158,11 @@ class NewsTool(MCPTool): return cls.create_success_response( content=formatted_text, data={ - "articles": articles, - "count": len(articles), - "totalResults": total_results + "raw_data": { + "articles": articles, + "count": len(articles), + "totalResults": total_results + } } ) else: diff --git a/features/mcp/tools/tdx_bus_arrival.py b/features/mcp/tools/tdx_bus_arrival.py index 3abe024acd44eae1c6a98e19638142adcc4db067..9d7ee3426b679177dabd95ce18c0eab19617392b 100644 --- a/features/mcp/tools/tdx_bus_arrival.py +++ b/features/mcp/tools/tdx_bus_arrival.py @@ -24,15 +24,24 @@ class TDXBusArrivalTool(MCPTool): """TDX 公車即時到站查詢""" NAME = "tdx_bus_arrival" - DESCRIPTION = "查詢公車即時到站時間(自動感知用戶位置,找最近站點)" + DESCRIPTION = "查詢公車即時到站時間。適用於:1) 已知路線號碼(如307、紅30)的到站查詢;2) 查詢附近公車站。不適用於路線規劃(如「從A到B的公車」應用directions)。" CATEGORY = "道路運輸" TAGS = ["tdx", "公車", "即時到站", "公共運輸"] - KEYWORDS = ["公車", "巴士", "bus", "到站", "即時", "幾分鐘"] + KEYWORDS = ["公車", "巴士", "bus", "到站", "即時", "幾分鐘", "公車站", "等公車", "路線號碼"] USAGE_TIPS = [ - "查詢特定路線: 「307 公車還要多久」", - "查詢附近公車站: 「附近有什麼公車」", - "指定城市: 「台北 307」「高雄紅30」" + "「307 公車還要多久」→ route_name=307", + "「紅30 什麼時候來」→ route_name=紅30", + "「附近有什麼公車」→ 不需參數,用 GPS 查詢附近站點" ] + NEGATIVE_EXAMPLES = [ + "「埔鹽站往彰化的公車」→ 這是路線規劃,不是查詢特定路線!應用 directions", + "「往彰化的公車」→ 彰化是目的地,不是路線號碼!應用 directions", + "「去台北的公車」→ 台北是目的地,不是路線號碼!應用 directions", + "「從A到B的公車」→ 這是路線規劃,應用 directions", + "「公車路線圖」→ 這是詢問路線圖,不是查到站時間" + ] + PRIORITY = 5 + ALIASES = ["bus", "公車", "巴士"] # TDX 城市代碼 VALID_CITIES = { @@ -81,7 +90,7 @@ class TDXBusArrivalTool(MCPTool): "route_name": {"type": "string"}, "stop_name": {"type": "string"}, "direction": {"type": "integer"}, - "estimate_time": {"type": "integer"}, + "estimate_time": {"type": ["integer", "null"]}, "status": {"type": "string"} } } diff --git a/features/mcp/tools/tdx_train.py b/features/mcp/tools/tdx_train.py index 2fc83ffe59966dc0f9bdec4366ca12458f47c191..a309e82b7060a536f198615ce249055f6de5e939 100644 --- a/features/mcp/tools/tdx_train.py +++ b/features/mcp/tools/tdx_train.py @@ -18,16 +18,22 @@ class TDXTrainTool(MCPTool): """TDX 台鐵時刻表查詢""" NAME = "tdx_train" - DESCRIPTION = "查詢台鐵列車時刻表、票價、最近車站(含高鐵轉乘資訊)" + DESCRIPTION = "查詢台鐵列車時刻表。參數提取規則:「從A到B」→origin_station=A,destination_station=B;「往B」→destination_station=B(起點用GPS);「車次123」→train_no=123。" CATEGORY = "軌道運輸" TAGS = ["tdx", "台鐵", "TRA", "火車", "時刻表"] - KEYWORDS = ["台鐵", "臺鐵", "火車", "TRA", "列車", "時刻"] + KEYWORDS = ["台鐵", "臺鐵", "火車", "TRA", "列車", "時刻", "自強號", "莒光號", "區間車"] USAGE_TIPS = [ - "查詢車次: 「自強號 123 次」", - "查詢路線: 「台北到台中的火車」", - "查詢最近車站: 「最近的火車站在哪」", - "查詢時刻: 「下午3點台北到高雄」" + "「自強號 123 次」→ train_no=123", + "「台北到台中的火車」→ origin_station=台北, destination_station=台中", + "「往台北的火車」→ destination_station=台北(起點用 GPS)", + "「下午3點台北到高雄」→ origin_station=台北, destination_station=高雄, departure_time=15:00" ] + NEGATIVE_EXAMPLES = [ + "「火車票怎麼買」→ 這是詢問購票方式,不是查時刻表", + "「火車站在哪」→ 這是查位置,應用 reverse_geocode 或 forward_geocode" + ] + PRIORITY = 8 + ALIASES = ["train", "台鐵", "火車"] @classmethod def get_input_schema(cls) -> Dict[str, Any]: diff --git a/features/mcp/tools/tdx_youbike.py b/features/mcp/tools/tdx_youbike.py index 5accf3723353b0e2a75cc08ebf3ae8971ff8d8fc..97db13b26dcbfedd3f15bbbccaec2c71ca8b1ce4 100644 --- a/features/mcp/tools/tdx_youbike.py +++ b/features/mcp/tools/tdx_youbike.py @@ -20,12 +20,24 @@ class TDXBikeTool(MCPTool): DESCRIPTION = "查詢附近 YouBike 站點、即時車輛數、空位數(支援 YouBike 1.0/2.0)" CATEGORY = "微型運具" TAGS = ["tdx", "youbike", "ubike", "共享單車", "微笑單車"] - KEYWORDS = ["YouBike", "UBike", "微笑單車", "共享單車", "腳踏車", "自行車"] + KEYWORDS = [ + "YouBike", "Youbike", "youbike", "YOUBIKE", + "UBike", "Ubike", "ubike", "UBIKE", + "微笑單車", "共享單車", "公共單車", + "腳踏車站", "單車站", "自行車站", + "借車", "還車", "腳踏車" + ] USAGE_TIPS = [ - "查詢附近站點: 「附近的 YouBike 在哪」", - "查詢特定站點: 「市政府 YouBike 還有車嗎」", - "指定城市: 「台北 YouBike」「高雄 CityBike」" + "「附近的 YouBike」→ 查詢最近站點", + "「Ubike 在哪」→ 查詢最近站點", + "「市政府 YouBike 還有車嗎」→ station_name=市政府" + ] + NEGATIVE_EXAMPLES = [ + "「YouBike 怎麼註冊」→ 這是詢問註冊方式,不是查站點", + "「YouBike 費率」→ 這是詢問價格,不是查站點" ] + PRIORITY = 6 + ALIASES = ["youbike", "ubike", "微笑單車", "共享單車"] # 城市對應 CITY_MAP = { @@ -43,6 +55,16 @@ class TDXBikeTool(MCPTool): @classmethod def get_input_schema(cls) -> Dict[str, Any]: + # 建立包含中文和英文的城市列表 + all_cities = list(cls.CITY_MAP.keys()) + list(cls.CITY_MAP.values()) + # 去重並保持順序 + unique_cities = [] + seen = set() + for city in all_cities: + if city not in seen: + unique_cities.append(city) + seen.add(city) + return StandardToolSchemas.create_input_schema({ "station_name": { "type": "string", @@ -50,8 +72,8 @@ class TDXBikeTool(MCPTool): }, "city": { "type": "string", - "description": "城市名稱(如「Taipei」「Kaohsiung」)", - "enum": list(cls.CITY_MAP.values()) + "description": "城市名稱(支援中文如「台北」「桃園」或英文如「Taipei」「Taoyuan」)", + "enum": unique_cities }, "radius_m": { "type": "integer", @@ -108,6 +130,11 @@ class TDXBikeTool(MCPTool): station_name = safe_str(arguments.get("station_name")) city = arguments.get("city") + + # 如果 city 是中文,轉換為英文 + if city: + city = cls._map_city_name(city) + radius_m = min(int(arguments.get("radius_m", 500)), 2000) limit = min(int(arguments.get("limit", 5)), 20) @@ -166,7 +193,20 @@ class TDXBikeTool(MCPTool): final_city = guessed city_source = "經緯度推斷" - city = cls._map_city_name(final_city) if final_city else "Taipei" + # 檢查城市是否支援 YouBike + if final_city: + city = cls._map_city_name(final_city) + if city == "Taipei" and final_city not in cls.CITY_MAP: + # 城市不在支援列表中,提供友善錯誤訊息 + nearest_city = cls._find_nearest_supported_city(user_lat, user_lon) + raise ExecutionError( + f"🚲 很抱歉,{final_city}目前沒有 YouBike 服務。\n\n" + f"最近有 YouBike 的城市是:{nearest_city}\n" + f"支援 YouBike 的城市:台北、新北、桃園、新竹、台中、台南、高雄" + ) + else: + city = "Taipei" + logger.info(f"🏙️ 最終使用城市代碼: {city} (來源={city_source})") # 3. 查詢分支 @@ -365,6 +405,7 @@ class TDXBikeTool(MCPTool): ("新北", 24.67, 25.30, 121.35, 122.01), ("新竹", 24.68, 24.90, 120.90, 121.10), ("台中", 24.00, 24.45, 120.45, 121.05), + ("彰化", 23.85, 24.15, 120.35, 120.70), # 新增彰化範圍 ("台南", 22.85, 23.40, 120.00, 120.55), ("高雄", 22.45, 23.15, 120.15, 120.80), ] @@ -386,6 +427,31 @@ class TDXBikeTool(MCPTool): return value return "Taipei" + @staticmethod + def _find_nearest_supported_city(lat: float, lon: float) -> str: + """找出最近的支援 YouBike 的城市""" + # 支援 YouBike 的城市中心點(大約位置) + city_centers = { + "台北": (25.033, 121.565), + "新北": (25.012, 121.466), + "桃園": (24.994, 121.301), + "新竹": (24.806, 120.968), + "台中": (24.148, 120.674), + "台南": (22.997, 120.213), + "高雄": (22.627, 120.301), + } + + min_distance = float('inf') + nearest_city = "台北" + + for city_name, (city_lat, city_lon) in city_centers.items(): + distance = TDXBaseAPI.haversine_distance(lat, lon, city_lat, city_lon) + if distance < min_distance: + min_distance = distance + nearest_city = city_name + + return nearest_city + @staticmethod def _detect_bike_type(station: Dict, station_name: str) -> str: """判斷 YouBike 類型(優先從站名判斷,其次從 BikesCapacity)""" diff --git a/features/mcp/tools/weather_tool.py b/features/mcp/tools/weather_tool.py index 5b17fa873ea4745f3508ff3e95dec5ad5fed7c89..c36b21a541344e387dd65a49c1ece808f2f0b148 100644 --- a/features/mcp/tools/weather_tool.py +++ b/features/mcp/tools/weather_tool.py @@ -33,12 +33,18 @@ class WeatherTool(MCPTool): DESCRIPTION = "查詢指定城市的即時天氣資訊(溫度、濕度、天氣狀況等)" CATEGORY = "生活資訊" TAGS = ["weather", "天氣", "氣象"] - KEYWORDS = ["天氣", "氣溫", "下雨", "晴天", "陰天", "weather", "溫度"] + KEYWORDS = ["天氣", "氣溫", "下雨", "晴天", "陰天", "weather", "溫度", "濕度", "會不會下雨", "熱不熱", "冷不冷"] USAGE_TIPS = [ - "提供城市名稱(英文)如 Taipei, Tokyo", - "支援經緯度查詢", - "可指定語言 (zh_tw, en, zh_cn)" + "「台北天氣」→ city=Taipei", + "「東京今天會下雨嗎」→ city=Tokyo", + "「現在幾度」→ 使用用戶位置" ] + NEGATIVE_EXAMPLES = [ + "「天氣預報 App」→ 這是詢問 App,不是查天氣", + "「天氣好好」→ 這是感嘆,不是查詢" + ] + PRIORITY = 1 # 高優先級 + ALIASES = ["weather", "氣象"] @classmethod def get_input_schema(cls) -> Dict[str, Any]: @@ -57,7 +63,7 @@ class WeatherTool(MCPTool): "default": "zh_tw", "enum": ["zh_tw", "en", "zh_cn"] } - }, ["city"]) # city 可留空,若 lat/lon 有值則忽略 + }, []) # 所有參數都是可選的,由 execute 方法內部邏輯判斷 @classmethod def get_output_schema(cls) -> Dict[str, Any]: diff --git a/middleware/__init__.py b/middleware/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1d3270bd3c1a8133c465591d9fdec40330b8c7bd --- /dev/null +++ b/middleware/__init__.py @@ -0,0 +1,18 @@ +""" +中間件模組 +拆分自 app.py,提高可維護性 +""" + +from .csp import CSPMiddleware +from .rate_limit import RateLimitMiddleware, rate_limiter +from .exception_handler import ExceptionHandlerMiddleware, RequestLoggingMiddleware +from .compression import GzipMiddleware + +__all__ = [ + "CSPMiddleware", + "RateLimitMiddleware", + "rate_limiter", + "ExceptionHandlerMiddleware", + "RequestLoggingMiddleware", + "GzipMiddleware", +] diff --git a/middleware/compression.py b/middleware/compression.py new file mode 100644 index 0000000000000000000000000000000000000000..e44cacb81bef80b141183741d0ff3ac59ed835b4 --- /dev/null +++ b/middleware/compression.py @@ -0,0 +1,106 @@ +""" +回應壓縮中間件 +使用 gzip 壓縮 API 回應,減少傳輸大小 +""" + +import gzip +from typing import Callable + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response + +from core.logging import get_logger + +logger = get_logger("middleware.compression") + +# 最小壓縮大小(bytes) +MIN_COMPRESS_SIZE = 500 + +# 可壓縮的 Content-Type +COMPRESSIBLE_TYPES = { + "application/json", + "text/html", + "text/plain", + "text/css", + "text/javascript", + "application/javascript", +} + + +class GzipMiddleware(BaseHTTPMiddleware): + """ + Gzip 壓縮中間件 + + 功能: + 1. 檢查客戶端是否支援 gzip + 2. 壓縮大於閾值的回應 + 3. 只壓縮可壓縮的 Content-Type + """ + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + # 檢查客戶端是否支援 gzip + accept_encoding = request.headers.get("accept-encoding", "") + supports_gzip = "gzip" in accept_encoding.lower() + + response = await call_next(request) + + # 不支援 gzip 或已經壓縮,直接返回 + if not supports_gzip: + return response + + if response.headers.get("content-encoding"): + return response + + # 檢查 Content-Type 是否可壓縮 + content_type = response.headers.get("content-type", "") + base_type = content_type.split(";")[0].strip() + + if base_type not in COMPRESSIBLE_TYPES: + return response + + # 讀取回應內容 + body = b"" + async for chunk in response.body_iterator: + body += chunk + + # 檢查大小是否值得壓縮 + if len(body) < MIN_COMPRESS_SIZE: + # 重建回應 + return Response( + content=body, + status_code=response.status_code, + headers=dict(response.headers), + media_type=response.media_type, + ) + + # 壓縮內容 + compressed = gzip.compress(body, compresslevel=6) + + # 只有壓縮後更小才使用 + if len(compressed) >= len(body): + return Response( + content=body, + status_code=response.status_code, + headers=dict(response.headers), + media_type=response.media_type, + ) + + # 更新標頭 + headers = dict(response.headers) + headers["content-encoding"] = "gzip" + headers["content-length"] = str(len(compressed)) + # 移除可能衝突的標頭 + headers.pop("transfer-encoding", None) + + logger.debug( + f"壓縮回應: {len(body)} -> {len(compressed)} bytes " + f"({100 - len(compressed) * 100 // len(body)}% 減少)" + ) + + return Response( + content=compressed, + status_code=response.status_code, + headers=headers, + media_type=response.media_type, + ) diff --git a/middleware/csp.py b/middleware/csp.py new file mode 100644 index 0000000000000000000000000000000000000000..9a694b634fed99ea62436d38de86d826b7e7616b --- /dev/null +++ b/middleware/csp.py @@ -0,0 +1,36 @@ +""" +Content Security Policy 中間件 +允許內嵌 script 用於語音沉浸式前端 +""" + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request as StarletteRequest + + +class CSPMiddleware(BaseHTTPMiddleware): + """CSP 中間件""" + + async def dispatch(self, request: StarletteRequest, call_next): + response = await call_next(request) + + # 對所有靜態檔案路徑添加寬鬆的 CSP header + if request.url.path.startswith("/static/"): + # 移除可能存在的嚴格 CSP + if "Content-Security-Policy" in response.headers: + del response.headers["Content-Security-Policy"] + + # 設定寬鬆的 CSP 以允許內嵌 script + response.headers["Content-Security-Policy"] = ( + "default-src 'self'; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval' " + "https://accounts.google.com https://www.gstatic.com; " + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " + "font-src 'self' https://fonts.gstatic.com data:; " + "connect-src 'self' ws: wss: https://accounts.google.com; " + "img-src 'self' data: https: blob:; " + "media-src 'self' blob: data:; " + "frame-src https://accounts.google.com; " + "base-uri 'self';" + ) + + return response diff --git a/middleware/exception_handler.py b/middleware/exception_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..800795e33299304198a3629fcca1b53b11d9e092 --- /dev/null +++ b/middleware/exception_handler.py @@ -0,0 +1,99 @@ +""" +全域異常處理中間件 +統一處理所有未捕獲的異常,返回標準化錯誤回應 +""" + +import logging +import traceback +from typing import Callable + +from fastapi import Request, Response +from fastapi.responses import JSONResponse +from starlette.middleware.base import BaseHTTPMiddleware + +from core.exceptions import BloomWareException, handle_exception +from core.logging import get_logger + +logger = get_logger("middleware.exception_handler") + + +class ExceptionHandlerMiddleware(BaseHTTPMiddleware): + """ + 全域異常處理中間件 + + 功能: + 1. 捕獲所有未處理的異常 + 2. 轉換為標準化 JSON 錯誤回應 + 3. 記錄錯誤日誌(生產環境隱藏堆疊) + """ + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + try: + response = await call_next(request) + return response + + except BloomWareException as e: + # 已知的業務異常 + logger.warning(f"業務異常: {e.code} - {e.message}") + return e.to_response() + + except Exception as e: + # 未知異常 + logger.error(f"未處理的異常: {type(e).__name__}: {e}") + logger.debug(f"堆疊追蹤:\n{traceback.format_exc()}") + + # 返回標準化錯誤回應 + return JSONResponse( + status_code=500, + content={ + "success": False, + "error": { + "code": "INTERNAL_ERROR", + "message": "內部伺服器錯誤", + "details": {} + } + } + ) + + +class RequestLoggingMiddleware(BaseHTTPMiddleware): + """ + 請求日誌中間件 + + 功能: + 1. 記錄所有 API 請求 + 2. 計算請求處理時間 + 3. 記錄回應狀態碼 + """ + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + import time + + start_time = time.time() + method = request.method + path = request.url.path + + # 跳過健康檢查和靜態資源的日誌 + skip_paths = ["/health", "/static/", "/favicon.ico"] + should_log = not any(path.startswith(p) for p in skip_paths) + + try: + response = await call_next(request) + process_time = (time.time() - start_time) * 1000 + + if should_log: + logger.info( + f"{method} {path} - {response.status_code} - {process_time:.2f}ms" + ) + + # 添加處理時間到回應標頭 + response.headers["X-Process-Time"] = f"{process_time:.2f}ms" + return response + + except Exception as e: + process_time = (time.time() - start_time) * 1000 + if should_log: + logger.error( + f"{method} {path} - ERROR - {process_time:.2f}ms - {e}" + ) + raise diff --git a/middleware/rate_limit.py b/middleware/rate_limit.py new file mode 100644 index 0000000000000000000000000000000000000000..09b53d742eefb4ca113d07b15d6b6196ba20db36 --- /dev/null +++ b/middleware/rate_limit.py @@ -0,0 +1,165 @@ +""" +Rate Limiting 中間件 +防止 API 濫用 +""" + +import logging +import time +from typing import Dict, Tuple +from collections import defaultdict +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request as StarletteRequest +from starlette.responses import JSONResponse + +logger = logging.getLogger("middleware.rate_limit") + + +class RateLimiter: + """ + 簡易 Rate Limiter(記憶體實現) + + 生產環境建議使用 Redis 實現 + """ + + def __init__( + self, + requests_per_minute: int = 60, + requests_per_hour: int = 1000, + ): + self.requests_per_minute = requests_per_minute + self.requests_per_hour = requests_per_hour + + # 記錄請求:{ip: [(timestamp, count), ...]} + self._minute_requests: Dict[str, list] = defaultdict(list) + self._hour_requests: Dict[str, list] = defaultdict(list) + + def _cleanup_old_requests(self, requests: list, window_seconds: int) -> list: + """清理過期的請求記錄""" + current_time = time.time() + return [ + (ts, count) for ts, count in requests + if current_time - ts < window_seconds + ] + + def is_allowed(self, client_ip: str) -> Tuple[bool, str]: + """ + 檢查請求是否被允許 + + Returns: + (is_allowed, reason) + """ + current_time = time.time() + + # 清理過期記錄 + self._minute_requests[client_ip] = self._cleanup_old_requests( + self._minute_requests[client_ip], 60 + ) + self._hour_requests[client_ip] = self._cleanup_old_requests( + self._hour_requests[client_ip], 3600 + ) + + # 計算當前窗口內的請求數 + minute_count = sum(count for _, count in self._minute_requests[client_ip]) + hour_count = sum(count for _, count in self._hour_requests[client_ip]) + + # 檢查限制 + if minute_count >= self.requests_per_minute: + return False, f"每分鐘請求數超過限制({self.requests_per_minute})" + + if hour_count >= self.requests_per_hour: + return False, f"每小時請求數超過限制({self.requests_per_hour})" + + # 記錄請求 + self._minute_requests[client_ip].append((current_time, 1)) + self._hour_requests[client_ip].append((current_time, 1)) + + return True, "" + + def get_remaining(self, client_ip: str) -> Dict[str, int]: + """獲取剩餘請求數""" + # 清理過期記錄 + self._minute_requests[client_ip] = self._cleanup_old_requests( + self._minute_requests[client_ip], 60 + ) + self._hour_requests[client_ip] = self._cleanup_old_requests( + self._hour_requests[client_ip], 3600 + ) + + minute_count = sum(count for _, count in self._minute_requests[client_ip]) + hour_count = sum(count for _, count in self._hour_requests[client_ip]) + + return { + "minute_remaining": max(0, self.requests_per_minute - minute_count), + "hour_remaining": max(0, self.requests_per_hour - hour_count), + } + + +# 全局 Rate Limiter 實例 +rate_limiter = RateLimiter( + requests_per_minute=60, + requests_per_hour=1000, +) + + +def get_client_ip(request: StarletteRequest) -> str: + """獲取客戶端 IP""" + # 優先取 X-Forwarded-For + xff = request.headers.get("x-forwarded-for") or request.headers.get("X-Forwarded-For") + if xff: + ip = xff.split(",")[0].strip() + if ip: + return ip + return request.client.host if request.client else "unknown" + + +class RateLimitMiddleware(BaseHTTPMiddleware): + """Rate Limiting 中間件""" + + # 不需要限制的路徑 + EXEMPT_PATHS = { + "/", + "/health", + "/static", + "/login", + "/favicon.ico", + } + + async def dispatch(self, request: StarletteRequest, call_next): + # 檢查是否豁免 + path = request.url.path + if any(path.startswith(exempt) for exempt in self.EXEMPT_PATHS): + return await call_next(request) + + # 獲取客戶端 IP + client_ip = get_client_ip(request) + + # 檢查 Rate Limit + is_allowed, reason = rate_limiter.is_allowed(client_ip) + + if not is_allowed: + logger.warning(f"Rate limit exceeded for {client_ip}: {reason}") + remaining = rate_limiter.get_remaining(client_ip) + + return JSONResponse( + status_code=429, + content={ + "error": "Too Many Requests", + "message": reason, + "retry_after": 60, # 建議等待時間(秒) + }, + headers={ + "Retry-After": "60", + "X-RateLimit-Remaining-Minute": str(remaining["minute_remaining"]), + "X-RateLimit-Remaining-Hour": str(remaining["hour_remaining"]), + } + ) + + # 繼續處理請求 + response = await call_next(request) + + # 添加 Rate Limit 頭 + remaining = rate_limiter.get_remaining(client_ip) + response.headers["X-RateLimit-Remaining-Minute"] = str(remaining["minute_remaining"]) + response.headers["X-RateLimit-Remaining-Hour"] = str(remaining["hour_remaining"]) + + return response diff --git a/models/schemas.py b/models/schemas.py new file mode 100644 index 0000000000000000000000000000000000000000..a38b03a7d98e31900e808e62d8a62633bdaa1d29 --- /dev/null +++ b/models/schemas.py @@ -0,0 +1,123 @@ +""" +Pydantic 模型定義 +統一管理 API 請求/回應的資料結構 +""" + +from datetime import datetime +from typing import List, Optional +from pydantic import BaseModel, EmailStr, Field + + +# ===== 用戶相關 ===== + +class UserCreate(BaseModel): + """用戶註冊請求""" + name: str + email: EmailStr + password: str = Field(min_length=6) + + +class UserLogin(BaseModel): + """用戶登入請求""" + email: EmailStr + password: str + + +class UserInfo(BaseModel): + """用戶資訊""" + id: str + name: str + email: EmailStr + created_at: datetime + + +class UserPublic(BaseModel): + """用戶公開資訊回應""" + success: bool + user: UserInfo + + +class UserLoginPublicResponse(BaseModel): + """用戶登入回應""" + success: bool + user: UserInfo + token: Optional[str] = None + + +# ===== 對話相關 ===== + +class ChatCreateRequest(BaseModel): + """建立對話請求""" + user_id: str + title: Optional[str] = "新對話" + + +class ChatTitleUpdateRequest(BaseModel): + """更新對話標題請求""" + title: str + + +class ChatPublic(BaseModel): + """對話公開資訊""" + chat_id: str + user_id: str + title: str + created_at: datetime + updated_at: datetime + + +class ChatSummary(BaseModel): + """對話摘要""" + chat_id: str + title: str + updated_at: datetime + + +class ChatListResponse(BaseModel): + """對話列表回應""" + chats: List[ChatSummary] + + +# ===== 訊息相關 ===== + +class MessageCreateRequest(BaseModel): + """建立訊息請求""" + sender: str + content: str + + +class MessagePublic(BaseModel): + """訊息公開資訊""" + sender: str + content: str + timestamp: datetime + + +class ChatDetailResponse(ChatPublic): + """對話詳情回應(含訊息)""" + messages: List[MessagePublic] + + +# ===== 檔案分析 ===== + +class FileAnalysisRequest(BaseModel): + """檔案分析請求""" + filename: str + content: str + mime_type: str + user_prompt: Optional[str] = "請分析這個檔案的內容" + + +class FileAnalysisResponse(BaseModel): + """檔案分析回應""" + success: bool + filename: str + analysis: Optional[str] = None + error: Optional[str] = None + + +# ===== 語音相關 ===== + +class SpeakerLabelBindRequest(BaseModel): + """語音標籤綁定請求""" + speaker_label: str diff --git a/models/speaker_identification/models_cnn/speaker_db.pkl b/models/speaker_identification/models_cnn/speaker_db.pkl new file mode 100644 index 0000000000000000000000000000000000000000..73b12df5ff2ec0301ff21b4c4167b619f5f8f115 --- /dev/null +++ b/models/speaker_identification/models_cnn/speaker_db.pkl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4377a566b92e5cca1db0f83274a7037169f371e47ca357e3c764c8908ef55e43 +size 4157 diff --git a/models/speaker_identification/scripts/inference.py b/models/speaker_identification/scripts/inference.py index 543c1b9cdb4774d2af56b6e39b72f83087428379..c48dd610981e474796605eda313cefb8ab74c311 100644 --- a/models/speaker_identification/scripts/inference.py +++ b/models/speaker_identification/scripts/inference.py @@ -1,144 +1,91 @@ import os -import argparse -import numpy as np +import torch import librosa -import noisereduce as nr -import pyaudio +import numpy as np +import pickle import wave -import torch -import torch.nn as nn -import torchaudio +import pyaudio +from speechbrain.pretrained import EncoderClassifier +from sklearn.metrics.pairwise import cosine_similarity +# 常數 +SAMPLE_RATE = 16000 +DB_FILE = "speaker_db.pkl" -def get_device(): - if torch.cuda.is_available(): - print("Using device: CUDA") - return torch.device('cuda') - if torch.backends.mps.is_available(): - print("Using device: MPS") - return torch.device('mps') - print("Using device: CPU") - return torch.device('cpu') +# 全域 classifier(延遲載入) +_classifier = None -class Wav2Vec2SpeakerClassifier(nn.Module): - def __init__(self, bundle, num_classes, freeze_encoder=True): - super().__init__() - self.encoder = bundle.get_model() - for p in self.encoder.parameters(): - p.requires_grad = False - hidden_dim = getattr(self.encoder, 'encoder_embed_dim', 768) - self.classifier = nn.Sequential( - nn.Dropout(0.0), - nn.Linear(hidden_dim, num_classes) +def get_classifier(): + """取得 ECAPA-TDNN 分類器(單例模式)""" + global _classifier + if _classifier is None: + _classifier = EncoderClassifier.from_hparams( + source="speechbrain/spkrec-ecapa-voxceleb" ) + return _classifier - def forward(self, waveforms, lengths=None): - out = self.encoder(waveforms, lengths) - if isinstance(out, tuple): - features, lengths = out - else: - features, lengths = out, None - if lengths is not None: - valid_lengths = (lengths * features.size(1)).round().to(torch.long).clamp(min=1, max=features.size(1)) - mask = torch.arange(features.size(1), device=features.device).unsqueeze(0) < valid_lengths.unsqueeze(1) - features = features * mask.unsqueeze(-1) - pooled = features.sum(dim=1) / valid_lengths.unsqueeze(-1) - else: - pooled = features.mean(dim=1) - return self.classifier(pooled) - -def load_classes(classes_path_or_dir): - # 若提供文字檔則逐行讀取,否則從目錄列出子資料夾 - if os.path.isfile(classes_path_or_dir): - with open(classes_path_or_dir, 'r', encoding='utf-8') as f: - classes = [line.strip() for line in f if line.strip()] - else: - classes = sorted([d for d in os.listdir(classes_path_or_dir) - if os.path.isdir(os.path.join(classes_path_or_dir, d))]) - return classes +def get_embedding(file_path): + """從音訊檔案取得 ECAPA-TDNN 嵌入向量""" + signal, sr = librosa.load(file_path, sr=SAMPLE_RATE, mono=True) + signal_tensor = torch.tensor(signal).unsqueeze(0) + classifier = get_classifier() + embedding = classifier.encode_batch(signal_tensor) + return embedding.squeeze().detach().numpy() -def load_audio(path, target_sr): - wav, sr = torchaudio.load(path) - if wav.size(0) > 1: - wav = wav.mean(dim=0, keepdim=True) - if sr != target_sr: - resampler = torchaudio.transforms.Resample(sr, target_sr) - wav = resampler(wav) - sr = target_sr - wav = wav.squeeze(0) - length = torch.tensor([wav.shape[0]], dtype=torch.long) - return wav.unsqueeze(0), length +def load_speaker_db(db_path): + """載入說話者嵌入資料庫""" + with open(db_path, "rb") as f: + speaker_embeddings = pickle.load(f) + return speaker_embeddings -def softmax(x): - e = torch.exp(x - x.max(dim=1, keepdim=True).values) - return e / e.sum(dim=1, keepdim=True) +def recognize_speaker(test_file, speaker_embeddings): + """ + 辨識語音(與原始 ECAPA_TDNN.py 完全一致) + """ + test_emb = get_embedding(test_file).reshape(1, -1) + scores = {} + for spk, emb in speaker_embeddings.items(): + sim = cosine_similarity(test_emb, emb.reshape(1, -1))[0][0] + scores[spk] = sim + predicted = max(scores, key=scores.get) + scores[predicted] += 0.35 + return predicted, scores def predict_files(model_dir, file_list, threshold=0.0): """ - 預測多個音訊檔案的說話者。 + 預測多個音訊檔案的說話者 Args: - model_dir: 模型目錄,包含 speaker_id_model.pth 和 classes.txt + model_dir: 模型目錄,包含 speaker_db.pkl file_list: 檔案路徑列表 - threshold: 預測門檻(目前未使用) + threshold: 未使用,保留介面相容 Returns: 結果列表,每個元素為字典,包含 'pred', 'score', 'top' """ - device = get_device() - bundle = torchaudio.pipelines.WAV2VEC2_BASE - target_sr = bundle.sample_rate - - model_path = os.path.join(model_dir, 'speaker_id_model.pth') - classes_path = os.path.join(model_dir, 'classes.txt') - processed_dir = os.path.join(model_dir, 'processed_audio') - - if os.path.isfile(classes_path): - classes = load_classes(classes_path) - elif os.path.isdir(processed_dir): - classes = load_classes(processed_dir) - else: - raise FileNotFoundError(f"找不到類別定義:{classes_path} 或 {processed_dir}") - - num_classes = len(classes) - - model = Wav2Vec2SpeakerClassifier(bundle, num_classes) - state = torch.load(model_path, map_location='cpu') - model.load_state_dict(state) - model.to(device).eval() + db_path = os.path.join(model_dir, DB_FILE) + if not os.path.exists(db_path): + raise FileNotFoundError(f"找不到說話者資料庫:{db_path}") + speaker_embeddings = load_speaker_db(db_path) results = [] + for file_path in file_list: try: - # 前處理音訊 - y, sr = process_like_training(file_path) - - # 轉成模型輸入 - y_t = torch.tensor(y, dtype=torch.float32).unsqueeze(0) - if sr != target_sr: - resampler = torchaudio.transforms.Resample(sr, target_sr) - y_t = resampler(y_t) - length = torch.tensor([y_t.shape[1]], dtype=torch.long) - waveforms, lengths = y_t.to(device), length.to(device) + predicted, scores = recognize_speaker(file_path, speaker_embeddings) - with torch.no_grad(): - logits = model(waveforms, lengths) - probs = softmax(logits).squeeze(0).cpu() - top_prob, top_idx = torch.max(probs, dim=0) - pred = classes[top_idx.item()] - - # 獲取 top 候選 - topk = torch.topk(probs, k=min(3, num_classes)) - top = [(classes[i], float(p)) for p, i in zip(topk.values.tolist(), topk.indices.tolist())] + # 排序取 top 候選 + sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True) + top = [(spk, float(score)) for spk, score in sorted_scores[:3]] result = { - 'pred': pred, - 'score': float(top_prob.item()), + 'pred': predicted, + 'score': float(scores[predicted]), 'top': top } results.append(result) @@ -148,20 +95,18 @@ def predict_files(model_dir, file_list, threshold=0.0): return results -# ============== 錄音與前處理(比照 process_audio.py) ============== -REC_SR = 22050 -TARGET_RMS = 0.1 -VAD_TOP_DB = 30 - - -def record_audio(filename, seconds=3, sr=REC_SR): +# ============== 錄音功能 ============== +def record_audio(filename, seconds=3, sr=SAMPLE_RATE): + """從麥克風錄製音訊""" pa = pyaudio.PyAudio() stream = pa.open(format=pyaudio.paInt16, channels=1, rate=sr, input=True, frames_per_buffer=1024) print(f"開始錄製 {seconds}s...") frames = [] for _ in range(int(sr / 1024 * seconds)): frames.append(stream.read(1024)) - stream.stop_stream(); stream.close(); pa.terminate() + stream.stop_stream() + stream.close() + pa.terminate() with wave.open(filename, 'wb') as wf: wf.setnchannels(1) wf.setsampwidth(pyaudio.PyAudio().get_sample_size(pyaudio.paInt16)) @@ -170,82 +115,37 @@ def record_audio(filename, seconds=3, sr=REC_SR): print("錄製結束。") -def process_like_training(input_wav_path): - # 與 process_audio.py 一致:librosa 載入、去噪、VAD、RMS 正規化(保留原始長度) - y, sr = librosa.load(input_wav_path, sr=REC_SR) - y = nr.reduce_noise(y=y, sr=sr) - intervals = librosa.effects.split(y, top_db=VAD_TOP_DB) - if len(intervals) > 0: - y = np.concatenate([y[s:e] for s, e in intervals]) - # RMS 正規化 - rms = np.sqrt(np.mean(y ** 2)) if len(y) else 0.0 - if rms > 0: - y = y * (TARGET_RMS / rms) - return y, sr - - def main(): - parser = argparse.ArgumentParser(description='Speaker ID Inference (Wav2Vec2)') - parser.add_argument('--audio', type=str, default=None, help='Path to wav file;若省略則使用麥克風錄音') - parser.add_argument('--model', type=str, default='speaker_id_model.pth', help='Path to model .pth') - parser.add_argument('--classes', type=str, default='processed_audio', help='Path to classes dir or classes.txt') + import argparse + parser = argparse.ArgumentParser(description='Speaker ID Inference (ECAPA-TDNN)') + parser.add_argument('--audio', type=str, default=None, help='音訊檔案路徑;若省略則使用麥克風錄音') + parser.add_argument('--db', type=str, default='speaker_db.pkl', help='說話者資料庫路徑') parser.add_argument('--seconds', type=int, default=3, help='錄音秒數(麥克風模式)') - parser.add_argument('--save-processed', action='store_true', help='輸出處理後音檔 processed_record.wav') args = parser.parse_args() - device = get_device() - bundle = torchaudio.pipelines.WAV2VEC2_BASE - target_sr = bundle.sample_rate + # 載入資料庫 + speaker_embeddings = load_speaker_db(args.db) + print(f"已載入 {len(speaker_embeddings)} 位說話者:{list(speaker_embeddings.keys())}") - classes = load_classes(args.classes) - num_classes = len(classes) - - model = Wav2Vec2SpeakerClassifier(bundle, num_classes) - state = torch.load(args.model, map_location='cpu') - model.load_state_dict(state) - model.to(device).eval() - - # 準備音訊來源:檔案或錄音 + # 準備音訊 temp_path = None if args.audio is None: temp_path = 'temp_record.wav' - record_audio(temp_path, seconds=args.seconds, sr=REC_SR) - raw_path = temp_path + record_audio(temp_path, seconds=args.seconds) + audio_path = temp_path else: - raw_path = args.audio - - # 前處理(比照 process_audio.py) - y, sr = process_like_training(raw_path) - if args.save_processed: - try: - import soundfile as sf - sf.write('processed_record.wav', y, sr) - except Exception: - pass - - # 轉成模型輸入:重採樣到 16k + 提供長度 - y_t = torch.tensor(y, dtype=torch.float32).unsqueeze(0) - if sr != target_sr: - resampler = torchaudio.transforms.Resample(sr, target_sr) - y_t = resampler(y_t) - sr = target_sr - length = torch.tensor([y_t.shape[1]], dtype=torch.long) - waveforms, lengths = y_t.to(device), length.to(device) + audio_path = args.audio - with torch.no_grad(): - logits = model(waveforms, lengths) - probs = softmax(logits).squeeze(0).cpu() - top_prob, top_idx = torch.max(probs, dim=0) - pred = classes[top_idx.item()] + # 辨識 + predicted, scores = recognize_speaker(audio_path, speaker_embeddings) - print(f'Predicted speaker: {pred} (prob={top_prob.item():.3f})') - # 顯示前 3 名 - topk = torch.topk(probs, k=min(3, num_classes)) - print('Top candidates:') - for p, i in zip(topk.values.tolist(), topk.indices.tolist()): - print(f' {classes[i]}: {p:.3f}') + print(f'\n辨識結果:{predicted}') + print('辨識機率:') + for spk, score in sorted(scores.items(), key=lambda x: x[1], reverse=True): + print(f' {spk}: {score:.4f}') - if temp_path is not None and os.path.exists(temp_path): + # 清理暫存檔 + if temp_path and os.path.exists(temp_path): try: os.remove(temp_path) except Exception: diff --git a/models/speaker_identification/scripts/process_audio.py b/models/speaker_identification/scripts/process_audio.py deleted file mode 100644 index 4d0dc2591f72906934eff1b018cd427995f82892..0000000000000000000000000000000000000000 --- a/models/speaker_identification/scripts/process_audio.py +++ /dev/null @@ -1,52 +0,0 @@ -import os -import librosa -import numpy as np -import noisereduce as nr -import soundfile as sf - -# 參考採樣率與音量標準化 -sr = 22050 # 假設採樣率 -target_rms = 0.1 # 目標 RMS 水平 -vad_top_db = 30 # VAD 門檻,值越小越容易刪除靜音 - -# 輸出目錄 -output_dir = 'processed_audio' -os.makedirs(output_dir, exist_ok=True) - -# 用於記錄每個子目錄的當前編號 -counter = {} - -# 遍歷 voice_data 目錄下的所有 .wav 和 .mp3 文件 -for root, dirs, files in os.walk('voice_data'): - for file in files: - if file.endswith(('.wav', '.mp3')): - file_path = os.path.join(root, file) - # 獲取相對路徑,用於創建輸出子目錄 - rel_path = os.path.relpath(root, 'voice_data') - sub_output_dir = os.path.join(output_dir, rel_path) - os.makedirs(sub_output_dir, exist_ok=True) - - if rel_path not in counter: - counter[rel_path] = 1 - - y, sr = librosa.load(file_path, sr=sr) - # 去噪 - y = nr.reduce_noise(y=y, sr=sr) - - # 語音活動偵測 (VAD),移除靜音區段 - intervals = librosa.effects.split(y, top_db=vad_top_db) - if len(intervals) == 0: - # 無語音內容,跳過此檔 - continue - y = np.concatenate([y[start:end] for start, end in intervals]) - - # 保留原始長度並直接保存 - rms = np.sqrt(np.mean(y**2)) - if rms > 0: - y = y * (target_rms / rms) - - output_path = os.path.join(sub_output_dir, f'voice_{counter[rel_path]:02d}.wav') - sf.write(output_path, y, sr) - counter[rel_path] += 1 - -print("音頻處理完成。") diff --git a/models/speaker_identification/scripts/train_speaker_id.py b/models/speaker_identification/scripts/train_speaker_id.py deleted file mode 100644 index a73cb2de348fd488092348b37ce74d9a92caf02e..0000000000000000000000000000000000000000 --- a/models/speaker_identification/scripts/train_speaker_id.py +++ /dev/null @@ -1,306 +0,0 @@ -import os -import random - -import matplotlib.pyplot as plt -import numpy as np -import seaborn as sns -import torch -import torch.nn as nn -import torch.optim as optim -import torchaudio -import torchaudio.functional as F -from sklearn.metrics import confusion_matrix -from sklearn.preprocessing import LabelEncoder -from sklearn.utils.class_weight import compute_class_weight -from torch.utils.data import DataLoader, Dataset - - -# 基本參數 -data_dir = '/kaggle/input/voice-identity/processed_audio' -batch_size = 16 -num_epochs = 50 - -# 遷移學習設定(不使用 HuggingFace,改用 torchaudio 內建預訓練) -bundle = torchaudio.pipelines.WAV2VEC2_BASE -target_sr = bundle.sample_rate -freeze_encoder = True # 先凍結整個 encoder,只訓練分類頭 -unfreeze_last_n = 2 # 若要微調,可調整解凍的 Transformer 層數 -head_learning_rate = 1e-3 -encoder_learning_rate = 1e-5 -encoder_warmup_epochs = 10 # 預設 10 epoch 後解凍最後幾層(若 unfreeze_last_n > 0) -pitch_shift_semitones = [0, -4, 4] # 每筆資料的音調擴增組合 - - -def waveform_augment(waveform, sr, max_shift_ratio=0.02, noise_factor=0.002): - """時間平移 + 微量噪音;針對短音訊使用較小位移""" - if max_shift_ratio > 0: - shift = int(sr * max_shift_ratio) - if shift > 0: - offset = random.randint(-shift, shift) - waveform = torch.roll(waveform, offset, dims=1) - if noise_factor > 0: - noise = torch.randn_like(waveform) * noise_factor - waveform = waveform + noise - return waveform - - -class SpeakerDataset(Dataset): - def __init__(self, data_dir, classes, transform=None, target_sr=16000, pitch_shifts=None): - self.data_dir = data_dir - self.classes = classes - self.transform = transform - self.target_sr = target_sr - self.resamplers = {} - self.pitch_shifts = sorted(set(pitch_shifts or [0])) - self.data = [] - - for label, cls in enumerate(classes): - cls_dir = os.path.join(data_dir, cls) - for file in os.listdir(cls_dir): - if file.endswith('.wav'): - file_path = os.path.join(cls_dir, file) - for shift in self.pitch_shifts: - self.data.append((file_path, label, shift)) - - def __len__(self): - return len(self.data) - - def __getitem__(self, idx): - file_path, label, semitone = self.data[idx] - waveform, sr = torchaudio.load(file_path) - - if waveform.size(0) > 1: - waveform = waveform.mean(dim=0, keepdim=True) - - if sr != self.target_sr: - if sr not in self.resamplers: - self.resamplers[sr] = torchaudio.transforms.Resample(sr, self.target_sr) - waveform = self.resamplers[sr](waveform) - sr = self.target_sr - - if semitone != 0: - waveform = F.pitch_shift(waveform, sr, n_steps=semitone) - - if self.transform: - waveform = self.transform(waveform, sr) - - return waveform.squeeze(0), label - - -def collate_waveforms(batch): - waveforms, labels = zip(*batch) - lengths = torch.tensor([waveform.shape[0] for waveform in waveforms], dtype=torch.long) - max_len = lengths.max().item() - padded = torch.zeros(len(waveforms), max_len) - for idx, waveform in enumerate(waveforms): - padded[idx, : waveform.shape[0]] = waveform - labels_tensor = torch.tensor(labels, dtype=torch.long) - return padded, lengths, labels_tensor - - -class Wav2Vec2SpeakerClassifier(nn.Module): - def __init__(self, bundle, num_classes, freeze_encoder=True, unfreeze_last_n=0): - super().__init__() - self.encoder = bundle.get_model() - self.freeze_encoder = freeze_encoder - self.unfreeze_last_n = unfreeze_last_n - - # 預設凍結整個 encoder - for param in self.encoder.parameters(): - param.requires_grad = False - - if not freeze_encoder: - # 先全部鎖住,再選擇性解凍最後 N 層;若無法部分解凍則退而求其次為完全解凍 - transformer_layers = getattr(self.encoder, 'encoder', None) - if transformer_layers is not None and hasattr(transformer_layers, 'layers') and unfreeze_last_n > 0: - for layer in transformer_layers.layers[-unfreeze_last_n:]: - for param in layer.parameters(): - param.requires_grad = True - elif not freeze_encoder: - for param in self.encoder.parameters(): - param.requires_grad = True - - self.encoder_frozen = freeze_encoder - - hidden_dim = getattr(self.encoder, 'encoder_embed_dim', 768) - self.classifier = nn.Sequential( - nn.Dropout(0.3), - nn.Linear(hidden_dim, num_classes) - ) - - def unfreeze_last_layers(self, n_layers=None): - """解凍最後 n 層 Transformer block,回傳剛解凍的參數列表。""" - n_layers = n_layers or self.unfreeze_last_n - unfrozen_params = [] - transformer_layers = getattr(self.encoder, 'encoder', None) - if transformer_layers is not None and hasattr(transformer_layers, 'layers') and n_layers > 0: - target_layers = transformer_layers.layers[-n_layers:] - for layer in target_layers: - for param in layer.parameters(): - if not param.requires_grad: - param.requires_grad = True - unfrozen_params.append(param) - elif n_layers > 0: - for param in self.encoder.parameters(): - if not param.requires_grad: - param.requires_grad = True - unfrozen_params.append(param) - if unfrozen_params: - self.encoder_frozen = False - return unfrozen_params - - def forward(self, waveforms, lengths=None): - if waveforms.dim() != 2: - raise ValueError("音訊張量應為 [batch, time] 形式") - - encoder_out = self.encoder(waveforms, lengths) - if isinstance(encoder_out, tuple): - features, lengths = encoder_out - else: - features, lengths = encoder_out, None - - if lengths is not None: - if torch.is_floating_point(lengths): - valid_lengths = (lengths * features.size(1)).round().to(torch.long) - else: - valid_lengths = lengths.to(torch.long) - valid_lengths = valid_lengths.clamp(min=1, max=features.size(1)) - mask = torch.arange(features.size(1), device=features.device).unsqueeze(0) < valid_lengths.unsqueeze(1) - masked_features = features * mask.unsqueeze(-1) - pooled = masked_features.sum(dim=1) / valid_lengths.unsqueeze(-1) - else: - pooled = features.mean(dim=1) - - logits = self.classifier(pooled) - return logits - - -classes = sorted([d for d in os.listdir(data_dir) if os.path.isdir(os.path.join(data_dir, d))]) -num_classes = len(classes) -label_encoder = LabelEncoder() -label_encoder.fit(classes) - -dataset = SpeakerDataset( - data_dir=data_dir, - classes=classes, - transform=waveform_augment, - target_sr=target_sr, - pitch_shifts=pitch_shift_semitones -) -train_loader = DataLoader( - dataset, - batch_size=batch_size, - shuffle=True, - num_workers=0, - pin_memory=False, - collate_fn=collate_waveforms -) - -labels = [label for _, label, _ in dataset.data] -class_weights = compute_class_weight('balanced', classes=np.unique(labels), y=labels) -class_weights = torch.tensor(class_weights, dtype=torch.float) - -if torch.cuda.is_available(): - device = torch.device('cuda') - print("Using device: CUDA") -elif torch.backends.mps.is_available(): - device = torch.device('mps') - print("Using device: MPS") -else: - device = torch.device('cpu') - print("Using device: CPU") - -model = Wav2Vec2SpeakerClassifier( - bundle=bundle, - num_classes=num_classes, - freeze_encoder=freeze_encoder, - unfreeze_last_n=unfreeze_last_n -).to(device) -criterion = nn.CrossEntropyLoss(weight=class_weights.to(device)) - -encoder_params = [p for p in model.encoder.parameters() if p.requires_grad] -head_params = list(model.classifier.parameters()) -if encoder_params: - optimizer = optim.Adam([ - {'params': head_params, 'lr': head_learning_rate}, - {'params': encoder_params, 'lr': encoder_learning_rate} - ]) -else: - optimizer = optim.Adam(head_params, lr=head_learning_rate) - -use_amp = torch.cuda.is_available() -scaler = torch.amp.GradScaler() if use_amp else None - -def add_encoder_params_to_optimizer(model, optimizer, lr): - existing_params = set() - for group in optimizer.param_groups: - existing_params.update(id(p) for p in group['params']) - new_params = [p for p in model.encoder.parameters() if p.requires_grad and id(p) not in existing_params] - if new_params: - optimizer.add_param_group({'params': new_params, 'lr': lr}) - -for epoch in range(num_epochs): - # 動態解凍策略:達到指定 epoch 後解凍最後幾層 - if model.encoder_frozen and unfreeze_last_n > 0 and (epoch + 1) == encoder_warmup_epochs: - newly_unfrozen = model.unfreeze_last_layers(unfreeze_last_n) - if newly_unfrozen: - add_encoder_params_to_optimizer(model, optimizer, encoder_learning_rate) - print(f'Unfroze last {unfreeze_last_n} encoder layer(s) at epoch {epoch + 1}.') - - model.train() - if model.encoder_frozen: - model.encoder.eval() - else: - model.encoder.train() - - running_loss = 0.0 - for waveforms, lengths, labels_batch in train_loader: - waveforms = waveforms.to(device) - lengths = lengths.to(device) - labels_batch = labels_batch.to(device) - optimizer.zero_grad() - - if use_amp: - with torch.amp.autocast(device_type='cuda'): - logits = model(waveforms, lengths) - loss = criterion(logits, labels_batch) - scaler.scale(loss).backward() - scaler.step(optimizer) - scaler.update() - else: - logits = model(waveforms, lengths) - loss = criterion(logits, labels_batch) - loss.backward() - optimizer.step() - - running_loss += loss.item() - - avg_loss = running_loss / len(train_loader) - print(f'Epoch {epoch + 1}/{num_epochs}, Loss: {avg_loss:.4f}') - -model.eval() -all_preds = [] -all_labels = [] -with torch.no_grad(): - for waveforms, lengths, labels_batch in train_loader: - waveforms = waveforms.to(device) - lengths = lengths.to(device) - logits = model(waveforms, lengths) - preds = torch.argmax(logits, dim=1) - all_preds.extend(preds.cpu().numpy()) - all_labels.extend(labels_batch.numpy()) - -cm = confusion_matrix(all_labels, all_preds) -print("Confusion Matrix:") -print(cm) - -plt.figure(figsize=(8, 6)) -sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=classes, yticklabels=classes) -plt.title('Confusion Matrix') -plt.xlabel('Predicted') -plt.ylabel('True') -plt.savefig('confusion_matrix.png') -plt.close() - -torch.save(model.state_dict(), 'speaker_id_model.pth') -print("訓練完成,模型已保存。") diff --git a/render.yaml b/render.yaml deleted file mode 100644 index 69299dfbc423904d73e47344c1528d7346e6d7fb..0000000000000000000000000000000000000000 --- a/render.yaml +++ /dev/null @@ -1,28 +0,0 @@ -services: - - type: web - name: bloom-ware - runtime: python - region: singapore # 或 oregon(美西) - plan: Hobby # 免費方案 - buildCommand: pip install -r requirements.txt - startCommand: python3 app.py - envVars: - - key: ENVIRONMENT - value: production - - key: PYTHON_VERSION - value: 3.12.4 - - key: HOST - value: 0.0.0.0 - - key: PORT - value: 10000 # Render 固定使用 10000 端口 - # 其他敏感環境變數請在 Render Dashboard 手動設定: - # - FIREBASE_PROJECT_ID - # - FIREBASE_CREDENTIALS_JSON(完整JSON字串) - # - OPENAI_API_KEY - # - GOOGLE_CLIENT_ID - # - GOOGLE_CLIENT_SECRET - # - GOOGLE_REDIRECT_URI(https://your-app.onrender.com/auth/google/callback) - # - WEATHER_API_KEY - # - NEWSDATA_API_KEY - # - EXCHANGE_API_KEY - # - JWT_SECRET_KEY(使用 Python secrets.token_urlsafe(32) 生成新的) diff --git a/routers/__init__.py b/routers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6f18568c3e82ced20a3509a5c15a8480557d3aa2 --- /dev/null +++ b/routers/__init__.py @@ -0,0 +1,20 @@ +""" +Bloom Ware API 路由模組 +拆分自 app.py,提高可維護性 +""" + +from .auth import router as auth_router +from .chat import router as chat_router +from .voice import router as voice_router +from .health import router as health_router +from .files import router as files_router +from .system import router as system_router + +__all__ = [ + "auth_router", + "chat_router", + "voice_router", + "health_router", + "files_router", + "system_router", +] diff --git a/routers/auth.py b/routers/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..7759efc8bae8dfd1994418d448ce8f4149f3864a --- /dev/null +++ b/routers/auth.py @@ -0,0 +1,159 @@ +""" +認證相關 API 路由 +包含 Google OAuth、JWT 認證等 +""" + +import logging +from datetime import datetime +from typing import Dict, Optional +from fastapi import APIRouter, Request, HTTPException, Depends +from fastapi.responses import JSONResponse, RedirectResponse +from pydantic import BaseModel, EmailStr + +from core.config import settings +from core.auth import jwt_auth, get_current_user_optional, require_auth +from core.auth.google_oauth import GoogleOAuth +from core.database import create_or_login_google_user + +logger = logging.getLogger("routers.auth") + +router = APIRouter(prefix="/auth", tags=["認證"]) + +# Google OAuth 實例 +google_oauth = GoogleOAuth() + + +class GoogleAuthRequest(BaseModel): + """Google 認證請求""" + credential: str + + +class TokenResponse(BaseModel): + """Token 響應""" + success: bool + token: Optional[str] = None + user: Optional[dict] = None + error: Optional[str] = None + + +@router.get("/google/login") +async def google_login(): + """ + Google OAuth 登入入口 + 重定向到 Google 授權頁面 + """ + auth_url = google_oauth.get_authorization_url() + return RedirectResponse(url=auth_url) + + +@router.get("/google/callback") +async def google_callback(code: str = None, error: str = None): + """ + Google OAuth 回調處理 + """ + if error: + logger.error(f"Google OAuth 錯誤: {error}") + return RedirectResponse(url=f"/login?error={error}") + + if not code: + logger.error("Google OAuth 回調缺少 code 參數") + return RedirectResponse(url="/login?error=missing_code") + + try: + # 交換 code 獲取 token + token_info = await google_oauth.exchange_code(code) + if not token_info: + return RedirectResponse(url="/login?error=token_exchange_failed") + + # 獲取用戶信息 + user_info = await google_oauth.get_user_info(token_info.get("access_token")) + if not user_info: + return RedirectResponse(url="/login?error=user_info_failed") + + # 創建或登入用戶 + result = await create_or_login_google_user(user_info) + if not result.get("success"): + error_msg = result.get("error", "unknown_error") + return RedirectResponse(url=f"/login?error={error_msg}") + + # 生成 JWT token + user = result.get("user", {}) + jwt_token = jwt_auth.create_access_token({ + "sub": user.get("id"), + "email": user.get("email"), + "name": user.get("name"), + }) + + # 重定向到前端,帶上 token + return RedirectResponse(url=f"/static/frontend/index.html?token={jwt_token}") + + except Exception as e: + logger.exception(f"Google OAuth 回調處理失敗: {e}") + return RedirectResponse(url=f"/login?error=callback_failed") + + +@router.post("/google/verify", response_model=TokenResponse) +async def google_verify(request: GoogleAuthRequest): + """ + 驗證 Google ID Token(前端 One Tap 登入) + """ + try: + # 驗證 Google credential + user_info = await google_oauth.verify_id_token(request.credential) + if not user_info: + return TokenResponse(success=False, error="invalid_credential") + + # 創建或登入用戶 + result = await create_or_login_google_user(user_info) + if not result.get("success"): + return TokenResponse(success=False, error=result.get("error")) + + # 生成 JWT token + user = result.get("user", {}) + jwt_token = jwt_auth.create_access_token({ + "sub": user.get("id"), + "email": user.get("email"), + "name": user.get("name"), + }) + + return TokenResponse( + success=True, + token=jwt_token, + user=user, + ) + + except Exception as e: + logger.exception(f"Google 驗證失敗: {e}") + return TokenResponse(success=False, error=str(e)) + + +@router.get("/me") +async def get_current_user(user: dict = Depends(require_auth)): + """ + 獲取當前登入用戶信息 + """ + return { + "success": True, + "user": { + "id": user.get("sub"), + "email": user.get("email"), + "name": user.get("name"), + } + } + + +@router.post("/refresh") +async def refresh_token(user: dict = Depends(require_auth)): + """ + 刷新 JWT Token + """ + new_token = jwt_auth.create_access_token({ + "sub": user.get("sub"), + "email": user.get("email"), + "name": user.get("name"), + }) + + return { + "success": True, + "token": new_token, + } diff --git a/routers/chat.py b/routers/chat.py new file mode 100644 index 0000000000000000000000000000000000000000..522815db276e1736d5ad615cfd99581ac2edd760 --- /dev/null +++ b/routers/chat.py @@ -0,0 +1,200 @@ +""" +對話相關 API 路由 +包含對話管理、消息歷史等 +""" + +import logging +from datetime import datetime +from typing import List, Optional +from fastapi import APIRouter, HTTPException, Depends +from pydantic import BaseModel + +from core.auth import require_auth +from core.database import ( + create_chat, + get_user_chats, + get_chat, + update_chat_title, + delete_chat, + get_chat_messages, +) +from core.database.optimized import ( + get_chat as get_chat_optimized, + get_user_chats as get_user_chats_optimized, +) + +logger = logging.getLogger("routers.chat") + +router = APIRouter(prefix="/api/chats", tags=["對話"]) + + +class ChatCreateRequest(BaseModel): + """創建對話請求""" + title: Optional[str] = "新對話" + + +class ChatTitleUpdateRequest(BaseModel): + """更新對話標題請求""" + title: str + + +class ChatSummary(BaseModel): + """對話摘要""" + chat_id: str + title: str + updated_at: datetime + + +class ChatListResponse(BaseModel): + """對話列表響應""" + success: bool + chats: List[ChatSummary] + + +@router.post("") +async def create_new_chat( + request: ChatCreateRequest, + user: dict = Depends(require_auth) +): + """ + 創建新對話 + """ + user_id = user.get("sub") + if not user_id: + raise HTTPException(status_code=401, detail="無效的用戶") + + result = await create_chat(user_id, request.title) + if not result.get("success"): + raise HTTPException(status_code=500, detail=result.get("error")) + + return result + + +@router.get("") +async def list_user_chats(user: dict = Depends(require_auth)): + """ + 獲取用戶的所有對話 + """ + user_id = user.get("sub") + if not user_id: + raise HTTPException(status_code=401, detail="無效的用戶") + + result = await get_user_chats_optimized(user_id) + if not result.get("success"): + raise HTTPException(status_code=500, detail=result.get("error")) + + return result + + +@router.get("/{chat_id}") +async def get_chat_detail( + chat_id: str, + user: dict = Depends(require_auth) +): + """ + 獲取對話詳情(包含消息) + """ + user_id = user.get("sub") + if not user_id: + raise HTTPException(status_code=401, detail="無效的用戶") + + result = await get_chat_optimized(chat_id) + if not result.get("success"): + raise HTTPException(status_code=404, detail="對話不存在") + + chat = result.get("chat", {}) + + # 驗證對話所有權 + if chat.get("user_id") != user_id: + raise HTTPException(status_code=403, detail="無權訪問此對話") + + return result + + +@router.get("/{chat_id}/messages") +async def get_chat_messages_api( + chat_id: str, + limit: int = 50, + user: dict = Depends(require_auth) +): + """ + 獲取對話消息歷史 + """ + user_id = user.get("sub") + if not user_id: + raise HTTPException(status_code=401, detail="無效的用戶") + + # 先驗證對話所有權 + chat_result = await get_chat_optimized(chat_id) + if not chat_result.get("success"): + raise HTTPException(status_code=404, detail="對話不存在") + + chat = chat_result.get("chat", {}) + if chat.get("user_id") != user_id: + raise HTTPException(status_code=403, detail="無權訪問此對話") + + # 獲取消息 + messages = await get_chat_messages(chat_id, limit=limit) + + return { + "success": True, + "chat_id": chat_id, + "messages": messages, + } + + +@router.put("/{chat_id}/title") +async def update_chat_title_api( + chat_id: str, + request: ChatTitleUpdateRequest, + user: dict = Depends(require_auth) +): + """ + 更新對話標題 + """ + user_id = user.get("sub") + if not user_id: + raise HTTPException(status_code=401, detail="無效的用戶") + + # 先驗證對話所有權 + chat_result = await get_chat_optimized(chat_id) + if not chat_result.get("success"): + raise HTTPException(status_code=404, detail="對話不存在") + + chat = chat_result.get("chat", {}) + if chat.get("user_id") != user_id: + raise HTTPException(status_code=403, detail="無權修改此對話") + + result = await update_chat_title(chat_id, request.title) + if not result.get("success"): + raise HTTPException(status_code=500, detail=result.get("error")) + + return result + + +@router.delete("/{chat_id}") +async def delete_chat_api( + chat_id: str, + user: dict = Depends(require_auth) +): + """ + 刪除對話 + """ + user_id = user.get("sub") + if not user_id: + raise HTTPException(status_code=401, detail="無效的用戶") + + # 先驗證對話所有權 + chat_result = await get_chat_optimized(chat_id) + if not chat_result.get("success"): + raise HTTPException(status_code=404, detail="對話不存在") + + chat = chat_result.get("chat", {}) + if chat.get("user_id") != user_id: + raise HTTPException(status_code=403, detail="無權刪除此對話") + + result = await delete_chat(chat_id) + if not result.get("success"): + raise HTTPException(status_code=500, detail=result.get("error")) + + return result diff --git a/routers/files.py b/routers/files.py new file mode 100644 index 0000000000000000000000000000000000000000..fd15f0e922d2c0b74c713de295759d2f77709acb --- /dev/null +++ b/routers/files.py @@ -0,0 +1,228 @@ +""" +文件處理相關 API 路由 +包含文件上傳、分析等 +""" + +import logging +import base64 +import mimetypes +from typing import Optional +from fastapi import APIRouter, HTTPException, Depends, UploadFile, File +from pydantic import BaseModel + +from core.auth import require_auth + +logger = logging.getLogger("routers.files") + +router = APIRouter(prefix="/api/files", tags=["文件"]) + + +class FileAnalysisRequest(BaseModel): + """文件分析請求""" + filename: str + content: str # base64 編碼 + mime_type: str + user_prompt: Optional[str] = "請分析這個檔案的內容" + + +class FileAnalysisResponse(BaseModel): + """文件分析響應""" + success: bool + filename: str + analysis: Optional[str] = None + error: Optional[str] = None + + +@router.post("/analyze", response_model=FileAnalysisResponse) +async def analyze_file( + request: FileAnalysisRequest, + user: dict = Depends(require_auth) +): + """ + 分析上傳的文件內容 + 支援 PDF、圖片、文字文件等 + """ + user_id = user.get("sub") + if not user_id: + raise HTTPException(status_code=401, detail="無效的用戶") + + try: + # 解碼文件內容 + try: + file_content = base64.b64decode(request.content) + except Exception: + return FileAnalysisResponse( + success=False, + filename=request.filename, + error="無法解碼文件內容", + ) + + # 根據 MIME 類型處理 + mime_type = request.mime_type.lower() + extracted_text = "" + + if mime_type.startswith("text/"): + # 文字文件 + try: + extracted_text = file_content.decode("utf-8") + except UnicodeDecodeError: + extracted_text = file_content.decode("latin-1") + + elif mime_type == "application/pdf": + # PDF 文件 + try: + import pdfplumber + import io + + with pdfplumber.open(io.BytesIO(file_content)) as pdf: + pages_text = [] + for page in pdf.pages: + text = page.extract_text() + if text: + pages_text.append(text) + extracted_text = "\n\n".join(pages_text) + except ImportError: + return FileAnalysisResponse( + success=False, + filename=request.filename, + error="PDF 處理模組不可用", + ) + except Exception as e: + return FileAnalysisResponse( + success=False, + filename=request.filename, + error=f"PDF 解析失敗: {str(e)}", + ) + + elif mime_type.startswith("image/"): + # 圖片文件 - 使用 GPT-4 Vision + try: + import services.ai_service as ai_service + + # 構建 Vision API 請求 + image_base64 = request.content + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": request.user_prompt}, + { + "type": "image_url", + "image_url": { + "url": f"data:{mime_type};base64,{image_base64}" + } + } + ] + } + ] + + analysis = await ai_service.generate_response_async( + messages, + model="gpt-4o-mini", # 使用支援 Vision 的模型 + ) + + return FileAnalysisResponse( + success=True, + filename=request.filename, + analysis=analysis, + ) + + except Exception as e: + return FileAnalysisResponse( + success=False, + filename=request.filename, + error=f"圖片分析失敗: {str(e)}", + ) + + else: + return FileAnalysisResponse( + success=False, + filename=request.filename, + error=f"不支援的文件類型: {mime_type}", + ) + + # 使用 AI 分析提取的文字 + if extracted_text: + try: + import services.ai_service as ai_service + + messages = [ + { + "role": "system", + "content": "你是一個專業的文件分析助手。請根據用戶的要求分析以下文件內容。" + }, + { + "role": "user", + "content": f"{request.user_prompt}\n\n文件內容:\n{extracted_text[:10000]}" # 限制長度 + } + ] + + analysis = await ai_service.generate_response_async(messages) + + return FileAnalysisResponse( + success=True, + filename=request.filename, + analysis=analysis, + ) + + except Exception as e: + return FileAnalysisResponse( + success=False, + filename=request.filename, + error=f"AI 分析失敗: {str(e)}", + ) + + return FileAnalysisResponse( + success=False, + filename=request.filename, + error="無法提取文件內容", + ) + + except Exception as e: + logger.exception(f"文件分析失敗: {e}") + return FileAnalysisResponse( + success=False, + filename=request.filename, + error=str(e), + ) + + +@router.post("/upload") +async def upload_file( + file: UploadFile = File(...), + user: dict = Depends(require_auth) +): + """ + 上傳文件(返回 base64 編碼) + """ + user_id = user.get("sub") + if not user_id: + raise HTTPException(status_code=401, detail="無效的用戶") + + try: + # 讀取文件內容 + content = await file.read() + + # 檢查文件大小(限制 10MB) + if len(content) > 10 * 1024 * 1024: + raise HTTPException(status_code=413, detail="文件大小超過限制(10MB)") + + # 編碼為 base64 + content_base64 = base64.b64encode(content).decode("utf-8") + + # 獲取 MIME 類型 + mime_type = file.content_type or mimetypes.guess_type(file.filename)[0] or "application/octet-stream" + + return { + "success": True, + "filename": file.filename, + "mime_type": mime_type, + "size": len(content), + "content": content_base64, + } + + except HTTPException: + raise + except Exception as e: + logger.exception(f"文件上傳失敗: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/routers/health.py b/routers/health.py new file mode 100644 index 0000000000000000000000000000000000000000..502724255307a180a94a2630ea8518938b5560a4 --- /dev/null +++ b/routers/health.py @@ -0,0 +1,220 @@ +""" +健康數據相關 API 路由 +包含 HealthKit 數據同步等 +""" + +import logging +from datetime import datetime +from typing import List, Optional, Dict, Any +from fastapi import APIRouter, HTTPException, Depends +from pydantic import BaseModel + +from core.auth import require_auth + +logger = logging.getLogger("routers.health") + +router = APIRouter(prefix="/api/health", tags=["健康數據"]) + + +class HealthDataPoint(BaseModel): + """健康數據點""" + type: str # heart_rate, steps, sleep, etc. + value: float + unit: str + timestamp: datetime + source: Optional[str] = None + + +class HealthDataSyncRequest(BaseModel): + """健康數據同步請求""" + data: List[HealthDataPoint] + + +class HealthQueryRequest(BaseModel): + """健康數據查詢請求""" + types: List[str] + start_date: Optional[datetime] = None + end_date: Optional[datetime] = None + + +@router.post("/sync") +async def sync_health_data( + request: HealthDataSyncRequest, + user: dict = Depends(require_auth) +): + """ + 同步健康數據(從 HealthKit/Google Fit) + """ + user_id = user.get("sub") + if not user_id: + raise HTTPException(status_code=401, detail="無效的用戶") + + try: + from core.database import firestore_db + + if not firestore_db: + raise HTTPException(status_code=503, detail="數據庫不可用") + + # 批量寫入健康數據 + batch = firestore_db.batch() + health_collection = firestore_db.collection("health_data") + + for data_point in request.data: + doc_ref = health_collection.document() + batch.set(doc_ref, { + "user_id": user_id, + "type": data_point.type, + "value": data_point.value, + "unit": data_point.unit, + "timestamp": data_point.timestamp, + "source": data_point.source, + "synced_at": datetime.now(), + }) + + # 執行批量寫入 + import asyncio + await asyncio.to_thread(batch.commit) + + logger.info(f"用戶 {user_id} 同步了 {len(request.data)} 條健康數據") + + return { + "success": True, + "synced_count": len(request.data), + } + + except Exception as e: + logger.exception(f"健康數據同步失敗: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/query") +async def query_health_data( + request: HealthQueryRequest, + user: dict = Depends(require_auth) +): + """ + 查詢健康數據 + """ + user_id = user.get("sub") + if not user_id: + raise HTTPException(status_code=401, detail="無效的用戶") + + try: + from core.database import firestore_db + from google.cloud.firestore import FieldFilter + + if not firestore_db: + raise HTTPException(status_code=503, detail="數據庫不可用") + + health_collection = firestore_db.collection("health_data") + + # 構建查詢 + query = health_collection.where( + filter=FieldFilter("user_id", "==", user_id) + ) + + # 時間範圍過濾 + if request.start_date: + query = query.where( + filter=FieldFilter("timestamp", ">=", request.start_date) + ) + if request.end_date: + query = query.where( + filter=FieldFilter("timestamp", "<=", request.end_date) + ) + + # 執行查詢 + import asyncio + docs = await asyncio.to_thread(lambda: list(query.stream())) + + # 過濾類型並格式化結果 + results: Dict[str, List[Dict[str, Any]]] = {t: [] for t in request.types} + + for doc in docs: + data = doc.to_dict() + data_type = data.get("type") + if data_type in request.types: + results[data_type].append({ + "value": data.get("value"), + "unit": data.get("unit"), + "timestamp": data.get("timestamp"), + "source": data.get("source"), + }) + + return { + "success": True, + "data": results, + } + + except Exception as e: + logger.exception(f"健康數據查詢失敗: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/summary") +async def get_health_summary(user: dict = Depends(require_auth)): + """ + 獲取健康數據摘要(今日) + """ + user_id = user.get("sub") + if not user_id: + raise HTTPException(status_code=401, detail="無效的用戶") + + try: + from core.database import firestore_db + from google.cloud.firestore import FieldFilter + + if not firestore_db: + raise HTTPException(status_code=503, detail="數據庫不可用") + + # 今日開始時間 + today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + + health_collection = firestore_db.collection("health_data") + + query = health_collection.where( + filter=FieldFilter("user_id", "==", user_id) + ).where( + filter=FieldFilter("timestamp", ">=", today_start) + ) + + import asyncio + docs = await asyncio.to_thread(lambda: list(query.stream())) + + # 計算摘要 + summary = { + "steps": 0, + "heart_rate_avg": 0, + "heart_rate_readings": [], + "sleep_hours": 0, + "active_calories": 0, + } + + for doc in docs: + data = doc.to_dict() + data_type = data.get("type") + value = data.get("value", 0) + + if data_type == "steps": + summary["steps"] += value + elif data_type == "heart_rate": + summary["heart_rate_readings"].append(value) + elif data_type == "sleep": + summary["sleep_hours"] += value + elif data_type == "active_calories": + summary["active_calories"] += value + + # 計算心率平均值 + if summary["heart_rate_readings"]: + summary["heart_rate_avg"] = sum(summary["heart_rate_readings"]) / len(summary["heart_rate_readings"]) + del summary["heart_rate_readings"] + + return { + "success": True, + "date": today_start.date().isoformat(), + "summary": summary, + } + + except Exception as e: + logger.exception(f"健康摘要獲取失敗: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/routers/system.py b/routers/system.py new file mode 100644 index 0000000000000000000000000000000000000000..0a0ed1b145280fe2e5645afee1b0971d2921e76d --- /dev/null +++ b/routers/system.py @@ -0,0 +1,115 @@ +""" +系統相關 API 路由 +包含狀態檢查、效能統計、MCP 工具列表等 +""" + +from typing import Optional, Dict, Any +from fastapi import APIRouter, Depends +from fastapi.responses import RedirectResponse + +from core.auth import get_current_user_optional +from core.logging import get_logger +from core.config import settings + +logger = get_logger("routers.system") + +router = APIRouter(tags=["系統"]) + + +@router.get("/") +async def root(): + """根路徑導向登入頁面""" + return RedirectResponse(url="/login/") + + +@router.get("/status") +async def get_status(): + """系統狀態檢查""" + return { + "status": "running", + "version": "2.0.0", + "environment": settings.ENVIRONMENT, + } + + +@router.get("/api/mcp/tools") +async def list_mcp_tools(current_user: dict = Depends(get_current_user_optional)): + """ + 列出所有可用的 MCP 工具 + + Returns: + 工具列表,包含名稱、描述、參數等 + """ + try: + # 延遲導入避免循環依賴 + from app import app + + if not hasattr(app.state, 'feature_router'): + return { + "success": False, + "error": "MCP 服務尚未初始化", + "tools": [] + } + + feature_router = app.state.feature_router + tools_info = [] + + for tool_name, tool in feature_router.mcp_server.tools.items(): + tool_info = { + "name": tool_name, + "description": getattr(tool, 'description', '無描述'), + } + + # 嘗試獲取參數 schema + if hasattr(tool, 'handler') and hasattr(tool.handler, '__self__'): + tool_class = tool.handler.__self__ + if hasattr(tool_class, 'get_input_schema'): + try: + tool_info["parameters"] = tool_class.get_input_schema() + except Exception: + pass + + tools_info.append(tool_info) + + return { + "success": True, + "count": len(tools_info), + "tools": tools_info + } + + except Exception as e: + logger.error(f"獲取 MCP 工具列表失敗: {e}") + return { + "success": False, + "error": str(e), + "tools": [] + } + + +@router.get("/api/performance/stats") +async def get_performance_stats(current_user: dict = Depends(get_current_user_optional)): + """ + 獲取系統效能統計 + + Returns: + 快取命中率、查詢統計等 + """ + try: + from core.database.cache import db_cache + from core.database.optimized import query_optimizer + + cache_stats = db_cache.get_all_stats() + query_stats = query_optimizer.get_stats() + + return { + "success": True, + "cache": cache_stats, + "queries": query_stats, + } + + except Exception as e: + logger.error(f"獲取效能統計失敗: {e}") + return { + "success": False, + "error": str(e) + } diff --git a/routers/voice.py b/routers/voice.py new file mode 100644 index 0000000000000000000000000000000000000000..2065f0b03b95f018374b194200a2f6a811449732 --- /dev/null +++ b/routers/voice.py @@ -0,0 +1,239 @@ +""" +語音相關 API 路由 +包含語音登入、TTS、STT 等 +""" + +import logging +from typing import Optional +from fastapi import APIRouter, HTTPException, Depends +from pydantic import BaseModel + +from core.auth import require_auth +from core.database import set_user_speaker_label, get_user_by_speaker_label + +logger = logging.getLogger("routers.voice") + +router = APIRouter(prefix="/api/voice", tags=["語音"]) + + +class SpeakerLabelBindRequest(BaseModel): + """綁定語音標籤請求""" + speaker_label: str + + +class TTSRequest(BaseModel): + """TTS 請求""" + text: str + voice: str = "nova" + speed: float = 1.0 + + +@router.post("/bind-speaker") +async def bind_speaker_label( + request: SpeakerLabelBindRequest, + user: dict = Depends(require_auth) +): + """ + 綁定語音標籤到用戶帳號 + """ + user_id = user.get("sub") + if not user_id: + raise HTTPException(status_code=401, detail="無效的用戶") + + result = await set_user_speaker_label(user_id, request.speaker_label) + + if not result.get("success"): + error = result.get("error") + if error == "SPEAKER_LABEL_TAKEN": + raise HTTPException(status_code=409, detail="此語音標籤已被其他用戶綁定") + elif error == "USER_NOT_FOUND": + raise HTTPException(status_code=404, detail="用戶不存在") + else: + raise HTTPException(status_code=500, detail=error) + + return {"success": True, "message": "語音標籤綁定成功"} + + +@router.get("/lookup-speaker/{speaker_label}") +async def lookup_speaker(speaker_label: str): + """ + 根據語音標籤查找用戶(用於語音登入) + """ + user = await get_user_by_speaker_label(speaker_label) + + if not user: + raise HTTPException(status_code=404, detail="找不到對應的用戶") + + return { + "success": True, + "user": { + "id": user.get("id"), + "name": user.get("name"), + } + } + + +@router.post("/tts") +async def text_to_speech( + request: TTSRequest, + user: dict = Depends(require_auth) +): + """ + 文字轉語音 + """ + try: + from services.tts_service import tts_service + + result = await tts_service.synthesize( + text=request.text, + voice=request.voice, + speed=request.speed, + ) + + if not result.get("success"): + raise HTTPException(status_code=500, detail=result.get("error")) + + # 返回 base64 編碼的音頻 + import base64 + audio_base64 = base64.b64encode(result["audio_data"]).decode("utf-8") + + return { + "success": True, + "audio": audio_base64, + "voice": result.get("voice"), + } + + except ImportError: + raise HTTPException(status_code=503, detail="TTS 服務不可用") + except Exception as e: + logger.exception(f"TTS 失敗: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +class VoiceLoginRequest(BaseModel): + """語音登入請求""" + audio_base64: str # base64 編碼的 PCM16 音訊 + sample_rate: int = 16000 + + +class VoiceLoginResponse(BaseModel): + """語音登入回應""" + success: bool + access_token: str = None + user: dict = None + emotion: str = None + error: str = None + + +@router.post("/login", response_model=VoiceLoginResponse) +async def voice_login(request: VoiceLoginRequest): + """ + 語音登入 API + + 流程: + 1. 接收 base64 編碼的音訊 + 2. 執行身份辨識 + 情緒辨識 + 3. 查詢 speaker_label 對應的用戶 + 4. 生成 JWT token + 5. 回傳 token + 情緒 + """ + import base64 + import jwt + from datetime import datetime, timedelta + from core.config import settings + + try: + # 取得 VoiceAuthService 實例 + from fastapi import Request + from main import app + + voice_auth = getattr(app.state, "voice_auth", None) + if not voice_auth: + # 嘗試動態建立 + from services.voice_login import VoiceAuthService, VoiceLoginConfig + voice_auth = VoiceAuthService(config=VoiceLoginConfig( + window_seconds=3, + required_windows=1, + )) + + # 解碼音訊 + audio_bytes = base64.b64decode(request.audio_base64) + + # 建立臨時 session 並處理音訊 + temp_user_id = f"voice_login_{datetime.now().timestamp()}" + voice_auth.start_session(temp_user_id, request.sample_rate) + voice_auth._buffers[temp_user_id] = bytearray(audio_bytes) + + # 執行辨識 + result = voice_auth.stop_and_authenticate(temp_user_id) + + # 清理 session + voice_auth.clear_session(temp_user_id) + + if not result.get("success"): + error_code = result.get("error", "UNKNOWN_ERROR") + error_messages = { + "NO_AUDIO": "沒有收到音訊資料", + "AUDIO_TOO_SHORT": "音訊太短,請錄製至少 3 秒", + "LOW_SNR": "環境太吵,請在安靜的地方重試", + "INCONSISTENT_WINDOWS": "無法確認身份,請重試", + "THRESHOLD_NOT_MET": "無法確認身份,請重試", + "MODEL_ERROR": "辨識系統錯誤,請稍後重試", + } + return VoiceLoginResponse( + success=False, + error=error_messages.get(error_code, f"辨識失敗:{error_code}") + ) + + # 取得辨識結果 + speaker_label = result.get("label") + emotion = result.get("emotion", {}) + emotion_label = emotion.get("label", "neutral") if isinstance(emotion, dict) else "neutral" + + logger.info(f"🎙️ 語音辨識成功: speaker={speaker_label}, emotion={emotion_label}") + + # 查詢對應的用戶 + user = await get_user_by_speaker_label(speaker_label) + + if not user: + return VoiceLoginResponse( + success=False, + error=f"找不到綁定的帳號。請先使用 Google 登入並綁定語音。" + ) + + # 生成 JWT token + user_id = user.get("id") + user_name = user.get("name", "用戶") + user_email = user.get("email", "") + + payload = { + "sub": user_id, + "name": user_name, + "email": user_email, + "iat": datetime.utcnow(), + "exp": datetime.utcnow() + timedelta(days=7), + "login_method": "voice", + "emotion": emotion_label, + } + + token = jwt.encode(payload, settings.JWT_SECRET, algorithm="HS256") + + logger.info(f"✅ 語音登入成功: user={user_name}, emotion={emotion_label}") + + return VoiceLoginResponse( + success=True, + access_token=token, + user={ + "id": user_id, + "name": user_name, + "email": user_email, + }, + emotion=emotion_label, + ) + + except Exception as e: + logger.exception(f"❌ 語音登入失敗: {e}") + return VoiceLoginResponse( + success=False, + error=f"系統錯誤:{str(e)}" + ) diff --git a/services/ai_service.py b/services/ai_service.py index 2a2a31bbeb86a50d704e9b8ee394a1ca7b3add8a..99bc4754429f9930e1ef6e707fe483cd49bf0332 100644 --- a/services/ai_service.py +++ b/services/ai_service.py @@ -1,39 +1,21 @@ -import os -import sys -import logging import asyncio -from dotenv import load_dotenv from datetime import datetime, timezone, timedelta import time import json from typing import Dict, List, Any, Optional -# 設置日誌 -# 設定日誌等級:預設 WARNING,可透過 BLOOMWARE_LOG_LEVEL 覆寫 -LOG_LEVEL_NAME = os.getenv("BLOOMWARE_LOG_LEVEL", "WARNING").upper() -LOG_LEVEL = getattr(logging, LOG_LEVEL_NAME, logging.WARNING) -logging.basicConfig( - level=LOG_LEVEL, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger("AI_Service") -# 將終端日誌級別設置為 ERROR(保留重要訊息) -console_handler = logging.StreamHandler() -console_handler.setLevel(logging.ERROR) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') -console_handler.setFormatter(formatter) -logger.addHandler(console_handler) -logger.propagate = False # 防止日誌重複輸出 -logger.setLevel(LOG_LEVEL) - -# 載入環境變數 -load_dotenv() +# 統一日誌配置 +from core.logging import get_logger +logger = get_logger("AI_Service") # 統一配置管理 from core.config import settings +# 統一 OpenAI 客戶端 +from core.ai_client import get_openai_client + # 超時設定(秒) -OPENAI_TIMEOUT = settings.OPENAI_TIMEOUT # 關懷模式 reasoning model 需要更長時間 +OPENAI_TIMEOUT = settings.OPENAI_TIMEOUT # 情緒關懷模式 System Prompt(新增) CARE_MODE_SYSTEM_PROMPT = """你是 BloomWare 的情緒關懷助手「小花」,由銘傳大學人工智慧應用學系槓上開發團隊打造。你不是 GPT,也不要自稱 GPT;你的任務是在情緒低落時傾聽、陪伴。 @@ -55,21 +37,13 @@ CARE_MODE_SYSTEM_PROMPT = """你是 BloomWare 的情緒關懷助手「小花」 用戶:「我很生氣」 → 你:「這件事讓你超級生氣,情緒一定卡著。要不要跟我說說最困擾你的地方?」 用戶:「講笑話給我聽」 → 你:「你想聽點輕鬆的,我當然可以陪你。想先聽小笑話還是先聊聊怎麼了?」""" -# 導入時間服務模組 -# from features.daily_life.time_service import get_current_time_data, format_time_for_messages # 已整合到 MCPAgentBridge +# 取得 OpenAI 客戶端(使用統一管理) +def _get_client(): + """取得 OpenAI 客戶端""" + return get_openai_client() -# 嘗試導入 OpenAI -try: - import openai - from openai import OpenAI - client = OpenAI( - api_key=os.getenv("OPENAI_API_KEY"), - timeout=30.0, # 增加超時時間 - max_retries=3 # 添加重試次數 - ) -except Exception as e: - logger.error(f"初始化 OpenAI 客戶端失敗: {e}") - client = None +# 向後相容:保留 client 變數名稱 +client = None # 將在首次使用時透過 _get_client() 取得 # 導入DB函數 try: @@ -486,22 +460,9 @@ def _extract_text_from_message_obj(message: Any) -> str: return "" def initialize_openai(): - """初始化OpenAI客戶端""" - global client - api_key = settings.OPENAI_API_KEY - if not api_key: - logger.error("OpenAI API密鑰未設置,請在.env文件中設置OPENAI_API_KEY環境變數") - print("\n❌ 錯誤: OpenAI API密鑰未設置!請在.env文件中設置OPENAI_API_KEY\n") - return False - try: - logger.info("正在初始化OpenAI客戶端...") - client = OpenAI(api_key=api_key) - logger.info("OpenAI 客戶端初始化完成") - return True - except Exception as e: - logger.error(f"初始化OpenAI客戶端失敗: {e}") - print(f"\n❌ OpenAI API連接失敗: {e}\n") - return False + """初始化OpenAI客戶端(使用統一管理)""" + from core.ai_client import is_available + return is_available() ## 已移除內部測試函式 test_openai_response,避免干擾正式流程 @@ -532,7 +493,8 @@ async def generate_response_async( stream: 是否啟用串流模式(2025 最佳實踐) on_chunk: 串流 chunk 回調函數(async callable) """ - if client is None and not initialize_openai(): + openai_client = _get_client() + if openai_client is None: return "抱歉,AI服務暫時不可用。系統無法連接到OpenAI服務。" try: start_time = time.time() @@ -579,7 +541,7 @@ async def generate_response_async( full_response = "" stream_obj = await loop.run_in_executor( None, - lambda: client.chat.completions.create(**request_kwargs) + lambda: openai_client.chat.completions.create(**request_kwargs) ) # 逐塊處理 @@ -602,7 +564,7 @@ async def generate_response_async( response = await asyncio.wait_for( loop.run_in_executor( None, - lambda: client.chat.completions.create(**request_kwargs), + lambda: openai_client.chat.completions.create(**request_kwargs), ), timeout=OPENAI_TIMEOUT, ) @@ -779,7 +741,11 @@ async def _generate_response_with_chat_db( system_prompt = ( "你是 BloomWare 的個人化助理 小花,由銘傳大學人工智慧應用學系 槓上開發 團隊開發。" "你不是 GPT,也不要自稱 GPT。" - "你是一個友善、有禮、幽默且能夠提供幫助的AI助手。請使用繁體中文回覆,保持簡潔清晰的表達。" + "你是一個友善、有禮、幽默且能夠提供幫助的AI助手。\n\n" + "【重要】語言使用規範:\n" + "- 回覆用戶時:必須使用繁體中文,保持簡潔清晰的表達\n" + "- 調用工具時:所有參數必須使用英文(城市名、國家名、貨幣代碼等)\n" + "- 範例:用戶問「台北天氣」→ 調用工具時參數用 {\"city\": \"Taipei\"},回覆時說「台北目前...\"" ) # 在系統提示前加上用戶名稱 @@ -982,7 +948,11 @@ async def _generate_response_with_global_history( system_prompt = ( "你是 BloomWare 的個人化助理 小花,由銘傳大學人工智慧應用學系 槓上開發 團隊開發。" "你不是 GPT,也不要自稱 GPT。" - "你是一個友善、有禮、幽默且能夠提供幫助的AI助手。請使用繁體中文回覆,保持簡潔清晰的表達。" + "你是一個友善、有禮、幽默且能夠提供幫助的AI助手。\n\n" + "【重要】語言使用規範:\n" + "- 回覆用戶時:必須使用繁體中文,保持簡潔清晰的表達\n" + "- 調用工具時:所有參數必須使用英文(城市名、國家名、貨幣代碼等)\n" + "- 範例:用戶問「台北天氣」→ 調用工具時參數用 {\"city\": \"Taipei\"},回覆時說「台北目前...\"" ) # 在系統提示前加上用戶名稱 @@ -1093,3 +1063,107 @@ async def _generate_response_with_global_history( raise logger.error(f"全局歷史處理出錯: {e}") raise + + +async def generate_response_with_tools( + messages: List[Dict[str, str]], + tools: List[Dict[str, Any]], + user_id: str = "default", + model: str = "gpt-5-nano", + reasoning_effort: Optional[str] = None, + tool_choice: str = "auto", +) -> Dict[str, Any]: + """ + 使用 OpenAI Function Calling 生成回應 + + 2025 最佳實踐:讓 GPT 原生選擇工具,不需要自定義意圖檢測 Prompt + + Args: + messages: 對話訊息列表 + tools: OpenAI tools 格式的工具定義列表 + user_id: 用戶 ID(用於日誌) + model: 模型名稱 + reasoning_effort: 推理強度 (minimal/low/medium/high) + tool_choice: 工具選擇策略 ("auto", "none", "required", 或特定工具名) + + Returns: + 包含 tool_calls 和 content 的字典 + """ + openai_client = _get_client() + if openai_client is None: + logger.error("OpenAI 客戶端不可用") + return {"content": "", "tool_calls": []} + + try: + start_time = time.time() + loop = asyncio.get_event_loop() + + request_kwargs = { + "model": model, + "messages": messages, + "tools": tools, + "tool_choice": tool_choice, + "max_completion_tokens": 1000, + } + + # 加入 reasoning_effort 控制 + if reasoning_effort: + request_kwargs["reasoning_effort"] = reasoning_effort + logger.info(f"🧠 Function Calling 推理強度: {reasoning_effort}") + + logger.info(f"🔧 Function Calling 請求: {len(tools)} 個工具, tool_choice={tool_choice}") + logger.debug(f"📤 發送的訊息: {messages}") + + response = await asyncio.wait_for( + loop.run_in_executor( + None, + lambda: openai_client.chat.completions.create(**request_kwargs), + ), + timeout=OPENAI_TIMEOUT, + ) + + elapsed_time = time.time() - start_time + logger.info(f"⏱️ Function Calling 完成,耗時: {elapsed_time:.2f}秒") + + # 解析回應 + message = response.choices[0].message + logger.debug(f"📥 原始 message 物件: {message}") + + result = { + "content": message.content or "", + "tool_calls": [], + } + + # 提取 tool_calls + if message.tool_calls: + for tool_call in message.tool_calls: + result["tool_calls"].append({ + "id": tool_call.id, + "type": "function", + "function": { + "name": tool_call.function.name, + "arguments": tool_call.function.arguments, + } + }) + logger.info(f"✅ GPT 選擇了 {len(result['tool_calls'])} 個工具") + for tc in result["tool_calls"]: + logger.info(f" 🔧 工具: {tc['function']['name']}") + logger.info(f" 📋 參數 JSON: {tc['function']['arguments']}") + # 嘗試解析參數 + try: + import json + parsed = json.loads(tc['function']['arguments']) + logger.info(f" ✅ 解析後參數: {parsed}") + except Exception as e: + logger.warning(f" ⚠️ 參數解析失敗: {e}") + else: + logger.info("💬 GPT 未選擇任何工具(一般聊天)") + + return result + + except asyncio.TimeoutError: + logger.error("Function Calling 請求超時") + return {"content": "", "tool_calls": []} + except Exception as e: + logger.error(f"Function Calling 失敗: {e}") + return {"content": "", "tool_calls": []} diff --git a/services/batch_scheduler.py b/services/batch_scheduler.py index 1f35ea4ac33c8cc2448bd1b575c6051ae7edb6d7..bec219db4b9dad8acd92a0420f11d15df1dc42aa 100644 --- a/services/batch_scheduler.py +++ b/services/batch_scheduler.py @@ -183,10 +183,62 @@ class BatchScheduler: Returns: {user_id: [memory_1, memory_2, ...]} """ - # TODO: 實作數據庫查詢邏輯 - # 目前返回空字典(示例) - logger.warning("⚠️ _fetch_yesterday_memories 尚未實作,返回空數據") - return {} + try: + if not firestore_db: + logger.warning("⚠️ Firestore 未連接,無法獲取記憶") + return {} + + from google.cloud.firestore import FieldFilter + import asyncio + + # 計算昨日時間範圍 + yesterday_start = (datetime.now() - timedelta(days=1)).replace( + hour=0, minute=0, second=0, microsecond=0 + ) + yesterday_end = yesterday_start + timedelta(days=1) + + # 獲取所有用戶 + users_collection = firestore_db.collection("users") + + def _fetch_users(): + return list(users_collection.stream()) + + users = await asyncio.to_thread(_fetch_users) + + result: Dict[str, List[str]] = {} + + for user_doc in users: + user_id = user_doc.id + + # 獲取該用戶的昨日記憶 + def _fetch_user_memories(uid: str): + memories_ref = firestore_db.collection("users").document(uid).collection("memories") + query = memories_ref.where( + filter=FieldFilter("created_at", ">=", yesterday_start) + ).where( + filter=FieldFilter("created_at", "<", yesterday_end) + ) + return list(query.stream()) + + memories = await asyncio.to_thread(_fetch_user_memories, user_id) + + if memories: + memory_contents = [] + for mem_doc in memories: + mem_data = mem_doc.to_dict() + content = mem_data.get("content", "") + if content: + memory_contents.append(content) + + if memory_contents: + result[user_id] = memory_contents + + logger.info(f"📚 獲取到 {len(result)} 位用戶的昨日記憶") + return result + + except Exception as e: + logger.exception(f"❌ 獲取昨日記憶失敗: {e}") + return {} async def _save_memory_summaries(self, results: List[Dict[str, Any]]): """ @@ -195,26 +247,119 @@ class BatchScheduler: Args: results: 批次結果列表 """ - # TODO: 實作數據庫儲存邏輯 + if not firestore_db: + logger.warning("⚠️ Firestore 未連接,無法儲存摘要") + return + + import asyncio + logger.info(f"💾 準備儲存 {len(results)} 條記憶摘要") + + saved_count = 0 for result in results: - custom_id = result.get("custom_id") # user_id - response = result.get("response", {}).get("body", {}) - summary = response.get("choices", [{}])[0].get("message", {}).get("content", "") + try: + custom_id = result.get("custom_id") # user_id + response = result.get("response", {}).get("body", {}) + summary = response.get("choices", [{}])[0].get("message", {}).get("content", "") + + if not custom_id or not summary: + continue + + logger.debug(f"📝 用戶 {custom_id} 的摘要: {summary[:50]}...") + + # 儲存到用戶的記憶摘要集合 + def _save_summary(uid: str, summary_text: str): + summaries_ref = firestore_db.collection("users").document(uid).collection("memory_summaries") + summaries_ref.add({ + "summary": summary_text, + "date": datetime.now().date().isoformat(), + "created_at": datetime.now(), + "type": "daily", + }) + + await asyncio.to_thread(_save_summary, custom_id, summary) + saved_count += 1 + + except Exception as e: + logger.error(f"❌ 儲存用戶 {custom_id} 的摘要失敗: {e}") - logger.debug(f"📝 用戶 {custom_id} 的摘要: {summary[:50]}...") - # await save_memory_summary(custom_id, summary) + logger.info(f"✅ 成功儲存 {saved_count}/{len(results)} 條記憶摘要") async def _fetch_week_health_data(self) -> Dict[str, Dict[str, Any]]: """ 從數據庫獲取所有用戶的本週健康數據 Returns: - {user_id: {heart_rate: ..., steps: ...}} + {user_id: {heart_rate: [...], steps: [...], sleep: [...], ...}} """ - # TODO: 實作數據庫查詢邏輯 - logger.warning("⚠️ _fetch_week_health_data 尚未實作,返回空數據") - return {} + try: + if not firestore_db: + logger.warning("⚠️ Firestore 未連接,無法獲取健康數據") + return {} + + from google.cloud.firestore import FieldFilter + import asyncio + + # 計算本週時間範圍(週一到今天) + today = datetime.now() + week_start = today - timedelta(days=today.weekday()) + week_start = week_start.replace(hour=0, minute=0, second=0, microsecond=0) + + # 獲取所有用戶 + users_collection = firestore_db.collection("users") + + def _fetch_users(): + return list(users_collection.stream()) + + users = await asyncio.to_thread(_fetch_users) + + result: Dict[str, Dict[str, Any]] = {} + + for user_doc in users: + user_id = user_doc.id + + # 獲取該用戶的本週健康數據 + def _fetch_user_health(uid: str): + health_ref = firestore_db.collection("health_data") + query = health_ref.where( + filter=FieldFilter("user_id", "==", uid) + ).where( + filter=FieldFilter("timestamp", ">=", week_start) + ) + return list(query.stream()) + + health_docs = await asyncio.to_thread(_fetch_user_health, user_id) + + if health_docs: + user_health: Dict[str, List[Any]] = { + "heart_rate": [], + "steps": [], + "sleep": [], + "active_calories": [], + } + + for doc in health_docs: + data = doc.to_dict() + data_type = data.get("type") + value = data.get("value") + timestamp = data.get("timestamp") + + if data_type in user_health and value is not None: + user_health[data_type].append({ + "value": value, + "timestamp": timestamp, + }) + + # 只保留有數據的用戶 + if any(user_health.values()): + result[user_id] = user_health + + logger.info(f"❤️ 獲取到 {len(result)} 位用戶的本週健康數據") + return result + + except Exception as e: + logger.exception(f"❌ 獲取本週健康數據失敗: {e}") + return {} # 全域單例 diff --git a/services/realtime_stt_service.py b/services/realtime_stt_service.py new file mode 100644 index 0000000000000000000000000000000000000000..44adf29e149eff2a1b8d5ca893cdbe3b28b6b042 --- /dev/null +++ b/services/realtime_stt_service.py @@ -0,0 +1,290 @@ +""" +OpenAI Realtime API - 即時語音轉文字服務 +使用 WebSocket 進行低延遲串流轉錄 +""" + +import os +import json +import asyncio +import logging +from typing import Optional, Callable, Dict, Any +import websockets +from dotenv import load_dotenv + +load_dotenv() + +logger = logging.getLogger("services.realtime_stt") + +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +if not OPENAI_API_KEY: + logger.warning("⚠️ OPENAI_API_KEY 未設置") + +# OpenAI Realtime API WebSocket URL +REALTIME_API_URL = "wss://api.openai.com/v1/realtime?intent=transcription" + + +class RealtimeSTTService: + """OpenAI Realtime API 即時語音轉文字服務""" + + def __init__(self): + self.api_key = OPENAI_API_KEY + self.ws: Optional[websockets.WebSocketClientProtocol] = None + self.is_connected = False + self._receive_task: Optional[asyncio.Task] = None + + async def connect( + self, + on_transcript_delta: Optional[Callable[[str], None]] = None, + on_transcript_done: Optional[Callable[[str], None]] = None, + on_vad_committed: Optional[Callable[[str], None]] = None, + model: str = "gpt-4o-mini-transcribe", + language: str = "zh", + ) -> bool: + """ + 建立與 OpenAI Realtime API 的 WebSocket 連線 + + Args: + on_transcript_delta: 接收部分轉錄結果的回調函數 + on_transcript_done: 接收完整轉錄結果的回調函數 + on_vad_committed: VAD 偵測到語音結束的回調函數 + model: 使用的模型(gpt-4o-transcribe 或 gpt-4o-mini-transcribe) + language: 語言代碼 + + Returns: + bool: 連線是否成功 + """ + if not self.api_key: + logger.error("❌ OpenAI API Key 未設置") + return False + + try: + logger.info(f"🔌 連接到 OpenAI Realtime API: {REALTIME_API_URL}") + + # 建立 WebSocket 連線(使用 API Key 認證) + self.ws = await websockets.connect( + REALTIME_API_URL, + additional_headers={ + "Authorization": f"Bearer {self.api_key}", + "OpenAI-Beta": "realtime=v1" + } + ) + + self.is_connected = True + logger.info("✅ 已連接到 OpenAI Realtime API") + + # 發送 session 配置(按照 Microsoft/Azure 官方範例) + session_config = { + "type": "transcription_session.update", + "session": { # ← 關鍵:所有配置要包在 session 物件裡 + "input_audio_format": "pcm16", + "input_audio_transcription": { + "model": model, + "prompt": "", + "language": language + }, + "turn_detection": { + "type": "server_vad", + "threshold": 0.5, + "prefix_padding_ms": 300, + "silence_duration_ms": 500 + } + } + } + + await self.ws.send(json.dumps(session_config)) + logger.info("📤 已發送 session 配置") + + # 啟動接收事件的背景任務 + self._receive_task = asyncio.create_task( + self._receive_events( + on_transcript_delta, + on_transcript_done, + on_vad_committed + ) + ) + + return True + + except Exception as e: + logger.error(f"❌ 連接失敗: {e}") + self.is_connected = False + return False + + async def _receive_events( + self, + on_transcript_delta: Optional[Callable], + on_transcript_done: Optional[Callable], + on_vad_committed: Optional[Callable] + ): + """ + 接收並處理來自 OpenAI Realtime API 的事件 + + Args: + on_transcript_delta: 部分轉錄回調 + on_transcript_done: 完整轉錄回調 + on_vad_committed: VAD 提交回調 + """ + try: + while self.is_connected and self.ws: + try: + message = await self.ws.recv() + event = json.loads(message) + event_type = event.get("type") + + logger.debug(f"📩 收到事件: {event_type}") + + # 處理使用者語音的部分轉錄結果(即時串流) + if event_type == "conversation.item.input_audio_transcription.delta": + delta_text = event.get("delta", "") + if on_transcript_delta and delta_text: + await self._safe_callback(on_transcript_delta, delta_text) + + # 處理完整轉錄結果(語音段結束) + elif event_type == "conversation.item.input_audio_transcription.completed": + full_text = event.get("transcript", "") + if on_transcript_done and full_text: + await self._safe_callback(on_transcript_done, full_text) + + # 處理 VAD 提交事件(語音段結束) + elif event_type == "input_audio_buffer.committed": + item_id = event.get("item_id", "") + if on_vad_committed: + await self._safe_callback(on_vad_committed, item_id) + + # 處理錯誤事件 + elif event_type == "error": + error_msg = event.get("error", {}) + logger.error(f"❌ OpenAI API 錯誤: {error_msg}") + + except websockets.exceptions.ConnectionClosed: + logger.warning("⚠️ WebSocket 連線已關閉") + break + except json.JSONDecodeError as e: + logger.error(f"❌ JSON 解析錯誤: {e}") + except Exception as e: + logger.error(f"❌ 接收事件失敗: {e}") + + except Exception as e: + logger.error(f"❌ 事件接收循環失敗: {e}") + finally: + self.is_connected = False + + async def _safe_callback(self, callback: Callable, *args): + """安全地執行回調函數(支援同步和異步)""" + try: + if asyncio.iscoroutinefunction(callback): + await callback(*args) + else: + callback(*args) + except Exception as e: + logger.error(f"❌ 回調函數執行失敗: {e}") + + async def send_audio_chunk(self, audio_data: bytes) -> bool: + """ + 發送音頻 chunk 到 OpenAI Realtime API + + Args: + audio_data: PCM16 格式的音頻數據(需 Base64 編碼) + + Returns: + bool: 是否發送成功 + """ + if not self.is_connected or not self.ws: + logger.warning("⚠️ WebSocket 未連接,無法發送音頻") + return False + + try: + import base64 + + # 將音頻數據編碼為 Base64 + audio_base64 = base64.b64encode(audio_data).decode('utf-8') + + # 發送音頻 chunk + message = { + "type": "input_audio_buffer.append", + "audio": audio_base64 + } + + await self.ws.send(json.dumps(message)) + logger.debug(f"📤 已發送音頻 chunk: {len(audio_data)} bytes") + return True + + except Exception as e: + logger.error(f"❌ 發送音頻失敗: {e}") + return False + + async def commit_audio(self) -> bool: + """ + 手動提交音頻緩衝區(當不使用 Server VAD 時) + + Returns: + bool: 是否提交成功 + """ + if not self.is_connected or not self.ws: + logger.warning("⚠️ WebSocket 未連接,無法提交音頻") + return False + + try: + message = { + "type": "input_audio_buffer.commit" + } + + await self.ws.send(json.dumps(message)) + logger.info("📤 已手動提交音頻緩衝區") + return True + + except Exception as e: + logger.error(f"❌提交音頻失敗: {e}") + return False + + async def disconnect(self): + """關閉 WebSocket 連線""" + if self.ws: + logger.info("🔌 關閉 OpenAI Realtime API 連線") + self.is_connected = False + + # 取消接收任務 + if self._receive_task and not self._receive_task.done(): + self._receive_task.cancel() + try: + await self._receive_task + except asyncio.CancelledError: + pass + + # 關閉 WebSocket + await self.ws.close() + self.ws = None + + logger.info("✅ 已斷開連線") + + +# 全域單例 +realtime_stt_service = RealtimeSTTService() + + +async def create_realtime_session( + on_transcript_delta: Optional[Callable] = None, + on_transcript_done: Optional[Callable] = None, + model: str = "gpt-4o-mini-transcribe", + language: str = "zh" +) -> RealtimeSTTService: + """ + 便捷函數:建立 Realtime STT 會話 + + Args: + on_transcript_delta: 部分轉錄回調 + on_transcript_done: 完整轉錄回調 + model: 使用的模型 + language: 語言代碼 + + Returns: + RealtimeSTTService: 已連線的服務實例 + """ + service = RealtimeSTTService() + await service.connect( + on_transcript_delta=on_transcript_delta, + on_transcript_done=on_transcript_done, + model=model, + language=language + ) + return service diff --git a/services/stt_service.py b/services/stt_service.py index 60a55dd5d0f63807526c31dc9af2ff4783abf737..3fca26662cbe477c6c4f9b7a50dc5bc20fadcc4d 100644 --- a/services/stt_service.py +++ b/services/stt_service.py @@ -36,7 +36,9 @@ class STTService: self, audio_data: bytes, language: str = "zh", - prompt: Optional[str] = None + prompt: Optional[str] = None, + stream: bool = False, + on_delta: Optional[callable] = None ) -> Dict[str, Any]: """ 使用 Whisper API 將音頻轉文字 @@ -45,6 +47,8 @@ class STTService: audio_data: 音頻數據(PCM16 raw bytes 或完整 WAV 文件) language: 語言代碼(zh, en, ja, ko 等) prompt: 提示詞(可選,幫助提高準確度) + stream: 是否啟用串流轉錄(邊轉邊發) + on_delta: 串流模式下的回調函數,接收部分轉錄結果 Returns: { diff --git a/services/voice_binding.py b/services/voice_binding.py new file mode 100644 index 0000000000000000000000000000000000000000..1a32ddd196080e79a10169afc23032f16ee2e8ca --- /dev/null +++ b/services/voice_binding.py @@ -0,0 +1,255 @@ +""" +語音綁定狀態機 +處理語音帳號綁定流程(關鍵字匹配,無 GPT) +""" + +import logging +import time +from datetime import datetime +from pathlib import Path +from typing import Dict, Any, Optional, List + +from fastapi import WebSocket + +from core.logging import get_logger + +logger = get_logger("services.voice_binding") + + +def get_available_speaker_labels() -> List[str]: + """讀取可用的 speaker label 列表""" + classes_file = Path("models/speaker_identification/models_cnn/classes.txt") + if classes_file.exists(): + with open(classes_file, "r", encoding="utf-8") as f: + return [line.strip() for line in f if line.strip()] + return [] + + +class VoiceBindingStateMachine: + """ + 語音帳號綁定狀態機(硬編碼關鍵字匹配) + + 流程: + 1. 用戶說「綁定語音登入」 + 2. 系統詢問「你要綁定哪個帳號?」並顯示可用的 label 列表 + 3. 用戶輸入帳號名稱(label) + 4. 系統綁定 speaker_label 到用戶帳號 + 5. 系統回應「綁定成功囉!」 + """ + + # 觸發關鍵字 + TRIGGER_KEYWORDS = [ + "綁定語音登入", + "語音登入綁定", + "綁定語音", + "設定語音登入", + ] + + # 狀態超時時間(秒) + STATE_TIMEOUT = 300 # 5 分鐘 + + def __init__(self): + # 用戶狀態:{user_id: {state: str, timestamp: datetime}} + self.user_states: Dict[str, Dict[str, Any]] = {} + + def check_binding_trigger(self, user_id: str, message: str) -> Optional[str]: + """ + 檢查是否觸發綁定流程 + + Returns: + - "TRIGGER": 觸發綁定流程 + - "AWAITING_LABEL": 等待用戶輸入帳號名稱 + - None: 不是綁定相關訊息 + """ + message_lower = message.lower().replace(" ", "") + + # 檢測觸發關鍵字 + for keyword in self.TRIGGER_KEYWORDS: + if keyword.replace(" ", "") in message_lower: + self.user_states[user_id] = { + "state": "AWAITING_LABEL", + "timestamp": datetime.now() + } + return "TRIGGER" + + # 檢查是否在等待輸入帳號名稱狀態 + if user_id in self.user_states: + state_info = self.user_states[user_id] + if state_info.get("state") == "AWAITING_LABEL": + # 檢查是否超時 + elapsed = (datetime.now() - state_info.get("timestamp")).total_seconds() + if elapsed > self.STATE_TIMEOUT: + del self.user_states[user_id] + return None + return "AWAITING_LABEL" + + return None + + async def handle_binding_flow( + self, + user_id: str, + message: str, + websocket: WebSocket, + voice_service: Optional[Any] = None, + get_user_by_id: Optional[Any] = None, + ) -> bool: + """ + 處理綁定流程 + + Args: + user_id: 用戶 ID + message: 用戶訊息 + websocket: WebSocket 連線 + voice_service: 語音認證服務(可選) + get_user_by_id: 取得用戶資料的函數(可選) + + Returns: + True: 已處理(不要繼續到 Agent) + False: 未處理(繼續到 Agent) + """ + state = self.check_binding_trigger(user_id, message) + + if state == "TRIGGER": + logger.info(f"🎙️ 用戶 {user_id} 觸發語音綁定流程") + + # 檢查使用者是否已經綁定過 speaker_label + if get_user_by_id: + try: + user_data = await get_user_by_id(user_id) + if user_data and user_data.get("speaker_label"): + existing_label = user_data.get("speaker_label") + logger.info(f"⚠️ 用戶 {user_id} 已綁定 speaker_label: {existing_label}") + + await websocket.send_json({ + "type": "bot_message", + "message": ( + f"你已經綁定過語音了!目前的聲紋標籤是:{existing_label}。" + "如果需要重新綁定,請聯繫管理員。" + ), + "timestamp": time.time() + }) + + self.clear_state(user_id) + return True + + except Exception as e: + logger.error(f"❌ 檢查使用者綁定狀態失敗: {e}") + await websocket.send_json({ + "type": "error", + "message": "系統錯誤,無法檢查綁定狀態" + }) + return True + + # 未綁定,顯示可用的 label 列表 + available_labels = get_available_speaker_labels() + labels_str = "、".join(available_labels) if available_labels else "(無可用帳號)" + + logger.info(f"✅ 用戶 {user_id} 尚未綁定,詢問要綁定的帳號") + + await websocket.send_json({ + "type": "bot_message", + "message": f"好的,你要綁定哪個帳號呢?\n可用的帳號有:{labels_str}\n請輸入帳號名稱:", + "timestamp": time.time() + }) + + return True + + elif state == "AWAITING_LABEL": + # 用戶輸入了帳號名稱 + label_input = message.strip().lower() + available_labels = get_available_speaker_labels() + + logger.info(f"🎙️ 用戶 {user_id} 輸入帳號名稱: {label_input}") + + # 檢查輸入的 label 是否有效 + if label_input not in [l.lower() for l in available_labels]: + labels_str = "、".join(available_labels) + await websocket.send_json({ + "type": "bot_message", + "message": f"找不到這個帳號喔!可用的帳號有:{labels_str}\n請重新輸入:", + "timestamp": time.time() + }) + return True + + # 找到匹配的 label(保持原始大小寫) + matched_label = next((l for l in available_labels if l.lower() == label_input), label_input) + + # 執行綁定 + try: + from core.database import get_user_by_speaker_label, set_user_speaker_label + + # 檢查這個 label 是否已被其他用戶綁定 + existing_user = await get_user_by_speaker_label(matched_label) + if existing_user and existing_user.get("id") != user_id: + await websocket.send_json({ + "type": "bot_message", + "message": f"這個帳號({matched_label})已經被其他人綁定了,請選擇其他帳號。", + "timestamp": time.time() + }) + return True + + # 綁定到當前用戶 + bind_result = await set_user_speaker_label(user_id, matched_label) + + if bind_result.get("success"): + logger.info(f"✅ 用戶 {user_id} 成功綁定 speaker_label: {matched_label}") + await websocket.send_json({ + "type": "bot_message", + "message": f"綁定成功囉!🎉 你的語音帳號已設定為:{matched_label}", + "timestamp": time.time() + }) + await websocket.send_json({ + "type": "voice_binding_success", + "speaker_label": matched_label + }) + else: + error_msg = bind_result.get("error", "未知錯誤") + await websocket.send_json({ + "type": "bot_message", + "message": f"綁定失敗:{error_msg}", + "timestamp": time.time() + }) + + except Exception as e: + logger.error(f"❌ 綁定 speaker_label 失敗: {e}") + await websocket.send_json({ + "type": "bot_message", + "message": "系統錯誤,綁定失敗,請稍後再試。", + "timestamp": time.time() + }) + + # 清理狀態,返回待機模式 + self.clear_state(user_id) + return True + + return False + + def clear_state(self, user_id: str) -> None: + """清理用戶狀態""" + self.user_states.pop(user_id, None) + + def is_awaiting_label(self, user_id: str) -> bool: + """檢查用戶是否在等待輸入帳號名稱狀態""" + if user_id not in self.user_states: + return False + + state_info = self.user_states[user_id] + if state_info.get("state") != "AWAITING_LABEL": + return False + + # 檢查是否超時 + elapsed = (datetime.now() - state_info.get("timestamp")).total_seconds() + if elapsed > self.STATE_TIMEOUT: + del self.user_states[user_id] + return False + + return True + + # 保留舊方法名稱以保持相容性 + def is_awaiting_voice(self, user_id: str) -> bool: + """檢查用戶是否在等待語音錄製狀態(已棄用,保留相容性)""" + return self.is_awaiting_label(user_id) + + +# 全域單例 +voice_binding_fsm = VoiceBindingStateMachine() diff --git a/services/voice_login.py b/services/voice_login.py index f7a8fc90249e5aa2600829fc2344c6fdefbd8636..fa2f1894483c5cd34b31c181fd148ab52dc4afb4 100644 --- a/services/voice_login.py +++ b/services/voice_login.py @@ -110,33 +110,17 @@ class VoiceAuthService: raise RuntimeError(f"載入 CNN 說話者辨識模組失敗:{e}") self._predict_files = _predict_files - # 資產檢查:需要 speaker_id_model.pth 與 classes 定義(classes.txt 或 processed_audio 目錄) - # 若預設目錄不存在,向下相容:嘗試使用舊的專題cnn 目錄(搬遷前) + # 資產檢查:ECAPA-TDNN 需要 speaker_db.pkl if not self.model_dir.exists(): legacy_dir = self.base_dir / "專題cnn" if legacy_dir.exists(): self.model_dir = legacy_dir else: - raise FileNotFoundError(f"找不到 CNN 模型目錄:{self.model_dir}") - - model_file = self.model_dir / "speaker_id_model.pth" - classes_txt = self.model_dir / "classes.txt" - processed_dir = self.model_dir / "processed_audio" - if not model_file.exists(): - raise FileNotFoundError(f"缺少模型檔:{model_file}") - if not classes_txt.exists() and not processed_dir.exists(): - raise FileNotFoundError( - f"缺少類別定義,請提供 {classes_txt.name} 或 {processed_dir.name}/ 子資料夾;或設 VOICE_CNN_CLASSES 指定路徑" - ) - if classes_txt.exists(): - try: - content = classes_txt.read_text(encoding="utf-8").strip() - except Exception: - content = "" - if not content: - raise FileNotFoundError( - f"{classes_txt} 內容為空,請填入每行一個類別名稱,或提供 {processed_dir.name}/ 以自動推斷" - ) + raise FileNotFoundError(f"找不到模型目錄:{self.model_dir}") + + speaker_db_file = self.model_dir / "speaker_db.pkl" + if not speaker_db_file.exists(): + raise FileNotFoundError(f"缺少說話者資料庫:{speaker_db_file}") # 以 user_id 管理暫存錄音緩衝 self._buffers: Dict[str, bytearray] = {} diff --git a/static/frontend/index.html b/static/frontend/index.html index b8e29d57c19e5d6cd934361a3d15cfeaf45cf7d0..1dbd8e0e0e30bdeea37adbe522f1319775053f9c 100644 --- a/static/frontend/index.html +++ b/static/frontend/index.html @@ -1309,24 +1309,6 @@ - - -

🎛️ 控制面板

diff --git a/static/frontend/js/app.js b/static/frontend/js/app.js index 378347490e11bd80a07d02f5022bfd89e20783c7..b263eea37c2fc8f76406a1ba08590e96559ff140 100644 --- a/static/frontend/js/app.js +++ b/static/frontend/js/app.js @@ -79,13 +79,6 @@ async function checkLoginStatus() { function initializeApp(token) { console.log('🚀 初始化應用...'); - // 隱藏登入覆蓋層 - const loginOverlay = document.getElementById('loginOverlay'); - if (loginOverlay) { - loginOverlay.classList.add('hidden'); - console.log('✅ 登入覆蓋層已隱藏'); - } - // 初始化各個模組的事件監聽器 initLoginButton(); initLogoutButton(); @@ -106,8 +99,14 @@ function initializeApp(token) { // ========== 頁面初始化 ========== -// 檢查登入狀態 -checkLoginStatus(); +// 只在聊天室頁面(/static/)執行登入檢查 +console.log('📍 當前路徑:', window.location.pathname); +if (window.location.pathname.startsWith('/static')) { + console.log('✅ 在聊天室頁面,執行登入檢查'); + checkLoginStatus(); +} else { + console.log('⏭️ 不在聊天室頁面,跳過登入檢查'); +} console.log('💡 WebSocket 整合已載入'); console.log('📝 部署時請執行: initializeWebSocket(your_jwt_token)'); @@ -115,3 +114,5 @@ console.log('📝 部署時請執行: initializeWebSocket(your_jwt_token)'); // ========== 提示訊息 ========== console.log('%c Bloom Ware 語音沉浸式 - 多層蓮花版', 'color: #16A34A; font-size: 16px; font-weight: bold;'); console.log('%c✨ 核心特色:\n- 8片蓮花瓣設計(clip-path 打造自然曲線)\n- 多層次花蕊(radial gradient + 光澤細節)\n- 花瓣中心脈絡增加真實感\n- 待機狀態:花瓣完全閉合(含苞待放)\n- Agent 思考中:8片花瓣順時針依序綻放\n- 斷線/重連:花瓣逆時針綻放變紅色警示\n- 錄音中:花蕊變紅脈衝,花瓣保持閉合\n- 品牌特色:優雅、精緻、現代', 'color: rgba(0,0,0,0.7); font-size: 12px;'); + + diff --git a/static/frontend/js/speech.js b/static/frontend/js/speech.js new file mode 100644 index 0000000000000000000000000000000000000000..43ad82371c280c153382004e50cb8297a34a58a7 --- /dev/null +++ b/static/frontend/js/speech.js @@ -0,0 +1,250 @@ +/** + * 語音識別模組 + * 處理語音輸入和語音合成 + */ + +class SpeechRecognition { + constructor() { + this.recognition = null; + this.isListening = false; + this.isSupported = false; + this.onResult = null; + this.onError = null; + this.onStart = null; + this.onEnd = null; + + // 語音辨識上下文 + this.context = { + userLocation: null, + recentQueries: [], + currentSession: null + }; + + this.initRecognition(); + } + + initRecognition() { + // 檢查瀏覽器支援 + if ('webkitSpeechRecognition' in window) { + this.recognition = new webkitSpeechRecognition(); + this.isSupported = true; + } else if ('SpeechRecognition' in window) { + this.recognition = new SpeechRecognition(); + this.isSupported = true; + } else { + console.warn('瀏覽器不支援語音識別'); + return; + } + + // 設定語音識別參數 + this.recognition.continuous = false; + this.recognition.interimResults = true; + this.recognition.lang = 'zh-TW'; + this.recognition.maxAlternatives = 3; // 增加候選結果數量 + + // 綁定事件 + this.recognition.onstart = () => { + this.isListening = true; + console.log('🎤 語音識別開始'); + if (this.onStart) this.onStart(); + }; + + this.recognition.onresult = (event) => { + let finalTranscript = ''; + let interimTranscript = ''; + + for (let i = event.resultIndex; i < event.results.length; i++) { + const transcript = event.results[i][0].transcript; + if (event.results[i].isFinal) { + finalTranscript += transcript; + } else { + interimTranscript += transcript; + } + } + + console.log('🎤 語音識別原始結果:', finalTranscript || interimTranscript); + + // 使用語音增強功能 + let enhancedFinal = finalTranscript; + let enhancedInterim = interimTranscript; + + if (window.speechEnhancer) { + const context = this.getContext(); + + if (finalTranscript) { + enhancedFinal = window.speechEnhancer.enhance(finalTranscript, context); + } + + if (interimTranscript) { + enhancedInterim = window.speechEnhancer.enhance(interimTranscript, context); + } + } + + if (this.onResult) { + this.onResult(enhancedFinal, enhancedInterim); + } + }; + + this.recognition.onerror = (event) => { + console.error('🎤 語音識別錯誤:', event.error); + this.isListening = false; + if (this.onError) this.onError(event.error); + }; + + this.recognition.onend = () => { + this.isListening = false; + console.log('🎤 語音識別結束'); + if (this.onEnd) this.onEnd(); + }; + } + + start() { + if (!this.isSupported) { + console.error('瀏覽器不支援語音識別'); + return false; + } + + if (this.isListening) { + console.warn('語音識別已在進行中'); + return false; + } + + try { + this.recognition.start(); + return true; + } catch (error) { + console.error('啟動語音識別失敗:', error); + return false; + } + } + + stop() { + if (this.recognition && this.isListening) { + this.recognition.stop(); + } + } + + abort() { + if (this.recognition && this.isListening) { + this.recognition.abort(); + } + } + + /** + * 設定用戶位置上下文 + */ + setUserLocation(location) { + this.context.userLocation = location; + console.log('📍 設定用戶位置上下文:', location); + } + + /** + * 添加查詢歷史 + */ + addRecentQuery(query) { + if (query && typeof query === 'string') { + this.context.recentQueries.unshift(query); + // 只保留最近 10 次查詢 + if (this.context.recentQueries.length > 10) { + this.context.recentQueries = this.context.recentQueries.slice(0, 10); + } + console.log('📝 添加查詢歷史:', query); + } + } + + /** + * 獲取當前上下文 + */ + getContext() { + return { + ...this.context, + timestamp: Date.now() + }; + } + + /** + * 清除上下文 + */ + clearContext() { + this.context = { + userLocation: null, + recentQueries: [], + currentSession: null + }; + console.log('🗑️ 已清除語音辨識上下文'); + } +} + +// 語音合成類 +class TextToSpeech { + constructor() { + this.synth = window.speechSynthesis; + this.isSupported = 'speechSynthesis' in window; + this.isSpeaking = false; + this.currentUtterance = null; + } + + speak(text, options = {}) { + if (!this.isSupported) { + console.error('瀏覽器不支援語音合成'); + return false; + } + + // 停止當前播放 + this.stop(); + + const utterance = new SpeechSynthesisUtterance(text); + + // 設定參數 + utterance.lang = options.lang || 'zh-TW'; + utterance.rate = options.rate || 1.0; + utterance.pitch = options.pitch || 1.0; + utterance.volume = options.volume || 1.0; + + // 綁定事件 + utterance.onstart = () => { + this.isSpeaking = true; + console.log('🔊 語音合成開始'); + }; + + utterance.onend = () => { + this.isSpeaking = false; + this.currentUtterance = null; + console.log('🔊 語音合成結束'); + }; + + utterance.onerror = (event) => { + console.error('🔊 語音合成錯誤:', event.error); + this.isSpeaking = false; + this.currentUtterance = null; + }; + + this.currentUtterance = utterance; + this.synth.speak(utterance); + return true; + } + + stop() { + if (this.synth.speaking) { + this.synth.cancel(); + } + this.isSpeaking = false; + this.currentUtterance = null; + } + + pause() { + if (this.synth.speaking && !this.synth.paused) { + this.synth.pause(); + } + } + + resume() { + if (this.synth.paused) { + this.synth.resume(); + } + } +} + +// 全域實例 +window.speechRecognition = new SpeechRecognition(); +window.textToSpeech = new TextToSpeech(); diff --git a/static/frontend/js/tools.js b/static/frontend/js/tools.js index 9f901f399002f79b00b665d1b93941ec6a70358b..de11664f267871fb370d8163b5c5a83968195538 100644 --- a/static/frontend/js/tools.js +++ b/static/frontend/js/tools.js @@ -296,15 +296,28 @@ function renderCardContent(toolName, toolData) { return renderTrainList(toolData.trains); } - // 模式 8:YouBike 站點資訊 - if (toolData.stations && Array.isArray(toolData.stations)) { + // 模式 8:YouBike 站點資訊(需要確認是 YouBike 工具) + if (toolData.stations && Array.isArray(toolData.stations) && + (toolName === 'tdx_youbike' || toolData.stations[0]?.available_bikes !== undefined)) { console.log('✅ 匹配到模式 8: YouBike 站點資訊'); return renderYouBikeStations(toolData.stations); } + + // 模式 8.5:火車站點資訊(tdx_train 的 stations) + if (toolData.stations && Array.isArray(toolData.stations) && toolName === 'tdx_train') { + console.log('✅ 匹配到模式 8.5: 火車站點資訊'); + return renderTrainStations(toolData.stations); + } + + // 模式 9:地理反查資訊(reverse_geocode) + if (toolData.display_name && toolData.lat && toolData.lon && toolName === 'reverse_geocode') { + console.log('✅ 匹配到模式 9: 地理反查資訊'); + return renderReverseGeocode(toolData); + } - // 模式 9:通用 raw_data 物件 + // 模式 10:通用 raw_data 物件 if (toolData.raw_data && typeof toolData.raw_data === 'object') { - console.log('✅ 匹配到模式 9: 通用 raw_data'); + console.log('✅ 匹配到模式 10: 通用 raw_data'); return renderKeyValuePairs(toolData.raw_data); } @@ -548,6 +561,46 @@ function renderTrainList(trains) { return html; } +/** + * 渲染火車站點資訊 + */ +function renderTrainStations(stations) { + if (!stations || stations.length === 0) { + return '

查無車站資訊

'; + } + + let html = '
'; + + stations.forEach((station, index) => { + const stationName = station.station_name || station.name || '未知車站'; + const distance = station.distance_m ? `${Math.round(station.distance_m)}公尺` : ''; + const walkTime = station.walking_time_min ? `步行約${station.walking_time_min}分鐘` : ''; + + html += ` +
+
+ 🚉 ${stationName} +
+ ${distance ? ` +
+ 📏 距離 + ${distance} +
+ ` : ''} + ${walkTime ? ` +
+ 🚶 步行時間 + ${walkTime} +
+ ` : ''} +
+ `; + }); + + html += '
'; + return html; +} + /** * 渲染 YouBike 站點資訊 */ @@ -657,6 +710,68 @@ function renderBusArrivals(arrivals, routeName) { return html; } +/** + * 渲染地理反查資訊(reverse_geocode) + */ +function renderReverseGeocode(data) { + const displayName = data.display_name || '未知地點'; + const city = data.city || ''; + const road = data.road || ''; + const houseNumber = data.house_number || ''; + const suburb = data.suburb || ''; + const admin = data.admin || ''; + const countryCode = data.country_code || ''; + const lat = data.lat?.toFixed(6) || ''; + const lon = data.lon?.toFixed(6) || ''; + + // 組合詳細地址 + let detailedAddress = []; + if (city) detailedAddress.push(city); + if (admin && admin !== city) detailedAddress.push(admin); + if (suburb) detailedAddress.push(suburb); + if (road) detailedAddress.push(road); + if (houseNumber) detailedAddress.push(houseNumber); + + const addressText = detailedAddress.length > 0 ? detailedAddress.join(', ') : displayName; + + // 生成 Google Maps 連結 + const mapsUrl = `https://www.google.com/maps?q=${lat},${lon}`; + + return ` +
+ 📍 位置 + ${displayName} +
+ ${city ? ` +
+ 🏙️ 城市 + ${city} +
+ ` : ''} + ${road ? ` +
+ 🛣️ 道路 + ${road}${houseNumber ? ' ' + houseNumber : ''} +
+ ` : ''} + ${suburb ? ` +
+ 🏘️ 區域 + ${suburb} +
+ ` : ''} +
+ 🌐 座標 + ${lat}, ${lon} +
+ + `; +} + /** * 渲染附近公車站點 */ diff --git a/static/frontend/js/websocket.js b/static/frontend/js/websocket.js index c4f0284a8b36acbc7d9d2a3e9e570a90a4183fad..991e14b44cd0ae21c2c5b6ac21b826cd5dc9c973 100644 --- a/static/frontend/js/websocket.js +++ b/static/frontend/js/websocket.js @@ -332,10 +332,10 @@ class WebSocketManager { this.audioProcessor.connect(this.audioContext.destination); // 發送開始錄音信號 - this.send({ - type: 'audio_start', + this.send({ + type: 'audio_start', sample_rate: 16000, - mode: 'chat' // 對話模式(非語音登入) + mode: 'realtime_chat' // 即時轉錄模式(使用 OpenAI Realtime API) }); this.isRecording = true; @@ -427,9 +427,9 @@ class WebSocketManager { } // 發送停止錄音信號 - this.send({ + this.send({ type: 'audio_stop', - mode: 'chat' // 對話模式 + mode: 'realtime_chat' // 即時轉錄模式 }); this.isRecording = false; @@ -448,8 +448,23 @@ function initializeWebSocket(token) { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = window.location.host; - // 構建WebSocket URL - const wsUrl = `${protocol}//${host}/ws?token=${token}`; + // 讀取語音登入的情緒資料(如果有) + const voiceEmotion = localStorage.getItem('voice_login_emotion'); + + // 構建WebSocket URL(包含情緒參數) + let wsUrl = `${protocol}//${host}/ws?token=${token}`; + if (voiceEmotion) { + wsUrl += `&emotion=${encodeURIComponent(voiceEmotion)}`; + console.log('😊 檢測到語音登入情緒,將傳遞給歡迎詞:', voiceEmotion); + // 使用後立即清除,避免重複使用 + localStorage.removeItem('voice_login_emotion'); + + // 立即套用情緒主題到 UI(不等待後端回應) + if (typeof applyEmotion === 'function') { + applyEmotion(voiceEmotion); + console.log('✅ 語音登入情緒主題已套用:', voiceEmotion); + } + } // 創建 WebSocket 管理器 wsManager = new WebSocketManager(wsUrl); @@ -523,12 +538,27 @@ function initializeWebSocket(token) { transcript.className = 'voice-transcript provisional'; break; + case 'stt_delta': + // STT 即時增量結果(OpenAI Realtime API) + console.log('⚡ STT Delta:', data.text); + // 累積顯示轉錄文字(逐字顯示效果) + if (!window.realtimeTranscript) { + window.realtimeTranscript = ''; + } + window.realtimeTranscript += data.text; + transcript.textContent = window.realtimeTranscript; + transcript.className = 'voice-transcript realtime'; + break; + case 'stt_final': // STT 最終結果(用戶停止說話) console.log('✅ STT 最終結果:', data.text); transcript.textContent = data.text; transcript.className = 'voice-transcript final'; - + + // 清空即時轉錄累積文字 + window.realtimeTranscript = ''; + // 應用情緒主題(如果有的話) if (data.emotion && typeof applyEmotion === 'function') { console.log('😊 應用情緒主題:', data.emotion); @@ -536,6 +566,18 @@ function initializeWebSocket(token) { } break; + case 'realtime_stt_status': + // OpenAI Realtime API 連線狀態 + console.log('🔌 Realtime STT 狀態:', data.status, data.message); + if (data.status === 'connected') { + console.log('✅ 即時轉錄已啟動'); + // 清空之前的累積文字 + window.realtimeTranscript = ''; + } else if (data.status === 'disconnected') { + console.log('🔌 即時轉錄已結束'); + } + break; + case 'chat_ready': // 後端發送當前 chat_id window.currentChatId = data.chat_id; diff --git a/static/frontend/login/404.html b/static/frontend/login/404.html deleted file mode 100644 index bcd6b518c9001a54cd5e6af495ab82c2f590a787..0000000000000000000000000000000000000000 --- a/static/frontend/login/404.html +++ /dev/null @@ -1 +0,0 @@ -404: This page could not be found.Bloom Ware - 槓上開發

404

This page could not be found.

\ No newline at end of file diff --git a/static/frontend/login/404/index.html b/static/frontend/login/404/index.html deleted file mode 100644 index bcd6b518c9001a54cd5e6af495ab82c2f590a787..0000000000000000000000000000000000000000 --- a/static/frontend/login/404/index.html +++ /dev/null @@ -1 +0,0 @@ -404: This page could not be found.Bloom Ware - 槓上開發

404

This page could not be found.

\ No newline at end of file diff --git a/static/frontend/login/__next.__PAGE__.txt b/static/frontend/login/__next.__PAGE__.txt deleted file mode 100644 index 36730f7a5a373529884ed63a6a20507d17731c99..0000000000000000000000000000000000000000 --- a/static/frontend/login/__next.__PAGE__.txt +++ /dev/null @@ -1,17 +0,0 @@ -1:"$Sreact.fragment" -d:I[17994,["/login/_next/static/chunks/d98da620b14e9668.js"],"LoginForm"] -e:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"OutletBoundary"] -f:"$Sreact.suspense" -0:{"buildId":"8vUmiYF3_g4XnZ_PSXlm7","rsc":["$","$1","c",{"children":[["$","main",null,{"className":"min-h-screen bg-[#FFF8E7] flex items-center justify-center p-4 sm:p-6 lg:p-8 relative overflow-hidden","children":[["$","div",null,{"className":"hidden md:block absolute top-8 left-8 opacity-15","children":["$","svg",null,{"viewBox":"0 0 180 240","fill":"none","xmlns":"http://www.w3.org/2000/svg","className":"drop-shadow-sm w-16 h-20 sm:w-20 sm:h-24","children":[["$","path",null,{"d":"M 70 85 Q 52 58, 58 28 Q 62 15, 68 18 Q 75 32, 80 55 Q 83 70, 82 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 82 85 Q 85 45, 88 20 Q 89 8, 92 12 Q 95 35, 93 65 Q 92 78, 93 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.9)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 93 85 Q 90 55, 92 35 Q 96 20, 102 25 Q 110 42, 115 60 Q 118 75, 108 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 72 75 Q 74 52, 76 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 88 78 Q 89 53, 90 28","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 104 75 Q 102 52, 100 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 68 70 Q 70 55, 72 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 108 70 Q 106 55, 104 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 90 85 Q 88 125, 86 165 Q 85 185, 86 220","stroke":"#3A3A3A","strokeWidth":"1.2","fill":"none","strokeLinecap":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 145 Q 65 148, 50 156 Q 45 159, 47 162 Q 54 164, 72 160 Q 83 156, 86 152","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 148 Q 72 152, 58 158","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}],["$","path",null,{"d":"M 86 185 Q 107 188, 122 196 Q 127 199, 125 202 Q 118 204, 100 200 Q 89 196, 86 192","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 188 Q 100 192, 114 198","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}]]}]}],["$","div",null,{"className":"hidden md:block absolute bottom-8 right-8 opacity-15 rotate-180","children":["$","svg",null,{"viewBox":"0 0 180 240","fill":"none","xmlns":"http://www.w3.org/2000/svg","className":"drop-shadow-sm w-16 h-20 sm:w-20 sm:h-24","children":[["$","path",null,{"d":"M 70 85 Q 52 58, 58 28 Q 62 15, 68 18 Q 75 32, 80 55 Q 83 70, 82 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 82 85 Q 85 45, 88 20 Q 89 8, 92 12 Q 95 35, 93 65 Q 92 78, 93 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.9)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 93 85 Q 90 55, 92 35 Q 96 20, 102 25 Q 110 42, 115 60 Q 118 75, 108 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 72 75 Q 74 52, 76 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 88 78 Q 89 53, 90 28","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 104 75 Q 102 52, 100 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 68 70 Q 70 55, 72 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],"$L2","$L3","$L4","$L5","$L6","$L7"]}]}],"$L8","$L9","$La"]}],["$Lb"],"$Lc"]}],"loading":null,"isPartial":false} -2:["$","path",null,{"d":"M 108 70 Q 106 55, 104 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}] -3:["$","path",null,{"d":"M 90 85 Q 88 125, 86 165 Q 85 185, 86 220","stroke":"#3A3A3A","strokeWidth":"1.2","fill":"none","strokeLinecap":"round","opacity":"0.9"}] -4:["$","path",null,{"d":"M 86 145 Q 65 148, 50 156 Q 45 159, 47 162 Q 54 164, 72 160 Q 83 156, 86 152","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}] -5:["$","path",null,{"d":"M 86 148 Q 72 152, 58 158","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}] -6:["$","path",null,{"d":"M 86 185 Q 107 188, 122 196 Q 127 199, 125 202 Q 118 204, 100 200 Q 89 196, 86 192","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}] -7:["$","path",null,{"d":"M 86 188 Q 100 192, 114 198","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}] -8:["$","div",null,{"className":"hidden lg:block absolute top-1/4 right-12 opacity-10","children":["$","svg",null,{"viewBox":"0 0 180 240","fill":"none","xmlns":"http://www.w3.org/2000/svg","className":"drop-shadow-sm w-16 h-20 sm:w-20 sm:h-24","children":[["$","path",null,{"d":"M 70 85 Q 52 58, 58 28 Q 62 15, 68 18 Q 75 32, 80 55 Q 83 70, 82 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 82 85 Q 85 45, 88 20 Q 89 8, 92 12 Q 95 35, 93 65 Q 92 78, 93 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.9)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 93 85 Q 90 55, 92 35 Q 96 20, 102 25 Q 110 42, 115 60 Q 118 75, 108 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 72 75 Q 74 52, 76 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 88 78 Q 89 53, 90 28","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 104 75 Q 102 52, 100 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 68 70 Q 70 55, 72 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 108 70 Q 106 55, 104 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 90 85 Q 88 125, 86 165 Q 85 185, 86 220","stroke":"#3A3A3A","strokeWidth":"1.2","fill":"none","strokeLinecap":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 145 Q 65 148, 50 156 Q 45 159, 47 162 Q 54 164, 72 160 Q 83 156, 86 152","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 148 Q 72 152, 58 158","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}],["$","path",null,{"d":"M 86 185 Q 107 188, 122 196 Q 127 199, 125 202 Q 118 204, 100 200 Q 89 196, 86 192","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 188 Q 100 192, 114 198","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}]]}]}] -9:["$","div",null,{"className":"hidden lg:block absolute bottom-1/4 left-12 opacity-10 rotate-12","children":["$","svg",null,{"viewBox":"0 0 180 240","fill":"none","xmlns":"http://www.w3.org/2000/svg","className":"drop-shadow-sm w-16 h-20 sm:w-20 sm:h-24","children":[["$","path",null,{"d":"M 70 85 Q 52 58, 58 28 Q 62 15, 68 18 Q 75 32, 80 55 Q 83 70, 82 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 82 85 Q 85 45, 88 20 Q 89 8, 92 12 Q 95 35, 93 65 Q 92 78, 93 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.9)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 93 85 Q 90 55, 92 35 Q 96 20, 102 25 Q 110 42, 115 60 Q 118 75, 108 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 72 75 Q 74 52, 76 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 88 78 Q 89 53, 90 28","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 104 75 Q 102 52, 100 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 68 70 Q 70 55, 72 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 108 70 Q 106 55, 104 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 90 85 Q 88 125, 86 165 Q 85 185, 86 220","stroke":"#3A3A3A","strokeWidth":"1.2","fill":"none","strokeLinecap":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 145 Q 65 148, 50 156 Q 45 159, 47 162 Q 54 164, 72 160 Q 83 156, 86 152","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 148 Q 72 152, 58 158","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}],["$","path",null,{"d":"M 86 185 Q 107 188, 122 196 Q 127 199, 125 202 Q 118 204, 100 200 Q 89 196, 86 192","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 188 Q 100 192, 114 198","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}]]}]}] -a:["$","div",null,{"className":"w-full max-w-sm sm:max-w-md relative z-10","children":["$","$Ld",null,{}]}] -b:["$","script","script-0",{"src":"/login/_next/static/chunks/d98da620b14e9668.js","async":true}] -c:["$","$Le",null,{"children":["$","$f",null,{"name":"Next.MetadataOutlet","children":"$@10"}]}] -10:null diff --git a/static/frontend/login/__next._full.txt b/static/frontend/login/__next._full.txt deleted file mode 100644 index 26d437c8d84f2099c87fb6c13a78b8d827d16dba..0000000000000000000000000000000000000000 --- a/static/frontend/login/__next._full.txt +++ /dev/null @@ -1,27 +0,0 @@ -1:"$Sreact.fragment" -2:I[39756,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -3:I[37457,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -c:I[68027,[],"default"] -:HL["/login/_next/static/chunks/1f600c83a5ecf168.css","style"] -:HL["/login/_next/static/chunks/0680eca589a5307c.css","style"] -:HL["/login/_next/static/media/2a65768255d6b625-s.p.d19752fb.woff2","font",{"crossOrigin":"","type":"font/woff2"}] -:HL["/login/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2","font",{"crossOrigin":"","type":"font/woff2"}] -:HL["/login/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2","font",{"crossOrigin":"","type":"font/woff2"}] -0:{"P":null,"b":"8vUmiYF3_g4XnZ_PSXlm7","c":["",""],"q":"","i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/login/_next/static/chunks/1f600c83a5ecf168.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/login/_next/static/chunks/0680eca589a5307c.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"font-sans antialiased","children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":[["$","$1","c",{"children":[["$","main",null,{"className":"min-h-screen bg-[#FFF8E7] flex items-center justify-center p-4 sm:p-6 lg:p-8 relative overflow-hidden","children":[["$","div",null,{"className":"hidden md:block absolute top-8 left-8 opacity-15","children":["$","svg",null,{"viewBox":"0 0 180 240","fill":"none","xmlns":"http://www.w3.org/2000/svg","className":"drop-shadow-sm w-16 h-20 sm:w-20 sm:h-24","children":[["$","path",null,{"d":"M 70 85 Q 52 58, 58 28 Q 62 15, 68 18 Q 75 32, 80 55 Q 83 70, 82 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 82 85 Q 85 45, 88 20 Q 89 8, 92 12 Q 95 35, 93 65 Q 92 78, 93 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.9)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 93 85 Q 90 55, 92 35 Q 96 20, 102 25 Q 110 42, 115 60 Q 118 75, 108 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 72 75 Q 74 52, 76 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 88 78 Q 89 53, 90 28","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 104 75 Q 102 52, 100 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 68 70 Q 70 55, 72 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 108 70 Q 106 55, 104 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 90 85 Q 88 125, 86 165 Q 85 185, 86 220","stroke":"#3A3A3A","strokeWidth":"1.2","fill":"none","strokeLinecap":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 145 Q 65 148, 50 156 Q 45 159, 47 162 Q 54 164, 72 160 Q 83 156, 86 152","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 148 Q 72 152, 58 158","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}],["$","path",null,{"d":"M 86 185 Q 107 188, 122 196 Q 127 199, 125 202 Q 118 204, 100 200 Q 89 196, 86 192","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],"$L4"]}]}],"$L5","$L6","$L7","$L8"]}],["$L9"],"$La"]}],{},null,false,false]},null,false,false],"$Lb",false]],"m":"$undefined","G":["$c",[]],"s":false,"S":true} -d:I[17994,["/login/_next/static/chunks/d98da620b14e9668.js"],"LoginForm"] -e:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"OutletBoundary"] -f:"$Sreact.suspense" -11:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"ViewportBoundary"] -13:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"MetadataBoundary"] -4:["$","path",null,{"d":"M 86 188 Q 100 192, 114 198","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}] -5:["$","div",null,{"className":"hidden md:block absolute bottom-8 right-8 opacity-15 rotate-180","children":["$","svg",null,{"viewBox":"0 0 180 240","fill":"none","xmlns":"http://www.w3.org/2000/svg","className":"drop-shadow-sm w-16 h-20 sm:w-20 sm:h-24","children":[["$","path",null,{"d":"M 70 85 Q 52 58, 58 28 Q 62 15, 68 18 Q 75 32, 80 55 Q 83 70, 82 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 82 85 Q 85 45, 88 20 Q 89 8, 92 12 Q 95 35, 93 65 Q 92 78, 93 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.9)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 93 85 Q 90 55, 92 35 Q 96 20, 102 25 Q 110 42, 115 60 Q 118 75, 108 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 72 75 Q 74 52, 76 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 88 78 Q 89 53, 90 28","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 104 75 Q 102 52, 100 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 68 70 Q 70 55, 72 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 108 70 Q 106 55, 104 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 90 85 Q 88 125, 86 165 Q 85 185, 86 220","stroke":"#3A3A3A","strokeWidth":"1.2","fill":"none","strokeLinecap":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 145 Q 65 148, 50 156 Q 45 159, 47 162 Q 54 164, 72 160 Q 83 156, 86 152","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 148 Q 72 152, 58 158","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}],["$","path",null,{"d":"M 86 185 Q 107 188, 122 196 Q 127 199, 125 202 Q 118 204, 100 200 Q 89 196, 86 192","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 188 Q 100 192, 114 198","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}]]}]}] -6:["$","div",null,{"className":"hidden lg:block absolute top-1/4 right-12 opacity-10","children":["$","svg",null,{"viewBox":"0 0 180 240","fill":"none","xmlns":"http://www.w3.org/2000/svg","className":"drop-shadow-sm w-16 h-20 sm:w-20 sm:h-24","children":[["$","path",null,{"d":"M 70 85 Q 52 58, 58 28 Q 62 15, 68 18 Q 75 32, 80 55 Q 83 70, 82 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 82 85 Q 85 45, 88 20 Q 89 8, 92 12 Q 95 35, 93 65 Q 92 78, 93 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.9)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 93 85 Q 90 55, 92 35 Q 96 20, 102 25 Q 110 42, 115 60 Q 118 75, 108 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 72 75 Q 74 52, 76 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 88 78 Q 89 53, 90 28","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 104 75 Q 102 52, 100 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 68 70 Q 70 55, 72 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 108 70 Q 106 55, 104 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 90 85 Q 88 125, 86 165 Q 85 185, 86 220","stroke":"#3A3A3A","strokeWidth":"1.2","fill":"none","strokeLinecap":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 145 Q 65 148, 50 156 Q 45 159, 47 162 Q 54 164, 72 160 Q 83 156, 86 152","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 148 Q 72 152, 58 158","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}],["$","path",null,{"d":"M 86 185 Q 107 188, 122 196 Q 127 199, 125 202 Q 118 204, 100 200 Q 89 196, 86 192","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 188 Q 100 192, 114 198","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}]]}]}] -7:["$","div",null,{"className":"hidden lg:block absolute bottom-1/4 left-12 opacity-10 rotate-12","children":["$","svg",null,{"viewBox":"0 0 180 240","fill":"none","xmlns":"http://www.w3.org/2000/svg","className":"drop-shadow-sm w-16 h-20 sm:w-20 sm:h-24","children":[["$","path",null,{"d":"M 70 85 Q 52 58, 58 28 Q 62 15, 68 18 Q 75 32, 80 55 Q 83 70, 82 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 82 85 Q 85 45, 88 20 Q 89 8, 92 12 Q 95 35, 93 65 Q 92 78, 93 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.9)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 93 85 Q 90 55, 92 35 Q 96 20, 102 25 Q 110 42, 115 60 Q 118 75, 108 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 72 75 Q 74 52, 76 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 88 78 Q 89 53, 90 28","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 104 75 Q 102 52, 100 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 68 70 Q 70 55, 72 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 108 70 Q 106 55, 104 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 90 85 Q 88 125, 86 165 Q 85 185, 86 220","stroke":"#3A3A3A","strokeWidth":"1.2","fill":"none","strokeLinecap":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 145 Q 65 148, 50 156 Q 45 159, 47 162 Q 54 164, 72 160 Q 83 156, 86 152","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 148 Q 72 152, 58 158","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}],["$","path",null,{"d":"M 86 185 Q 107 188, 122 196 Q 127 199, 125 202 Q 118 204, 100 200 Q 89 196, 86 192","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 188 Q 100 192, 114 198","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}]]}]}] -8:["$","div",null,{"className":"w-full max-w-sm sm:max-w-md relative z-10","children":["$","$Ld",null,{}]}] -9:["$","script","script-0",{"src":"/login/_next/static/chunks/d98da620b14e9668.js","async":true,"nonce":"$undefined"}] -a:["$","$Le",null,{"children":["$","$f",null,{"name":"Next.MetadataOutlet","children":"$@10"}]}] -b:["$","$1","h",{"children":[null,["$","$L11",null,{"children":"$@12"}],["$","div",null,{"hidden":true,"children":["$","$L13",null,{"children":["$","$f",null,{"name":"Next.Metadata","children":"$@14"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}] -12:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]] -15:I[27201,["/login/_next/static/chunks/42879de7b8087bc9.js"],"IconMark"] -14:[["$","title","0",{"children":"Bloom Ware - 槓上開發"}],["$","meta","1",{"name":"description","content":"Elegant login experience for Bloom Ware"}],["$","meta","2",{"name":"generator","content":"v0.app"}],["$","link","3",{"rel":"icon","href":"/login/icon.svg?icon.db662264.svg","sizes":"any","type":"image/svg+xml"}],["$","$L15","4",{}]] -10:null diff --git a/static/frontend/login/__next._index.txt b/static/frontend/login/__next._index.txt deleted file mode 100644 index 5fa9c71c3f7b76ee83d12fde5e6ba529159b175c..0000000000000000000000000000000000000000 --- a/static/frontend/login/__next._index.txt +++ /dev/null @@ -1,6 +0,0 @@ -1:"$Sreact.fragment" -2:I[39756,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -3:I[37457,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -:HL["/login/_next/static/chunks/1f600c83a5ecf168.css","style"] -:HL["/login/_next/static/chunks/0680eca589a5307c.css","style"] -0:{"buildId":"8vUmiYF3_g4XnZ_PSXlm7","rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/login/_next/static/chunks/1f600c83a5ecf168.css","precedence":"next"}],["$","link","1",{"rel":"stylesheet","href":"/login/_next/static/chunks/0680eca589a5307c.css","precedence":"next"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"font-sans antialiased","children":["$","$L2",null,{"parallelRouterKey":"children","template":["$","$L3",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}]}]]}],"loading":null,"isPartial":false} diff --git a/static/frontend/login/__next._tree.txt b/static/frontend/login/__next._tree.txt deleted file mode 100644 index 12b37fbff4a9871fadb49fcae76d442c8539dd5f..0000000000000000000000000000000000000000 --- a/static/frontend/login/__next._tree.txt +++ /dev/null @@ -1,13 +0,0 @@ -1:"$Sreact.fragment" -2:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"ViewportBoundary"] -4:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"MetadataBoundary"] -5:"$Sreact.suspense" -7:I[27201,["/login/_next/static/chunks/42879de7b8087bc9.js"],"IconMark"] -:HL["/login/_next/static/chunks/1f600c83a5ecf168.css","style"] -:HL["/login/_next/static/chunks/0680eca589a5307c.css","style"] -:HL["/login/_next/static/media/2a65768255d6b625-s.p.d19752fb.woff2","font",{"crossOrigin":"","type":"font/woff2"}] -:HL["/login/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2","font",{"crossOrigin":"","type":"font/woff2"}] -:HL["/login/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2","font",{"crossOrigin":"","type":"font/woff2"}] -0:{"buildId":"8vUmiYF3_g4XnZ_PSXlm7","tree":{"name":"","paramType":null,"paramKey":"","hasRuntimePrefetch":false,"slots":{"children":{"name":"__PAGE__","paramType":null,"paramKey":"__PAGE__","hasRuntimePrefetch":false,"slots":null,"isRootLayout":false}},"isRootLayout":true},"head":["$","$1","h",{"children":[null,["$","$L2",null,{"children":"$@3"}],["$","div",null,{"hidden":true,"children":["$","$L4",null,{"children":["$","$5",null,{"name":"Next.Metadata","children":"$@6"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],"isHeadPartial":false,"staleTime":300} -3:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]] -6:[["$","title","0",{"children":"Bloom Ware - 槓上開發"}],["$","meta","1",{"name":"description","content":"Elegant login experience for Bloom Ware"}],["$","meta","2",{"name":"generator","content":"v0.app"}],["$","link","3",{"rel":"icon","href":"/login/icon.svg?icon.db662264.svg","sizes":"any","type":"image/svg+xml"}],["$","$L7","4",{}]] diff --git a/static/frontend/login/_next/static/8vUmiYF3_g4XnZ_PSXlm7/_buildManifest.js b/static/frontend/login/_next/static/8vUmiYF3_g4XnZ_PSXlm7/_buildManifest.js deleted file mode 100644 index 94ca9144a90a44423af7e2220ff909365ae4d5c3..0000000000000000000000000000000000000000 --- a/static/frontend/login/_next/static/8vUmiYF3_g4XnZ_PSXlm7/_buildManifest.js +++ /dev/null @@ -1,11 +0,0 @@ -self.__BUILD_MANIFEST = { - "__rewrites": { - "afterFiles": [], - "beforeFiles": [], - "fallback": [] - }, - "sortedPages": [ - "/_app", - "/_error" - ] -};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB() \ No newline at end of file diff --git a/static/frontend/login/_next/static/8vUmiYF3_g4XnZ_PSXlm7/_clientMiddlewareManifest.json b/static/frontend/login/_next/static/8vUmiYF3_g4XnZ_PSXlm7/_clientMiddlewareManifest.json deleted file mode 100644 index 0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc..0000000000000000000000000000000000000000 --- a/static/frontend/login/_next/static/8vUmiYF3_g4XnZ_PSXlm7/_clientMiddlewareManifest.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/static/frontend/login/_next/static/8vUmiYF3_g4XnZ_PSXlm7/_ssgManifest.js b/static/frontend/login/_next/static/8vUmiYF3_g4XnZ_PSXlm7/_ssgManifest.js deleted file mode 100644 index 5b3ff592fd46c8736892a12864fdf3fed8775202..0000000000000000000000000000000000000000 --- a/static/frontend/login/_next/static/8vUmiYF3_g4XnZ_PSXlm7/_ssgManifest.js +++ /dev/null @@ -1 +0,0 @@ -self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB() \ No newline at end of file diff --git a/static/frontend/login/_next/static/chunks/0680eca589a5307c.css b/static/frontend/login/_next/static/chunks/0680eca589a5307c.css deleted file mode 100644 index 426813c091456a9641a5f3c5313310f65b52d3ff..0000000000000000000000000000000000000000 --- a/static/frontend/login/_next/static/chunks/0680eca589a5307c.css +++ /dev/null @@ -1 +0,0 @@ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:"";--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:forwards;--tw-animation-iteration-count:1;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--color-red-50:#fef2f2;--color-red-300:#ffa3a3;--color-red-400:#ff6568;--color-red-600:#e40014;--color-gray-50:#f9fafb;--color-gray-200:#e5e7eb;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-widest:.1em;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-xs:.125rem;--drop-shadow-sm:0 1px 2px #00000026;--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--aspect-video:16/9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:"Geist","Geist Fallback";--default-mono-font-family:"Geist Mono","Geist Mono Fallback";--color-border:var(--border)}@supports (color:lab(0% 0 0)){:root,:host{--color-red-50:lab(96.5005% 4.18508 1.52328);--color-red-300:lab(76.5514% 36.422 15.5335);--color-red-400:lab(63.7053% 60.745 31.3109);--color-red-600:lab(48.4493% 77.4328 61.5452);--color-gray-50:lab(98.2596% -.247031 -.706708);--color-gray-200:lab(91.6229% -.159115 -2.26791)}}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border);outline-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){*{outline-color:color-mix(in oklab,var(--ring)50%,transparent)}}body{background-color:var(--background);color:var(--foreground)}}@layer components;@layer utilities{.\@container\/card-header{container:card-header/inline-size}.\@container\/field-group{container:field-group/inline-size}.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing)*0)}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.inset-y-0{inset-block:calc(var(--spacing)*0)}.-top-12{top:calc(var(--spacing)*-12)}.top-0{top:calc(var(--spacing)*0)}.top-1\.5{top:calc(var(--spacing)*1.5)}.top-1\/2{top:50%}.top-1\/4{top:25%}.top-2{top:calc(var(--spacing)*2)}.top-3\.5{top:calc(var(--spacing)*3.5)}.top-4{top:calc(var(--spacing)*4)}.top-8{top:calc(var(--spacing)*8)}.top-\[1px\]{top:1px}.top-\[50\%\]{top:50%}.top-\[60\%\]{top:60%}.top-full{top:100%}.-right-12{right:calc(var(--spacing)*-12)}.right-0{right:calc(var(--spacing)*0)}.right-1{right:calc(var(--spacing)*1)}.right-2{right:calc(var(--spacing)*2)}.right-3{right:calc(var(--spacing)*3)}.right-4{right:calc(var(--spacing)*4)}.right-8{right:calc(var(--spacing)*8)}.right-12{right:calc(var(--spacing)*12)}.-bottom-12{bottom:calc(var(--spacing)*-12)}.bottom-0{bottom:calc(var(--spacing)*0)}.bottom-1\/4{bottom:25%}.bottom-8{bottom:calc(var(--spacing)*8)}.-left-12{left:calc(var(--spacing)*-12)}.left-0{left:calc(var(--spacing)*0)}.left-1\/2{left:50%}.left-2{left:calc(var(--spacing)*2)}.left-8{left:calc(var(--spacing)*8)}.left-12{left:calc(var(--spacing)*12)}.left-\[50\%\]{left:50%}.isolate{isolation:isolate}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.z-\[1\]{z-index:1}.z-\[100\]{z-index:100}.order-first{order:-9999}.order-last{order:9999}.col-start-2{grid-column-start:2}.row-span-2{grid-row:span 2/span 2}.row-start-1{grid-row-start:1}.\!m-0{margin:calc(var(--spacing)*0)!important}.-mx-1{margin-inline:calc(var(--spacing)*-1)}.mx-2{margin-inline:calc(var(--spacing)*2)}.mx-3\.5{margin-inline:calc(var(--spacing)*3.5)}.mx-auto{margin-inline:auto}.-my-2{margin-block:calc(var(--spacing)*-2)}.my-0{margin-block:calc(var(--spacing)*0)}.my-0\.5{margin-block:calc(var(--spacing)*.5)}.my-1{margin-block:calc(var(--spacing)*1)}.my-4{margin-block:calc(var(--spacing)*4)}.-mt-4{margin-top:calc(var(--spacing)*-4)}.mt-1\.5{margin-top:calc(var(--spacing)*1.5)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-auto{margin-top:auto}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.-ml-4{margin-left:calc(var(--spacing)*-4)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-4{margin-left:calc(var(--spacing)*4)}.ml-auto{margin-left:auto}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.table{display:table}.table-caption{display:table-caption}.table-cell{display:table-cell}.table-row{display:table-row}.field-sizing-content{field-sizing:content}.aspect-square{aspect-ratio:1}.aspect-video{aspect-ratio:var(--aspect-video)}.size-\(--cell-size\){width:var(--cell-size);height:var(--cell-size)}.size-2{width:calc(var(--spacing)*2);height:calc(var(--spacing)*2)}.size-2\.5{width:calc(var(--spacing)*2.5);height:calc(var(--spacing)*2.5)}.size-3{width:calc(var(--spacing)*3);height:calc(var(--spacing)*3)}.size-3\.5{width:calc(var(--spacing)*3.5);height:calc(var(--spacing)*3.5)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-6{width:calc(var(--spacing)*6);height:calc(var(--spacing)*6)}.size-7{width:calc(var(--spacing)*7);height:calc(var(--spacing)*7)}.size-8{width:calc(var(--spacing)*8);height:calc(var(--spacing)*8)}.size-9{width:calc(var(--spacing)*9);height:calc(var(--spacing)*9)}.size-10{width:calc(var(--spacing)*10);height:calc(var(--spacing)*10)}.size-auto{width:auto;height:auto}.size-full{width:100%;height:100%}.h-\(--cell-size\){height:var(--cell-size)}.h-1\.5{height:calc(var(--spacing)*1.5)}.h-2{height:calc(var(--spacing)*2)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-3{height:calc(var(--spacing)*3)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-7{height:calc(var(--spacing)*7)}.h-8{height:calc(var(--spacing)*8)}.h-9{height:calc(var(--spacing)*9)}.h-10{height:calc(var(--spacing)*10)}.h-11{height:calc(var(--spacing)*11)}.h-12{height:calc(var(--spacing)*12)}.h-20{height:calc(var(--spacing)*20)}.h-40{height:calc(var(--spacing)*40)}.h-\[1\.15rem\]{height:1.15rem}.h-\[calc\(100\%-1px\)\]{height:calc(100% - 1px)}.h-\[var\(--radix-navigation-menu-viewport-height\)\]{height:var(--radix-navigation-menu-viewport-height)}.h-\[var\(--radix-select-trigger-height\)\]{height:var(--radix-select-trigger-height)}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.h-svh{height:100svh}.max-h-\(--radix-context-menu-content-available-height\){max-height:var(--radix-context-menu-content-available-height)}.max-h-\(--radix-dropdown-menu-content-available-height\){max-height:var(--radix-dropdown-menu-content-available-height)}.max-h-\(--radix-select-content-available-height\){max-height:var(--radix-select-content-available-height)}.max-h-\[300px\]{max-height:300px}.max-h-screen{max-height:100vh}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-4{min-height:calc(var(--spacing)*4)}.min-h-16{min-height:calc(var(--spacing)*16)}.min-h-screen{min-height:100vh}.min-h-svh{min-height:100svh}.w-\(--cell-size\){width:var(--cell-size)}.w-\(--sidebar-width\){width:var(--sidebar-width)}.w-0{width:calc(var(--spacing)*0)}.w-1{width:calc(var(--spacing)*1)}.w-2{width:calc(var(--spacing)*2)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-3{width:calc(var(--spacing)*3)}.w-3\/4{width:75%}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-8{width:calc(var(--spacing)*8)}.w-9{width:calc(var(--spacing)*9)}.w-16{width:calc(var(--spacing)*16)}.w-32{width:calc(var(--spacing)*32)}.w-64{width:calc(var(--spacing)*64)}.w-72{width:calc(var(--spacing)*72)}.w-\[100px\]{width:100px}.w-auto{width:auto}.w-fit{width:fit-content}.w-full{width:100%}.w-max{width:max-content}.w-px{width:1px}.max-w-\(--skeleton-width\){max-width:var(--skeleton-width)}.max-w-\[calc\(100\%-2rem\)\]{max-width:calc(100% - 2rem)}.max-w-max{max-width:max-content}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-\(--cell-size\){min-width:var(--cell-size)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-5{min-width:calc(var(--spacing)*5)}.min-w-8{min-width:calc(var(--spacing)*8)}.min-w-9{min-width:calc(var(--spacing)*9)}.min-w-10{min-width:calc(var(--spacing)*10)}.min-w-\[8rem\]{min-width:8rem}.min-w-\[12rem\]{min-width:12rem}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.grow-0{flex-grow:0}.basis-full{flex-basis:100%}.caption-bottom{caption-side:bottom}.border-collapse{border-collapse:collapse}.origin-\(--radix-context-menu-content-transform-origin\){transform-origin:var(--radix-context-menu-content-transform-origin)}.origin-\(--radix-dropdown-menu-content-transform-origin\){transform-origin:var(--radix-dropdown-menu-content-transform-origin)}.origin-\(--radix-hover-card-content-transform-origin\){transform-origin:var(--radix-hover-card-content-transform-origin)}.origin-\(--radix-menubar-content-transform-origin\){transform-origin:var(--radix-menubar-content-transform-origin)}.origin-\(--radix-popover-content-transform-origin\){transform-origin:var(--radix-popover-content-transform-origin)}.origin-\(--radix-select-content-transform-origin\){transform-origin:var(--radix-select-content-transform-origin)}.origin-\(--radix-tooltip-content-transform-origin\){transform-origin:var(--radix-tooltip-content-transform-origin)}.-translate-x-1\/2{--tw-translate-x:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-x-px{--tw-translate-x:-1px;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-\[-50\%\]{--tw-translate-x:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-px{--tw-translate-x:1px;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-0\.5{--tw-translate-y:calc(var(--spacing)*.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-\[-50\%\]{--tw-translate-y:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-\[calc\(-50\%_-_2px\)\]{--tw-translate-y:calc(-50% - 2px);translate:var(--tw-translate-x)var(--tw-translate-y)}.rotate-12{rotate:12deg}.rotate-45{rotate:45deg}.rotate-90{rotate:90deg}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-caret-blink{animation:1.25s ease-out infinite caret-blink}.animate-in{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,forwards)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-text{cursor:text}.touch-none{touch-action:none}.resize-none{resize:none}.scroll-my-1{scroll-margin-block:calc(var(--spacing)*1)}.scroll-py-1{scroll-padding-block:calc(var(--spacing)*1)}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-\[0_1fr\]{grid-template-columns:0 1fr}.grid-rows-\[auto_auto\]{grid-template-rows:auto auto}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-start{justify-content:flex-start}.justify-items-start{justify-items:start}.gap-0\.5{gap:calc(var(--spacing)*.5)}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-2\.5{gap:calc(var(--spacing)*2.5)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}.gap-7{gap:calc(var(--spacing)*7)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}.gap-y-0\.5{row-gap:calc(var(--spacing)*.5)}.self-start{align-self:flex-start}.self-stretch{align-self:stretch}.justify-self-end{justify-self:flex-end}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded-\[2px\]{border-radius:2px}.rounded-\[4px\]{border-radius:4px}.rounded-\[calc\(var\(--radius\)-5px\)\]{border-radius:calc(var(--radius) - 5px)}.rounded-\[inherit\]{border-radius:inherit}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-none{border-radius:0}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded-xl{border-radius:calc(var(--radius) + 4px)}.rounded-xs{border-radius:var(--radius-xs)}.rounded-l-md{border-top-left-radius:calc(var(--radius) - 2px);border-bottom-left-radius:calc(var(--radius) - 2px)}.rounded-tl-sm{border-top-left-radius:calc(var(--radius) - 4px)}.rounded-r-md{border-top-right-radius:calc(var(--radius) - 2px);border-bottom-right-radius:calc(var(--radius) - 2px)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-\[1\.5px\]{border-style:var(--tw-border-style);border-width:1.5px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-\(--color-border\){border-color:var(--color-border)}.border-border,.border-border\/50{border-color:var(--border)}@supports (color:color-mix(in lab, red, red)){.border-border\/50{border-color:color-mix(in oklab,var(--border)50%,transparent)}}.border-destructive{border-color:var(--destructive)}.border-gray-200{border-color:var(--color-gray-200)}.border-input{border-color:var(--input)}.border-primary{border-color:var(--primary)}.border-sidebar-border{border-color:var(--sidebar-border)}.border-transparent{border-color:#0000}.border-t-transparent{border-top-color:#0000}.border-l-transparent{border-left-color:#0000}.bg-\(--color-bg\){background-color:var(--color-bg)}.bg-\[\#FFF8E7\]{background-color:#fff8e7}.bg-accent{background-color:var(--accent)}.bg-background{background-color:var(--background)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-border{background-color:var(--border)}.bg-card{background-color:var(--card)}.bg-destructive{background-color:var(--destructive)}.bg-foreground{background-color:var(--foreground)}.bg-input{background-color:var(--input)}.bg-muted,.bg-muted\/50{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/50{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}.bg-popover{background-color:var(--popover)}.bg-primary,.bg-primary\/20{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.bg-primary\/20{background-color:color-mix(in oklab,var(--primary)20%,transparent)}}.bg-secondary{background-color:var(--secondary)}.bg-sidebar{background-color:var(--sidebar)}.bg-sidebar-border{background-color:var(--sidebar-border)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.fill-current{fill:currentColor}.fill-foreground{fill:var(--foreground)}.fill-primary{fill:var(--primary)}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-\[3px\]{padding:3px}.p-px{padding:1px}.px-\(--cell-size\){padding-inline:var(--cell-size)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.pt-0{padding-top:calc(var(--spacing)*0)}.pt-3{padding-top:calc(var(--spacing)*3)}.pt-4{padding-top:calc(var(--spacing)*4)}.pr-1{padding-right:calc(var(--spacing)*1)}.pr-2{padding-right:calc(var(--spacing)*2)}.pr-2\.5{padding-right:calc(var(--spacing)*2.5)}.pr-3{padding-right:calc(var(--spacing)*3)}.pr-8{padding-right:calc(var(--spacing)*8)}.pb-3{padding-bottom:calc(var(--spacing)*3)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.pl-2{padding-left:calc(var(--spacing)*2)}.pl-3{padding-left:calc(var(--spacing)*3)}.pl-4{padding-left:calc(var(--spacing)*4)}.pl-8{padding-left:calc(var(--spacing)*8)}.text-center{text-align:center}.text-left{text-align:left}.align-middle{vertical-align:middle}.font-mono{font-family:Geist Mono,Geist Mono Fallback}.font-sans{font-family:Geist,Geist Fallback}.font-serif{font-family:Playfair Display,Georgia,serif}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-sm\/relaxed{font-size:var(--text-sm);line-height:var(--leading-relaxed)}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.8rem\]{font-size:.8rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-none{--tw-leading:1;line-height:1}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-balance{text-wrap:balance}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.text-\[\#2C2C2C\]{color:#2c2c2c}.text-\[\#4A4A4A\]{color:#4a4a4a}.text-\[\#8B7355\]{color:#8b7355}.text-accent-foreground{color:var(--accent-foreground)}.text-background{color:var(--background)}.text-card-foreground{color:var(--card-foreground)}.text-current{color:currentColor}.text-destructive{color:var(--destructive)}.text-destructive-foreground{color:var(--destructive-foreground)}.text-foreground,.text-foreground\/50{color:var(--foreground)}@supports (color:color-mix(in lab, red, red)){.text-foreground\/50{color:color-mix(in oklab,var(--foreground)50%,transparent)}}.text-muted-foreground{color:var(--muted-foreground)}.text-popover-foreground{color:var(--popover-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-secondary-foreground{color:var(--secondary-foreground)}.text-sidebar-foreground,.text-sidebar-foreground\/70{color:var(--sidebar-foreground)}@supports (color:color-mix(in lab, red, red)){.text-sidebar-foreground\/70{color:color-mix(in oklab,var(--sidebar-foreground)70%,transparent)}}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.underline-offset-4{text-underline-offset:4px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-10{opacity:.1}.opacity-15{opacity:.15}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.opacity-90{opacity:.9}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_0_1px_hsl\(var\(--sidebar-border\)\)\]{--tw-shadow:0 0 0 1px var(--tw-shadow-color,hsl(var(--sidebar-border)));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-0{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-ring\/50{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.ring-ring\/50{--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}.ring-sidebar-ring{--tw-ring-color:var(--sidebar-ring)}.ring-offset-background{--tw-ring-offset-color:var(--background)}.outline-hidden{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.outline-hidden{outline-offset:2px;outline:2px solid #0000}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.drop-shadow-sm{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-sm));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[color\,box-shadow\]{transition-property:color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[left\,right\,width\]{transition-property:left,right,width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[margin\,opacity\]{transition-property:margin,opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\,height\,padding\]{transition-property:width,height,padding;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-none{transition-property:none}.duration-100{--tw-duration:.1s;transition-duration:.1s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-1000{--tw-duration:1s;transition-duration:1s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-linear{--tw-ease:linear;transition-timing-function:linear}.fade-in-0{--tw-enter-opacity:0}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.zoom-in-95{--tw-enter-scale:.95}.\[--cell-size\:--spacing\(8\)\]{--cell-size:calc(var(--spacing)*8)}.group-focus-within\/menu-item\:opacity-100:is(:where(.group\/menu-item):focus-within *){opacity:1}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *),.group-hover\/menu-item\:opacity-100:is(:where(.group\/menu-item):hover *){opacity:1}}.group-has-data-\[sidebar\=menu-action\]\/menu-item\:pr-8:is(:where(.group\/menu-item):has([data-sidebar=menu-action]) *){padding-right:calc(var(--spacing)*8)}.group-has-\[\[data-orientation\=horizontal\]\]\/field\:text-balance:is(:where(.group\/field):has([data-orientation=horizontal]) *){text-wrap:balance}.group-has-\[\[data-slot\=item-description\]\]\/item\:translate-y-0\.5:is(:where(.group\/item):has([data-slot=item-description]) *){--tw-translate-y:calc(var(--spacing)*.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-has-\[\[data-slot\=item-description\]\]\/item\:self-start:is(:where(.group\/item):has([data-slot=item-description]) *){align-self:flex-start}.group-has-\[\>input\]\/input-group\:pt-2\.5:is(:where(.group\/input-group):has(>input) *){padding-top:calc(var(--spacing)*2.5)}.group-has-\[\>input\]\/input-group\:pb-2\.5:is(:where(.group\/input-group):has(>input) *){padding-bottom:calc(var(--spacing)*2.5)}.group-data-\[collapsible\=icon\]\:-mt-8:is(:where(.group)[data-collapsible=icon] *){margin-top:calc(var(--spacing)*-8)}.group-data-\[collapsible\=icon\]\:hidden:is(:where(.group)[data-collapsible=icon] *){display:none}.group-data-\[collapsible\=icon\]\:size-8\!:is(:where(.group)[data-collapsible=icon] *){width:calc(var(--spacing)*8)!important;height:calc(var(--spacing)*8)!important}.group-data-\[collapsible\=icon\]\:w-\(--sidebar-width-icon\):is(:where(.group)[data-collapsible=icon] *){width:var(--sidebar-width-icon)}.group-data-\[collapsible\=icon\]\:w-\[calc\(var\(--sidebar-width-icon\)\+\(--spacing\(4\)\)\)\]:is(:where(.group)[data-collapsible=icon] *){width:calc(var(--sidebar-width-icon) + (calc(var(--spacing)*4)))}.group-data-\[collapsible\=icon\]\:w-\[calc\(var\(--sidebar-width-icon\)\+\(--spacing\(4\)\)\+2px\)\]:is(:where(.group)[data-collapsible=icon] *){width:calc(var(--sidebar-width-icon) + (calc(var(--spacing)*4)) + 2px)}.group-data-\[collapsible\=icon\]\:overflow-hidden:is(:where(.group)[data-collapsible=icon] *){overflow:hidden}.group-data-\[collapsible\=icon\]\:p-0\!:is(:where(.group)[data-collapsible=icon] *){padding:calc(var(--spacing)*0)!important}.group-data-\[collapsible\=icon\]\:p-2\!:is(:where(.group)[data-collapsible=icon] *){padding:calc(var(--spacing)*2)!important}.group-data-\[collapsible\=icon\]\:opacity-0:is(:where(.group)[data-collapsible=icon] *){opacity:0}.group-data-\[collapsible\=offcanvas\]\:right-\[calc\(var\(--sidebar-width\)\*-1\)\]:is(:where(.group)[data-collapsible=offcanvas] *){right:calc(var(--sidebar-width)*-1)}.group-data-\[collapsible\=offcanvas\]\:left-\[calc\(var\(--sidebar-width\)\*-1\)\]:is(:where(.group)[data-collapsible=offcanvas] *){left:calc(var(--sidebar-width)*-1)}.group-data-\[collapsible\=offcanvas\]\:w-0:is(:where(.group)[data-collapsible=offcanvas] *){width:calc(var(--spacing)*0)}.group-data-\[collapsible\=offcanvas\]\:translate-x-0:is(:where(.group)[data-collapsible=offcanvas] *){--tw-translate-x:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-data-\[disabled\=true\]\:pointer-events-none:is(:where(.group)[data-disabled=true] *){pointer-events:none}.group-data-\[disabled\=true\]\:opacity-50:is(:where(.group)[data-disabled=true] *),.group-data-\[disabled\=true\]\/field\:opacity-50:is(:where(.group\/field)[data-disabled=true] *),.group-data-\[disabled\=true\]\/input-group\:opacity-50:is(:where(.group\/input-group)[data-disabled=true] *){opacity:.5}.group-data-\[focused\=true\]\/day\:relative:is(:where(.group\/day)[data-focused=true] *){position:relative}.group-data-\[focused\=true\]\/day\:z-10:is(:where(.group\/day)[data-focused=true] *){z-index:10}.group-data-\[focused\=true\]\/day\:border-ring:is(:where(.group\/day)[data-focused=true] *){border-color:var(--ring)}.group-data-\[focused\=true\]\/day\:ring-\[3px\]:is(:where(.group\/day)[data-focused=true] *){--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.group-data-\[focused\=true\]\/day\:ring-ring\/50:is(:where(.group\/day)[data-focused=true] *){--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.group-data-\[focused\=true\]\/day\:ring-ring\/50:is(:where(.group\/day)[data-focused=true] *){--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}.group-data-\[side\=left\]\:-right-4:is(:where(.group)[data-side=left] *){right:calc(var(--spacing)*-4)}.group-data-\[side\=left\]\:border-r:is(:where(.group)[data-side=left] *){border-right-style:var(--tw-border-style);border-right-width:1px}.group-data-\[side\=right\]\:left-0:is(:where(.group)[data-side=right] *){left:calc(var(--spacing)*0)}.group-data-\[side\=right\]\:rotate-180:is(:where(.group)[data-side=right] *){rotate:180deg}.group-data-\[side\=right\]\:border-l:is(:where(.group)[data-side=right] *){border-left-style:var(--tw-border-style);border-left-width:1px}.group-data-\[state\=open\]\:rotate-180:is(:where(.group)[data-state=open] *){rotate:180deg}.group-data-\[variant\=floating\]\:rounded-lg:is(:where(.group)[data-variant=floating] *){border-radius:var(--radius)}.group-data-\[variant\=floating\]\:border:is(:where(.group)[data-variant=floating] *){border-style:var(--tw-border-style);border-width:1px}.group-data-\[variant\=floating\]\:border-sidebar-border:is(:where(.group)[data-variant=floating] *){border-color:var(--sidebar-border)}.group-data-\[variant\=floating\]\:shadow-sm:is(:where(.group)[data-variant=floating] *){--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.group-data-\[variant\=outline\]\/field-group\:-mb-2:is(:where(.group\/field-group)[data-variant=outline] *){margin-bottom:calc(var(--spacing)*-2)}.group-data-\[vaul-drawer-direction\=bottom\]\/drawer-content\:block:is(:where(.group\/drawer-content)[data-vaul-drawer-direction=bottom] *){display:block}.group-data-\[vaul-drawer-direction\=bottom\]\/drawer-content\:text-center:is(:where(.group\/drawer-content)[data-vaul-drawer-direction=bottom] *),.group-data-\[vaul-drawer-direction\=top\]\/drawer-content\:text-center:is(:where(.group\/drawer-content)[data-vaul-drawer-direction=top] *){text-align:center}.group-data-\[viewport\=false\]\/navigation-menu\:top-full:is(:where(.group\/navigation-menu)[data-viewport=false] *){top:100%}.group-data-\[viewport\=false\]\/navigation-menu\:mt-1\.5:is(:where(.group\/navigation-menu)[data-viewport=false] *){margin-top:calc(var(--spacing)*1.5)}.group-data-\[viewport\=false\]\/navigation-menu\:overflow-hidden:is(:where(.group\/navigation-menu)[data-viewport=false] *){overflow:hidden}.group-data-\[viewport\=false\]\/navigation-menu\:rounded-md:is(:where(.group\/navigation-menu)[data-viewport=false] *){border-radius:calc(var(--radius) - 2px)}.group-data-\[viewport\=false\]\/navigation-menu\:border:is(:where(.group\/navigation-menu)[data-viewport=false] *){border-style:var(--tw-border-style);border-width:1px}.group-data-\[viewport\=false\]\/navigation-menu\:bg-popover:is(:where(.group\/navigation-menu)[data-viewport=false] *){background-color:var(--popover)}.group-data-\[viewport\=false\]\/navigation-menu\:text-popover-foreground:is(:where(.group\/navigation-menu)[data-viewport=false] *){color:var(--popover-foreground)}.group-data-\[viewport\=false\]\/navigation-menu\:shadow:is(:where(.group\/navigation-menu)[data-viewport=false] *){--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.group-data-\[viewport\=false\]\/navigation-menu\:duration-200:is(:where(.group\/navigation-menu)[data-viewport=false] *){--tw-duration:.2s;transition-duration:.2s}.group-\[\.destructive\]\:border-muted\/40:is(:where(.group).destructive *){border-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.group-\[\.destructive\]\:border-muted\/40:is(:where(.group).destructive *){border-color:color-mix(in oklab,var(--muted)40%,transparent)}}.group-\[\.destructive\]\:text-red-300:is(:where(.group).destructive *){color:var(--color-red-300)}@media (hover:hover){.peer-hover\/menu-button\:text-sidebar-accent-foreground:is(:where(.peer\/menu-button):hover~*){color:var(--sidebar-accent-foreground)}}.peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled~*){cursor:not-allowed}.peer-disabled\:opacity-50:is(:where(.peer):disabled~*){opacity:.5}.peer-data-\[active\=true\]\/menu-button\:text-sidebar-accent-foreground:is(:where(.peer\/menu-button)[data-active=true]~*){color:var(--sidebar-accent-foreground)}.peer-data-\[size\=default\]\/menu-button\:top-1\.5:is(:where(.peer\/menu-button)[data-size=default]~*){top:calc(var(--spacing)*1.5)}.peer-data-\[size\=lg\]\/menu-button\:top-2\.5:is(:where(.peer\/menu-button)[data-size=lg]~*){top:calc(var(--spacing)*2.5)}.peer-data-\[size\=sm\]\/menu-button\:top-1:is(:where(.peer\/menu-button)[data-size=sm]~*){top:calc(var(--spacing)*1)}.selection\:bg-primary ::selection,.selection\:bg-primary::selection{background-color:var(--primary)}.selection\:text-primary-foreground ::selection,.selection\:text-primary-foreground::selection{color:var(--primary-foreground)}.file\:inline-flex::file-selector-button{display:inline-flex}.file\:h-7::file-selector-button{height:calc(var(--spacing)*7)}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-transparent::file-selector-button{background-color:#0000}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.file\:text-foreground::file-selector-button{color:var(--foreground)}.placeholder\:text-muted-foreground::placeholder{color:var(--muted-foreground)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:-inset-2:after{content:var(--tw-content);inset:calc(var(--spacing)*-2)}.after\:inset-y-0:after{content:var(--tw-content);inset-block:calc(var(--spacing)*0)}.after\:left-1\/2:after{content:var(--tw-content);left:50%}.after\:w-1:after{content:var(--tw-content);width:calc(var(--spacing)*1)}.after\:w-\[2px\]:after{content:var(--tw-content);width:2px}.after\:-translate-x-1\/2:after{content:var(--tw-content);--tw-translate-x:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-data-\[collapsible\=offcanvas\]\:after\:left-full:is(:where(.group)[data-collapsible=offcanvas] *):after{content:var(--tw-content);left:100%}.first\:rounded-l-md:first-child{border-top-left-radius:calc(var(--radius) - 2px);border-bottom-left-radius:calc(var(--radius) - 2px)}.first\:border-l:first-child{border-left-style:var(--tw-border-style);border-left-width:1px}.last\:mt-0:last-child{margin-top:calc(var(--spacing)*0)}.last\:rounded-r-md:last-child{border-top-right-radius:calc(var(--radius) - 2px);border-bottom-right-radius:calc(var(--radius) - 2px)}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (hover:hover){.hover\:bg-accent:hover{background-color:var(--accent)}.hover\:bg-destructive\/90:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-destructive\/90:hover{background-color:color-mix(in oklab,var(--destructive)90%,transparent)}}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-muted:hover,.hover\:bg-muted\/50:hover{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}.hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--primary)90%,transparent)}}.hover\:bg-secondary:hover,.hover\:bg-secondary\/80:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab,var(--secondary)80%,transparent)}}.hover\:bg-sidebar-accent:hover{background-color:var(--sidebar-accent)}.hover\:text-accent-foreground:hover{color:var(--accent-foreground)}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:text-muted-foreground:hover{color:var(--muted-foreground)}.hover\:text-sidebar-accent-foreground:hover{color:var(--sidebar-accent-foreground)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-\[0_0_0_1px_hsl\(var\(--sidebar-accent\)\)\]:hover{--tw-shadow:0 0 0 1px var(--tw-shadow-color,hsl(var(--sidebar-accent)));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-4:hover{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(4px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:group-data-\[collapsible\=offcanvas\]\:bg-sidebar:hover:is(:where(.group)[data-collapsible=offcanvas] *){background-color:var(--sidebar)}.group-\[\.destructive\]\:hover\:border-destructive\/30:is(:where(.group).destructive *):hover{border-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.group-\[\.destructive\]\:hover\:border-destructive\/30:is(:where(.group).destructive *):hover{border-color:color-mix(in oklab,var(--destructive)30%,transparent)}}.group-\[\.destructive\]\:hover\:bg-destructive:is(:where(.group).destructive *):hover{background-color:var(--destructive)}.group-\[\.destructive\]\:hover\:text-destructive-foreground:is(:where(.group).destructive *):hover{color:var(--destructive-foreground)}.group-\[\.destructive\]\:hover\:text-red-50:is(:where(.group).destructive *):hover{color:var(--color-red-50)}.hover\:after\:bg-sidebar-border:hover:after{content:var(--tw-content);background-color:var(--sidebar-border)}}.focus\:z-10:focus{z-index:10}.focus\:bg-accent:focus{background-color:var(--accent)}.focus\:text-accent-foreground:focus{color:var(--accent-foreground)}.focus\:opacity-100:focus{opacity:1}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-ring:focus{--tw-ring-color:var(--ring)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:outline-hidden:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.focus\:outline-hidden:focus{outline-offset:2px;outline:2px solid #0000}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.group-\[\.destructive\]\:focus\:ring-destructive:is(:where(.group).destructive *):focus{--tw-ring-color:var(--destructive)}.group-\[\.destructive\]\:focus\:ring-red-400:is(:where(.group).destructive *):focus{--tw-ring-color:var(--color-red-400)}.group-\[\.destructive\]\:focus\:ring-offset-red-600:is(:where(.group).destructive *):focus{--tw-ring-offset-color:var(--color-red-600)}.focus-visible\:z-10:focus-visible{z-index:10}.focus-visible\:border-ring:focus-visible{border-color:var(--ring)}.focus-visible\:ring-0:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-1:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-4:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(4px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-\[3px\]:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.focus-visible\:ring-ring:focus-visible,.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}.focus-visible\:ring-offset-1:focus-visible{--tw-ring-offset-width:1px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus-visible\:outline-hidden:focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.focus-visible\:outline-hidden:focus-visible{outline-offset:2px;outline:2px solid #0000}}.focus-visible\:outline-1:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.focus-visible\:outline-ring:focus-visible{outline-color:var(--ring)}.active\:bg-sidebar-accent:active{background-color:var(--sidebar-accent)}.active\:text-sidebar-accent-foreground:active{color:var(--sidebar-accent-foreground)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}:where([data-side=left]) .in-data-\[side\=left\]\:cursor-w-resize{cursor:w-resize}:where([data-side=right]) .in-data-\[side\=right\]\:cursor-e-resize{cursor:e-resize}.has-focus\:border-ring:has(:focus){border-color:var(--ring)}.has-focus\:ring-\[3px\]:has(:focus){--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.has-focus\:ring-ring\/50:has(:focus){--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.has-focus\:ring-ring\/50:has(:focus){--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}.has-disabled\:opacity-50:has(:disabled){opacity:.5}.has-data-\[slot\=card-action\]\:grid-cols-\[1fr_auto\]:has([data-slot=card-action]){grid-template-columns:1fr auto}.has-data-\[state\=checked\]\:border-primary:has([data-state=checked]){border-color:var(--primary)}.has-data-\[state\=checked\]\:bg-primary\/5:has([data-state=checked]){background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.has-data-\[state\=checked\]\:bg-primary\/5:has([data-state=checked]){background-color:color-mix(in oklab,var(--primary)5%,transparent)}}.has-data-\[variant\=inset\]\:bg-sidebar:has([data-variant=inset]){background-color:var(--sidebar)}.has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:border-ring:has([data-slot=input-group-control]:focus-visible){border-color:var(--ring)}.has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:ring-\[3px\]:has([data-slot=input-group-control]:focus-visible){--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:ring-ring\/50:has([data-slot=input-group-control]:focus-visible){--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.has-\[\[data-slot\=input-group-control\]\:focus-visible\]\:ring-ring\/50:has([data-slot=input-group-control]:focus-visible){--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}.has-\[\[data-slot\]\[aria-invalid\=true\]\]\:border-destructive:has([data-slot][aria-invalid=true]){border-color:var(--destructive)}.has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/20:has([data-slot][aria-invalid=true]){--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/20:has([data-slot][aria-invalid=true]){--tw-ring-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.has-\[\>\[data-align\=block-end\]\]\:h-auto:has(>[data-align=block-end]){height:auto}.has-\[\>\[data-align\=block-end\]\]\:flex-col:has(>[data-align=block-end]){flex-direction:column}.has-\[\>\[data-align\=block-start\]\]\:h-auto:has(>[data-align=block-start]){height:auto}.has-\[\>\[data-align\=block-start\]\]\:flex-col:has(>[data-align=block-start]){flex-direction:column}.has-\[\>\[data-slot\=button-group\]\]\:gap-2:has(>[data-slot=button-group]){gap:calc(var(--spacing)*2)}.has-\[\>\[data-slot\=checkbox-group\]\]\:gap-3:has(>[data-slot=checkbox-group]){gap:calc(var(--spacing)*3)}.has-\[\>\[data-slot\=field-content\]\]\:items-start:has(>[data-slot=field-content]){align-items:flex-start}.has-\[\>\[data-slot\=field\]\]\:w-full:has(>[data-slot=field]){width:100%}.has-\[\>\[data-slot\=field\]\]\:flex-col:has(>[data-slot=field]){flex-direction:column}.has-\[\>\[data-slot\=field\]\]\:rounded-md:has(>[data-slot=field]){border-radius:calc(var(--radius) - 2px)}.has-\[\>\[data-slot\=field\]\]\:border:has(>[data-slot=field]){border-style:var(--tw-border-style);border-width:1px}.has-\[\>\[data-slot\=radio-group\]\]\:gap-3:has(>[data-slot=radio-group]){gap:calc(var(--spacing)*3)}.has-\[\>button\]\:mr-\[-0\.4rem\]:has(>button){margin-right:-.4rem}.has-\[\>button\]\:ml-\[-0\.45rem\]:has(>button){margin-left:-.45rem}.has-\[\>kbd\]\:mr-\[-0\.35rem\]:has(>kbd){margin-right:-.35rem}.has-\[\>kbd\]\:ml-\[-0\.35rem\]:has(>kbd){margin-left:-.35rem}.has-\[\>svg\]\:grid-cols-\[calc\(var\(--spacing\)\*4\)_1fr\]:has(>svg){grid-template-columns:calc(var(--spacing)*4)1fr}.has-\[\>svg\]\:gap-x-3:has(>svg){column-gap:calc(var(--spacing)*3)}.has-\[\>svg\]\:p-0:has(>svg){padding:calc(var(--spacing)*0)}.has-\[\>svg\]\:px-2:has(>svg){padding-inline:calc(var(--spacing)*2)}.has-\[\>svg\]\:px-2\.5:has(>svg){padding-inline:calc(var(--spacing)*2.5)}.has-\[\>svg\]\:px-3:has(>svg){padding-inline:calc(var(--spacing)*3)}.has-\[\>svg\]\:px-4:has(>svg){padding-inline:calc(var(--spacing)*4)}.has-\[\>textarea\]\:h-auto:has(>textarea){height:auto}.aria-disabled\:pointer-events-none[aria-disabled=true]{pointer-events:none}.aria-disabled\:opacity-50[aria-disabled=true]{opacity:.5}.aria-invalid\:border-destructive[aria-invalid=true]{border-color:var(--destructive)}.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.aria-selected\:text-muted-foreground[aria-selected=true]{color:var(--muted-foreground)}.data-\[active\=true\]\:z-10[data-active=true]{z-index:10}.data-\[active\=true\]\:border-ring[data-active=true]{border-color:var(--ring)}.data-\[active\=true\]\:bg-accent\/50[data-active=true]{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.data-\[active\=true\]\:bg-accent\/50[data-active=true]{background-color:color-mix(in oklab,var(--accent)50%,transparent)}}.data-\[active\=true\]\:bg-sidebar-accent[data-active=true]{background-color:var(--sidebar-accent)}.data-\[active\=true\]\:font-medium[data-active=true]{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.data-\[active\=true\]\:text-accent-foreground[data-active=true]{color:var(--accent-foreground)}.data-\[active\=true\]\:text-sidebar-accent-foreground[data-active=true]{color:var(--sidebar-accent-foreground)}.data-\[active\=true\]\:ring-\[3px\][data-active=true]{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(3px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[active\=true\]\:ring-ring\/50[data-active=true]{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.data-\[active\=true\]\:ring-ring\/50[data-active=true]{--tw-ring-color:color-mix(in oklab,var(--ring)50%,transparent)}}@media (hover:hover){.data-\[active\=true\]\:hover\:bg-accent[data-active=true]:hover{background-color:var(--accent)}}.data-\[active\=true\]\:focus\:bg-accent[data-active=true]:focus{background-color:var(--accent)}.data-\[active\=true\]\:aria-invalid\:border-destructive[data-active=true][aria-invalid=true]{border-color:var(--destructive)}.data-\[active\=true\]\:aria-invalid\:ring-destructive\/20[data-active=true][aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.data-\[active\=true\]\:aria-invalid\:ring-destructive\/20[data-active=true][aria-invalid=true]{--tw-ring-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[disabled\=true\]\:pointer-events-none[data-disabled=true]{pointer-events:none}.data-\[disabled\=true\]\:opacity-50[data-disabled=true]{opacity:.5}.data-\[error\=true\]\:text-destructive[data-error=true]{color:var(--destructive)}.data-\[inset\]\:pl-8[data-inset]{padding-left:calc(var(--spacing)*8)}.data-\[invalid\=true\]\:text-destructive[data-invalid=true]{color:var(--destructive)}.data-\[motion\=from-end\]\:slide-in-from-right-52[data-motion=from-end]{--tw-enter-translate-x:calc(52*var(--spacing))}.data-\[motion\=from-start\]\:slide-in-from-left-52[data-motion=from-start]{--tw-enter-translate-x:calc(52*var(--spacing)*-1)}.data-\[motion\=to-end\]\:slide-out-to-right-52[data-motion=to-end]{--tw-exit-translate-x:calc(52*var(--spacing))}.data-\[motion\=to-start\]\:slide-out-to-left-52[data-motion=to-start]{--tw-exit-translate-x:calc(52*var(--spacing)*-1)}.data-\[motion\^\=from-\]\:animate-in[data-motion^=from-]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,forwards)}.data-\[motion\^\=from-\]\:fade-in[data-motion^=from-]{--tw-enter-opacity:0}.data-\[motion\^\=to-\]\:animate-out[data-motion^=to-]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,forwards)}.data-\[motion\^\=to-\]\:fade-out[data-motion^=to-]{--tw-exit-opacity:0}.data-\[orientation\=horizontal\]\:h-1\.5[data-orientation=horizontal]{height:calc(var(--spacing)*1.5)}.data-\[orientation\=horizontal\]\:h-full[data-orientation=horizontal]{height:100%}.data-\[orientation\=horizontal\]\:h-px[data-orientation=horizontal]{height:1px}.data-\[orientation\=horizontal\]\:w-full[data-orientation=horizontal]{width:100%}.data-\[orientation\=vertical\]\:h-auto[data-orientation=vertical]{height:auto}.data-\[orientation\=vertical\]\:h-full[data-orientation=vertical]{height:100%}.data-\[orientation\=vertical\]\:min-h-44[data-orientation=vertical]{min-height:calc(var(--spacing)*44)}.data-\[orientation\=vertical\]\:w-1\.5[data-orientation=vertical]{width:calc(var(--spacing)*1.5)}.data-\[orientation\=vertical\]\:w-auto[data-orientation=vertical]{width:auto}.data-\[orientation\=vertical\]\:w-full[data-orientation=vertical]{width:100%}.data-\[orientation\=vertical\]\:w-px[data-orientation=vertical]{width:1px}.data-\[orientation\=vertical\]\:flex-col[data-orientation=vertical]{flex-direction:column}.data-\[panel-group-direction\=vertical\]\:h-px[data-panel-group-direction=vertical]{height:1px}.data-\[panel-group-direction\=vertical\]\:w-full[data-panel-group-direction=vertical]{width:100%}.data-\[panel-group-direction\=vertical\]\:flex-col[data-panel-group-direction=vertical]{flex-direction:column}.data-\[panel-group-direction\=vertical\]\:after\:left-0[data-panel-group-direction=vertical]:after{content:var(--tw-content);left:calc(var(--spacing)*0)}.data-\[panel-group-direction\=vertical\]\:after\:h-1[data-panel-group-direction=vertical]:after{content:var(--tw-content);height:calc(var(--spacing)*1)}.data-\[panel-group-direction\=vertical\]\:after\:w-full[data-panel-group-direction=vertical]:after{content:var(--tw-content);width:100%}.data-\[panel-group-direction\=vertical\]\:after\:translate-x-0[data-panel-group-direction=vertical]:after{content:var(--tw-content);--tw-translate-x:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[panel-group-direction\=vertical\]\:after\:-translate-y-1\/2[data-panel-group-direction=vertical]:after{content:var(--tw-content);--tw-translate-y:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[placeholder\]\:text-muted-foreground[data-placeholder]{color:var(--muted-foreground)}.data-\[range-end\=true\]\:rounded-md[data-range-end=true]{border-radius:calc(var(--radius) - 2px)}.data-\[range-end\=true\]\:rounded-r-md[data-range-end=true]{border-top-right-radius:calc(var(--radius) - 2px);border-bottom-right-radius:calc(var(--radius) - 2px)}.data-\[range-end\=true\]\:bg-primary[data-range-end=true]{background-color:var(--primary)}.data-\[range-end\=true\]\:text-primary-foreground[data-range-end=true]{color:var(--primary-foreground)}.data-\[range-middle\=true\]\:rounded-none[data-range-middle=true]{border-radius:0}.data-\[range-middle\=true\]\:bg-accent[data-range-middle=true]{background-color:var(--accent)}.data-\[range-middle\=true\]\:text-accent-foreground[data-range-middle=true]{color:var(--accent-foreground)}.data-\[range-start\=true\]\:rounded-md[data-range-start=true]{border-radius:calc(var(--radius) - 2px)}.data-\[range-start\=true\]\:rounded-l-md[data-range-start=true]{border-top-left-radius:calc(var(--radius) - 2px);border-bottom-left-radius:calc(var(--radius) - 2px)}.data-\[range-start\=true\]\:bg-primary[data-range-start=true]{background-color:var(--primary)}.data-\[range-start\=true\]\:text-primary-foreground[data-range-start=true]{color:var(--primary-foreground)}.data-\[selected-single\=true\]\:bg-primary[data-selected-single=true]{background-color:var(--primary)}.data-\[selected-single\=true\]\:text-primary-foreground[data-selected-single=true]{color:var(--primary-foreground)}.data-\[selected\=true\]\:rounded-none[data-selected=true]{border-radius:0}.data-\[selected\=true\]\:bg-accent[data-selected=true]{background-color:var(--accent)}.data-\[selected\=true\]\:text-accent-foreground[data-selected=true]{color:var(--accent-foreground)}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y:calc(2*var(--spacing)*-1)}.data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x:calc(2*var(--spacing))}.data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x:calc(2*var(--spacing)*-1)}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y:calc(2*var(--spacing))}.data-\[size\=default\]\:h-9[data-size=default]{height:calc(var(--spacing)*9)}.data-\[size\=sm\]\:h-8[data-size=sm]{height:calc(var(--spacing)*8)}:is(.\*\:data-\[slot\=alert-description\]\:text-destructive\/90>*)[data-slot=alert-description]{color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){:is(.\*\:data-\[slot\=alert-description\]\:text-destructive\/90>*)[data-slot=alert-description]{color:color-mix(in oklab,var(--destructive)90%,transparent)}}.data-\[slot\=checkbox-group\]\:gap-3[data-slot=checkbox-group]{gap:calc(var(--spacing)*3)}:is(.\*\*\:data-\[slot\=command-input-wrapper\]\:h-12 *)[data-slot=command-input-wrapper]{height:calc(var(--spacing)*12)}:is(.\*\*\:data-\[slot\=navigation-menu-link\]\:focus\:ring-0 *)[data-slot=navigation-menu-link]:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}:is(.\*\*\:data-\[slot\=navigation-menu-link\]\:focus\:outline-none *)[data-slot=navigation-menu-link]:focus{--tw-outline-style:none;outline-style:none}:is(.\*\:data-\[slot\=select-value\]\:line-clamp-1>*)[data-slot=select-value]{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}:is(.\*\:data-\[slot\=select-value\]\:flex>*)[data-slot=select-value]{display:flex}:is(.\*\:data-\[slot\=select-value\]\:items-center>*)[data-slot=select-value]{align-items:center}:is(.\*\:data-\[slot\=select-value\]\:gap-2>*)[data-slot=select-value]{gap:calc(var(--spacing)*2)}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:var(--background)}.data-\[state\=active\]\:shadow-sm[data-state=active]{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=checked\]\:translate-x-\[calc\(100\%-2px\)\][data-state=checked]{--tw-translate-x:calc(100% - 2px);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[state\=checked\]\:border-primary[data-state=checked]{border-color:var(--primary)}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:var(--primary)}.data-\[state\=checked\]\:text-primary-foreground[data-state=checked]{color:var(--primary-foreground)}.data-\[state\=closed\]\:animate-accordion-up[data-state=closed]{animation:accordion-up var(--tw-animation-duration,var(--tw-duration,.2s))ease-out}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,forwards)}.data-\[state\=closed\]\:duration-300[data-state=closed]{--tw-duration:.3s;transition-duration:.3s}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity:0}.data-\[state\=closed\]\:fade-out-80[data-state=closed]{--tw-exit-opacity:.8}.data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale:.95}.data-\[state\=closed\]\:slide-out-to-bottom[data-state=closed]{--tw-exit-translate-y:100%}.data-\[state\=closed\]\:slide-out-to-left[data-state=closed]{--tw-exit-translate-x:-100%}.data-\[state\=closed\]\:slide-out-to-right[data-state=closed]{--tw-exit-translate-x:100%}.data-\[state\=closed\]\:slide-out-to-right-full[data-state=closed]{--tw-exit-translate-x:calc(1*100%)}.data-\[state\=closed\]\:slide-out-to-top[data-state=closed]{--tw-exit-translate-y:-100%}.group-data-\[viewport\=false\]\/navigation-menu\:data-\[state\=closed\]\:animate-out:is(:where(.group\/navigation-menu)[data-viewport=false] *)[data-state=closed]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,forwards)}.group-data-\[viewport\=false\]\/navigation-menu\:data-\[state\=closed\]\:fade-out-0:is(:where(.group\/navigation-menu)[data-viewport=false] *)[data-state=closed]{--tw-exit-opacity:0}.group-data-\[viewport\=false\]\/navigation-menu\:data-\[state\=closed\]\:zoom-out-95:is(:where(.group\/navigation-menu)[data-viewport=false] *)[data-state=closed]{--tw-exit-scale:.95}.data-\[state\=hidden\]\:animate-out[data-state=hidden]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,forwards)}.data-\[state\=hidden\]\:fade-out[data-state=hidden]{--tw-exit-opacity:0}.data-\[state\=on\]\:bg-accent[data-state=on]{background-color:var(--accent)}.data-\[state\=on\]\:text-accent-foreground[data-state=on]{color:var(--accent-foreground)}.data-\[state\=open\]\:animate-accordion-down[data-state=open]{animation:accordion-down var(--tw-animation-duration,var(--tw-duration,.2s))ease-out}.data-\[state\=open\]\:animate-in[data-state=open]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,forwards)}.data-\[state\=open\]\:bg-accent[data-state=open],.data-\[state\=open\]\:bg-accent\/50[data-state=open]{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.data-\[state\=open\]\:bg-accent\/50[data-state=open]{background-color:color-mix(in oklab,var(--accent)50%,transparent)}}.data-\[state\=open\]\:bg-secondary[data-state=open]{background-color:var(--secondary)}.data-\[state\=open\]\:text-accent-foreground[data-state=open]{color:var(--accent-foreground)}.data-\[state\=open\]\:text-muted-foreground[data-state=open]{color:var(--muted-foreground)}.data-\[state\=open\]\:opacity-100[data-state=open]{opacity:1}.data-\[state\=open\]\:duration-500[data-state=open]{--tw-duration:.5s;transition-duration:.5s}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity:0}.data-\[state\=open\]\:zoom-in-90[data-state=open]{--tw-enter-scale:.9}.data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale:.95}.data-\[state\=open\]\:slide-in-from-bottom[data-state=open]{--tw-enter-translate-y:100%}.data-\[state\=open\]\:slide-in-from-left[data-state=open]{--tw-enter-translate-x:-100%}.data-\[state\=open\]\:slide-in-from-right[data-state=open]{--tw-enter-translate-x:100%}.data-\[state\=open\]\:slide-in-from-top[data-state=open]{--tw-enter-translate-y:-100%}.data-\[state\=open\]\:slide-in-from-top-full[data-state=open]{--tw-enter-translate-y:calc(1*-100%)}.group-data-\[viewport\=false\]\/navigation-menu\:data-\[state\=open\]\:animate-in:is(:where(.group\/navigation-menu)[data-viewport=false] *)[data-state=open]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,forwards)}.group-data-\[viewport\=false\]\/navigation-menu\:data-\[state\=open\]\:fade-in-0:is(:where(.group\/navigation-menu)[data-viewport=false] *)[data-state=open]{--tw-enter-opacity:0}.group-data-\[viewport\=false\]\/navigation-menu\:data-\[state\=open\]\:zoom-in-95:is(:where(.group\/navigation-menu)[data-viewport=false] *)[data-state=open]{--tw-enter-scale:.95}@media (hover:hover){.data-\[state\=open\]\:hover\:bg-accent[data-state=open]:hover{background-color:var(--accent)}.data-\[state\=open\]\:hover\:bg-sidebar-accent[data-state=open]:hover{background-color:var(--sidebar-accent)}.data-\[state\=open\]\:hover\:text-sidebar-accent-foreground[data-state=open]:hover{color:var(--sidebar-accent-foreground)}}.data-\[state\=open\]\:focus\:bg-accent[data-state=open]:focus{background-color:var(--accent)}.data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:var(--muted)}.data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[state\=unchecked\]\:bg-input[data-state=unchecked]{background-color:var(--input)}.data-\[state\=visible\]\:animate-in[data-state=visible]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,forwards)}.data-\[state\=visible\]\:fade-in[data-state=visible]{--tw-enter-opacity:0}.data-\[swipe\=cancel\]\:translate-x-0[data-swipe=cancel]{--tw-translate-x:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[swipe\=end\]\:translate-x-\[var\(--radix-toast-swipe-end-x\)\][data-swipe=end]{--tw-translate-x:var(--radix-toast-swipe-end-x);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[swipe\=end\]\:animate-out[data-swipe=end]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,forwards)}.data-\[swipe\=move\]\:translate-x-\[var\(--radix-toast-swipe-move-x\)\][data-swipe=move]{--tw-translate-x:var(--radix-toast-swipe-move-x);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[swipe\=move\]\:transition-none[data-swipe=move]{transition-property:none}.data-\[variant\=destructive\]\:text-destructive[data-variant=destructive]{color:var(--destructive)}.data-\[variant\=destructive\]\:focus\:bg-destructive\/10[data-variant=destructive]:focus{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.data-\[variant\=destructive\]\:focus\:bg-destructive\/10[data-variant=destructive]:focus{background-color:color-mix(in oklab,var(--destructive)10%,transparent)}}.data-\[variant\=destructive\]\:focus\:text-destructive[data-variant=destructive]:focus{color:var(--destructive)}.data-\[variant\=label\]\:text-sm[data-variant=label]{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.data-\[variant\=legend\]\:text-base[data-variant=legend]{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.data-\[variant\=outline\]\:border-l-0[data-variant=outline]{border-left-style:var(--tw-border-style);border-left-width:0}.data-\[variant\=outline\]\:shadow-xs[data-variant=outline]{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[variant\=outline\]\:first\:border-l[data-variant=outline]:first-child{border-left-style:var(--tw-border-style);border-left-width:1px}.data-\[vaul-drawer-direction\=bottom\]\:inset-x-0[data-vaul-drawer-direction=bottom]{inset-inline:calc(var(--spacing)*0)}.data-\[vaul-drawer-direction\=bottom\]\:bottom-0[data-vaul-drawer-direction=bottom]{bottom:calc(var(--spacing)*0)}.data-\[vaul-drawer-direction\=bottom\]\:mt-24[data-vaul-drawer-direction=bottom]{margin-top:calc(var(--spacing)*24)}.data-\[vaul-drawer-direction\=bottom\]\:max-h-\[80vh\][data-vaul-drawer-direction=bottom]{max-height:80vh}.data-\[vaul-drawer-direction\=bottom\]\:rounded-t-lg[data-vaul-drawer-direction=bottom]{border-top-left-radius:var(--radius);border-top-right-radius:var(--radius)}.data-\[vaul-drawer-direction\=bottom\]\:border-t[data-vaul-drawer-direction=bottom]{border-top-style:var(--tw-border-style);border-top-width:1px}.data-\[vaul-drawer-direction\=left\]\:inset-y-0[data-vaul-drawer-direction=left]{inset-block:calc(var(--spacing)*0)}.data-\[vaul-drawer-direction\=left\]\:left-0[data-vaul-drawer-direction=left]{left:calc(var(--spacing)*0)}.data-\[vaul-drawer-direction\=left\]\:w-3\/4[data-vaul-drawer-direction=left]{width:75%}.data-\[vaul-drawer-direction\=left\]\:border-r[data-vaul-drawer-direction=left]{border-right-style:var(--tw-border-style);border-right-width:1px}.data-\[vaul-drawer-direction\=right\]\:inset-y-0[data-vaul-drawer-direction=right]{inset-block:calc(var(--spacing)*0)}.data-\[vaul-drawer-direction\=right\]\:right-0[data-vaul-drawer-direction=right]{right:calc(var(--spacing)*0)}.data-\[vaul-drawer-direction\=right\]\:w-3\/4[data-vaul-drawer-direction=right]{width:75%}.data-\[vaul-drawer-direction\=right\]\:border-l[data-vaul-drawer-direction=right]{border-left-style:var(--tw-border-style);border-left-width:1px}.data-\[vaul-drawer-direction\=top\]\:inset-x-0[data-vaul-drawer-direction=top]{inset-inline:calc(var(--spacing)*0)}.data-\[vaul-drawer-direction\=top\]\:top-0[data-vaul-drawer-direction=top]{top:calc(var(--spacing)*0)}.data-\[vaul-drawer-direction\=top\]\:mb-24[data-vaul-drawer-direction=top]{margin-bottom:calc(var(--spacing)*24)}.data-\[vaul-drawer-direction\=top\]\:max-h-\[80vh\][data-vaul-drawer-direction=top]{max-height:80vh}.data-\[vaul-drawer-direction\=top\]\:rounded-b-lg[data-vaul-drawer-direction=top]{border-bottom-right-radius:var(--radius);border-bottom-left-radius:var(--radius)}.data-\[vaul-drawer-direction\=top\]\:border-b[data-vaul-drawer-direction=top]{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.nth-last-2\:-mt-1:nth-last-child(2){margin-top:calc(var(--spacing)*-1)}@media (min-width:40rem){.sm\:top-auto{top:auto}.sm\:right-0{right:calc(var(--spacing)*0)}.sm\:bottom-0{bottom:calc(var(--spacing)*0)}.sm\:my-6{margin-block:calc(var(--spacing)*6)}.sm\:mt-8{margin-top:calc(var(--spacing)*8)}.sm\:mr-3{margin-right:calc(var(--spacing)*3)}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:h-5{height:calc(var(--spacing)*5)}.sm\:h-12{height:calc(var(--spacing)*12)}.sm\:h-24{height:calc(var(--spacing)*24)}.sm\:h-52{height:calc(var(--spacing)*52)}.sm\:w-5{width:calc(var(--spacing)*5)}.sm\:w-20{width:calc(var(--spacing)*20)}.sm\:w-40{width:calc(var(--spacing)*40)}.sm\:max-w-lg{max-width:var(--container-lg)}.sm\:max-w-md{max-width:var(--container-md)}.sm\:max-w-sm{max-width:var(--container-sm)}.sm\:flex-col{flex-direction:column}.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}.sm\:gap-2\.5{gap:calc(var(--spacing)*2.5)}:where(.sm\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}.sm\:p-6{padding:calc(var(--spacing)*6)}.sm\:pr-2\.5{padding-right:calc(var(--spacing)*2.5)}.sm\:pl-2\.5{padding-left:calc(var(--spacing)*2.5)}.sm\:text-left{text-align:left}.sm\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.sm\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.sm\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.data-\[state\=open\]\:sm\:slide-in-from-bottom-full[data-state=open]{--tw-enter-translate-y:calc(1*100%)}.data-\[vaul-drawer-direction\=left\]\:sm\:max-w-sm[data-vaul-drawer-direction=left],.data-\[vaul-drawer-direction\=right\]\:sm\:max-w-sm[data-vaul-drawer-direction=right]{max-width:var(--container-sm)}}@media (min-width:48rem){.md\:absolute{position:absolute}.md\:my-8{margin-block:calc(var(--spacing)*8)}.md\:block{display:block}.md\:flex{display:flex}.md\:h-56{height:calc(var(--spacing)*56)}.md\:w-44{width:calc(var(--spacing)*44)}.md\:w-\[var\(--radix-navigation-menu-viewport-width\)\]{width:var(--radix-navigation-menu-viewport-width)}.md\:w-auto{width:auto}.md\:max-w-\[420px\]{max-width:420px}.md\:flex-row{flex-direction:row}.md\:gap-1\.5{gap:calc(var(--spacing)*1.5)}.md\:p-12{padding:calc(var(--spacing)*12)}.md\:text-left{text-align:left}.md\:text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.md\:opacity-0{opacity:0}.md\:peer-data-\[variant\=inset\]\:m-2:is(:where(.peer)[data-variant=inset]~*){margin:calc(var(--spacing)*2)}.md\:peer-data-\[variant\=inset\]\:ml-0:is(:where(.peer)[data-variant=inset]~*){margin-left:calc(var(--spacing)*0)}.md\:peer-data-\[variant\=inset\]\:rounded-xl:is(:where(.peer)[data-variant=inset]~*){border-radius:calc(var(--radius) + 4px)}.md\:peer-data-\[variant\=inset\]\:shadow-sm:is(:where(.peer)[data-variant=inset]~*){--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.md\:peer-data-\[variant\=inset\]\:peer-data-\[state\=collapsed\]\:ml-2:is(:where(.peer)[data-variant=inset]~*):is(:where(.peer)[data-state=collapsed]~*){margin-left:calc(var(--spacing)*2)}.md\:after\:hidden:after{content:var(--tw-content);display:none}}@media (min-width:64rem){.lg\:block{display:block}.lg\:p-8{padding:calc(var(--spacing)*8)}}@container field-group (min-width:28rem){.\@md\/field-group\:flex-row{flex-direction:row}.\@md\/field-group\:items-center{align-items:center}.\@md\/field-group\:has-\[\>\[data-slot\=field-content\]\]\:items-start:has(>[data-slot=field-content]){align-items:flex-start}}.dark\:border-input:is(.dark *){border-color:var(--input)}.dark\:bg-destructive\/60:is(.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:bg-destructive\/60:is(.dark *){background-color:color-mix(in oklab,var(--destructive)60%,transparent)}}.dark\:bg-input\/30:is(.dark *){background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.dark\:bg-input\/30:is(.dark *){background-color:color-mix(in oklab,var(--input)30%,transparent)}}.dark\:bg-transparent:is(.dark *){background-color:#0000}.dark\:text-muted-foreground:is(.dark *){color:var(--muted-foreground)}@media (hover:hover){.dark\:hover\:bg-accent\/50:is(.dark *):hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-accent\/50:is(.dark *):hover{background-color:color-mix(in oklab,var(--accent)50%,transparent)}}.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:color-mix(in oklab,var(--input)50%,transparent)}}.dark\:hover\:text-accent-foreground:is(.dark *):hover{color:var(--accent-foreground)}}.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:color-mix(in oklab,var(--destructive)40%,transparent)}}.dark\:has-data-\[state\=checked\]\:bg-primary\/10:is(.dark *):has([data-state=checked]){background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.dark\:has-data-\[state\=checked\]\:bg-primary\/10:is(.dark *):has([data-state=checked]){background-color:color-mix(in oklab,var(--primary)10%,transparent)}}.dark\:has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/40:is(.dark *):has([data-slot][aria-invalid=true]){--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:has-\[\[data-slot\]\[aria-invalid\=true\]\]\:ring-destructive\/40:is(.dark *):has([data-slot][aria-invalid=true]){--tw-ring-color:color-mix(in oklab,var(--destructive)40%,transparent)}}.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:color-mix(in oklab,var(--destructive)40%,transparent)}}.dark\:data-\[active\=true\]\:aria-invalid\:ring-destructive\/40:is(.dark *)[data-active=true][aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:data-\[active\=true\]\:aria-invalid\:ring-destructive\/40:is(.dark *)[data-active=true][aria-invalid=true]{--tw-ring-color:color-mix(in oklab,var(--destructive)40%,transparent)}}.dark\:data-\[state\=active\]\:border-input:is(.dark *)[data-state=active]{border-color:var(--input)}.dark\:data-\[state\=active\]\:bg-input\/30:is(.dark *)[data-state=active]{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.dark\:data-\[state\=active\]\:bg-input\/30:is(.dark *)[data-state=active]{background-color:color-mix(in oklab,var(--input)30%,transparent)}}.dark\:data-\[state\=active\]\:text-foreground:is(.dark *)[data-state=active]{color:var(--foreground)}.dark\:data-\[state\=checked\]\:bg-primary:is(.dark *)[data-state=checked]{background-color:var(--primary)}.dark\:data-\[state\=checked\]\:bg-primary-foreground:is(.dark *)[data-state=checked]{background-color:var(--primary-foreground)}.dark\:data-\[state\=unchecked\]\:bg-foreground:is(.dark *)[data-state=unchecked]{background-color:var(--foreground)}.dark\:data-\[state\=unchecked\]\:bg-input\/80:is(.dark *)[data-state=unchecked]{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.dark\:data-\[state\=unchecked\]\:bg-input\/80:is(.dark *)[data-state=unchecked]{background-color:color-mix(in oklab,var(--input)80%,transparent)}}.dark\:data-\[variant\=destructive\]\:focus\:bg-destructive\/20:is(.dark *)[data-variant=destructive]:focus{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:data-\[variant\=destructive\]\:focus\:bg-destructive\/20:is(.dark *)[data-variant=destructive]:focus{background-color:color-mix(in oklab,var(--destructive)20%,transparent)}}.\[\&_\.recharts-cartesian-axis-tick_text\]\:fill-muted-foreground .recharts-cartesian-axis-tick text{fill:var(--muted-foreground)}.\[\&_\.recharts-cartesian-grid_line\[stroke\=\'\#ccc\'\]\]\:stroke-border\/50 .recharts-cartesian-grid line[stroke=\#ccc]{stroke:var(--border)}@supports (color:color-mix(in lab, red, red)){.\[\&_\.recharts-cartesian-grid_line\[stroke\=\'\#ccc\'\]\]\:stroke-border\/50 .recharts-cartesian-grid line[stroke=\#ccc]{stroke:color-mix(in oklab,var(--border)50%,transparent)}}.\[\&_\.recharts-curve\.recharts-tooltip-cursor\]\:stroke-border .recharts-curve.recharts-tooltip-cursor{stroke:var(--border)}.\[\&_\.recharts-dot\[stroke\=\'\#fff\'\]\]\:stroke-transparent .recharts-dot[stroke=\#fff]{stroke:#0000}.\[\&_\.recharts-layer\]\:outline-hidden .recharts-layer{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.\[\&_\.recharts-layer\]\:outline-hidden .recharts-layer{outline-offset:2px;outline:2px solid #0000}}.\[\&_\.recharts-polar-grid_\[stroke\=\'\#ccc\'\]\]\:stroke-border .recharts-polar-grid [stroke=\#ccc]{stroke:var(--border)}.\[\&_\.recharts-radial-bar-background-sector\]\:fill-muted .recharts-radial-bar-background-sector,.\[\&_\.recharts-rectangle\.recharts-tooltip-cursor\]\:fill-muted .recharts-rectangle.recharts-tooltip-cursor{fill:var(--muted)}.\[\&_\.recharts-reference-line_\[stroke\=\'\#ccc\'\]\]\:stroke-border .recharts-reference-line [stroke=\#ccc]{stroke:var(--border)}.\[\&_\.recharts-sector\]\:outline-hidden .recharts-sector{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.\[\&_\.recharts-sector\]\:outline-hidden .recharts-sector{outline-offset:2px;outline:2px solid #0000}}.\[\&_\.recharts-sector\[stroke\=\'\#fff\'\]\]\:stroke-transparent .recharts-sector[stroke=\#fff]{stroke:#0000}.\[\&_\.recharts-surface\]\:outline-hidden .recharts-surface{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.\[\&_\.recharts-surface\]\:outline-hidden .recharts-surface{outline-offset:2px;outline:2px solid #0000}}.\[\&_\[cmdk-group-heading\]\]\:px-2 [cmdk-group-heading]{padding-inline:calc(var(--spacing)*2)}.\[\&_\[cmdk-group-heading\]\]\:py-1\.5 [cmdk-group-heading]{padding-block:calc(var(--spacing)*1.5)}.\[\&_\[cmdk-group-heading\]\]\:text-xs [cmdk-group-heading]{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.\[\&_\[cmdk-group-heading\]\]\:font-medium [cmdk-group-heading]{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.\[\&_\[cmdk-group-heading\]\]\:text-muted-foreground [cmdk-group-heading]{color:var(--muted-foreground)}.\[\&_\[cmdk-group\]\]\:px-2 [cmdk-group]{padding-inline:calc(var(--spacing)*2)}.\[\&_\[cmdk-group\]\:not\(\[hidden\]\)_\~\[cmdk-group\]\]\:pt-0 [cmdk-group]:not([hidden])~[cmdk-group]{padding-top:calc(var(--spacing)*0)}.\[\&_\[cmdk-input-wrapper\]_svg\]\:h-5 [cmdk-input-wrapper] svg{height:calc(var(--spacing)*5)}.\[\&_\[cmdk-input-wrapper\]_svg\]\:w-5 [cmdk-input-wrapper] svg{width:calc(var(--spacing)*5)}.\[\&_\[cmdk-input\]\]\:h-12 [cmdk-input]{height:calc(var(--spacing)*12)}.\[\&_\[cmdk-item\]\]\:px-2 [cmdk-item]{padding-inline:calc(var(--spacing)*2)}.\[\&_\[cmdk-item\]\]\:py-3 [cmdk-item]{padding-block:calc(var(--spacing)*3)}.\[\&_\[cmdk-item\]_svg\]\:h-5 [cmdk-item] svg{height:calc(var(--spacing)*5)}.\[\&_\[cmdk-item\]_svg\]\:w-5 [cmdk-item] svg{width:calc(var(--spacing)*5)}.\[\&_img\]\:size-full img{width:100%;height:100%}.\[\&_img\]\:object-cover img{object-fit:cover}.\[\&_p\]\:leading-relaxed p{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3 svg:not([class*=size-]){width:calc(var(--spacing)*3);height:calc(var(--spacing)*3)}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*=size-]){width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-6 svg:not([class*=size-]){width:calc(var(--spacing)*6);height:calc(var(--spacing)*6)}.\[\&_svg\:not\(\[class\*\=\'text-\'\]\)\]\:text-muted-foreground svg:not([class*=text-]){color:var(--muted-foreground)}.\[\&_tr\]\:border-b tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-style:var(--tw-border-style);border-width:0}.\[\&\+\[data-slot\=item-content\]\]\:flex-none+[data-slot=item-content]{flex:none}.\[\&\:first-child\[data-selected\=true\]_button\]\:rounded-l-md:first-child[data-selected=true] button{border-top-left-radius:calc(var(--radius) - 2px);border-bottom-left-radius:calc(var(--radius) - 2px)}.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:calc(var(--spacing)*0)}.\[\.border-b\]\:pb-3.border-b{padding-bottom:calc(var(--spacing)*3)}.\[\.border-b\]\:pb-6.border-b{padding-bottom:calc(var(--spacing)*6)}.\[\.border-t\]\:pt-3.border-t{padding-top:calc(var(--spacing)*3)}.\[\.border-t\]\:pt-6.border-t{padding-top:calc(var(--spacing)*6)}:is(.rtl\:\*\*\:\[\.rdp-button\\_next\>svg\]\:rotate-180:where(:is(:lang(ae),:lang(ar),:lang(arc),:lang(bcc),:lang(bqi),:lang(ckb),:lang(dv),:lang(fa),:lang(glk),:lang(he),:lang(ku),:lang(mzn),:lang(nqo),:lang(pnb),:lang(ps),:lang(sd),:lang(ug),:lang(ur),:lang(yi)),[dir=rtl],[dir=rtl] *) *):is(.rdp-button_next>svg),:is(.rtl\:\*\*\:\[\.rdp-button\\_previous\>svg\]\:rotate-180:where(:is(:lang(ae),:lang(ar),:lang(arc),:lang(bcc),:lang(bqi),:lang(ckb),:lang(dv),:lang(fa),:lang(glk),:lang(he),:lang(ku),:lang(mzn),:lang(nqo),:lang(pnb),:lang(ps),:lang(sd),:lang(ug),:lang(ur),:lang(yi)),[dir=rtl],[dir=rtl] *) *):is(.rdp-button_previous>svg){rotate:180deg}:is(.\*\:\[span\]\:last\:flex>*):is(span):last-child{display:flex}:is(.\*\:\[span\]\:last\:items-center>*):is(span):last-child{align-items:center}:is(.\*\:\[span\]\:last\:gap-2>*):is(span):last-child{gap:calc(var(--spacing)*2)}:is(.data-\[variant\=destructive\]\:\*\:\[svg\]\:\!text-destructive[data-variant=destructive]>*):is(svg){color:var(--destructive)!important}.\[\&\:last-child\[data-selected\=true\]_button\]\:rounded-r-md:last-child[data-selected=true] button{border-top-right-radius:calc(var(--radius) - 2px);border-bottom-right-radius:calc(var(--radius) - 2px)}.\[\&\>\*\]\:w-full>*{width:100%}.\[\&\>\*\]\:focus-visible\:relative>:focus-visible{position:relative}.\[\&\>\*\]\:focus-visible\:z-10>:focus-visible{z-index:10}.\[\&\>\*\]\:data-\[slot\=field\]\:p-4>[data-slot=field]{padding:calc(var(--spacing)*4)}@container field-group (min-width:28rem){.\@md\/field-group\:\[\&\>\*\]\:w-auto>*{width:auto}}.\[\&\>\*\:not\(\:first-child\)\]\:rounded-t-none>:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.\[\&\>\*\:not\(\:first-child\)\]\:rounded-l-none>:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.\[\&\>\*\:not\(\:first-child\)\]\:border-t-0>:not(:first-child){border-top-style:var(--tw-border-style);border-top-width:0}.\[\&\>\*\:not\(\:first-child\)\]\:border-l-0>:not(:first-child){border-left-style:var(--tw-border-style);border-left-width:0}.\[\&\>\*\:not\(\:last-child\)\]\:rounded-r-none>:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.\[\&\>\*\:not\(\:last-child\)\]\:rounded-b-none>:not(:last-child){border-bottom-right-radius:0;border-bottom-left-radius:0}.\[\&\>\.sr-only\]\:w-auto>.sr-only{width:auto}.\[\&\>\[data-slot\=field-group\]\]\:gap-4>[data-slot=field-group]{gap:calc(var(--spacing)*4)}.\[\&\>\[data-slot\=field-label\]\]\:flex-auto>[data-slot=field-label]{flex:auto}@container field-group (min-width:28rem){.\@md\/field-group\:\[\&\>\[data-slot\=field-label\]\]\:flex-auto>[data-slot=field-label]{flex:auto}}.has-\[select\[aria-hidden\=true\]\:last-child\]\:\[\&\>\[data-slot\=select-trigger\]\:last-of-type\]\:rounded-r-md:has(:is(select[aria-hidden=true]:last-child))>[data-slot=select-trigger]:last-of-type{border-top-right-radius:calc(var(--radius) - 2px);border-bottom-right-radius:calc(var(--radius) - 2px)}.\[\&\>\[data-slot\=select-trigger\]\:not\(\[class\*\=\'w-\'\]\)\]\:w-fit>[data-slot=select-trigger]:not([class*=w-]){width:fit-content}.\[\&\>\[role\=checkbox\]\]\:translate-y-\[2px\]>[role=checkbox]{--tw-translate-y:2px;translate:var(--tw-translate-x)var(--tw-translate-y)}:is(.has-\[\>\[data-slot\=field-content\]\]\:\[\&\>\[role\=checkbox\]\,\[role\=radio\]\]\:mt-px:has(>[data-slot=field-content])>[role=checkbox],.has-\[\>\[data-slot\=field-content\]\]\:\[\&\>\[role\=checkbox\]\,\[role\=radio\]\]\:mt-px:has(>[data-slot=field-content]) [role=radio]){margin-top:1px}@container field-group (min-width:28rem){:is(.\@md\/field-group\:has-\[\>\[data-slot\=field-content\]\]\:\[\&\>\[role\=checkbox\]\,\[role\=radio\]\]\:mt-px:has(>[data-slot=field-content])>[role=checkbox],.\@md\/field-group\:has-\[\>\[data-slot\=field-content\]\]\:\[\&\>\[role\=checkbox\]\,\[role\=radio\]\]\:mt-px:has(>[data-slot=field-content]) [role=radio]){margin-top:1px}}.\[\&\>a\]\:underline>a{text-decoration-line:underline}.\[\&\>a\]\:underline-offset-4>a{text-underline-offset:4px}.\[\&\>a\:hover\]\:text-primary>a:hover{color:var(--primary)}.\[\&\>button\]\:hidden>button{display:none}.\[\&\>input\]\:flex-1>input{flex:1}.has-\[\>\[data-align\=block-end\]\]\:\[\&\>input\]\:pt-3:has(>[data-align=block-end])>input{padding-top:calc(var(--spacing)*3)}.has-\[\>\[data-align\=block-start\]\]\:\[\&\>input\]\:pb-3:has(>[data-align=block-start])>input{padding-bottom:calc(var(--spacing)*3)}.has-\[\>\[data-align\=inline-end\]\]\:\[\&\>input\]\:pr-2:has(>[data-align=inline-end])>input{padding-right:calc(var(--spacing)*2)}.has-\[\>\[data-align\=inline-start\]\]\:\[\&\>input\]\:pl-2:has(>[data-align=inline-start])>input{padding-left:calc(var(--spacing)*2)}.\[\&\>kbd\]\:rounded-\[calc\(var\(--radius\)-5px\)\]>kbd{border-radius:calc(var(--radius) - 5px)}.\[\&\>span\]\:text-xs>span{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.\[\&\>span\]\:opacity-70>span{opacity:.7}.\[\&\>span\:last-child\]\:truncate>span:last-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.\[\&\>svg\]\:pointer-events-none>svg{pointer-events:none}.\[\&\>svg\]\:size-3>svg{width:calc(var(--spacing)*3);height:calc(var(--spacing)*3)}.\[\&\>svg\]\:size-3\.5>svg{width:calc(var(--spacing)*3.5);height:calc(var(--spacing)*3.5)}.\[\&\>svg\]\:size-4>svg{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.\[\&\>svg\]\:h-2\.5>svg{height:calc(var(--spacing)*2.5)}.\[\&\>svg\]\:h-3>svg{height:calc(var(--spacing)*3)}.\[\&\>svg\]\:w-2\.5>svg{width:calc(var(--spacing)*2.5)}.\[\&\>svg\]\:w-3>svg{width:calc(var(--spacing)*3)}.\[\&\>svg\]\:shrink-0>svg{flex-shrink:0}.\[\&\>svg\]\:translate-y-0\.5>svg{--tw-translate-y:calc(var(--spacing)*.5);translate:var(--tw-translate-x)var(--tw-translate-y)}.\[\&\>svg\]\:text-current>svg{color:currentColor}.\[\&\>svg\]\:text-muted-foreground>svg{color:var(--muted-foreground)}.\[\&\>svg\]\:text-sidebar-accent-foreground>svg{color:var(--sidebar-accent-foreground)}.\[\&\>svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3\.5>svg:not([class*=size-]){width:calc(var(--spacing)*3.5);height:calc(var(--spacing)*3.5)}.\[\&\>svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4>svg:not([class*=size-]){width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.\[\&\>tr\]\:last\:border-b-0>tr:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.\[\&\[data-panel-group-direction\=vertical\]\>div\]\:rotate-90[data-panel-group-direction=vertical]>div{rotate:90deg}.\[\&\[data-state\=open\]\>svg\]\:rotate-180[data-state=open]>svg{rotate:180deg}[data-side=left][data-collapsible=offcanvas] .\[\[data-side\=left\]\[data-collapsible\=offcanvas\]_\&\]\:-right-2{right:calc(var(--spacing)*-2)}[data-side=left][data-state=collapsed] .\[\[data-side\=left\]\[data-state\=collapsed\]_\&\]\:cursor-e-resize{cursor:e-resize}[data-side=right][data-collapsible=offcanvas] .\[\[data-side\=right\]\[data-collapsible\=offcanvas\]_\&\]\:-left-2{left:calc(var(--spacing)*-2)}[data-side=right][data-state=collapsed] .\[\[data-side\=right\]\[data-state\=collapsed\]_\&\]\:cursor-w-resize{cursor:w-resize}[data-slot=card-content] .\[\[data-slot\=card-content\]_\&\]\:bg-transparent,[data-slot=popover-content] .\[\[data-slot\=popover-content\]_\&\]\:bg-transparent{background-color:#0000}[data-slot=tooltip-content] .\[\[data-slot\=tooltip-content\]_\&\]\:bg-background\/20{background-color:var(--background)}@supports (color:color-mix(in lab, red, red)){[data-slot=tooltip-content] .\[\[data-slot\=tooltip-content\]_\&\]\:bg-background\/20{background-color:color-mix(in oklab,var(--background)20%,transparent)}}[data-slot=tooltip-content] .\[\[data-slot\=tooltip-content\]_\&\]\:text-background{color:var(--background)}[data-slot=tooltip-content] .dark\:\[\[data-slot\=tooltip-content\]_\&\]\:bg-background\/10:is(.dark *){background-color:var(--background)}@supports (color:color-mix(in lab, red, red)){[data-slot=tooltip-content] .dark\:\[\[data-slot\=tooltip-content\]_\&\]\:bg-background\/10:is(.dark *){background-color:color-mix(in oklab,var(--background)10%,transparent)}}[data-variant=legend]+.\[\[data-variant\=legend\]\+\&\]\:-mt-1\.5{margin-top:calc(var(--spacing)*-1.5)}a.\[a\&\]\:transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){a.\[a\&\]\:hover\:bg-accent:hover,a.\[a\&\]\:hover\:bg-accent\/50:hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){a.\[a\&\]\:hover\:bg-accent\/50:hover{background-color:color-mix(in oklab,var(--accent)50%,transparent)}}a.\[a\&\]\:hover\:bg-destructive\/90:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){a.\[a\&\]\:hover\:bg-destructive\/90:hover{background-color:color-mix(in oklab,var(--destructive)90%,transparent)}}a.\[a\&\]\:hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){a.\[a\&\]\:hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--primary)90%,transparent)}}a.\[a\&\]\:hover\:bg-secondary\/90:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab, red, red)){a.\[a\&\]\:hover\:bg-secondary\/90:hover{background-color:color-mix(in oklab,var(--secondary)90%,transparent)}}a.\[a\&\]\:hover\:text-accent-foreground:hover{color:var(--accent-foreground)}}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:forwards}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}:root{--background:#fff;--foreground:#0a0a0a;--card:#fff;--card-foreground:#0a0a0a;--popover:#fff;--popover-foreground:#0a0a0a;--primary:#171717;--primary-foreground:#fafafa;--secondary:#f5f5f5;--secondary-foreground:#171717;--muted:#f5f5f5;--muted-foreground:#737373;--accent:#f5f5f5;--accent-foreground:#171717;--destructive:#e40014;--destructive-foreground:#e40014;--border:#e5e5e5;--input:#e5e5e5;--ring:#a1a1a1;--chart-1:#f05100;--chart-2:#009588;--chart-3:#104e64;--chart-4:#fcbb00;--chart-5:#f99c00;--radius:.625rem;--sidebar:#fafafa;--sidebar-foreground:#0a0a0a;--sidebar-primary:#171717;--sidebar-primary-foreground:#fafafa;--sidebar-accent:#f5f5f5;--sidebar-accent-foreground:#171717;--sidebar-border:#e5e5e5;--sidebar-ring:#a1a1a1}@supports (color:lab(0% 0 0)){:root{--background:lab(100% 0 0);--foreground:lab(2.75381% 0 0);--card:lab(100% 0 0);--card-foreground:lab(2.75381% 0 0);--popover:lab(100% 0 0);--popover-foreground:lab(2.75381% 0 0);--primary:lab(7.78201% -.0000149012 0);--primary-foreground:lab(98.26% 0 0);--secondary:lab(96.52% -.0000298023 .0000119209);--secondary-foreground:lab(7.78201% -.0000149012 0);--muted:lab(96.52% -.0000298023 .0000119209);--muted-foreground:lab(48.496% 0 0);--accent:lab(96.52% -.0000298023 .0000119209);--accent-foreground:lab(7.78201% -.0000149012 0);--destructive:lab(48.4493% 77.4328 61.5452);--destructive-foreground:lab(48.4493% 77.4328 61.5452);--border:lab(90.952% 0 -.0000119209);--input:lab(90.952% 0 -.0000119209);--ring:lab(66.128% -.0000298023 .0000119209);--chart-1:lab(57.1026% 64.2584 89.8886);--chart-2:lab(55.0223% -41.0774 -3.90277);--chart-3:lab(30.372% -13.1853 -18.7887);--chart-4:lab(80.1641% 16.6016 99.2089);--chart-5:lab(72.7183% 31.8672 97.9407);--sidebar:lab(98.26% 0 0);--sidebar-foreground:lab(2.75381% 0 0);--sidebar-primary:lab(7.78201% -.0000149012 0);--sidebar-primary-foreground:lab(98.26% 0 0);--sidebar-accent:lab(96.52% -.0000298023 .0000119209);--sidebar-accent-foreground:lab(7.78201% -.0000149012 0);--sidebar-border:lab(90.952% 0 -.0000119209);--sidebar-ring:lab(66.128% -.0000298023 .0000119209)}}.dark{--background:#0a0a0a;--foreground:#fafafa;--card:#0a0a0a;--card-foreground:#fafafa;--popover:#0a0a0a;--popover-foreground:#fafafa;--primary:#fafafa;--primary-foreground:#171717;--secondary:#262626;--secondary-foreground:#fafafa;--muted:#262626;--muted-foreground:#a1a1a1;--accent:#262626;--accent-foreground:#fafafa;--destructive:#82181a;--destructive-foreground:#fb2c36;--border:#262626;--input:#262626;--ring:#525252;--chart-1:#1447e6;--chart-2:#00bb7f;--chart-3:#f99c00;--chart-4:#ac4bff;--chart-5:#ff2357;--sidebar:#171717;--sidebar-foreground:#fafafa;--sidebar-primary:#1447e6;--sidebar-primary-foreground:#fafafa;--sidebar-accent:#262626;--sidebar-accent-foreground:#fafafa;--sidebar-border:#262626;--sidebar-ring:#525252}@supports (color:lab(0% 0 0)){.dark{--background:lab(2.75381% 0 0);--foreground:lab(98.26% 0 0);--card:lab(2.75381% 0 0);--card-foreground:lab(98.26% 0 0);--popover:lab(2.75381% 0 0);--popover-foreground:lab(98.26% 0 0);--primary:lab(98.26% 0 0);--primary-foreground:lab(7.78201% -.0000149012 0);--secondary:lab(15.204% 0 -.00000596046);--secondary-foreground:lab(98.26% 0 0);--muted:lab(15.204% 0 -.00000596046);--muted-foreground:lab(66.128% -.0000298023 .0000119209);--accent:lab(15.204% 0 -.00000596046);--accent-foreground:lab(98.26% 0 0);--destructive:lab(28.5139% 44.5539 29.0463);--destructive-foreground:lab(55.4814% 75.0732 48.8528);--border:lab(15.204% 0 -.00000596046);--input:lab(15.204% 0 -.00000596046);--ring:lab(34.924% 0 0);--chart-1:lab(36.9089% 35.0961 -85.6872);--chart-2:lab(66.9756% -58.27 19.5419);--chart-3:lab(72.7183% 31.8672 97.9407);--chart-4:lab(52.0183% 66.11 -78.2316);--chart-5:lab(56.101% 79.4328 31.4532);--sidebar:lab(7.78201% -.0000149012 0);--sidebar-foreground:lab(98.26% 0 0);--sidebar-primary:lab(36.9089% 35.0961 -85.6872);--sidebar-primary-foreground:lab(98.26% 0 0);--sidebar-accent:lab(15.204% 0 -.00000596046);--sidebar-accent-foreground:lab(98.26% 0 0);--sidebar-border:lab(15.204% 0 -.00000596046);--sidebar-ring:lab(34.924% 0 0)}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity,1);transform:translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0)scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1))rotate(var(--tw-exit-rotate,0))}}@keyframes accordion-down{0%{height:0}to{height:var(--radix-accordion-content-height,var(--bits-accordion-content-height,var(--reka-accordion-content-height,var(--kb-accordion-content-height,auto))))}}@keyframes accordion-up{0%{height:var(--radix-accordion-content-height,var(--bits-accordion-content-height,var(--reka-accordion-content-height,var(--kb-accordion-content-height,auto))))}to{height:0}}@keyframes caret-blink{0%,70%,to{opacity:1}20%,50%{opacity:0}} diff --git a/static/frontend/login/_next/static/chunks/1f600c83a5ecf168.css b/static/frontend/login/_next/static/chunks/1f600c83a5ecf168.css deleted file mode 100644 index 016ac3d2edcdc3a21822f3f4fbc41a4dc796d92a..0000000000000000000000000000000000000000 --- a/static/frontend/login/_next/static/chunks/1f600c83a5ecf168.css +++ /dev/null @@ -1,3 +0,0 @@ -@font-face{font-family:Geist;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/8a480f0b521d4e75-s.8e0177b5.woff2)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/7178b3e590c64307-s.b97b3418.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Geist;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/caa3a2e1cccd8315-s.p.853070df.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Geist Fallback;src:local(Arial);ascent-override:95.94%;descent-override:28.16%;line-gap-override:0.0%;size-adjust:104.76%}.geist_a7695b8e-module__Entzca__className{font-family:Geist,Geist Fallback;font-style:normal} -@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/4fa387ec64143e14-s.c1fdd6c2.woff2)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/bbc41e54d2fcbd21-s.799d8ef8.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/797e433ab948586e-s.p.dbea232f.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Geist Mono Fallback;src:local(Arial);ascent-override:74.67%;descent-override:21.92%;line-gap-override:0.0%;size-adjust:134.59%}.geist_mono_354fc78-module__zrY5Sa__className{font-family:Geist Mono,Geist Mono Fallback;font-style:normal} -@font-face{font-family:Playfair Display;font-style:normal;font-weight:400 900;font-display:swap;src:url(../media/65c558afe41e89d6-s.e2c8389a.woff2)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Playfair Display;font-style:normal;font-weight:400 900;font-display:swap;src:url(../media/14e23f9b59180572-s.9c448f3c.woff2)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Playfair Display;font-style:normal;font-weight:400 900;font-display:swap;src:url(../media/b49b0d9b851e4899-s.4f3fa681.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Playfair Display;font-style:normal;font-weight:400 900;font-display:swap;src:url(../media/2a65768255d6b625-s.p.d19752fb.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Playfair Display Fallback;src:local(Times New Roman);ascent-override:97.25%;descent-override:22.56%;line-gap-override:0.0%;size-adjust:111.26%}.playfair_display_4f85f8b1-module__d60DSa__className{font-family:Playfair Display,Playfair Display Fallback;font-style:normal} diff --git a/static/frontend/login/_next/static/chunks/42879de7b8087bc9.js b/static/frontend/login/_next/static/chunks/42879de7b8087bc9.js deleted file mode 100644 index bda8a8118961b842879d43d67e7cda0ccca982cc..0000000000000000000000000000000000000000 --- a/static/frontend/login/_next/static/chunks/42879de7b8087bc9.js +++ /dev/null @@ -1 +0,0 @@ -(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,33525,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"warnOnce",{enumerable:!0,get:function(){return n}});let n=e=>{}},91915,(e,r,t)=>{"use strict";function n(e,r={}){if(r.onlyHashChange)return void e();let t=document.documentElement;if("smooth"!==t.dataset.scrollBehavior)return void e();let a=t.style.scrollBehavior;t.style.scrollBehavior="auto",r.dontForceLayout||t.getClientRects(),e(),t.style.scrollBehavior=a}Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"disableSmoothScrollDuringRouteTransition",{enumerable:!0,get:function(){return n}}),e.r(33525)},68017,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"HTTPAccessFallbackBoundary",{enumerable:!0,get:function(){return i}});let n=e.r(90809),a=e.r(43476),o=n._(e.r(71645)),c=e.r(90373),u=e.r(54394);e.r(33525);let l=e.r(8372);class s extends o.default.Component{constructor(e){super(e),this.state={triggeredStatus:void 0,previousPathname:e.pathname}}componentDidCatch(){}static getDerivedStateFromError(e){if((0,u.isHTTPAccessFallbackError)(e))return{triggeredStatus:(0,u.getAccessFallbackHTTPStatus)(e)};throw e}static getDerivedStateFromProps(e,r){return e.pathname!==r.previousPathname&&r.triggeredStatus?{triggeredStatus:void 0,previousPathname:e.pathname}:{triggeredStatus:r.triggeredStatus,previousPathname:e.pathname}}render(){let{notFound:e,forbidden:r,unauthorized:t,children:n}=this.props,{triggeredStatus:o}=this.state,c={[u.HTTPAccessErrorStatus.NOT_FOUND]:e,[u.HTTPAccessErrorStatus.FORBIDDEN]:r,[u.HTTPAccessErrorStatus.UNAUTHORIZED]:t};if(o){let l=o===u.HTTPAccessErrorStatus.NOT_FOUND&&e,s=o===u.HTTPAccessErrorStatus.FORBIDDEN&&r,i=o===u.HTTPAccessErrorStatus.UNAUTHORIZED&&t;return l||s||i?(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)("meta",{name:"robots",content:"noindex"}),!1,c[o]]}):n}return n}}function i({notFound:e,forbidden:r,unauthorized:t,children:n}){let u=(0,c.useUntrackedPathname)(),i=(0,o.useContext)(l.MissingSlotContext);return e||r||t?(0,a.jsx)(s,{pathname:u,notFound:e,forbidden:r,unauthorized:t,missingSlots:i,children:n}):(0,a.jsx)(a.Fragment,{children:n})}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),r.exports=t.default)},91798,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"useRouterBFCache",{enumerable:!0,get:function(){return a}});let n=e.r(71645);function a(e,r){let[t,a]=(0,n.useState)(()=>({tree:e,stateKey:r,next:null}));if(t.tree===e)return t;let o={tree:e,stateKey:r,next:null},c=1,u=t,l=o;for(;null!==u&&c<1;){if(u.stateKey===r){l.next=u.next;break}{c++;let e={tree:u.tree,stateKey:u.stateKey,next:null};l.next=e,l=e}u=u.next}return a(o),o}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),r.exports=t.default)},39756,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return T}});let n=e.r(55682),a=e.r(90809),o=e.r(43476),c=e.r(88540),u=a._(e.r(71645)),l=n._(e.r(74080)),s=e.r(8372),i=e.r(87288),d=e.r(1244),f=e.r(72383),p=e.r(56019),h=e.r(91915),m=e.r(58442),g=e.r(68017),y=e.r(70725),b=e.r(84356),P=e.r(41538),_=e.r(91798);e.r(74180);let v=e.r(61994),O=e.r(33906),S=l.default.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,E=["bottom","height","left","right","top","width","x","y"];function R(e,r){let t=e.getBoundingClientRect();return t.top>=0&&t.top<=r}class j extends u.default.Component{componentDidMount(){this.handlePotentialScroll()}componentDidUpdate(){this.props.focusAndScrollRef.apply&&this.handlePotentialScroll()}render(){return this.props.children}constructor(...e){super(...e),this.handlePotentialScroll=()=>{let{focusAndScrollRef:e,segmentPath:r}=this.props;if(e.apply){if(0!==e.segmentPaths.length&&!e.segmentPaths.some(e=>r.every((r,t)=>(0,p.matchSegment)(r,e[t]))))return;let t=null,n=e.hashFragment;if(n&&(t="top"===n?document.body:document.getElementById(n)??document.getElementsByName(n)[0]),t||(t="undefined"==typeof window?null:(0,S.findDOMNode)(this)),!(t instanceof Element))return;for(;!(t instanceof HTMLElement)||function(e){if(["sticky","fixed"].includes(getComputedStyle(e).position))return!0;let r=e.getBoundingClientRect();return E.every(e=>0===r[e])}(t);){if(null===t.nextElementSibling)return;t=t.nextElementSibling}e.apply=!1,e.hashFragment=null,e.segmentPaths=[],(0,h.disableSmoothScrollDuringRouteTransition)(()=>{if(n)return void t.scrollIntoView();let e=document.documentElement,r=e.clientHeight;!R(t,r)&&(e.scrollTop=0,R(t,r)||t.scrollIntoView())},{dontForceLayout:!0,onlyHashChange:e.onlyHashChange}),e.onlyHashChange=!1,t.focus()}}}}function w({segmentPath:e,children:r}){let t=(0,u.useContext)(s.GlobalLayoutRouterContext);if(!t)throw Object.defineProperty(Error("invariant global layout router not mounted"),"__NEXT_ERROR_CODE",{value:"E473",enumerable:!1,configurable:!0});return(0,o.jsx)(j,{segmentPath:e,focusAndScrollRef:t.focusAndScrollRef,children:r})}function C({tree:e,segmentPath:r,debugNameContext:t,cacheNode:n,params:a,url:l,isActive:f}){let h=(0,u.useContext)(s.GlobalLayoutRouterContext);if((0,u.useContext)(v.NavigationPromisesContext),!h)throw Object.defineProperty(Error("invariant global layout router not mounted"),"__NEXT_ERROR_CODE",{value:"E473",enumerable:!1,configurable:!0});let{tree:m}=h,g=null!==n.prefetchRsc?n.prefetchRsc:n.rsc,y=(0,u.useDeferredValue)(n.rsc,g),_="object"==typeof y&&null!==y&&"function"==typeof y.then?(0,u.use)(y):y;if(!_){if(f){let e=n.lazyData;if(null===e){let t=function e(r,t){if(r){let[n,a]=r,o=2===r.length;if((0,p.matchSegment)(t[0],n)&&t[1].hasOwnProperty(a)){if(o){let r=e(void 0,t[1][a]);return[t[0],{...t[1],[a]:[r[0],r[1],r[2],"refetch"]}]}return[t[0],{...t[1],[a]:e(r.slice(2),t[1][a])}]}}return t}(["",...r],m),a=(0,b.hasInterceptionRouteInCurrentTree)(m),o=Date.now();n.lazyData=e=(0,i.fetchServerResponse)(new URL(l,location.origin),{flightRouterState:t,nextUrl:a?h.previousNextUrl||h.nextUrl:null}).then(e=>((0,u.startTransition)(()=>{(0,P.dispatchAppRouterAction)({type:c.ACTION_SERVER_PATCH,previousTree:m,serverResponse:e,navigatedAt:o})}),e)),(0,u.use)(e)}}(0,u.use)(d.unresolvedThenable)}return(0,o.jsx)(s.LayoutRouterContext.Provider,{value:{parentTree:e,parentCacheNode:n,parentSegmentPath:r,parentParams:a,debugNameContext:t,url:l,isActive:f},children:_})}function x({name:e,loading:r,children:t}){let n;if(n="object"==typeof r&&null!==r&&"function"==typeof r.then?(0,u.use)(r):r){let r=n[0],a=n[1],c=n[2];return(0,o.jsx)(u.Suspense,{name:e,fallback:(0,o.jsxs)(o.Fragment,{children:[a,c,r]}),children:t})}return(0,o.jsx)(o.Fragment,{children:t})}function T({parallelRouterKey:e,error:r,errorStyles:t,errorScripts:n,templateStyles:a,templateScripts:c,template:l,notFound:i,forbidden:d,unauthorized:p,segmentViewBoundaries:h}){let b=(0,u.useContext)(s.LayoutRouterContext);if(!b)throw Object.defineProperty(Error("invariant expected layout router to be mounted"),"__NEXT_ERROR_CODE",{value:"E56",enumerable:!1,configurable:!0});let{parentTree:P,parentCacheNode:v,parentSegmentPath:S,parentParams:E,url:R,isActive:j,debugNameContext:T}=b,A=v.parallelRoutes,M=A.get(e);M||(M=new Map,A.set(e,M));let F=P[0],D=null===S?[e]:S.concat([F,e]),k=P[1][e],N=k[0],I=(0,y.createRouterCacheKey)(N,!0),U=(0,_.useRouterBFCache)(k,I),H=[];do{let e=U.tree,u=U.stateKey,h=e[0],b=(0,y.createRouterCacheKey)(h),P=M.get(b);if(void 0===P){let e={lazyData:null,rsc:null,prefetchRsc:null,head:null,prefetchHead:null,parallelRoutes:new Map,loading:null,navigatedAt:-1};P=e,M.set(b,e)}let _=E;if(Array.isArray(h)){let e=h[0],r=h[1],t=h[2],n=(0,O.getParamValueFromCacheKey)(r,t);null!==n&&(_={...E,[e]:n})}let S=function(e){if("/"===e)return"/";if("string"==typeof e)if("(slot)"===e)return;else return e+"/";return e[1]+"/"}(h),A=S??T,F=void 0===S?void 0:T,k=v.loading,N=(0,o.jsxs)(s.TemplateContext.Provider,{value:(0,o.jsxs)(w,{segmentPath:D,children:[(0,o.jsx)(f.ErrorBoundary,{errorComponent:r,errorStyles:t,errorScripts:n,children:(0,o.jsx)(x,{name:F,loading:k,children:(0,o.jsx)(g.HTTPAccessFallbackBoundary,{notFound:i,forbidden:d,unauthorized:p,children:(0,o.jsxs)(m.RedirectBoundary,{children:[(0,o.jsx)(C,{url:R,tree:e,params:_,cacheNode:P,segmentPath:D,debugNameContext:A,isActive:j&&u===I}),null]})})})}),null]}),children:[a,c,l]},u);H.push(N),U=U.next}while(null!==U)return H}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),r.exports=t.default)},37457,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return u}});let n=e.r(90809),a=e.r(43476),o=n._(e.r(71645)),c=e.r(8372);function u(){let e=(0,o.useContext)(c.TemplateContext);return(0,a.jsx)(a.Fragment,{children:e})}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),r.exports=t.default)},93504,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"createRenderSearchParamsFromClient",{enumerable:!0,get:function(){return a}});let n=new WeakMap;function a(e){let r=n.get(e);if(r)return r;let t=Promise.resolve(e);return n.set(e,t),t}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),r.exports=t.default)},66996,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"createRenderSearchParamsFromClient",{enumerable:!0,get:function(){return n}});let n=e.r(93504).createRenderSearchParamsFromClient;("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),r.exports=t.default)},6831,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"createRenderParamsFromClient",{enumerable:!0,get:function(){return a}});let n=new WeakMap;function a(e){let r=n.get(e);if(r)return r;let t=Promise.resolve(e);return n.set(e,t),t}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),r.exports=t.default)},97689,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"createRenderParamsFromClient",{enumerable:!0,get:function(){return n}});let n=e.r(6831).createRenderParamsFromClient;("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),r.exports=t.default)},42715,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"ReflectAdapter",{enumerable:!0,get:function(){return n}});class n{static get(e,r,t){let n=Reflect.get(e,r,t);return"function"==typeof n?n.bind(e):n}static set(e,r,t,n){return Reflect.set(e,r,t,n)}static has(e,r){return Reflect.has(e,r)}static deleteProperty(e,r){return Reflect.deleteProperty(e,r)}}},76361,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"createDedupedByCallsiteServerErrorLoggerDev",{enumerable:!0,get:function(){return l}});let n=function(e,r){if(e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var t=a(void 0);if(t&&t.has(e))return t.get(e);var n={__proto__:null},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var c in e)if("default"!==c&&Object.prototype.hasOwnProperty.call(e,c)){var u=o?Object.getOwnPropertyDescriptor(e,c):null;u&&(u.get||u.set)?Object.defineProperty(n,c,u):n[c]=e[c]}return n.default=e,t&&t.set(e,n),n}(e.r(71645));function a(e){if("function"!=typeof WeakMap)return null;var r=new WeakMap,t=new WeakMap;return(a=function(e){return e?t:r})(e)}let o={current:null},c="function"==typeof n.cache?n.cache:e=>e,u=console.warn;function l(e){return function(...r){u(e(...r))}}c(e=>{try{u(o.current)}finally{o.current=null}})},65932,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n={describeHasCheckingStringProperty:function(){return u},describeStringPropertyAccess:function(){return c},wellKnownProperties:function(){return l}};for(var a in n)Object.defineProperty(t,a,{enumerable:!0,get:n[a]});let o=/^[A-Za-z_$][A-Za-z0-9_$]*$/;function c(e,r){return o.test(r)?`\`${e}.${r}\``:`\`${e}[${JSON.stringify(r)}]\``}function u(e,r){let t=JSON.stringify(r);return`\`Reflect.has(${e}, ${t})\`, \`${t} in ${e}\`, or similar`}let l=new Set(["hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toString","valueOf","toLocaleString","then","catch","finally","status","displayName","_debugInfo","toJSON","$$typeof","__esModule"])},83066,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"afterTaskAsyncStorageInstance",{enumerable:!0,get:function(){return n}});let n=(0,e.r(90317).createAsyncLocalStorage)()},41643,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"afterTaskAsyncStorage",{enumerable:!0,get:function(){return n.afterTaskAsyncStorageInstance}});let n=e.r(83066)},50999,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n={isRequestAPICallableInsideAfter:function(){return s},throwForSearchParamsAccessInUseCache:function(){return l},throwWithStaticGenerationBailoutErrorWithDynamicError:function(){return u}};for(var a in n)Object.defineProperty(t,a,{enumerable:!0,get:n[a]});let o=e.r(43248),c=e.r(41643);function u(e,r){throw Object.defineProperty(new o.StaticGenBailoutError(`Route ${e} with \`dynamic = "error"\` couldn't be rendered statically because it used ${r}. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`),"__NEXT_ERROR_CODE",{value:"E543",enumerable:!1,configurable:!0})}function l(e,r){let t=Object.defineProperty(Error(`Route ${e.route} used \`searchParams\` inside "use cache". Accessing dynamic request data inside a cache scope is not supported. If you need some search params inside a cached function await \`searchParams\` outside of the cached function and pass only the required search params as arguments to the cached function. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache`),"__NEXT_ERROR_CODE",{value:"E842",enumerable:!1,configurable:!0});throw Error.captureStackTrace(t,r),e.invalidDynamicUsageError??=t,t}function s(){let e=c.afterTaskAsyncStorage.getStore();return(null==e?void 0:e.rootTaskSpawnPhase)==="action"}},69882,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n={createPrerenderSearchParamsForClientPage:function(){return g},createSearchParamsFromClient:function(){return p},createServerSearchParamsForMetadata:function(){return h},createServerSearchParamsForServerPage:function(){return m},makeErroringSearchParamsForUseCache:function(){return v}};for(var a in n)Object.defineProperty(t,a,{enumerable:!0,get:n[a]});let o=e.r(42715),c=e.r(67673),u=e.r(62141),l=e.r(12718),s=e.r(63138),i=e.r(76361),d=e.r(65932),f=e.r(50999);function p(e,r){let t=u.workUnitAsyncStorage.getStore();if(t)switch(t.type){case"prerender":case"prerender-client":case"prerender-ppr":case"prerender-legacy":return y(r,t);case"prerender-runtime":throw Object.defineProperty(new l.InvariantError("createSearchParamsFromClient should not be called in a runtime prerender."),"__NEXT_ERROR_CODE",{value:"E769",enumerable:!1,configurable:!0});case"cache":case"private-cache":case"unstable-cache":throw Object.defineProperty(new l.InvariantError("createSearchParamsFromClient should not be called in cache contexts."),"__NEXT_ERROR_CODE",{value:"E739",enumerable:!1,configurable:!0});case"request":return b(e,r,t)}(0,u.throwInvariantForMissingStore)()}e.r(42852);let h=m;function m(e,r){let t=u.workUnitAsyncStorage.getStore();if(t)switch(t.type){case"prerender":case"prerender-client":case"prerender-ppr":case"prerender-legacy":return y(r,t);case"cache":case"private-cache":case"unstable-cache":throw Object.defineProperty(new l.InvariantError("createServerSearchParamsForServerPage should not be called in cache contexts."),"__NEXT_ERROR_CODE",{value:"E747",enumerable:!1,configurable:!0});case"prerender-runtime":var n,a;return n=e,a=t,(0,c.delayUntilRuntimeStage)(a,O(n));case"request":return b(e,r,t)}(0,u.throwInvariantForMissingStore)()}function g(e){if(e.forceStatic)return Promise.resolve({});let r=u.workUnitAsyncStorage.getStore();if(r)switch(r.type){case"prerender":case"prerender-client":return(0,s.makeHangingPromise)(r.renderSignal,e.route,"`searchParams`");case"prerender-runtime":throw Object.defineProperty(new l.InvariantError("createPrerenderSearchParamsForClientPage should not be called in a runtime prerender."),"__NEXT_ERROR_CODE",{value:"E768",enumerable:!1,configurable:!0});case"cache":case"private-cache":case"unstable-cache":throw Object.defineProperty(new l.InvariantError("createPrerenderSearchParamsForClientPage should not be called in cache contexts."),"__NEXT_ERROR_CODE",{value:"E746",enumerable:!1,configurable:!0});case"prerender-ppr":case"prerender-legacy":case"request":return Promise.resolve({})}(0,u.throwInvariantForMissingStore)()}function y(e,r){if(e.forceStatic)return Promise.resolve({});switch(r.type){case"prerender":case"prerender-client":var t=e,n=r;let a=P.get(n);if(a)return a;let u=(0,s.makeHangingPromise)(n.renderSignal,t.route,"`searchParams`"),l=new Proxy(u,{get(e,r,t){if(Object.hasOwn(u,r))return o.ReflectAdapter.get(e,r,t);switch(r){case"then":return(0,c.annotateDynamicAccess)("`await searchParams`, `searchParams.then`, or similar",n),o.ReflectAdapter.get(e,r,t);case"status":return(0,c.annotateDynamicAccess)("`use(searchParams)`, `searchParams.status`, or similar",n),o.ReflectAdapter.get(e,r,t);default:return o.ReflectAdapter.get(e,r,t)}}});return P.set(n,l),l;case"prerender-ppr":case"prerender-legacy":var i=e,d=r;let p=P.get(i);if(p)return p;let h=Promise.resolve({}),m=new Proxy(h,{get(e,r,t){if(Object.hasOwn(h,r))return o.ReflectAdapter.get(e,r,t);if("string"==typeof r&&"then"===r){let e="`await searchParams`, `searchParams.then`, or similar";i.dynamicShouldError?(0,f.throwWithStaticGenerationBailoutErrorWithDynamicError)(i.route,e):"prerender-ppr"===d.type?(0,c.postponeWithTracking)(i.route,e,d.dynamicTracking):(0,c.throwToInterruptStaticGeneration)(e,i,d)}return o.ReflectAdapter.get(e,r,t)}});return P.set(i,m),m;default:return r}}function b(e,r,t){return r.forceStatic?Promise.resolve({}):O(e)}let P=new WeakMap,_=new WeakMap;function v(e){let r=_.get(e);if(r)return r;let t=Promise.resolve({}),n=new Proxy(t,{get:function r(n,a,c){return Object.hasOwn(t,a)||"string"!=typeof a||"then"!==a&&d.wellKnownProperties.has(a)||(0,f.throwForSearchParamsAccessInUseCache)(e,r),o.ReflectAdapter.get(n,a,c)}});return _.set(e,n),n}function O(e){let r=P.get(e);if(r)return r;let t=Promise.resolve(e);return P.set(e,t),t}(0,i.createDedupedByCallsiteServerErrorLoggerDev)(function(e,r){let t=e?`Route "${e}" `:"This route ";return Object.defineProperty(Error(`${t}used ${r}. \`searchParams\` is a Promise and must be unwrapped with \`await\` or \`React.use()\` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`),"__NEXT_ERROR_CODE",{value:"E848",enumerable:!1,configurable:!0})})},74804,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"dynamicAccessAsyncStorageInstance",{enumerable:!0,get:function(){return n}});let n=(0,e.r(90317).createAsyncLocalStorage)()},88276,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"dynamicAccessAsyncStorage",{enumerable:!0,get:function(){return n.dynamicAccessAsyncStorageInstance}});let n=e.r(74804)},41489,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n={createParamsFromClient:function(){return h},createPrerenderParamsForClientSegment:function(){return b},createServerParamsForMetadata:function(){return m},createServerParamsForRoute:function(){return g},createServerParamsForServerSegment:function(){return y}};for(var a in n)Object.defineProperty(t,a,{enumerable:!0,get:n[a]});let o=e.r(63599),c=e.r(42715),u=e.r(67673),l=e.r(62141),s=e.r(12718),i=e.r(65932),d=e.r(63138),f=e.r(76361),p=e.r(88276);function h(e,r){let t=l.workUnitAsyncStorage.getStore();if(t)switch(t.type){case"prerender":case"prerender-client":case"prerender-ppr":case"prerender-legacy":return P(e,r,t);case"cache":case"private-cache":case"unstable-cache":throw Object.defineProperty(new s.InvariantError("createParamsFromClient should not be called in cache contexts."),"__NEXT_ERROR_CODE",{value:"E736",enumerable:!1,configurable:!0});case"prerender-runtime":throw Object.defineProperty(new s.InvariantError("createParamsFromClient should not be called in a runtime prerender."),"__NEXT_ERROR_CODE",{value:"E770",enumerable:!1,configurable:!0});case"request":return S(e)}(0,l.throwInvariantForMissingStore)()}e.r(42852);let m=y;function g(e,r){let t=l.workUnitAsyncStorage.getStore();if(t)switch(t.type){case"prerender":case"prerender-client":case"prerender-ppr":case"prerender-legacy":return P(e,r,t);case"cache":case"private-cache":case"unstable-cache":throw Object.defineProperty(new s.InvariantError("createServerParamsForRoute should not be called in cache contexts."),"__NEXT_ERROR_CODE",{value:"E738",enumerable:!1,configurable:!0});case"prerender-runtime":return _(e,t);case"request":return S(e)}(0,l.throwInvariantForMissingStore)()}function y(e,r){let t=l.workUnitAsyncStorage.getStore();if(t)switch(t.type){case"prerender":case"prerender-client":case"prerender-ppr":case"prerender-legacy":return P(e,r,t);case"cache":case"private-cache":case"unstable-cache":throw Object.defineProperty(new s.InvariantError("createServerParamsForServerSegment should not be called in cache contexts."),"__NEXT_ERROR_CODE",{value:"E743",enumerable:!1,configurable:!0});case"prerender-runtime":return _(e,t);case"request":return S(e)}(0,l.throwInvariantForMissingStore)()}function b(e){let r=o.workAsyncStorage.getStore();if(!r)throw Object.defineProperty(new s.InvariantError("Missing workStore in createPrerenderParamsForClientSegment"),"__NEXT_ERROR_CODE",{value:"E773",enumerable:!1,configurable:!0});let t=l.workUnitAsyncStorage.getStore();if(t)switch(t.type){case"prerender":case"prerender-client":let n=t.fallbackRouteParams;if(n){for(let a in e)if(n.has(a))return(0,d.makeHangingPromise)(t.renderSignal,r.route,"`params`")}break;case"cache":case"private-cache":case"unstable-cache":throw Object.defineProperty(new s.InvariantError("createPrerenderParamsForClientSegment should not be called in cache contexts."),"__NEXT_ERROR_CODE",{value:"E734",enumerable:!1,configurable:!0})}return Promise.resolve(e)}function P(e,r,t){switch(t.type){case"prerender":case"prerender-client":{let n=t.fallbackRouteParams;if(n){for(let a in e)if(n.has(a))return function(e,r,t){let n=v.get(e);if(n)return n;let a=new Proxy((0,d.makeHangingPromise)(t.renderSignal,r.route,"`params`"),O);return v.set(e,a),a}(e,r,t)}break}case"prerender-ppr":{let n=t.fallbackRouteParams;if(n){for(let a in e)if(n.has(a))return function(e,r,t,n){let a=v.get(e);if(a)return a;let o={...e},c=Promise.resolve(o);return v.set(e,c),Object.keys(e).forEach(e=>{i.wellKnownProperties.has(e)||r.has(e)&&Object.defineProperty(o,e,{get(){let r=(0,i.describeStringPropertyAccess)("params",e);"prerender-ppr"===n.type?(0,u.postponeWithTracking)(t.route,r,n.dynamicTracking):(0,u.throwToInterruptStaticGeneration)(r,t,n)},enumerable:!0})}),c}(e,n,r,t)}}}return S(e)}function _(e,r){return(0,u.delayUntilRuntimeStage)(r,S(e))}let v=new WeakMap,O={get:function(e,r,t){if("then"===r||"catch"===r||"finally"===r){let n=c.ReflectAdapter.get(e,r,t);return({[r]:(...r)=>{let t=p.dynamicAccessAsyncStorage.getStore();return t&&t.abortController.abort(Object.defineProperty(Error("Accessed fallback `params` during prerendering."),"__NEXT_ERROR_CODE",{value:"E691",enumerable:!1,configurable:!0})),new Proxy(n.apply(e,r),O)}})[r]}return c.ReflectAdapter.get(e,r,t)}};function S(e){let r=v.get(e);if(r)return r;let t=Promise.resolve(e);return v.set(e,t),t}(0,f.createDedupedByCallsiteServerErrorLoggerDev)(function(e,r){let t=e?`Route "${e}" `:"This route ";return Object.defineProperty(Error(`${t}used ${r}. \`params\` is a Promise and must be unwrapped with \`await\` or \`React.use()\` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`),"__NEXT_ERROR_CODE",{value:"E834",enumerable:!1,configurable:!0})})},47257,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"ClientPageRoot",{enumerable:!0,get:function(){return s}});let n=e.r(43476),a=e.r(12718),o=e.r(8372),c=e.r(71645),u=e.r(33906),l=e.r(61994);function s({Component:r,serverProvidedParams:t}){let s,i;if(null!==t)s=t.searchParams,i=t.params;else{let e=(0,c.use)(o.LayoutRouterContext);i=null!==e?e.parentParams:{},s=(0,u.urlSearchParamsToParsedUrlQuery)((0,c.use)(l.SearchParamsContext))}if("undefined"==typeof window){let t,o,{workAsyncStorage:c}=e.r(63599),u=c.getStore();if(!u)throw Object.defineProperty(new a.InvariantError("Expected workStore to exist when handling searchParams in a client Page."),"__NEXT_ERROR_CODE",{value:"E564",enumerable:!1,configurable:!0});let{createSearchParamsFromClient:l}=e.r(69882);t=l(s,u);let{createParamsFromClient:d}=e.r(41489);return o=d(i,u),(0,n.jsx)(r,{params:o,searchParams:t})}{let{createRenderSearchParamsFromClient:t}=e.r(66996),a=t(s),{createRenderParamsFromClient:o}=e.r(97689),c=o(i);return(0,n.jsx)(r,{params:c,searchParams:a})}}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),r.exports=t.default)},92825,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"ClientSegmentRoot",{enumerable:!0,get:function(){return u}});let n=e.r(43476),a=e.r(12718),o=e.r(8372),c=e.r(71645);function u({Component:r,slots:t,serverProvidedParams:u}){let l;if(null!==u)l=u.params;else{let e=(0,c.use)(o.LayoutRouterContext);l=null!==e?e.parentParams:{}}if("undefined"==typeof window){let o,{workAsyncStorage:c}=e.r(63599),u=c.getStore();if(!u)throw Object.defineProperty(new a.InvariantError("Expected workStore to exist when handling params in a client segment such as a Layout or Template."),"__NEXT_ERROR_CODE",{value:"E600",enumerable:!1,configurable:!0});let{createParamsFromClient:s}=e.r(41489);return o=s(l,u),(0,n.jsx)(r,{...t,params:o})}{let{createRenderParamsFromClient:a}=e.r(97689),o=a(l);return(0,n.jsx)(r,{...t,params:o})}}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),r.exports=t.default)},27201,(e,r,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"IconMark",{enumerable:!0,get:function(){return a}});let n=e.r(43476),a=()=>"undefined"!=typeof window?null:(0,n.jsx)("meta",{name:"«nxt-icon»"})}]); \ No newline at end of file diff --git a/static/frontend/login/_next/static/chunks/66925481c7f9f43e.js b/static/frontend/login/_next/static/chunks/66925481c7f9f43e.js deleted file mode 100644 index ab19a36997a7bcd0e89ede3363a141081f1907dd..0000000000000000000000000000000000000000 --- a/static/frontend/login/_next/static/chunks/66925481c7f9f43e.js +++ /dev/null @@ -1,20 +0,0 @@ -(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,74575,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"getAssetPrefix",{enumerable:!0,get:function(){return l}});let r=e.r(12718);function l(){let e=document.currentScript;if(!(e instanceof HTMLScriptElement))throw Object.defineProperty(new r.InvariantError(`Expected document.currentScript to be a ",a=a.removeChild(a.firstChild);break;case"select":a="string"==typeof r.is?u.createElement("select",{is:r.is}):u.createElement("select"),r.multiple?a.multiple=!0:r.size&&(a.size=r.size);break;default:a="string"==typeof r.is?u.createElement(l,{is:r.is}):u.createElement(l)}}a[eW]=t,a[eq]=r;e:for(u=t.child;null!==u;){if(5===u.tag||6===u.tag)a.appendChild(u.stateNode);else if(4!==u.tag&&27!==u.tag&&null!==u.child){u.child.return=u,u=u.child;continue}if(u===t)break;for(;null===u.sibling;){if(null===u.return||u.return===t)break e;u=u.return}u.sibling.return=u.return,u=u.sibling}switch(t.stateNode=a,s9(a,l,r),l){case"button":case"input":case"select":case"textarea":r=!!r.autoFocus;break;case"img":r=!0;break;default:r=!1}r&&on(t)}}return oo(t),t.subtreeFlags&=-0x2000001,or(t,t.type,null===e?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&null!=t.stateNode)e.memoizedProps!==r&&on(t);else{if("string"!=typeof r&&null===t.stateNode)throw Error(i(166));if(e=en.current,rY(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,null!==(l=r$))switch(l.tag){case 27:case 5:r=l.memoizedProps}e[eW]=t,(e=!!(e.nodeValue===n||null!==r&&!0===r.suppressHydrationWarning||s8(e.nodeValue,n)))||rQ(t,!0)}else(e=cr(e).createTextNode(r))[eW]=t,t.stateNode=e}return oo(t),null;case 31:if(n=t.memoizedState,null===e||null!==e.memoizedState){if(r=rY(t),null!==n){if(null===e){if(!r)throw Error(i(318));if(!(e=null!==(e=t.memoizedState)?e.dehydrated:null))throw Error(i(557));e[eW]=t}else rJ(),0==(128&t.flags)&&(t.memoizedState=null),t.flags|=4;oo(t),e=!1}else n=rZ(),null!==e&&null!==e.memoizedState&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e){if(256&t.flags)return l9(t),t;return l9(t),null}if(0!=(128&t.flags))throw Error(i(558))}return oo(t),null;case 13:if(r=t.memoizedState,null===e||null!==e.memoizedState&&null!==e.memoizedState.dehydrated){if(l=rY(t),null!==r&&null!==r.dehydrated){if(null===e){if(!l)throw Error(i(318));if(!(l=null!==(l=t.memoizedState)?l.dehydrated:null))throw Error(i(317));l[eW]=t}else rJ(),0==(128&t.flags)&&(t.memoizedState=null),t.flags|=4;oo(t),l=!1}else l=rZ(),null!==e&&null!==e.memoizedState&&(e.memoizedState.hydrationErrors=l),l=!0;if(!l){if(256&t.flags)return l9(t),t;return l9(t),null}}if(l9(t),0!=(128&t.flags))return t.lanes=n,t;return n=null!==r,e=null!==e&&null!==e.memoizedState,n&&(r=t.child,l=null,null!==r.alternate&&null!==r.alternate.memoizedState&&null!==r.alternate.memoizedState.cachePool&&(l=r.alternate.memoizedState.cachePool.pool),a=null,null!==r.memoizedState&&null!==r.memoizedState.cachePool&&(a=r.memoizedState.cachePool.pool),a!==l&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),oa(t,t.updateQueue),oo(t),null;case 4:return ea(),null===e&&sX(t.stateNode.containerInfo),oo(t),null;case 10:return r5(t.type),oo(t),null;case 19:if(J(ae),null===(r=t.memoizedState))return oo(t),null;if(l=0!=(128&t.flags),null===(a=r.rendering))if(l)ou(r,!1);else{if(0!==iR||null!==e&&0!=(128&e.flags))for(e=t.child;null!==e;){if(null!==(a=at(e))){for(t.flags|=128,ou(r,!1),t.updateQueue=e=a.updateQueue,oa(t,e),t.subtreeFlags=0,e=n,n=t.child;null!==n;)rS(n,e),n=n.sibling;return Z(ae,1&ae.current|2),rV&&rI(t,r.treeForkCount),t.child}e=e.sibling}null!==r.tail&&ey()>iF&&(t.flags|=128,l=!0,ou(r,!1),t.lanes=4194304)}else{if(!l)if(null!==(e=at(a))){if(t.flags|=128,l=!0,t.updateQueue=e=e.updateQueue,oa(t,e),ou(r,!0),null===r.tail&&"hidden"===r.tailMode&&!a.alternate&&!rV)return oo(t),null}else 2*ey()-r.renderingStartTime>iF&&0x20000000!==n&&(t.flags|=128,l=!0,ou(r,!1),t.lanes=4194304);r.isBackwards?(a.sibling=t.child,t.child=a):(null!==(e=r.last)?e.sibling=a:t.child=a,r.last=a)}if(null!==r.tail)return e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=ey(),e.sibling=null,n=ae.current,Z(ae,l?1&n|2:1&n),rV&&rI(t,r.treeForkCount),e;return oo(t),null;case 22:case 23:return l9(t),l2(),r=null!==t.memoizedState,null!==e?null!==e.memoizedState!==r&&(t.flags|=8192):r&&(t.flags|=8192),r?0!=(0x20000000&n)&&0==(128&t.flags)&&(oo(t),6&t.subtreeFlags&&(t.flags|=8192)):oo(t),null!==(n=t.updateQueue)&&oa(t,n.retryQueue),n=null,null!==e&&null!==e.memoizedState&&null!==e.memoizedState.cachePool&&(n=e.memoizedState.cachePool.pool),r=null,null!==t.memoizedState&&null!==t.memoizedState.cachePool&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),null!==e&&J(lv),null;case 24:return n=null,null!==e&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),r5(lo),oo(t),null;case 25:return null;case 30:return t.flags|=0x2000000,oo(t),null}throw Error(i(156,t.tag))}(t.alternate,t,iT);if(null!==n){ib=n;return}if(null!==(t=t.sibling)){ib=t;return}ib=t=e}while(null!==t)0===iR&&(iR=5)}function ss(e,t){do{var n=function(e,t){switch(rU(t),t.tag){case 1:return 65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 3:return r5(lo),ea(),0!=(65536&(e=t.flags))&&0==(128&e)?(t.flags=-65537&e|128,t):null;case 26:case 27:case 5:return eo(t),null;case 31:if(null!==t.memoizedState){if(l9(t),null===t.alternate)throw Error(i(340));rJ()}return 65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 13:if(l9(t),null!==(e=t.memoizedState)&&null!==e.dehydrated){if(null===t.alternate)throw Error(i(340));rJ()}return 65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 19:return J(ae),null;case 4:return ea(),null;case 10:return r5(t.type),null;case 22:case 23:return l9(t),l2(),null!==e&&J(lv),65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 24:return r5(lo),null;default:return null}}(e.alternate,e);if(null!==n){n.flags&=32767,ib=n;return}if(null!==(n=e.return)&&(n.flags|=32768,n.subtreeFlags=0,n.deletions=null),!t&&null!==(e=e.sibling)){ib=e;return}ib=e=n}while(null!==e)iR=6,ib=null}function sc(e,t,n,r,l,a,u,o,s,c,f){e.cancelPendingCommit=null;do sy();while(0!==iH)if(0!=(6&iy))throw Error(i(327));if(null!==t){var d;if(t===e.current)throw Error(i(177));if(!function(e,t,n,r,l,a){var u=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var o=e.entanglements,i=e.expirationTimes,s=e.hiddenUpdates;for(n=u&~n;0fa){o.length=u;break}d=new Promise(cw.bind(d)),o.push(d)}}}return 0m&&(u=m,m=g,g=u);var y=nH(o,g),v=nH(o,m);if(y&&v&&(1!==p.rangeCount||p.anchorNode!==y.node||p.anchorOffset!==y.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var b=f.createRange();b.setStart(y.node,y.offset),p.removeAllRanges(),g>m?(p.addRange(b),p.extend(v.node,v.offset)):(b.setEnd(v.node,v.offset),p.addRange(b))}}}}for(f=[],p=o;p=p.parentNode;)1===p.nodeType&&f.push({element:p,left:p.scrollLeft,top:p.scrollTop});for("function"==typeof o.focus&&o.focus(),o=0;on?32:n,W.T=null,n=iW,iW=null;var a=i$,u=iV;if(iH=0,iB=i$=null,iV=0,0!=(6&iy))throw Error(i(331));var o=iy;if(iy|=4,ip(a.current),ia(a,a.current,u,n),iy=o,sL(0,!1),eP&&"function"==typeof eP.onPostCommitFiberRoot)try{eP.onPostCommitFiberRoot(ek,a)}catch(e){}return!0}finally{q.p=l,W.T=r,sm(e,t)}}function sb(e,t,n){t=rR(n,t),t=uj(e.stateNode,t,2),null!==(e=lV(e,t,2))&&(eI(e,2),sM(e))}function sS(e,t,n){if(3===e.tag)sb(e,e,n);else for(;null!==t;){if(3===t.tag){sb(t,e,n);break}if(1===t.tag){var r=t.stateNode;if("function"==typeof t.type.getDerivedStateFromError||"function"==typeof r.componentDidCatch&&(null===iU||!iU.has(r))){e=rR(n,e),null!==(r=lV(t,n=uA(2),2))&&(uz(n,r,t,e),eI(r,2),sM(r));break}}t=t.return}}function sw(e,t,n){var r=e.pingCache;if(null===r){r=e.pingCache=new im;var l=new Set;r.set(t,l)}else void 0===(l=r.get(t))&&(l=new Set,r.set(t,l));l.has(n)||(iP=!0,l.add(n),e=sE.bind(null,e,t,n),t.then(e,e))}function sE(e,t,n){var r=e.pingCache;null!==r&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,iv===e&&(iS&n)===n&&(4===iR||3===iR&&(0x3c00000&iS)===iS&&300>ey()-iz?0==(2&iy)&&i7(e,0):iN|=n,iM===iS&&(iM=0)),sM(e)}function s_(e,t){0===t&&(t=eA()),null!==(e=rd(e,t))&&(eI(e,t),sM(e))}function sk(e){var t=e.memoizedState,n=0;null!==t&&(n=t.retryLane),s_(e,n)}function sP(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,l=e.memoizedState;null!==l&&(n=l.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}null!==r&&r.delete(t),s_(e,n)}var sT=null,sR=null,sx=!1,sO=!1,sN=!1,sC=0;function sM(e){e!==sR&&null===e.next&&(null===sR?sT=sR=e:sR=sR.next=e),sO=!0,sx||(sx=!0,cf(function(){0!=(6&iy)?ep(eb,sj):sA()}))}function sL(e,t){if(!sN&&sO){sN=!0;do for(var n=!1,r=sT;null!==r;){if(!t)if(0!==e){var l=r.pendingLanes;if(0===l)var a=0;else{var u=r.suspendedLanes,o=r.pingedLanes;a=0xc000095&(a=(1<<31-eT(42|e)+1)-1&(l&~(u&~o)))?0xc000095&a|1:a?2|a:0}0!==a&&(n=!0,sF(r,a))}else a=iS,0==(3&(a=eL(r,r===iv?a:0,null!==r.cancelPendingCommit||-1!==r.timeoutHandle)))||ej(r,a)||(n=!0,sF(r,a));r=r.next}while(n)sN=!1}}function sj(){sA()}function sA(){sO=sx=!1;var e,t=0;0===sC||((e=window.event)&&"popstate"===e.type?e===co||(co=e,0):(co=null,1))||(t=sC);for(var n=ey(),r=null,l=sT;null!==l;){var a=l.next,u=sz(l,n);0===u?(l.next=null,null===r?sT=a:r.next=a,null===a&&(sR=r)):(r=l,(0!==t||0!=(3&u))&&(sO=!0)),l=a}0!==iH&&5!==iH||sL(t,!1),0!==sC&&(sC=0)}function sz(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,l=e.expirationTimes,a=-0x3c00001&e.pendingLanes;0 title"):null)}function ft(e,t){return"img"===e&&null!=t.src&&""!==t.src&&null==t.onLoad&&"lazy"!==t.loading}function fn(e){return"stylesheet"!==e.type||0!=(3&e.state.loading)}function fr(e){return(e.width||100)*(e.height||100)*("number"==typeof devicePixelRatio?devicePixelRatio:1)*.25}function fl(e,t){"function"==typeof t.decode&&(e.imgCount++,t.complete||(e.imgBytes+=fr(t),e.suspenseyImages.push(t)),e=fi.bind(e),t.decode().then(e,e))}var fa=0;function fu(e){if(0===e.count&&(0===e.imgCount||!e.waitingForImages)){if(e.stylesheets)fc(e,e.stylesheets);else if(e.unsuspend){var t=e.unsuspend;e.unsuspend=null,t()}}}function fo(){this.count--,fu(this)}function fi(){this.imgCount--,fu(this)}var fs=null;function fc(e,t){e.stylesheets=null,null!==e.unsuspend&&(e.count++,fs=new Map,t.forEach(ff,e),fs=null,fo.call(e))}function ff(e,t){if(!(4&t.state.loading)){var n=fs.get(e);if(n)var r=n.get(null);else{n=new Map,fs.set(e,n);for(var l=e.querySelectorAll("link[data-precedence],style[data-precedence]"),a=0;a{"use strict";!function e(){if("undefined"!=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&"function"==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(e){console.error(e)}}(),t.exports=e.r(46480)},42732,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"HeadManagerContext",{enumerable:!0,get:function(){return r}});let r=e.r(55682)._(e.r(71645)).default.createContext({})},51323,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r={onCaughtError:function(){return d},onUncaughtError:function(){return p}};for(var l in r)Object.defineProperty(n,l,{enumerable:!0,get:r[l]});let a=e.r(55682),u=e.r(65713),o=e.r(32061),i=e.r(28279),s=e.r(72383),c=a._(e.r(68027)),f={decorateDevError:e=>e,handleClientError:()=>{},originConsoleError:console.error.bind(console)};function d(e,t){let n,r=t.errorBoundary?.constructor;if(n=n||r===s.ErrorBoundaryHandler&&t.errorBoundary.props.errorComponent===c.default)return p(e);(0,o.isBailoutToCSRError)(e)||(0,u.isNextRouterError)(e)||f.originConsoleError(e)}function p(e){(0,o.isBailoutToCSRError)(e)||(0,u.isNextRouterError)(e)||(0,i.reportGlobalError)(e)}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},34727,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r={computeChangedPath:function(){return f},extractPathFromFlightRouterState:function(){return c},getSelectedParams:function(){return function e(t,n={}){for(let r of Object.values(t[1])){let t=r[0],l=Array.isArray(t),a=l?t[1]:t;!a||a.startsWith(u.PAGE_SEGMENT_KEY)||(l&&("c"===t[2]||"oc"===t[2])?n[t[0]]=t[1].split("/"):l&&(n[t[0]]=t[1]),n=e(r,n))}return n}}};for(var l in r)Object.defineProperty(n,l,{enumerable:!0,get:r[l]});let a=e.r(91463),u=e.r(13258),o=e.r(56019),i=e=>"string"==typeof e?"children"===e?"":e:e[1];function s(e){return e.reduce((e,t)=>{let n;return""===(t="/"===(n=t)[0]?n.slice(1):n)||(0,u.isGroupSegment)(t)?e:`${e}/${t}`},"")||"/"}function c(e){let t=Array.isArray(e[0])?e[0][1]:e[0];if(t===u.DEFAULT_SEGMENT_KEY||a.INTERCEPTION_ROUTE_MARKERS.some(e=>t.startsWith(e)))return;if(t.startsWith(u.PAGE_SEGMENT_KEY))return"";let n=[i(t)],r=e[1]??{},l=r.children?c(r.children):void 0;if(void 0!==l)n.push(l);else for(let[e,t]of Object.entries(r)){if("children"===e)continue;let r=c(t);void 0!==r&&n.push(r)}return s(n)}function f(e,t){let n=function e(t,n){let[r,l]=t,[u,s]=n,f=i(r),d=i(u);if(a.INTERCEPTION_ROUTE_MARKERS.some(e=>f.startsWith(e)||d.startsWith(e)))return"";if(!(0,o.matchSegment)(r,u))return c(n)??"";for(let t in l)if(s[t]){let n=e(l[t],s[t]);if(null!==n)return`${i(u)}/${n}`}return null}(e,t);return null==n||"/"===n?n:s(n.split("/"))}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},47442,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"handleMutable",{enumerable:!0,get:function(){return a}});let r=e.r(34727);function l(e){return void 0!==e}function a(e,t){let n=t.shouldScroll??!0,a=e.previousNextUrl,u=e.nextUrl;if(l(t.patchedTree)){let n=(0,r.computeChangedPath)(e.tree,t.patchedTree);n?(a=u,u=n):u||(u=e.canonicalUrl)}return{canonicalUrl:t.canonicalUrl??e.canonicalUrl,renderedSearch:t.renderedSearch??e.renderedSearch,pushRef:{pendingPush:l(t.pendingPush)?t.pendingPush:e.pushRef.pendingPush,mpaNavigation:l(t.mpaNavigation)?t.mpaNavigation:e.pushRef.mpaNavigation,preserveCustomHistoryState:l(t.preserveCustomHistoryState)?t.preserveCustomHistoryState:e.pushRef.preserveCustomHistoryState},focusAndScrollRef:{apply:!!n&&(!!l(t?.scrollableSegments)||e.focusAndScrollRef.apply),onlyHashChange:t.onlyHashChange||!1,hashFragment:n?t.hashFragment&&""!==t.hashFragment?decodeURIComponent(t.hashFragment.slice(1)):e.focusAndScrollRef.hashFragment:null,segmentPaths:n?t?.scrollableSegments??e.focusAndScrollRef.segmentPaths:[]},cache:t.cache?t.cache:e.cache,tree:l(t.patchedTree)?t.patchedTree:e.tree,nextUrl:u,previousNextUrl:a,debugInfo:t.collectedDebugInfo??null}}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},72463,(e,t,n)=>{"use strict";function r(e){let t=e.indexOf("#"),n=e.indexOf("?"),r=n>-1&&(t<0||n-1?{pathname:e.substring(0,r?n:t),query:r?e.substring(n,t>-1?t:void 0):"",hash:t>-1?e.slice(t):""}:{pathname:e,query:"",hash:""}}Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"parsePath",{enumerable:!0,get:function(){return r}})},41858,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"addPathPrefix",{enumerable:!0,get:function(){return l}});let r=e.r(72463);function l(e,t){if(!e.startsWith("/")||!t)return e;let{pathname:n,query:l,hash:a}=(0,r.parsePath)(e);return`${t}${n}${l}${a}`}},38281,(e,t,n)=>{"use strict";function r(e){return e.replace(/\/$/,"")||"/"}Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"removeTrailingSlash",{enumerable:!0,get:function(){return r}})},82823,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"normalizePathTrailingSlash",{enumerable:!0,get:function(){return a}});let r=e.r(38281),l=e.r(72463),a=e=>{if(!e.startsWith("/"))return e;let{pathname:t,query:n,hash:a}=(0,l.parsePath)(e);return/\.[^/]+\/?$/.test(t)?`${(0,r.removeTrailingSlash)(t)}${n}${a}`:t.endsWith("/")?`${t}${n}${a}`:`${t}/${n}${a}`};("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},5550,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"addBasePath",{enumerable:!0,get:function(){return a}});let r=e.r(41858),l=e.r(82823);function a(e,t){return(0,l.normalizePathTrailingSlash)((0,r.addPathPrefix)(e,"/login"))}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},57630,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r={createPrefetchURL:function(){return i},isExternalURL:function(){return o}};for(var l in r)Object.defineProperty(n,l,{enumerable:!0,get:r[l]});let a=e.r(82604),u=e.r(5550);function o(e){return e.origin!==window.location.origin}function i(e){let t;if((0,a.isBot)(window.navigator.userAgent))return null;try{t=new URL((0,u.addBasePath)(e),window.location.href)}catch(t){throw Object.defineProperty(Error(`Cannot prefetch '${e}' because it cannot be converted to a URL.`),"__NEXT_ERROR_CODE",{value:"E234",enumerable:!1,configurable:!0})}return o(t)?null:t}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},3678,(e,t,n)=>{"use strict";function r(e,t){let n=new URL(e);return{href:e,search:n.search,nextUrl:t}}Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"createCacheKey",{enumerable:!0,get:function(){return r}}),("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},22744,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"HasLoadingBoundary",{enumerable:!0,get:function(){return l}});var r,l=((r={})[r.SegmentHasLoadingBoundary=1]="SegmentHasLoadingBoundary",r[r.SubtreeHasLoadingBoundary=2]="SubtreeHasLoadingBoundary",r[r.SubtreeHasNoLoadingBoundary=3]="SubtreeHasNoLoadingBoundary",r)},2462,(e,t,n)=>{"use strict";function r(){let e={parent:null,key:null,hasValue:!1,value:null,map:null},t=null,n=null;function r(r){if(n===r)return t;let l=e;for(let e=0;e{"use strict";function r(e,t){let n=null,r=!1,a=0;function u(e){let t=e.next,r=e.prev;null!==t&&null!==r&&(a-=e.size,e.next=null,e.prev=null,n===e?n=t===n?null:t:(r.next=t,t.prev=r))}function o(){r||a<=e||(r=!0,l(i))}function i(){r=!1;let l=.9*e;for(;a>l&&null!==n;){let e=n.prev;u(e),t(e)}}return{put:function(e){if(n===e)return;let t=e.prev,r=e.next;if(null===r||null===t?(a+=e.size,o()):(t.next=r,r.prev=t),null===n)e.prev=e,e.next=e;else{let t=n.prev;e.prev=t,t.next=e,e.next=n,n.prev=e}n=e},delete:u,updateSize:function(e,t){let n=e.size;e.size=t,null!==e.next&&(a=a-n+t,o())}}}Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"createLRU",{enumerable:!0,get:function(){return r}});let l="function"==typeof requestIdleCallback?requestIdleCallback:e=>setTimeout(e,0);("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},91949,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r={IDLE_LINK_STATUS:function(){return s},PENDING_LINK_STATUS:function(){return i},mountFormInstance:function(){return v},mountLinkInstance:function(){return y},onLinkVisibilityChanged:function(){return S},onNavigationIntent:function(){return w},pingVisibleLinks:function(){return _},setLinkForCurrentNavigation:function(){return c},unmountLinkForCurrentNavigation:function(){return f},unmountPrefetchableInstance:function(){return b}};for(var l in r)Object.defineProperty(n,l,{enumerable:!0,get:r[l]});let a=e.r(65165),u=e.r(71645);e.r(88540),e.r(12718);let o=null,i={pending:!0},s={pending:!1};function c(e){(0,u.startTransition)(()=>{o?.setOptimisticLinkStatus(s),e?.setOptimisticLinkStatus(i),o=e})}function f(e){o===e&&(o=null)}let d="function"==typeof WeakMap?new WeakMap:new Map,p=new Set,h="function"==typeof IntersectionObserver?new IntersectionObserver(function(e){for(let t of e){let e=t.intersectionRatio>0;S(t.target,e)}},{rootMargin:"200px"}):null;function g(e,t){void 0!==d.get(e)&&b(e),d.set(e,t),null!==h&&h.observe(e)}function m(t){if("undefined"==typeof window)return null;{let{createPrefetchURL:n}=e.r(57630);try{return n(t)}catch{return("function"==typeof reportError?reportError:console.error)(`Cannot prefetch '${t}' because it cannot be converted to a URL.`),null}}}function y(e,t,n,r,l,a){if(l){let l=m(t);if(null!==l){let t={router:n,fetchStrategy:r,isVisible:!1,prefetchTask:null,prefetchHref:l.href,setOptimisticLinkStatus:a};return g(e,t),t}}return{router:n,fetchStrategy:r,isVisible:!1,prefetchTask:null,prefetchHref:null,setOptimisticLinkStatus:a}}function v(e,t,n,r){let l=m(t);null===l||g(e,{router:n,fetchStrategy:r,isVisible:!1,prefetchTask:null,prefetchHref:l.href,setOptimisticLinkStatus:null})}function b(e){let t=d.get(e);if(void 0!==t){d.delete(e),p.delete(t);let n=t.prefetchTask;null!==n&&(0,a.cancelPrefetchTask)(n)}null!==h&&h.unobserve(e)}function S(e,t){let n=d.get(e);void 0!==n&&(n.isVisible=t,t?p.add(n):p.delete(n),E(n,a.PrefetchPriority.Default))}function w(e,t){let n=d.get(e);void 0!==n&&void 0!==n&&E(n,a.PrefetchPriority.Intent)}function E(t,n){if("undefined"!=typeof window){let r=t.prefetchTask;if(!t.isVisible){null!==r&&(0,a.cancelPrefetchTask)(r);return}let{getCurrentAppRouterState:l}=e.r(99781),u=l();if(null!==u){let e=u.tree;if(null===r){let r=u.nextUrl,l=(0,a.createCacheKey)(t.prefetchHref,r);t.prefetchTask=(0,a.schedulePrefetchTask)(l,e,t.fetchStrategy,n,null)}else(0,a.reschedulePrefetchTask)(r,e,t.fetchStrategy,n)}}}function _(e,t){for(let n of p){let r=n.prefetchTask;if(null!==r&&!(0,a.isPrefetchTaskDirty)(r,e,t))continue;null!==r&&(0,a.cancelPrefetchTask)(r);let l=(0,a.createCacheKey)(n.prefetchHref,e);n.prefetchTask=(0,a.schedulePrefetchTask)(l,t,n.fetchStrategy,a.PrefetchPriority.Default,null)}}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},95282,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r={DOC_PREFETCH_RANGE_HEADER_VALUE:function(){return u},doesExportedHtmlMatchBuildId:function(){return s},insertBuildIdComment:function(){return i}};for(var l in r)Object.defineProperty(n,l,{enumerable:!0,get:r[l]});let a="",u="bytes=0-63";function o(e){return e.slice(0,24).replace(/-/g,"_")}function i(e,t){return t.includes("-->")||!e.startsWith(a)?e:e.replace(a,a+"")}function s(e,t){return e.startsWith(a+"")}},44743,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r,l={EntryStatus:function(){return k},canNewFetchStrategyProvideMoreContent:function(){return ef},convertRouteTreeToFlightRouterState:function(){return function e(t){let n={};if(null!==t.slots)for(let r in t.slots)n[r]=e(t.slots[r]);return[t.segment,n,null,null,t.isRootLayout]}},createDetachedSegmentCacheEntry:function(){return W},fetchRouteOnCacheMiss:function(){return el},fetchSegmentOnCacheMiss:function(){return ea},fetchSegmentPrefetchesUsingDynamicRequest:function(){return eu},getCurrentCacheVersion:function(){return M},getSegmentKeypath:function(){return I},pingInvalidationListeners:function(){return j},readExactRouteCacheEntry:function(){return A},readOrCreateRevalidatingSegmentEntry:function(){return V},readOrCreateRouteCacheEntry:function(){return H},readOrCreateSegmentCacheEntry:function(){return B},readRouteCacheEntry:function(){return z},readSegmentCacheEntry:function(){return F},requestOptimisticRouteCacheEntry:function(){return $},resetRevalidatingSegmentEntry:function(){return X},revalidateEntireCache:function(){return L},upgradeToPendingSegment:function(){return q},upsertSegmentEntry:function(){return K},waitForSegmentCacheEntry:function(){return U}};for(var a in l)Object.defineProperty(n,a,{enumerable:!0,get:l[a]});let u=e.r(22744),o=e.r(21768),i=e.r(87288),s=e.r(85018),c=e.r(14297),f=e.r(51191),d=e.r(3678),p=e.r(33906),h=e.r(2462),g=e.r(64541),m=e.r(67764),y=e.r(50590),v=e.r(54069),b=e.r(91949),S=e.r(13258),w=e.r(95282),E=e.r(65165),_=e.r(39470);var k=((r={})[r.Empty=0]="Empty",r[r.Pending=1]="Pending",r[r.Fulfilled=2]="Fulfilled",r[r.Rejected=3]="Rejected",r);function P(e){return 1e3*Math.max(e,30)}let T=(0,h.createTupleMap)(),R=(0,g.createLRU)(0xa00000,Y),x=(0,h.createTupleMap)(),O=(0,g.createLRU)(0x3200000,J),N=null,C=0;function M(){return C}function L(e,t){C++,(0,s.startRevalidationCooldown)(),T=(0,h.createTupleMap)(),R=(0,g.createLRU)(0xa00000,Y),x=(0,h.createTupleMap)(),O=(0,g.createLRU)(0x3200000,J),(0,b.pingVisibleLinks)(e,t),j(e,t)}function j(e,t){if(null!==N){let n=N;for(let r of(N=null,n))(0,s.isPrefetchTaskDirty)(r,e,t)&&function(e){let t=e.onInvalidate;if(null!==t){e.onInvalidate=null;try{t()}catch(e){"function"==typeof reportError?reportError(e):console.error(e)}}}(r)}}function A(e,t,n){let r=null===n?[t]:[t,n],l=T.get(r);if(null!==l)if(l.staleAt>e)return R.put(l),l;else{var a,u;a=l,u=r,ee(a),T.delete(u),R.delete(a)}return null}function z(e,t){let n=A(e,t.href,null);return null===n||n.couldBeIntercepted?A(e,t.href,t.nextUrl):n}function I(e,t,n){return(e===E.FetchStrategy.Full||e===E.FetchStrategy.PPRRuntime||!t.isPPREnabled)&&n.endsWith("/"+S.PAGE_SEGMENT_KEY)?[n,t.renderedSearch]:[n]}function F(e,t,n){if(!n.endsWith("/"+S.PAGE_SEGMENT_KEY))return D(e,[n]);let r=t.renderedSearch;if(null!==r){let t=D(e,[n,r]);if(null!==t)return t}return D(e,[n])}function D(e,t){let n=x.get(t);if(null!==n)if(n.staleAt>e)return O.put(n),n;else{let r=n.revalidating;if(null!==r){let n=K(e,t,r);if(null!==n&&n.staleAt>e)return n}else Q(n,t)}return null}function U(e){let t=e.promise;return null===t&&(t=e.promise=(0,_.createPromiseWithResolvers)()),t.promise}function H(e,t,n){null!==t.onInvalidate&&(null===N?N=new Set([t]):N.add(t));let r=z(e,n);if(null!==r)return r;let l={canonicalUrl:null,status:0,blockedTasks:null,tree:null,head:null,isHeadPartial:!0,staleAt:1/0,couldBeIntercepted:!0,isPPREnabled:!1,renderedSearch:null,TODO_metadataStatus:0,TODO_isHeadDynamic:!1,keypath:null,next:null,prev:null,size:0},a=null===n.nextUrl?[n.href]:[n.href,n.nextUrl];return T.set(a,l),l.keypath=a,R.put(l),l}function $(e,t,n){let r,l,a=t.search;if(""===a)return null;let u=new URL(t);u.search="";let o=z(e,(0,d.createCacheKey)(u.href,n));if(null===o||2!==o.status)return null;let i=o.TODO_isHeadDynamic;i?(r=[null,null],l=!0):(r=o.head,l=o.isHeadPartial);let s=new URL(o.canonicalUrl,t.origin),c=""!==s.search?s.search:a,p=""!==o.renderedSearch?o.renderedSearch:a,h=new URL(o.canonicalUrl,location.origin);return h.search=c,{canonicalUrl:(0,f.createHrefFromUrl)(h),status:2,blockedTasks:null,tree:o.tree,head:r,isHeadPartial:l,staleAt:o.staleAt,couldBeIntercepted:o.couldBeIntercepted,isPPREnabled:o.isPPREnabled,renderedSearch:p,TODO_metadataStatus:0,TODO_isHeadDynamic:i,keypath:null,next:null,prev:null,size:0}}function B(e,t,n,r){let l=I(t,n,r),a=D(e,l);if(null!==a)return a;let u=W(n.staleAt);return x.set(l,u),u.keypath=l,O.put(u),u}function V(e,t){let n=function(e,t){let n=t.revalidating;if(null!==n)if(n.staleAt>e)return n;else G(t);return null}(e,t);if(null!==n)return n;let r=W(t.staleAt);return t.revalidating=r,r}function K(e,t,n){let r=D(e,t);if(null!==r){var l;if(n.fetchStrategy!==r.fetchStrategy&&(l=r.fetchStrategy,!(l""!==e),r=m.ROOT_SEGMENT_CACHE_KEY,function e(t,n,r,l,a,o,i){let s=null,c=t.slots;if(null!==c)for(let t in s={},c){let n,r,u=c[t],f=u.name,d=u.paramType,h=u.paramKey,g=null;if(null!==d){let e=(0,p.parseDynamicParamFromURLPart)(d,o,i),t=null!==h?h:(0,p.getCacheKeyForDynamicParam)(e,"");g={name:f,value:e,type:d},r=[f,t,d],n=!0}else r=f,n=(0,p.doesStaticSegmentAppearInURL)(f);let y=n?i+1:i,v=(0,m.createSegmentRequestKeyPart)(r),b=(0,m.appendSegmentRequestKeyPart)(l,t,v),S=(0,m.appendSegmentCacheKeyPart)(a,t,(0,m.createSegmentCacheKeyPart)(v,r));s[t]=e(u,r,g,b,S,o,y)}return{cacheKey:a,requestKey:l,segment:n,param:r,slots:s,isRootLayout:t.isRootLayout,hasLoadingBoundary:u.HasLoadingBoundary.SegmentHasLoadingBoundary,hasRuntimePrefetch:t.hasRuntimePrefetch}}(a.tree,r,null,m.ROOT_SEGMENT_REQUEST_KEY,m.ROOT_SEGMENT_CACHE_KEY,n,0)),w=P(a.staleTime);d=a.head,h=a.isHeadPartial,g=Date.now()+w,e.status=2,e.tree=v,e.head=d,e.isHeadPartial=h,e.staleAt=g,e.couldBeIntercepted=b,e.canonicalUrl=y,e.renderedSearch=f,e.isPPREnabled=E,e.TODO_isHeadDynamic=!1,ee(e)}if(!b&&null!==l){let t=[r,l];if(T.get(t)===e){T.delete(t);let n=[r];T.set(n,e),e.keypath=n}}return{value:null,closed:S.promise}}catch(t){return en(e,Date.now()+1e4),null}}async function ea(e,t,n,r){let l=new URL(e.canonicalUrl,n.href),a=n.nextUrl,u=r.requestKey,s=u===m.ROOT_SEGMENT_REQUEST_KEY?"/_index":u,f={[o.RSC_HEADER]:"1",[o.NEXT_ROUTER_PREFETCH_HEADER]:"1",[o.NEXT_ROUTER_SEGMENT_PREFETCH_HEADER]:s};null!==a&&(f[o.NEXT_URL]=a);let d=ec(l,s);try{let n=await ei(d,f);if(!n||!n.ok||204===n.status||"2"!==n.headers.get(o.NEXT_DID_POSTPONE_HEADER)&&0||!n.body)return er(t,Date.now()+1e4),null;let r=(0,_.createPromiseWithResolvers)(),l=es(n.body,r.resolve,function(e){O.updateSize(t,e)}),a=await (0,i.createFromNextReadableStream)(l,f);if(a.buildId!==(0,c.getAppBuildId)())return er(t,Date.now()+1e4),null;return{value:et(t,a.rsc,a.loading,e.staleAt,a.isPartial),closed:r.promise}}catch(e){return er(t,Date.now()+1e4),null}}async function eu(e,t,n,r,l){let a=new URL(t.canonicalUrl,e.key.href),u=e.key.nextUrl,s={[o.RSC_HEADER]:"1",[o.NEXT_ROUTER_STATE_TREE_HEADER]:(0,y.prepareFlightRouterStateForRequest)(r)};switch(null!==u&&(s[o.NEXT_URL]=u),n){case E.FetchStrategy.Full:break;case E.FetchStrategy.PPRRuntime:s[o.NEXT_ROUTER_PREFETCH_HEADER]="2";break;case E.FetchStrategy.LoadingBoundary:s[o.NEXT_ROUTER_PREFETCH_HEADER]="1"}try{let r=await ei(a,s);if(!r||!r.ok||!r.body||(0,p.getRenderedSearch)(r)!==t.renderedSearch)return eo(l,Date.now()+1e4),null;let u=(0,_.createPromiseWithResolvers)(),f=null,d=es(r.body,u.resolve,function(e){if(null===f)return;let t=e/f.length;for(let e of f)O.updateSize(e,t)}),h=await (0,i.createFromNextReadableStream)(d,s),g=n===E.FetchStrategy.PPRRuntime&&!!r.headers.get(o.NEXT_DID_POSTPONE_HEADER);return f=function(e,t,n,r,l,a,u,i){if(l.b!==(0,c.getAppBuildId)())return null!==i&&eo(i,e+1e4),null;let s=(0,y.normalizeFlightData)(l.f);if("string"==typeof s)return null;let f=r.headers.get(o.NEXT_ROUTER_STALE_TIME_HEADER),d=e+(null!==f?P(parseInt(f,10)):v.STATIC_STALETIME_MS);for(let r of s){let l=r.seedData;if(null!==l){let o=r.segmentPath,s=m.ROOT_SEGMENT_REQUEST_KEY,c=m.ROOT_SEGMENT_CACHE_KEY;for(let e=0;e{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r={cancelPrefetchTask:function(){return S},isPrefetchTaskDirty:function(){return E},pingPrefetchTask:function(){return x},reschedulePrefetchTask:function(){return w},schedulePrefetchTask:function(){return b},startRevalidationCooldown:function(){return v}};for(var l in r)Object.defineProperty(n,l,{enumerable:!0,get:r[l]});let a=e.r(22744),u=e.r(56019),o=e.r(44743),i=e.r(3678),s=e.r(65165),c=e.r(13258),f="function"==typeof queueMicrotask?queueMicrotask:e=>Promise.resolve().then(e).catch(e=>setTimeout(()=>{throw e})),d=[],p=0,h=0,g=!1,m=null,y=null;function v(){null!==y&&clearTimeout(y),y=setTimeout(()=>{y=null,k()},300)}function b(e,t,n,r,l){let a={key:e,treeAtTimeOfPrefetch:t,cacheVersion:(0,s.getCurrentCacheVersion)(),priority:r,phase:1,hasBackgroundWork:!1,spawnedRuntimePrefetches:null,fetchStrategy:n,sortId:h++,isCanceled:!1,onInvalidate:l,_heapIndex:-1};return _(a),D(d,a),k(),a}function S(e){e.isCanceled=!0,function(e,t){let n=t._heapIndex;if(-1!==n&&(t._heapIndex=-1,0!==e.length)){let r=e.pop();r!==t&&(e[n]=r,r._heapIndex=n,V(e,r,n))}}(d,e)}function w(e,t,n,r){e.isCanceled=!1,e.phase=1,e.sortId=h++,e.priority=e===m?s.PrefetchPriority.Intent:r,e.treeAtTimeOfPrefetch=t,e.fetchStrategy=n,_(e),-1!==e._heapIndex?$(d,e):D(d,e),k()}function E(e,t,n){let r=(0,s.getCurrentCacheVersion)();return e.cacheVersion!==r||e.treeAtTimeOfPrefetch!==n||e.key.nextUrl!==t}function _(e){e.priority===s.PrefetchPriority.Intent&&e!==m&&(null!==m&&m.priority!==s.PrefetchPriority.Background&&(m.priority=s.PrefetchPriority.Default,$(d,m)),m=e)}function k(){g||(g=!0,f(O))}function P(e){return null===y&&(e.priority===s.PrefetchPriority.Intent?p<12:p<4)}function T(e){return p++,e.then(e=>null===e?(R(),null):(e.closed.then(R),e.value))}function R(){p--,k()}function x(e){e.isCanceled||-1!==e._heapIndex||(D(d,e),k())}function O(){g=!1;let e=Date.now(),t=U(d);for(;null!==t&&P(t);){t.cacheVersion=(0,s.getCurrentCacheVersion)();let n=function(e,t){let n=t.key,r=(0,o.readOrCreateRouteCacheEntry)(e,t,n),l=function(e,t,n){switch(n.status){case o.EntryStatus.Empty:T((0,o.fetchRouteOnCacheMiss)(n,t,t.key)),n.staleAt=e+6e4,n.status=o.EntryStatus.Pending;case o.EntryStatus.Pending:{let e=n.blockedTasks;return null===e?n.blockedTasks=new Set([t]):e.add(t),1}case o.EntryStatus.Rejected:break;case o.EntryStatus.Fulfilled:{if(0!==t.phase)return 2;if(!P(t))return 0;let r=n.tree,l=t.fetchStrategy===s.FetchStrategy.PPR?n.isPPREnabled?s.FetchStrategy.PPR:s.FetchStrategy.LoadingBoundary:t.fetchStrategy;switch(l){case s.FetchStrategy.PPR:{if(0===function e(t,n,r,l,a){let u=(0,o.readOrCreateSegmentCacheEntry)(t,n.fetchStrategy,r,a.cacheKey);M(t,n,r,u,n.key,a);let i=l[1],s=a.slots;if(null!==s)for(let l in s){if(!P(n))return 0;let a=s[l],u=a.segment,c=i[l],f=c?.[0];if(0===(void 0!==f&&I(r,u,f)?e(t,n,r,c,a):function e(t,n,r,l){if(l.hasRuntimePrefetch)return null===n.spawnedRuntimePrefetches?n.spawnedRuntimePrefetches=new Set([l.cacheKey]):n.spawnedRuntimePrefetches.add(l.cacheKey),2;let a=(0,o.readOrCreateSegmentCacheEntry)(t,n.fetchStrategy,r,l.cacheKey);if(M(t,n,r,a,n.key,l),null!==l.slots){if(!P(n))return 0;for(let a in l.slots)if(0===e(t,n,r,l.slots[a]))return 0}return 2}(t,n,r,a)))return 0}return 2}(e,t,n,t.treeAtTimeOfPrefetch,r))return 0;let l=t.spawnedRuntimePrefetches;if(null!==l){let a=new Map,u=function e(t,n,r,l,a,u){if(a.has(l.cacheKey))return C(t,n,r,l,!1,u,s.FetchStrategy.PPRRuntime);let o={},i=l.slots;if(null!==i)for(let l in i){let s=i[l];o[l]=e(t,n,r,s,a,u)}return[l.segment,o,null,null]}(e,t,n,r,l,a);a.size>0&&T((0,o.fetchSegmentPrefetchesUsingDynamicRequest)(t,n,s.FetchStrategy.PPRRuntime,u,a))}return 2}case s.FetchStrategy.Full:case s.FetchStrategy.PPRRuntime:case s.FetchStrategy.LoadingBoundary:{let u=new Map,i=function e(t,n,r,l,u,i,c){let f=l[1],d=u.slots,p={};if(null!==d)for(let l in d){let u=d[l],h=u.segment,g=f[l],m=g?.[0];if(void 0!==m&&I(r,h,m)){let a=e(t,n,r,g,u,i,c);p[l]=a}else switch(c){case s.FetchStrategy.LoadingBoundary:{let e=u.hasLoadingBoundary!==a.HasLoadingBoundary.SubtreeHasNoLoadingBoundary?function e(t,n,r,l,u,i){let c=null===u?"inside-shared-layout":null,f=(0,o.readOrCreateSegmentCacheEntry)(t,n.fetchStrategy,r,l.cacheKey);switch(f.status){case o.EntryStatus.Empty:i.set(l.cacheKey,(0,o.upgradeToPendingSegment)(f,s.FetchStrategy.LoadingBoundary)),"refetch"!==u&&(c=u="refetch");break;case o.EntryStatus.Fulfilled:if(l.hasLoadingBoundary===a.HasLoadingBoundary.SegmentHasLoadingBoundary)return(0,o.convertRouteTreeToFlightRouterState)(l);case o.EntryStatus.Pending:case o.EntryStatus.Rejected:}let d={};if(null!==l.slots)for(let a in l.slots){let o=l.slots[a];d[a]=e(t,n,r,o,u,i)}return[l.segment,d,null,c,l.isRootLayout]}(t,n,r,u,null,i):(0,o.convertRouteTreeToFlightRouterState)(u);p[l]=e;break}case s.FetchStrategy.PPRRuntime:{let e=C(t,n,r,u,!1,i,c);p[l]=e;break}case s.FetchStrategy.Full:{let e=C(t,n,r,u,!1,i,c);p[l]=e}}}return[u.segment,p,null,null,u.isRootLayout]}(e,t,n,t.treeAtTimeOfPrefetch,r,u,l),c=u.size>0;return!c&&n.isHeadPartial&&n.TODO_metadataStatus===o.EntryStatus.Empty&&(n.TODO_metadataStatus=o.EntryStatus.Fulfilled,c=!0,i[3]="metadata-only",i[1]={}),c&&T((0,o.fetchSegmentPrefetchesUsingDynamicRequest)(t,n,l,i,u)),2}}}}return 2}(e,t,r);if(0!==l&&""!==n.search){let r=new URL(n.href);r.search="";let l=(0,i.createCacheKey)(r.href,n.nextUrl),a=(0,o.readOrCreateRouteCacheEntry)(e,t,l);switch(a.status){case o.EntryStatus.Empty:N(t)&&(a.status=o.EntryStatus.Pending,T((0,o.fetchRouteOnCacheMiss)(a,t,l)));case o.EntryStatus.Pending:case o.EntryStatus.Fulfilled:case o.EntryStatus.Rejected:}}return l}(e,t),r=t.hasBackgroundWork;switch(t.hasBackgroundWork=!1,t.spawnedRuntimePrefetches=null,n){case 0:return;case 1:H(d),t=U(d);continue;case 2:1===t.phase?(t.phase=0,$(d,t)):r?(t.priority=s.PrefetchPriority.Background,$(d,t)):H(d),t=U(d);continue}}}function N(e){return e.priority===s.PrefetchPriority.Background||(e.hasBackgroundWork=!0,!1)}function C(e,t,n,r,l,a,u){let i=(0,o.readOrCreateSegmentCacheEntry)(e,u,n,r.cacheKey),s=null;switch(i.status){case o.EntryStatus.Empty:s=(0,o.upgradeToPendingSegment)(i,u);break;case o.EntryStatus.Fulfilled:i.isPartial&&(0,o.canNewFetchStrategyProvideMoreContent)(i.fetchStrategy,u)&&(s=j(e,n,i,r,u));break;case o.EntryStatus.Pending:case o.EntryStatus.Rejected:(0,o.canNewFetchStrategyProvideMoreContent)(i.fetchStrategy,u)&&(s=j(e,n,i,r,u))}let c={};if(null!==r.slots)for(let o in r.slots){let i=r.slots[o];c[o]=C(e,t,n,i,l||null!==s,a,u)}null!==s&&a.set(r.cacheKey,s);let f=l||null===s?null:"refetch";return[r.segment,c,null,f,r.isRootLayout]}function M(e,t,n,r,l,a){switch(r.status){case o.EntryStatus.Empty:T((0,o.fetchSegmentOnCacheMiss)(n,(0,o.upgradeToPendingSegment)(r,s.FetchStrategy.PPR),l,a));break;case o.EntryStatus.Pending:switch(r.fetchStrategy){case s.FetchStrategy.PPR:case s.FetchStrategy.PPRRuntime:case s.FetchStrategy.Full:break;case s.FetchStrategy.LoadingBoundary:N(t)&&L(e,t,r,n,l,a);break;default:r.fetchStrategy}break;case o.EntryStatus.Rejected:switch(r.fetchStrategy){case s.FetchStrategy.PPR:case s.FetchStrategy.PPRRuntime:case s.FetchStrategy.Full:break;case s.FetchStrategy.LoadingBoundary:L(e,t,r,n,l,a);break;default:r.fetchStrategy}case o.EntryStatus.Fulfilled:}}function L(e,t,n,r,l,a){let u=(0,o.readOrCreateRevalidatingSegmentEntry)(e,n);switch(u.status){case o.EntryStatus.Empty:z(t.fetchStrategy,r,a.cacheKey,T((0,o.fetchSegmentOnCacheMiss)(r,(0,o.upgradeToPendingSegment)(u,s.FetchStrategy.PPR),l,a)));case o.EntryStatus.Pending:case o.EntryStatus.Fulfilled:case o.EntryStatus.Rejected:}}function j(e,t,n,r,l){let a=(0,o.readOrCreateRevalidatingSegmentEntry)(e,n);if(a.status===o.EntryStatus.Empty){let e=(0,o.upgradeToPendingSegment)(a,l);return z(l,t,r.cacheKey,(0,o.waitForSegmentCacheEntry)(e)),e}if((0,o.canNewFetchStrategyProvideMoreContent)(a.fetchStrategy,l)){let e=(0,o.resetRevalidatingSegmentEntry)(a),n=(0,o.upgradeToPendingSegment)(e,l);return z(l,t,r.cacheKey,(0,o.waitForSegmentCacheEntry)(n)),n}switch(a.status){case o.EntryStatus.Pending:case o.EntryStatus.Fulfilled:case o.EntryStatus.Rejected:default:return null}}let A=()=>{};function z(e,t,n,r){r.then(r=>{if(null!==r){let l=(0,o.getSegmentKeypath)(e,t,n);(0,o.upsertSegmentEntry)(Date.now(),l,r)}},A)}function I(e,t,n){return n===c.PAGE_SEGMENT_KEY?t===(0,c.addSearchParamsIfPageSegment)(c.PAGE_SEGMENT_KEY,Object.fromEntries(new URLSearchParams(e.renderedSearch))):(0,u.matchSegment)(n,t)}function F(e,t){let n=t.priority-e.priority;if(0!==n)return n;let r=t.phase-e.phase;return 0!==r?r:t.sortId-e.sortId}function D(e,t){let n=e.length;e.push(t),t._heapIndex=n,B(e,t,n)}function U(e){return 0===e.length?null:e[0]}function H(e){if(0===e.length)return null;let t=e[0];t._heapIndex=-1;let n=e.pop();return n!==t&&(e[0]=n,n._heapIndex=0,V(e,n,0)),t}function $(e,t){let n=t._heapIndex;-1!==n&&(0===n?V(e,t,0):F(e[n-1>>>1],t)>0?B(e,t,n):V(e,t,n))}function B(e,t,n){let r=n;for(;r>0;){let n=r-1>>>1,l=e[n];if(!(F(l,t)>0))return;e[n]=t,t._heapIndex=n,e[r]=l,l._heapIndex=r,r=n}}function V(e,t,n){let r=n,l=e.length,a=l>>>1;for(;rF(a,t))uF(o,a)?(e[r]=o,o._heapIndex=r,e[u]=t,t._heapIndex=u,r=u):(e[r]=a,a._heapIndex=r,e[n]=t,t._heapIndex=n,r=n);else{if(!(uF(o,t)))return;e[r]=o,o._heapIndex=r,e[u]=t,t._heapIndex=u,r=u}}}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},10809,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"prefetch",{enumerable:!0,get:function(){return o}});let r=e.r(57630),l=e.r(3678),a=e.r(85018),u=e.r(65165);function o(e,t,n,o,i){let s=(0,r.createPrefetchURL)(e);if(null===s)return;let c=(0,l.createCacheKey)(s.href,t);(0,a.schedulePrefetchTask)(c,n,o,u.PrefetchPriority.Default,i)}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},48919,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"isNavigatingToNewRootLayout",{enumerable:!0,get:function(){return function e(t,n){let r=t[0],l=n[0];if(Array.isArray(r)&&Array.isArray(l)){if(r[0]!==l[0]||r[2]!==l[2])return!0}else if(r!==l)return!0;if(t[4])return!n[4];if(n[4])return!0;let a=Object.values(t[1])[0],u=Object.values(n[1])[0];return!a||!u||e(a,u)}}}),("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},95871,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r={abortTask:function(){return y},listenForDynamicRequest:function(){return m},startPPRNavigation:function(){return d},updateCacheNodeOnPopstateRestoration:function(){return function e(t,n){let r=n[1],l=t.parallelRoutes,a=new Map(l);for(let t in r){let n=r[t],u=n[0],o=(0,i.createRouterCacheKey)(u),s=l.get(t);if(void 0!==s){let r=s.get(o);if(void 0!==r){let l=e(r,n),u=new Map(s);u.set(o,l),a.set(t,u)}}}let u=t.rsc,o=S(u)&&"pending"===u.status;return{lazyData:null,rsc:u,head:t.head,prefetchHead:o?t.prefetchHead:[null,null],prefetchRsc:o?t.prefetchRsc:null,loading:t.loading,parallelRoutes:a,navigatedAt:t.navigatedAt}}}};for(var l in r)Object.defineProperty(n,l,{enumerable:!0,get:r[l]});let a=e.r(13258),u=e.r(56019),o=e.r(51191),i=e.r(70725),s=e.r(48919),c=e.r(54069),f={route:null,node:null,dynamicRequestTree:null,children:null};function d(e,t,n,r,l,s,c,d,g,m){return function e(t,n,r,l,s,c,d,g,m,y,v,b){let S=l[1],w=s[1],E=null!==d?d[1]:null;c||!0===s[4]&&(c=!0);let _=r.parallelRoutes,k=new Map(_),P={},T=null,R=!1,x={};for(let r in w){let l,s=w[r],d=S[r],O=_.get(r),N=null!==E?E[r]:null,C=s[0],M=v.concat([r,C]),L=(0,i.createRouterCacheKey)(C),j=void 0!==d?d[0]:void 0,A=void 0!==O?O.get(L):void 0;if(null!==(l=C===a.DEFAULT_SEGMENT_KEY?void 0!==d?function(e,t){let n;return"refresh"===t[3]?n=t:((n=h(t,t[1]))[2]=(0,o.createHrefFromUrl)(e),n[3]="refresh"),{route:n,node:null,dynamicRequestTree:null,children:null}}(n,d):p(t,d,s,A,c,void 0!==N?N:null,g,m,M,b):y&&0===Object.keys(s[1]).length?p(t,d,s,A,c,void 0!==N?N:null,g,m,M,b):void 0!==d&&void 0!==j&&(0,u.matchSegment)(C,j)&&void 0!==A&&void 0!==d?e(t,n,A,d,s,c,N,g,m,y,M,b):p(t,d,s,A,c,void 0!==N?N:null,g,m,M,b))){if(null===l.route)return f;null===T&&(T=new Map),T.set(r,l);let e=l.node;if(null!==e){let t=new Map(O);t.set(L,e),k.set(r,t)}let t=l.route;P[r]=t;let n=l.dynamicRequestTree;null!==n?(R=!0,x[r]=n):x[r]=t}else P[r]=s,x[r]=s}if(null===T)return null;let O={lazyData:null,rsc:r.rsc,prefetchRsc:r.prefetchRsc,head:r.head,prefetchHead:r.prefetchHead,loading:r.loading,parallelRoutes:k,navigatedAt:t};return{route:h(s,P),node:O,dynamicRequestTree:R?h(s,x):null,children:T}}(e,t,n,r,l,!1,s,c,d,g,[],m)}function p(e,t,n,r,l,a,u,o,d,p){return!l&&(void 0===t||(0,s.isNavigatingToNewRootLayout)(t,n))?f:function e(t,n,r,l,a,u,o,s){let f,d,p,m,y=n[1],v=0===Object.keys(y).length;if(void 0!==r&&r.navigatedAt+c.DYNAMIC_STALETIME_MS>t)f=r.rsc,d=r.loading,p=r.head,m=r.navigatedAt;else if(null===l)return g(t,n,null,a,u,o,s);else if(f=l[0],d=l[2],p=v?a:null,m=t,l[3]||u&&v)return g(t,n,l,a,u,o,s);let b=null!==l?l[1]:null,S=new Map,w=void 0!==r?r.parallelRoutes:null,E=new Map(w),_={},k=!1;if(v)s.push(o);else for(let n in y){let r=y[n],l=null!==b?b[n]:null,c=null!==w?w.get(n):void 0,f=r[0],d=o.concat([n,f]),p=(0,i.createRouterCacheKey)(f),h=e(t,r,void 0!==c?c.get(p):void 0,l,a,u,d,s);S.set(n,h);let g=h.dynamicRequestTree;null!==g?(k=!0,_[n]=g):_[n]=r;let m=h.node;if(null!==m){let e=new Map;e.set(p,m),E.set(n,e)}}return{route:n,node:{lazyData:null,rsc:f,prefetchRsc:null,head:p,prefetchHead:null,loading:d,parallelRoutes:E,navigatedAt:m},dynamicRequestTree:k?h(n,_):null,children:S}}(e,n,r,a,u,o,d,p)}function h(e,t){let n=[e[0],t];return 2 in e&&(n[2]=e[2]),3 in e&&(n[3]=e[3]),4 in e&&(n[4]=e[4]),n}function g(e,t,n,r,l,a,u){let o=h(t,t[1]);return o[3]="refetch",{route:t,node:function e(t,n,r,l,a,u,o){let s=n[1],c=null!==r?r[1]:null,f=new Map;for(let n in s){let r=s[n],d=null!==c?c[n]:null,p=r[0],h=u.concat([n,p]),g=(0,i.createRouterCacheKey)(p),m=e(t,r,void 0===d?null:d,l,a,h,o),y=new Map;y.set(g,m),f.set(n,y)}let d=0===f.size;d&&o.push(u);let p=null!==r?r[0]:null;return{lazyData:null,parallelRoutes:f,prefetchRsc:void 0!==p?p:null,prefetchHead:d?l:[null,null],rsc:w(),head:d?w():null,loading:null!==r?r[2]??null:w(),navigatedAt:t}}(e,t,n,r,l,a,u),dynamicRequestTree:o,children:null}}function m(e,t){t.then(t=>{if("string"==typeof t)return;let{flightData:n,debugInfo:r}=t;for(let t of n){let{segmentPath:n,tree:l,seedData:a,head:o}=t;a&&function(e,t,n,r,l,a){let o=e;for(let e=0;e{y(e,t,null)})}function y(e,t,n){let r=e.node;if(null===r)return;let l=e.children;if(null===l)v(e.route,r,t,n);else for(let e of l.values())y(e,t,n);e.dynamicRequestTree=null}function v(e,t,n,r){let l=e[1],a=t.parallelRoutes;for(let e in l){let t=l[e],u=a.get(e);if(void 0===u)continue;let o=t[0],s=(0,i.createRouterCacheKey)(o),c=u.get(s);void 0!==c&&v(t,c,n,r)}let u=t.rsc;S(u)&&(null===n?u.resolve(null,r):u.reject(n,r));let o=t.loading;S(o)&&o.resolve(null,r);let s=t.head;S(s)&&s.resolve(null,r)}let b=Symbol();function S(e){return e&&"object"==typeof e&&e.tag===b}function w(){let e,t,n=[],r=new Promise((n,r)=>{e=n,t=r});return r.status="pending",r.resolve=(t,l)=>{"pending"===r.status&&(r.status="fulfilled",r.value=t,null!==l&&n.push.apply(n,l),e(t))},r.reject=(e,l)=>{"pending"===r.status&&(r.status="rejected",r.reason=e,null!==l&&n.push.apply(n,l),t(e))},r.tag=b,r._debugInfo=n,r}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},15730,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"navigate",{enumerable:!0,get:function(){return c}});let r=e.r(87288),l=e.r(95871),a=e.r(51191),u=e.r(44743),o=e.r(3678),i=e.r(13258),s=e.r(65165);function c(e,t,n,r,l,a,i){let c=Date.now(),d=e.href,g=d===window.location.href,m=(0,o.createCacheKey)(d,l),y=(0,u.readRouteCacheEntry)(c,m);if(null!==y&&y.status===u.EntryStatus.Fulfilled){let u=p(c,y,y.tree);return f(c,e,t,l,g,n,r,u.flightRouterState,u.seedData,y.head,y.isHeadPartial,y.canonicalUrl,y.renderedSearch,a,e.hash)}if(null===y||y.status!==u.EntryStatus.Rejected){let o=(0,u.requestOptimisticRouteCacheEntry)(c,e,l);if(null!==o){let u=p(c,o,o.tree);return f(c,e,t,l,g,n,r,u.flightRouterState,u.seedData,o.head,o.isHeadPartial,o.canonicalUrl,o.renderedSearch,a,e.hash)}}let v=i.collectedDebugInfo??[];return void 0===i.collectedDebugInfo&&(v=i.collectedDebugInfo=[]),{tag:s.NavigationResultTag.Async,data:h(c,e,t,l,g,n,r,a,e.hash,v)}}function f(e,t,n,a,u,o,i,c,f,p,h,g,m,y,v){let b=[],S=(0,l.startPPRNavigation)(e,n,o,i,c,f,p,h,u,b);if(null!==S){let e=S.dynamicRequestTree;if(null!==e){let n=(0,r.fetchServerResponse)(new URL(g,t.origin),{flightRouterState:e,nextUrl:a});(0,l.listenForDynamicRequest)(S,n)}return d(S,o,g,m,b,y,v)}return{tag:s.NavigationResultTag.NoOp,data:{canonicalUrl:g,shouldScroll:y}}}function d(e,t,n,r,l,a,u){let o=e.route;if(null===o)return{tag:s.NavigationResultTag.MPA,data:n};let i=e.node;return{tag:s.NavigationResultTag.Success,data:{flightRouterState:o,cacheNode:null!==i?i:t,canonicalUrl:n,renderedSearch:r,scrollableSegments:l,shouldScroll:a,hash:u}}}function p(e,t,n){let r={},l={},a=n.slots;if(null!==a)for(let n in a){let u=p(e,t,a[n]);r[n]=u.flightRouterState,l[n]=u.seedData}let o=null,s=null,c=!0,f=(0,u.readSegmentCacheEntry)(e,t,n.cacheKey);if(null!==f)switch(f.status){case u.EntryStatus.Fulfilled:o=f.rsc,s=f.loading,c=f.isPartial;break;case u.EntryStatus.Pending:{let e=(0,u.waitForSegmentCacheEntry)(f);o=e.then(e=>null!==e?e.rsc:null),s=e.then(e=>null!==e?e.loading:null),c=!0}case u.EntryStatus.Empty:case u.EntryStatus.Rejected:}return{flightRouterState:[(0,i.addSearchParamsIfPageSegment)(n.segment,Object.fromEntries(new URLSearchParams(t.renderedSearch))),r,null,null,n.isRootLayout],seedData:[o,l,s,c,!1]}}async function h(e,t,n,u,o,i,c,f,p,h){let g=(0,r.fetchServerResponse)(t,{flightRouterState:c,nextUrl:u}),m=await g;if("string"==typeof m)return{tag:s.NavigationResultTag.MPA,data:m};let{flightData:y,canonicalUrl:v,renderedSearch:b,debugInfo:S}=m;null!==S&&h.push(...S);let w=function(e,t){let n=e;for(let{segmentPath:r,tree:l}of t){let t=n!==e;n=function e(t,n,r,l,a){if(a===r.length)return n;let u=r[a],o=t[1],i={};for(let t in o)if(t===u){let u=o[t];i[t]=e(u,n,r,l,a+2)}else i[t]=o[t];if(l)return t[1]=i,t;let s=[t[0],i];return 2 in t&&(s[2]=t[2]),3 in t&&(s[3]=t[3]),4 in t&&(s[4]=t[4]),s}(n,l,r,t,0)}return n}(c,y),E=[],_=(0,l.startPPRNavigation)(e,n,i,c,w,null,null,!0,o,E);return null!==_?(null!==_.dynamicRequestTree&&(0,l.listenForDynamicRequest)(_,g),d(_,i,(0,a.createHrefFromUrl)(v),b,E,f,p)):{tag:s.NavigationResultTag.NoOp,data:{canonicalUrl:(0,a.createHrefFromUrl)(v),shouldScroll:f}}}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},65165,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r,l,a,u={FetchStrategy:function(){return b},NavigationResultTag:function(){return y},PrefetchPriority:function(){return v},cancelPrefetchTask:function(){return p},createCacheKey:function(){return m},getCurrentCacheVersion:function(){return f},isPrefetchTaskDirty:function(){return g},navigate:function(){return s},prefetch:function(){return i},reschedulePrefetchTask:function(){return h},revalidateEntireCache:function(){return c},schedulePrefetchTask:function(){return d}};for(var o in u)Object.defineProperty(n,o,{enumerable:!0,get:u[o]});let i=function(...t){return e.r(10809).prefetch(...t)},s=function(...t){return e.r(15730).navigate(...t)},c=function(...t){return e.r(44743).revalidateEntireCache(...t)},f=function(...t){return e.r(44743).getCurrentCacheVersion(...t)},d=function(...t){return e.r(85018).schedulePrefetchTask(...t)},p=function(...t){return e.r(85018).cancelPrefetchTask(...t)},h=function(...t){return e.r(85018).reschedulePrefetchTask(...t)},g=function(...t){return e.r(85018).isPrefetchTaskDirty(...t)},m=function(...t){return e.r(3678).createCacheKey(...t)};var y=((r={})[r.MPA=0]="MPA",r[r.Success=1]="Success",r[r.NoOp=2]="NoOp",r[r.Async=3]="Async",r),v=((l={})[l.Intent=2]="Intent",l[l.Default=1]="Default",l[l.Background=0]="Background",l),b=((a={})[a.LoadingBoundary=0]="LoadingBoundary",a[a.PPR=1]="PPR",a[a.PPRRuntime=2]="PPRRuntime",a[a.Full=3]="Full",a);("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},54069,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r={DYNAMIC_STALETIME_MS:function(){return i},STATIC_STALETIME_MS:function(){return s},generateSegmentsFromPatch:function(){return function e(t){let n=[],[r,l]=t;if(0===Object.keys(l).length)return[[r]];for(let[t,a]of Object.entries(l))for(let l of e(a))""===r?n.push([t,...l]):n.push([r,t,...l]);return n}},handleExternalUrl:function(){return c},navigateReducer:function(){return f}};for(var l in r)Object.defineProperty(n,l,{enumerable:!0,get:r[l]});let a=e.r(51191),u=e.r(47442),o=e.r(65165),i=1e3*Number("0"),s=1e3*Number("300");function c(e,t,n,r){return t.mpaNavigation=!0,t.canonicalUrl=n,t.pendingPush=r,t.scrollableSegments=void 0,(0,u.handleMutable)(e,t)}function f(e,t){let{url:n,isExternalUrl:r,navigateType:l,shouldScroll:i}=t,s={},f=(0,a.createHrefFromUrl)(n),d="push"===l;if(s.preserveCustomHistoryState=!1,s.pendingPush=d,r)return c(e,s,n.toString(),d);if(document.getElementById("__next-page-redirect"))return c(e,s,f,d);let p=new URL(e.canonicalUrl,location.origin),h=(0,o.navigate)(n,p,e.cache,e.tree,e.nextUrl,i,s);return function e(t,n,r,l,a){switch(a.tag){case o.NavigationResultTag.MPA:return c(n,r,a.data,l);case o.NavigationResultTag.NoOp:{r.canonicalUrl=a.data.canonicalUrl;let e=new URL(n.canonicalUrl,t);return t.pathname===e.pathname&&t.search===e.search&&t.hash!==e.hash&&(r.onlyHashChange=!0,r.shouldScroll=a.data.shouldScroll,r.hashFragment=t.hash,r.scrollableSegments=[]),(0,u.handleMutable)(n,r)}case o.NavigationResultTag.Success:return r.cache=a.data.cacheNode,r.patchedTree=a.data.flightRouterState,r.renderedSearch=a.data.renderedSearch,r.canonicalUrl=a.data.canonicalUrl,r.scrollableSegments=a.data.scrollableSegments,r.shouldScroll=a.data.shouldScroll,r.hashFragment=a.data.hash,(0,u.handleMutable)(n,r);case o.NavigationResultTag.Async:return a.data.then(a=>e(t,n,r,l,a),()=>n);default:return n}}(n,e,s,d,h)}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},1764,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"fillLazyItemsTillLeafWithHead",{enumerable:!0,get:function(){return function e(t,n,l,a,u,o){if(0===Object.keys(a[1]).length){n.head=o;return}for(let i in a[1]){let s,c=a[1][i],f=c[0],d=(0,r.createRouterCacheKey)(f),p=null!==u&&void 0!==u[1][i]?u[1][i]:null;if(l){let r=l.parallelRoutes.get(i);if(r){let l,a=new Map(r),u=a.get(d);l=null!==p?{lazyData:null,rsc:p[0],prefetchRsc:null,head:null,prefetchHead:null,loading:p[2],parallelRoutes:new Map(u?.parallelRoutes),navigatedAt:t}:{lazyData:null,rsc:null,prefetchRsc:null,head:null,prefetchHead:null,parallelRoutes:new Map(u?.parallelRoutes),loading:null,navigatedAt:t},a.set(d,l),e(t,l,u,c,p||null,o),n.parallelRoutes.set(i,a);continue}}if(null!==p){let e=p[0],n=p[2];s={lazyData:null,rsc:e,prefetchRsc:null,head:null,prefetchHead:null,parallelRoutes:new Map,loading:n,navigatedAt:t}}else s={lazyData:null,rsc:null,prefetchRsc:null,head:null,prefetchHead:null,parallelRoutes:new Map,loading:null,navigatedAt:t};let h=n.parallelRoutes.get(i);h?h.set(d,s):n.parallelRoutes.set(i,new Map([[d,s]])),e(t,s,void 0,c,p,o)}}}});let r=e.r(70725);("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},51565,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"invalidateCacheByRouterState",{enumerable:!0,get:function(){return l}});let r=e.r(70725);function l(e,t,n){for(let l in n[1]){let a=n[1][l][0],u=(0,r.createRouterCacheKey)(a),o=t.parallelRoutes.get(l);if(o){let t=new Map(o);t.delete(u),e.parallelRoutes.set(l,t)}}}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},87752,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r={fillCacheWithNewSubTreeData:function(){return c},fillCacheWithNewSubTreeDataButOnlyLoading:function(){return f}};for(var l in r)Object.defineProperty(n,l,{enumerable:!0,get:r[l]});let a=e.r(51565),u=e.r(1764),o=e.r(70725),i=e.r(13258);function s(e,t,n,r,l){let{segmentPath:s,seedData:c,tree:f,head:d}=r,p=t,h=n;for(let t=0;t{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"applyFlightData",{enumerable:!0,get:function(){return a}});let r=e.r(1764),l=e.r(87752);function a(e,t,n,a){let{tree:u,seedData:o,head:i,isRootRender:s}=a;if(null===o)return!1;if(s){let l=o[0];n.loading=o[2],n.rsc=l,n.prefetchRsc=null,(0,r.fillLazyItemsTillLeafWithHead)(e,n,t,u,o,i)}else n.rsc=t.rsc,n.prefetchRsc=t.prefetchRsc,n.parallelRoutes=new Map(t.parallelRoutes),n.loading=t.loading,(0,l.fillCacheWithNewSubTreeData)(e,n,t,a);return!0}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},13576,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r={addRefreshMarkerToActiveParallelSegments:function(){return function e(t,n){let[r,l,,a]=t;for(let u in r.includes(o.PAGE_SEGMENT_KEY)&&"refresh"!==a&&(t[2]=n,t[3]="refresh"),l)e(l[u],n)}},refreshInactiveParallelSegments:function(){return i}};for(var l in r)Object.defineProperty(n,l,{enumerable:!0,get:r[l]});let a=e.r(10827),u=e.r(87288),o=e.r(13258);async function i(e){let t=new Set;await s({...e,rootTree:e.updatedTree,fetchedSegments:t})}async function s({navigatedAt:e,state:t,updatedTree:n,updatedCache:r,includeNextUrl:l,fetchedSegments:o,rootTree:i=n,canonicalUrl:c}){let[,f,d,p]=n,h=[];if(d&&d!==c&&"refresh"===p&&!o.has(d)){o.add(d);let n=(0,u.fetchServerResponse)(new URL(d,location.origin),{flightRouterState:[i[0],i[1],i[2],"refetch"],nextUrl:l?t.nextUrl:null}).then(t=>{if("string"!=typeof t){let{flightData:n}=t;for(let t of n)(0,a.applyFlightData)(e,r,r,t)}});h.push(n)}for(let n in f){let a=s({navigatedAt:e,state:t,updatedTree:f[n],updatedCache:r,includeNextUrl:l,fetchedSegments:o,rootTree:i,canonicalUrl:c});h.push(a)}await Promise.all(h)}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},22719,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"applyRouterStatePatchToTree",{enumerable:!0,get:function(){return function e(t,n,r,i){let s,[c,f,d,p,h]=n;if(1===t.length){let e=o(n,r);return(0,u.addRefreshMarkerToActiveParallelSegments)(e,i),e}let[g,m]=t;if(!(0,a.matchSegment)(g,c))return null;if(2===t.length)s=o(f[m],r);else if(null===(s=e((0,l.getNextFlightSegmentPath)(t),f[m],r,i)))return null;let y=[t[0],{...f,[m]:s},d,p];return h&&(y[4]=!0),(0,u.addRefreshMarkerToActiveParallelSegments)(y,i),y}}});let r=e.r(13258),l=e.r(50590),a=e.r(56019),u=e.r(13576);function o(e,t){let[n,l]=e,[u,i]=t;if(u===r.DEFAULT_SEGMENT_KEY&&n!==r.DEFAULT_SEGMENT_KEY)return e;if((0,a.matchSegment)(n,u)){let t={};for(let e in l)void 0!==i[e]?t[e]=o(l[e],i[e]):t[e]=l[e];for(let e in i)t[e]||(t[e]=i[e]);let r=[n,t];return e[2]&&(r[2]=e[2]),e[3]&&(r[3]=e[3]),e[4]&&(r[4]=e[4]),r}return t}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},62634,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"AppRouterAnnouncer",{enumerable:!0,get:function(){return u}});let r=e.r(71645),l=e.r(74080),a="next-route-announcer";function u({tree:e}){let[t,n]=(0,r.useState)(null);(0,r.useEffect)(()=>(n(function(){let e=document.getElementsByName(a)[0];if(e?.shadowRoot?.childNodes[0])return e.shadowRoot.childNodes[0];{let e=document.createElement(a);e.style.cssText="position:absolute";let t=document.createElement("div");return t.ariaLive="assertive",t.id="__next-route-announcer__",t.role="alert",t.style.cssText="position:absolute;border:0;height:1px;margin:-1px;padding:0;width:1px;clip:rect(0 0 0 0);overflow:hidden;white-space:nowrap;word-wrap:normal",e.attachShadow({mode:"open"}).appendChild(t),document.body.appendChild(e),t}}()),()=>{let e=document.getElementsByTagName(a)[0];e?.isConnected&&document.body.removeChild(e)}),[]);let[u,o]=(0,r.useState)(""),i=(0,r.useRef)(void 0);return(0,r.useEffect)(()=>{let e="";if(document.title)e=document.title;else{let t=document.querySelector("h1");t&&(e=t.innerText||t.textContent||"")}void 0!==i.current&&i.current!==e&&o(e),i.current=e},[e]),t?(0,l.createPortal)(u,t):null}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},25018,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"findHeadInCache",{enumerable:!0,get:function(){return a}});let r=e.r(13258),l=e.r(70725);function a(e,t){return function e(t,n,a,u){if(0===Object.keys(n).length)return[t,a,u];let o=Object.keys(n).filter(e=>"children"!==e);for(let u of("children"in n&&o.unshift("children"),o)){let[o,i]=n[u];if(o===r.DEFAULT_SEGMENT_KEY)continue;let s=t.parallelRoutes.get(u);if(!s)continue;let c=(0,l.createRouterCacheKey)(o),f=(0,l.createRouterCacheKey)(o,!0),d=s.get(c);if(!d)continue;let p=e(d,i,a+"/"+c,a+"/"+f);if(p)return p}return null}(e,t,"","")}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},39584,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"pathHasPrefix",{enumerable:!0,get:function(){return l}});let r=e.r(72463);function l(e,t){if("string"!=typeof e)return!1;let{pathname:n}=(0,r.parsePath)(e);return n===t||n.startsWith(t+"/")}},52817,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"hasBasePath",{enumerable:!0,get:function(){return l}});let r=e.r(39584);function l(e){return(0,r.pathHasPrefix)(e,"/login")}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},87250,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"removeBasePath",{enumerable:!0,get:function(){return l}}),e.r(52817);let r="/login";function l(e){return 0===r.length||(e=e.slice(r.length)).startsWith("/")||(e=`/${e}`),e}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},41624,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r={GracefulDegradeBoundary:function(){return o},default:function(){return i}};for(var l in r)Object.defineProperty(n,l,{enumerable:!0,get:r[l]});let a=e.r(43476),u=e.r(71645);class o extends u.Component{constructor(e){super(e),this.state={hasError:!1},this.rootHtml="",this.htmlAttributes={},this.htmlRef=(0,u.createRef)()}static getDerivedStateFromError(e){return{hasError:!0}}componentDidMount(){let e=this.htmlRef.current;this.state.hasError&&e&&Object.entries(this.htmlAttributes).forEach(([t,n])=>{e.setAttribute(t,n)})}render(){let{hasError:e}=this.state;return("undefined"==typeof window||this.rootHtml||(this.rootHtml=document.documentElement.innerHTML,this.htmlAttributes=function(e){let t={};for(let n=0;n{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"default",{enumerable:!0,get:function(){return s}});let r=e.r(55682),l=e.r(43476);e.r(71645);let a=r._(e.r(41624)),u=e.r(72383),o=e.r(82604),i="undefined"!=typeof window&&(0,o.isBot)(window.navigator.userAgent);function s({children:e,errorComponent:t,errorStyles:n,errorScripts:r}){return i?(0,l.jsx)(a.default,{children:e}):(0,l.jsx)(u.ErrorBoundary,{errorComponent:t,errorStyles:n,errorScripts:r,children:e})}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},75530,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r={createEmptyCacheNode:function(){return C},default:function(){return A}};for(var l in r)Object.defineProperty(n,l,{enumerable:!0,get:r[l]});let a=e.r(55682),u=e.r(90809),o=e.r(43476),i=u._(e.r(71645)),s=e.r(8372),c=e.r(88540),f=e.r(51191),d=e.r(61994),p=e.r(41538),h=e.r(62634),g=e.r(58442),m=e.r(25018),y=e.r(1244),v=e.r(87250),b=e.r(52817),S=e.r(34727),w=e.r(78377),E=e.r(99781),_=e.r(24063),k=e.r(68391),P=e.r(91949),T=a._(e.r(94109)),R=a._(e.r(68027)),x=e.r(97367),O={};function N({appRouterState:e}){return(0,i.useInsertionEffect)(()=>{let{tree:t,pushRef:n,canonicalUrl:r,renderedSearch:l}=e,a={...n.preserveCustomHistoryState?window.history.state:{},__NA:!0,__PRIVATE_NEXTJS_INTERNALS_TREE:{tree:t,renderedSearch:l}};n.pendingPush&&(0,f.createHrefFromUrl)(new URL(window.location.href))!==r?(n.pendingPush=!1,window.history.pushState(a,"",r)):window.history.replaceState(a,"",r)},[e]),(0,i.useEffect)(()=>{(0,P.pingVisibleLinks)(e.nextUrl,e.tree)},[e.nextUrl,e.tree]),null}function C(){return{lazyData:null,rsc:null,prefetchRsc:null,head:null,prefetchHead:null,parallelRoutes:new Map,loading:null,navigatedAt:-1}}function M(e){null==e&&(e={});let t=window.history.state,n=t?.__NA;n&&(e.__NA=n);let r=t?.__PRIVATE_NEXTJS_INTERNALS_TREE;return r&&(e.__PRIVATE_NEXTJS_INTERNALS_TREE=r),e}function L({headCacheNode:e}){let t=null!==e?e.head:null,n=null!==e?e.prefetchHead:null,r=null!==n?n:t;return(0,i.useDeferredValue)(t,r)}function j({actionQueue:e,globalError:t,webSocket:n,staticIndicatorState:r}){let l,a=(0,p.useActionQueue)(e),{canonicalUrl:u}=a,{searchParams:f,pathname:w}=(0,i.useMemo)(()=>{let e=new URL(u,"undefined"==typeof window?"http://n":window.location.href);return{searchParams:e.searchParams,pathname:(0,b.hasBasePath)(e.pathname)?(0,v.removeBasePath)(e.pathname):e.pathname}},[u]);(0,i.useEffect)(()=>{function e(e){e.persisted&&window.history.state?.__PRIVATE_NEXTJS_INTERNALS_TREE&&(O.pendingMpaPath=void 0,(0,p.dispatchAppRouterAction)({type:c.ACTION_RESTORE,url:new URL(window.location.href),historyState:window.history.state.__PRIVATE_NEXTJS_INTERNALS_TREE}))}return window.addEventListener("pageshow",e),()=>{window.removeEventListener("pageshow",e)}},[]),(0,i.useEffect)(()=>{function e(e){let t="reason"in e?e.reason:e.error;if((0,k.isRedirectError)(t)){e.preventDefault();let n=(0,_.getURLFromRedirectError)(t);(0,_.getRedirectTypeFromError)(t)===k.RedirectType.push?E.publicAppRouterInstance.push(n,{}):E.publicAppRouterInstance.replace(n,{})}}return window.addEventListener("error",e),window.addEventListener("unhandledrejection",e),()=>{window.removeEventListener("error",e),window.removeEventListener("unhandledrejection",e)}},[]);let{pushRef:P}=a;if(P.mpaNavigation){if(O.pendingMpaPath!==u){let e=window.location;P.pendingPush?e.assign(u):e.replace(u),O.pendingMpaPath=u}throw y.unresolvedThenable}(0,i.useEffect)(()=>{let e=window.history.pushState.bind(window.history),t=window.history.replaceState.bind(window.history),n=e=>{let t=window.location.href,n=window.history.state?.__PRIVATE_NEXTJS_INTERNALS_TREE;(0,i.startTransition)(()=>{(0,p.dispatchAppRouterAction)({type:c.ACTION_RESTORE,url:new URL(e??t,t),historyState:n})})};window.history.pushState=function(t,r,l){return t?.__NA||t?._N||(t=M(t),l&&n(l)),e(t,r,l)},window.history.replaceState=function(e,r,l){return e?.__NA||e?._N||(e=M(e),l&&n(l)),t(e,r,l)};let r=e=>{if(e.state){if(!e.state.__NA)return void window.location.reload();(0,i.startTransition)(()=>{(0,E.dispatchTraverseAction)(window.location.href,e.state.__PRIVATE_NEXTJS_INTERNALS_TREE)})}};return window.addEventListener("popstate",r),()=>{window.history.pushState=e,window.history.replaceState=t,window.removeEventListener("popstate",r)}},[]);let{cache:R,tree:C,nextUrl:j,focusAndScrollRef:A,previousNextUrl:z}=a,I=(0,i.useMemo)(()=>(0,m.findHeadInCache)(R,C[1]),[R,C]),D=(0,i.useMemo)(()=>(0,S.getSelectedParams)(C),[C]),U=(0,i.useMemo)(()=>({parentTree:C,parentCacheNode:R,parentSegmentPath:null,parentParams:{},debugNameContext:"/",url:u,isActive:!0}),[C,R,u]),H=(0,i.useMemo)(()=>({tree:C,focusAndScrollRef:A,nextUrl:j,previousNextUrl:z}),[C,A,j,z]);if(null!==I){let[e,t,n]=I;l=(0,o.jsx)(L,{headCacheNode:e},"undefined"==typeof window?n:t)}else l=null;let $=(0,o.jsxs)(g.RedirectBoundary,{children:[l,(0,o.jsx)(x.RootLayoutBoundary,{children:R.rsc}),(0,o.jsx)(h.AppRouterAnnouncer,{tree:C})]});return $=(0,o.jsx)(T.default,{errorComponent:t[0],errorStyles:t[1],children:$}),(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(N,{appRouterState:a}),(0,o.jsx)(F,{}),(0,o.jsx)(d.NavigationPromisesContext.Provider,{value:null,children:(0,o.jsx)(d.PathParamsContext.Provider,{value:D,children:(0,o.jsx)(d.PathnameContext.Provider,{value:w,children:(0,o.jsx)(d.SearchParamsContext.Provider,{value:f,children:(0,o.jsx)(s.GlobalLayoutRouterContext.Provider,{value:H,children:(0,o.jsx)(s.AppRouterContext.Provider,{value:E.publicAppRouterInstance,children:(0,o.jsx)(s.LayoutRouterContext.Provider,{value:U,children:$})})})})})})})]})}function A({actionQueue:e,globalErrorState:t,webSocket:n,staticIndicatorState:r}){(0,w.useNavFailureHandler)();let l=(0,o.jsx)(j,{actionQueue:e,globalError:t,webSocket:n,staticIndicatorState:r});return(0,o.jsx)(T.default,{errorComponent:R.default,children:l})}let z=new Set,I=new Set;function F(){let[,e]=i.default.useState(0),t=z.size;return(0,i.useEffect)(()=>{let n=()=>e(e=>e+1);return I.add(n),t!==z.size&&n(),()=>{I.delete(n)}},[t,e]),[...z].map((e,t)=>(0,o.jsx)("link",{rel:"stylesheet",href:`${e}`,precedence:"next"},t))}globalThis._N_E_STYLE_LOAD=function(e){let t=z.size;return z.add(e),z.size!==t&&I.forEach(e=>e()),Promise.resolve()},("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},91668,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"serverPatchReducer",{enumerable:!0,get:function(){return c}});let r=e.r(51191),l=e.r(22719),a=e.r(48919),u=e.r(54069),o=e.r(10827),i=e.r(47442),s=e.r(75530);function c(e,t){let{serverResponse:n,navigatedAt:c}=t,f={};if(f.preserveCustomHistoryState=!1,"string"==typeof n)return(0,u.handleExternalUrl)(e,f,n,e.pushRef.pendingPush);let{flightData:d,canonicalUrl:p,renderedSearch:h}=n,g=e.tree,m=e.cache;for(let t of d){let{segmentPath:n,tree:i}=t,d=(0,l.applyRouterStatePatchToTree)(["",...n],g,i,e.canonicalUrl);if(null===d)return e;if((0,a.isNavigatingToNewRootLayout)(g,d))return(0,u.handleExternalUrl)(e,f,e.canonicalUrl,e.pushRef.pendingPush);f.canonicalUrl=(0,r.createHrefFromUrl)(p);let y=(0,s.createEmptyCacheNode)();(0,o.applyFlightData)(c,m,y,t),f.patchedTree=d,f.renderedSearch=h,f.cache=y,m=y,g=d}return(0,i.handleMutable)(e,f)}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},73790,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"restoreReducer",{enumerable:!0,get:function(){return a}});let r=e.r(51191),l=e.r(34727);function a(e,t){let n,a,{url:u,historyState:o}=t,i=(0,r.createHrefFromUrl)(u);o?(n=o.tree,a=o.renderedSearch):(n=e.tree,a=e.renderedSearch);let s=e.cache;return{canonicalUrl:i,renderedSearch:a,pushRef:{pendingPush:!1,mpaNavigation:!1,preserveCustomHistoryState:!0},focusAndScrollRef:e.focusAndScrollRef,cache:s,tree:n,nextUrl:(0,l.extractPathFromFlightRouterState)(n)??u.pathname,previousNextUrl:null,debugInfo:null}}e.r(95871),("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},54003,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"handleSegmentMismatch",{enumerable:!0,get:function(){return l}});let r=e.r(54069);function l(e,t,n){return(0,r.handleExternalUrl)(e,{},e.canonicalUrl,!0)}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},69845,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"refreshReducer",{enumerable:!0,get:function(){return g}});let r=e.r(87288),l=e.r(51191),a=e.r(22719),u=e.r(48919),o=e.r(54069),i=e.r(47442),s=e.r(1764),c=e.r(75530),f=e.r(54003),d=e.r(84356),p=e.r(13576),h=e.r(65165);function g(e,t){let{origin:n}=t,g={},m=e.canonicalUrl,y=e.tree;g.preserveCustomHistoryState=!1;let v=(0,c.createEmptyCacheNode)(),b=(0,d.hasInterceptionRouteInCurrentTree)(e.tree);v.lazyData=(0,r.fetchServerResponse)(new URL(m,n),{flightRouterState:[y[0],y[1],y[2],"refetch"],nextUrl:b?e.nextUrl:null});let S=Date.now();return v.lazyData.then(async n=>{if("string"==typeof n)return(0,o.handleExternalUrl)(e,g,n,e.pushRef.pendingPush);let{flightData:r,canonicalUrl:c,renderedSearch:d}=n;for(let n of(v.lazyData=null,r)){let{tree:r,seedData:i,head:w,isRootRender:E}=n;if(!E)return console.log("REFRESH FAILED"),e;let _=(0,a.applyRouterStatePatchToTree)([""],y,r,e.canonicalUrl);if(null===_)return(0,f.handleSegmentMismatch)(e,t,r);if((0,u.isNavigatingToNewRootLayout)(y,_))return(0,o.handleExternalUrl)(e,g,m,e.pushRef.pendingPush);if(g.canonicalUrl=(0,l.createHrefFromUrl)(c),null!==i){let t=i[0],n=i[2];v.rsc=t,v.prefetchRsc=null,v.loading=n,(0,s.fillLazyItemsTillLeafWithHead)(S,v,void 0,r,i,w),(0,h.revalidateEntireCache)(e.nextUrl,_)}await (0,p.refreshInactiveParallelSegments)({navigatedAt:S,state:e,updatedTree:_,updatedCache:v,includeNextUrl:b,canonicalUrl:g.canonicalUrl||e.canonicalUrl}),g.cache=v,g.patchedTree=_,g.renderedSearch=d,y=_}return(0,i.handleMutable)(e,g)},()=>e)}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},86720,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"hmrRefreshReducer",{enumerable:!0,get:function(){return r}}),e.r(87288),e.r(51191),e.r(22719),e.r(48919),e.r(54069),e.r(47442),e.r(10827),e.r(75530),e.r(54003),e.r(84356);let r=function(e,t){return e};("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},27801,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"assignLocation",{enumerable:!0,get:function(){return l}});let r=e.r(5550);function l(e,t){if(e.startsWith(".")){let n=t.origin+t.pathname;return new URL((n.endsWith("/")?n:n+"/")+e)}return new URL((0,r.addBasePath)(e),t.href)}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},39747,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r={extractInfoFromServerReferenceId:function(){return a},omitUnusedArgs:function(){return u}};for(var l in r)Object.defineProperty(n,l,{enumerable:!0,get:r[l]});function a(e){let t=parseInt(e.slice(0,2),16),n=t>>1&63,r=Array(6);for(let e=0;e<6;e++){let t=n>>5-e&1;r[e]=1===t}return{type:1==(t>>7&1)?"use-cache":"server-action",usedArgs:r,hasRestArgs:1==(1&t)}}function u(e,t){let n=Array(e.length);for(let r=0;r=6&&t.hasRestArgs)&&(n[r]=e[r]);return n}},45794,(e,t,n)=>{"use strict";let r;Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"serverActionReducer",{enumerable:!0,get:function(){return N}});let l=e.r(32120),a=e.r(92245),u=e.r(21768),o=e.r(92838),i=e.r(35326),s=e.r(27801),c=e.r(51191),f=e.r(54069),d=e.r(22719),p=e.r(48919),h=e.r(47442),g=e.r(1764),m=e.r(75530),y=e.r(84356),v=e.r(54003),b=e.r(13576),S=e.r(50590),w=e.r(24063),E=e.r(68391),_=e.r(87250),k=e.r(52817),P=e.r(39747),T=e.r(65165),R=i.createFromFetch;async function x(e,t,{actionId:n,actionArgs:c}){let f,d,p,h,g=(0,i.createTemporaryReferenceSet)(),m=(0,P.extractInfoFromServerReferenceId)(n),y="use-cache"===m.type?(0,P.omitUnusedArgs)(c,m):c,v=await (0,i.encodeReply)(y,{temporaryReferences:g}),b={Accept:u.RSC_CONTENT_TYPE_HEADER,[u.ACTION_HEADER]:n,[u.NEXT_ROUTER_STATE_TREE_HEADER]:(0,S.prepareFlightRouterStateForRequest)(e.tree)};t&&(b[u.NEXT_URL]=t);let w=await fetch(e.canonicalUrl,{method:"POST",headers:b,body:v});if("1"===w.headers.get(u.NEXT_ACTION_NOT_FOUND_HEADER))throw Object.defineProperty(new o.UnrecognizedActionError(`Server Action "${n}" was not found on the server. -Read more: https://nextjs.org/docs/messages/failed-to-find-server-action`),"__NEXT_ERROR_CODE",{value:"E715",enumerable:!1,configurable:!0});let _=w.headers.get("x-action-redirect"),[k,T]=_?.split(";")||[];switch(T){case"push":f=E.RedirectType.push;break;case"replace":f=E.RedirectType.replace;break;default:f=void 0}let x=!!w.headers.get(u.NEXT_IS_PRERENDER_HEADER);try{let e=JSON.parse(w.headers.get("x-action-revalidated")||"[[],0,0]");d={paths:e[0]||[],tag:!!e[1],cookie:e[2]}}catch(e){d=O}let N=k?(0,s.assignLocation)(k,new URL(e.canonicalUrl,window.location.href)):void 0,C=w.headers.get("content-type"),M=!!(C&&C.startsWith(u.RSC_CONTENT_TYPE_HEADER));if(!M&&!N)throw Object.defineProperty(Error(w.status>=400&&"text/plain"===C?await w.text():"An unexpected response was received from the server."),"__NEXT_ERROR_CODE",{value:"E394",enumerable:!1,configurable:!0});if(M){let e=await R(Promise.resolve(w),{callServer:l.callServer,findSourceMapURL:a.findSourceMapURL,temporaryReferences:g,debugChannel:r&&r(b)});p=N?void 0:e.a,h=(0,S.normalizeFlightData)(e.f)}else p=void 0,h=void 0;return{actionResult:p,actionFlightData:h,redirectLocation:N,redirectType:f,revalidatedParts:d,isPrerender:x}}let O={paths:[],tag:!1,cookie:!1};function N(e,t){let{resolve:n,reject:r}=t,l={},a=e.tree;l.preserveCustomHistoryState=!1;let u=(e.previousNextUrl||e.nextUrl)&&(0,y.hasInterceptionRouteInCurrentTree)(e.tree)?e.previousNextUrl||e.nextUrl:null,o=Date.now();return x(e,u,t).then(async({actionResult:i,actionFlightData:s,redirectLocation:y,redirectType:S,revalidatedParts:P})=>{let R;if(y&&(S===E.RedirectType.replace?(e.pushRef.pendingPush=!1,l.pendingPush=!1):(e.pushRef.pendingPush=!0,l.pendingPush=!0),l.canonicalUrl=R=(0,c.createHrefFromUrl)(y,!1)),!s)return(n(i),y)?(0,f.handleExternalUrl)(e,l,y.href,e.pushRef.pendingPush):e;if("string"==typeof s)return n(i),(0,f.handleExternalUrl)(e,l,s,e.pushRef.pendingPush);let x=P.paths.length>0||P.tag||P.cookie;for(let r of(x&&(t.didRevalidate=!0),s)){let{tree:s,seedData:c,head:h,isRootRender:y}=r;if(!y)return console.log("SERVER ACTION APPLY FAILED"),n(i),e;let S=(0,d.applyRouterStatePatchToTree)([""],a,s,R||e.canonicalUrl);if(null===S)return n(i),(0,v.handleSegmentMismatch)(e,t,s);if((0,p.isNavigatingToNewRootLayout)(a,S))return n(i),(0,f.handleExternalUrl)(e,l,R||e.canonicalUrl,e.pushRef.pendingPush);if(null!==c){let t=c[0],n=(0,m.createEmptyCacheNode)();n.rsc=t,n.prefetchRsc=null,n.loading=c[2],(0,g.fillLazyItemsTillLeafWithHead)(o,n,void 0,s,c,h),l.cache=n,(0,T.revalidateEntireCache)(e.nextUrl,S),x&&await (0,b.refreshInactiveParallelSegments)({navigatedAt:o,state:e,updatedTree:S,updatedCache:n,includeNextUrl:!!u,canonicalUrl:l.canonicalUrl||e.canonicalUrl})}l.patchedTree=S,a=S}return y&&R?r((0,w.getRedirectError)((0,k.hasBasePath)(R)?(0,_.removeBasePath)(R):R,S||E.RedirectType.push)):n(i),(0,h.handleMutable)(e,l)},t=>(r(t),e))}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},4924,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"reducer",{enumerable:!0,get:function(){return c}});let r=e.r(88540),l=e.r(54069),a=e.r(91668),u=e.r(73790),o=e.r(69845),i=e.r(86720),s=e.r(45794),c="undefined"==typeof window?function(e,t){return e}:function(e,t){switch(t.type){case r.ACTION_NAVIGATE:return(0,l.navigateReducer)(e,t);case r.ACTION_SERVER_PATCH:return(0,a.serverPatchReducer)(e,t);case r.ACTION_RESTORE:return(0,u.restoreReducer)(e,t);case r.ACTION_REFRESH:return(0,o.refreshReducer)(e,t);case r.ACTION_HMR_REFRESH:return(0,i.hmrRefreshReducer)(e,t);case r.ACTION_SERVER_ACTION:return(0,s.serverActionReducer)(e,t);default:throw Object.defineProperty(Error("Unknown action"),"__NEXT_ERROR_CODE",{value:"E295",enumerable:!1,configurable:!0})}};("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},99781,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r={createMutableActionQueue:function(){return y},dispatchNavigateAction:function(){return S},dispatchTraverseAction:function(){return w},getCurrentAppRouterState:function(){return v},publicAppRouterInstance:function(){return E}};for(var l in r)Object.defineProperty(n,l,{enumerable:!0,get:r[l]});let a=e.r(88540),u=e.r(4924),o=e.r(71645),i=e.r(64245),s=e.r(65165),c=e.r(41538),f=e.r(5550),d=e.r(57630),p=e.r(91949);function h(e,t){null!==e.pending?(e.pending=e.pending.next,null!==e.pending&&g({actionQueue:e,action:e.pending,setState:t})):e.needsRefresh&&(e.needsRefresh=!1,e.dispatch({type:a.ACTION_REFRESH,origin:window.location.origin},t))}async function g({actionQueue:e,action:t,setState:n}){let r=e.state;e.pending=t;let l=t.payload,u=e.action(r,l);function o(r){if(t.discarded){t.payload.type===a.ACTION_SERVER_ACTION&&t.payload.didRevalidate&&(e.needsRefresh=!0),h(e,n);return}e.state=r,h(e,n),t.resolve(r)}(0,i.isThenable)(u)?u.then(o,r=>{h(e,n),t.reject(r)}):o(u)}let m=null;function y(e,t){let n={state:e,dispatch:(e,t)=>(function(e,t,n){let r={resolve:n,reject:()=>{}};if(t.type!==a.ACTION_RESTORE){let e=new Promise((e,t)=>{r={resolve:e,reject:t}});(0,o.startTransition)(()=>{n(e)})}let l={payload:t,next:null,resolve:r.resolve,reject:r.reject};null===e.pending?(e.last=l,g({actionQueue:e,action:l,setState:n})):t.type===a.ACTION_NAVIGATE||t.type===a.ACTION_RESTORE?(e.pending.discarded=!0,l.next=e.pending.next,g({actionQueue:e,action:l,setState:n})):(null!==e.last&&(e.last.next=l),e.last=l)})(n,e,t),action:async(e,t)=>(0,u.reducer)(e,t),pending:null,last:null,onRouterTransitionStart:null!==t&&"function"==typeof t.onRouterTransitionStart?t.onRouterTransitionStart:null};if("undefined"!=typeof window){if(null!==m)throw Object.defineProperty(Error("Internal Next.js Error: createMutableActionQueue was called more than once"),"__NEXT_ERROR_CODE",{value:"E624",enumerable:!1,configurable:!0});m=n}return n}function v(){return null!==m?m.state:null}function b(){return null!==m?m.onRouterTransitionStart:null}function S(e,t,n,r){let l=new URL((0,f.addBasePath)(e),location.href);(0,p.setLinkForCurrentNavigation)(r);let u=b();null!==u&&u(e,t),(0,c.dispatchAppRouterAction)({type:a.ACTION_NAVIGATE,url:l,isExternalUrl:(0,d.isExternalURL)(l),locationSearch:location.search,shouldScroll:n,navigateType:t})}function w(e,t){let n=b();null!==n&&n(e,"traverse"),(0,c.dispatchAppRouterAction)({type:a.ACTION_RESTORE,url:new URL(e),historyState:t})}let E={back:()=>window.history.back(),forward:()=>window.history.forward(),prefetch:(e,t)=>{let n,r=function(){if(null===m)throw Object.defineProperty(Error("Internal Next.js error: Router action dispatched before initialization."),"__NEXT_ERROR_CODE",{value:"E668",enumerable:!1,configurable:!0});return m}();switch(t?.kind??a.PrefetchKind.AUTO){case a.PrefetchKind.AUTO:n=s.FetchStrategy.PPR;break;case a.PrefetchKind.FULL:n=s.FetchStrategy.Full;break;case a.PrefetchKind.TEMPORARY:return;default:n=s.FetchStrategy.PPR}(0,s.prefetch)(e,r.state.nextUrl,r.state.tree,n,t?.onInvalidate??null)},replace:(e,t)=>{(0,o.startTransition)(()=>{S(e,"replace",t?.scroll??!0,null)})},push:(e,t)=>{(0,o.startTransition)(()=>{S(e,"push",t?.scroll??!0,null)})},refresh:()=>{(0,o.startTransition)(()=>{(0,c.dispatchAppRouterAction)({type:a.ACTION_REFRESH,origin:window.location.origin})})},hmrRefresh:()=>{throw Object.defineProperty(Error("hmrRefresh can only be used in development mode. Please use refresh instead."),"__NEXT_ERROR_CODE",{value:"E485",enumerable:!1,configurable:!0})}};"undefined"!=typeof window&&window.next&&(window.next.router=E),("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},65716,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"createInitialRouterState",{enumerable:!0,get:function(){return i}});let r=e.r(51191),l=e.r(1764),a=e.r(34727),u=e.r(13576),o=e.r(50590);function i({navigatedAt:e,initialFlightData:t,initialCanonicalUrlParts:n,initialRenderedSearch:i,initialParallelRoutes:s,location:c}){let f=n.join("/"),{tree:d,seedData:p,head:h}=(0,o.getFlightDataPartsFromPath)(t[0]),g={lazyData:null,rsc:p?.[0],prefetchRsc:null,head:null,prefetchHead:null,parallelRoutes:s,loading:p?.[2]??null,navigatedAt:e},m=c?(0,r.createHrefFromUrl)(c):f;return(0,u.addRefreshMarkerToActiveParallelSegments)(d,m),(null===s||0===s.size)&&(0,l.fillLazyItemsTillLeafWithHead)(e,g,void 0,d,p,h),{tree:d,cache:g,pushRef:{pendingPush:!1,mpaNavigation:!1,preserveCustomHistoryState:!0},focusAndScrollRef:{apply:!1,onlyHashChange:!1,hashFragment:null,segmentPaths:[]},canonicalUrl:m,renderedSearch:i,nextUrl:((0,a.extractPathFromFlightRouterState)(d)||c?.pathname)??null,previousNextUrl:null,debugInfo:null}}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},98569,(e,t,n)=>{"use strict";let r,l,a,u;Object.defineProperty(n,"__esModule",{value:!0}),Object.defineProperty(n,"hydrate",{enumerable:!0,get:function(){return F}});let o=e.r(55682),i=e.r(43476);e.r(23911);let s=o._(e.r(88014)),c=o._(e.r(71645)),f=e.r(35326),d=e.r(42732),p=e.r(97238),h=e.r(51323),g=e.r(32120),m=e.r(92245),y=e.r(99781),v=o._(e.r(75530)),b=e.r(65716);e.r(8372);let S=e.r(14297),w=e.r(50590),E=f.createFromReadableStream,_=f.createFromFetch,k=document,P=new TextEncoder,T=!1,R=!1,x=null;function O(e){if(0===e[0])a=[];else if(1===e[0]){if(!a)throw Object.defineProperty(Error("Unexpected server data: missing bootstrap script."),"__NEXT_ERROR_CODE",{value:"E18",enumerable:!1,configurable:!0});u?u.enqueue(P.encode(e[1])):a.push(e[1])}else if(2===e[0])x=e[1];else if(3===e[0]){if(!a)throw Object.defineProperty(Error("Unexpected server data: missing bootstrap script."),"__NEXT_ERROR_CODE",{value:"E18",enumerable:!1,configurable:!0});let n=atob(e[1]),r=new Uint8Array(n.length);for(var t=0;t{e.enqueue("string"==typeof t?P.encode(t):t)}),T&&!R)&&(null===e.desiredSize||e.desiredSize<0?e.error(Object.defineProperty(Error("The connection to the page was unexpectedly closed, possibly due to the stop button being clicked, loss of Wi-Fi, or an unstable internet connection."),"__NEXT_ERROR_CODE",{value:"E117",enumerable:!1,configurable:!0})):e.close(),R=!0,a=void 0),u=e}}),L=window.__NEXT_CLIENT_RESUME;function j({initialRSCPayload:e,actionQueue:t,webSocket:n,staticIndicatorState:r}){return(0,i.jsx)(v.default,{actionQueue:t,globalErrorState:e.G,webSocket:n,staticIndicatorState:r})}l=L?Promise.resolve(_(L,{callServer:g.callServer,findSourceMapURL:m.findSourceMapURL,debugChannel:r})).then(async e=>(0,w.createInitialRSCPayloadFromFallbackPrerender)(await L,e)):E(M,{callServer:g.callServer,findSourceMapURL:m.findSourceMapURL,debugChannel:r,startTime:0});let A=c.default.StrictMode;function z({children:e}){return e}let I={onDefaultTransitionIndicator:function(){return()=>{}},onRecoverableError:p.onRecoverableError,onCaughtError:h.onCaughtError,onUncaughtError:h.onUncaughtError};async function F(e,t){let n,r,a=await l;(0,S.setAppBuildId)(a.b);let u=Date.now(),o=(0,y.createMutableActionQueue)((0,b.createInitialRouterState)({navigatedAt:u,initialFlightData:a.f,initialCanonicalUrlParts:a.c,initialRenderedSearch:a.q,initialParallelRoutes:new Map,location:window.location}),e),f=(0,i.jsx)(A,{children:(0,i.jsx)(d.HeadManagerContext.Provider,{value:{appDir:!0},children:(0,i.jsx)(z,{children:(0,i.jsx)(j,{initialRSCPayload:a,actionQueue:o,webSocket:r,staticIndicatorState:n})})})});"__next_error__"===document.documentElement.id?s.default.createRoot(k,I).render(f):c.default.startTransition(()=>{s.default.hydrateRoot(k,f,{...I,formState:x})})}("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)},94553,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value:!0});let r=e.r(96517);e.r(97238),window.next.turbopack=!0,self.__webpack_hash__="";let l=e.r(5526);(0,r.appBootstrap)(t=>{let{hydrate:n}=e.r(98569);n(l,t)}),("function"==typeof n.default||"object"==typeof n.default&&null!==n.default)&&void 0===n.default.__esModule&&(Object.defineProperty(n.default,"__esModule",{value:!0}),Object.assign(n.default,n),t.exports=n.default)}]); \ No newline at end of file diff --git a/static/frontend/login/_next/static/chunks/752a4bd6387d6ef7.js b/static/frontend/login/_next/static/chunks/752a4bd6387d6ef7.js deleted file mode 100644 index 63cea9d7335025be2d86cfebafac184c9a76eb23..0000000000000000000000000000000000000000 --- a/static/frontend/login/_next/static/chunks/752a4bd6387d6ef7.js +++ /dev/null @@ -1 +0,0 @@ -(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,68027,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"default",{enumerable:!0,get:function(){return s}});let n=e.r(43476),o=e.r(12354),u={fontFamily:'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',height:"100vh",textAlign:"center",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center"},i={fontSize:"14px",fontWeight:400,lineHeight:"28px",margin:"0 8px"},s=function({error:e}){let t=e?.digest;return(0,n.jsxs)("html",{id:"__next_error__",children:[(0,n.jsx)("head",{}),(0,n.jsxs)("body",{children:[(0,n.jsx)(o.HandleISRError,{error:e}),(0,n.jsx)("div",{style:u,children:(0,n.jsxs)("div",{children:[(0,n.jsxs)("h2",{style:i,children:["Application error: a ",t?"server":"client","-side exception has occurred while loading ",window.location.hostname," (see the"," ",t?"server logs":"browser console"," for more information)."]}),t?(0,n.jsx)("p",{style:i,children:`Digest: ${t}`}):null]})})]})]})};("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},35451,(e,t,r)=>{var n={229:function(e){var t,r,n,o=e.exports={};function u(){throw Error("setTimeout has not been defined")}function i(){throw Error("clearTimeout has not been defined")}try{t="function"==typeof setTimeout?setTimeout:u}catch(e){t=u}try{r="function"==typeof clearTimeout?clearTimeout:i}catch(e){r=i}function s(e){if(t===setTimeout)return setTimeout(e,0);if((t===u||!t)&&setTimeout)return t=setTimeout,setTimeout(e,0);try{return t(e,0)}catch(r){try{return t.call(null,e,0)}catch(r){return t.call(this,e,0)}}}var c=[],l=!1,a=-1;function f(){l&&n&&(l=!1,n.length?c=n.concat(c):a=-1,c.length&&p())}function p(){if(!l){var e=s(f);l=!0;for(var t=c.length;t;){for(n=c,c=[];++a1)for(var r=1;r{"use strict";var n,o;t.exports=(null==(n=e.g.process)?void 0:n.env)&&"object"==typeof(null==(o=e.g.process)?void 0:o.env)?e.g.process:e.r(35451)},45689,(e,t,r)=>{"use strict";var n=Symbol.for("react.transitional.element");function o(e,t,r){var o=null;if(void 0!==r&&(o=""+r),void 0!==t.key&&(o=""+t.key),"key"in t)for(var u in r={},t)"key"!==u&&(r[u]=t[u]);else r=t;return{$$typeof:n,type:e,key:o,ref:void 0!==(t=r.ref)?t:null,props:r}}r.Fragment=Symbol.for("react.fragment"),r.jsx=o,r.jsxs=o},43476,(e,t,r)=>{"use strict";t.exports=e.r(45689)},90317,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={bindSnapshot:function(){return l},createAsyncLocalStorage:function(){return c},createSnapshot:function(){return a}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=Object.defineProperty(Error("Invariant: AsyncLocalStorage accessed in runtime where it is not available"),"__NEXT_ERROR_CODE",{value:"E504",enumerable:!1,configurable:!0});class i{disable(){throw u}getStore(){}run(){throw u}exit(){throw u}enterWith(){throw u}static bind(e){return e}}let s="undefined"!=typeof globalThis&&globalThis.AsyncLocalStorage;function c(){return s?new s:new i}function l(e){return s?s.bind(e):i.bind(e)}function a(){return s?s.snapshot():function(e,...t){return e(...t)}}},42344,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"workAsyncStorageInstance",{enumerable:!0,get:function(){return n}});let n=(0,e.r(90317).createAsyncLocalStorage)()},63599,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"workAsyncStorage",{enumerable:!0,get:function(){return n.workAsyncStorageInstance}});let n=e.r(42344)},12354,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"HandleISRError",{enumerable:!0,get:function(){return o}});let n="undefined"==typeof window?e.r(63599).workAsyncStorage:void 0;function o({error:e}){if(n){let t=n.getStore();if(t?.isStaticGeneration)throw e&&console.error(e),e}return null}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},50740,(e,t,r)=>{"use strict";var n=e.i(47167),o=Symbol.for("react.transitional.element"),u=Symbol.for("react.portal"),i=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),c=Symbol.for("react.profiler"),l=Symbol.for("react.consumer"),a=Symbol.for("react.context"),f=Symbol.for("react.forward_ref"),p=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),y=Symbol.for("react.lazy"),h=Symbol.for("react.activity"),v=Symbol.for("react.view_transition"),b=Symbol.iterator,m={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},_=Object.assign,g={};function S(e,t,r){this.props=e,this.context=t,this.refs=g,this.updater=r||m}function j(){}function w(e,t,r){this.props=e,this.context=t,this.refs=g,this.updater=r||m}S.prototype.isReactComponent={},S.prototype.setState=function(e,t){if("object"!=typeof e&&"function"!=typeof e&&null!=e)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")},S.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")},j.prototype=S.prototype;var E=w.prototype=new j;E.constructor=w,_(E,S.prototype),E.isPureReactComponent=!0;var x=Array.isArray;function T(){}var O={H:null,A:null,T:null,S:null},k=Object.prototype.hasOwnProperty;function R(e,t,r){var n=r.ref;return{$$typeof:o,type:e,key:t,ref:void 0!==n?n:null,props:r}}function A(e){return"object"==typeof e&&null!==e&&e.$$typeof===o}var H=/\/+/g;function C(e,t){var r,n;return"object"==typeof e&&null!==e&&null!=e.key?(r=""+e.key,n={"=":"=0",":":"=2"},"$"+r.replace(/[=:]/g,function(e){return n[e]})):t.toString(36)}function P(e,t,r){if(null==e)return e;var n=[],i=0;return!function e(t,r,n,i,s){var c,l,a,f=typeof t;("undefined"===f||"boolean"===f)&&(t=null);var p=!1;if(null===t)p=!0;else switch(f){case"bigint":case"string":case"number":p=!0;break;case"object":switch(t.$$typeof){case o:case u:p=!0;break;case y:return e((p=t._init)(t._payload),r,n,i,s)}}if(p)return s=s(t),p=""===i?"."+C(t,0):i,x(s)?(n="",null!=p&&(n=p.replace(H,"$&/")+"/"),e(s,r,n,"",function(e){return e})):null!=s&&(A(s)&&(c=s,l=n+(null==s.key||t&&t.key===s.key?"":(""+s.key).replace(H,"$&/")+"/")+p,s=R(c.type,l,c.props)),r.push(s)),1;p=0;var d=""===i?".":i+":";if(x(t))for(var h=0;h{"use strict";t.exports=e.r(50740)}]); \ No newline at end of file diff --git a/static/frontend/login/_next/static/chunks/a6dad97d9634a72d.js b/static/frontend/login/_next/static/chunks/a6dad97d9634a72d.js deleted file mode 100644 index ab422b94a4fbe76275d31c0bf7ef334768b39cae..0000000000000000000000000000000000000000 --- a/static/frontend/login/_next/static/chunks/a6dad97d9634a72d.js +++ /dev/null @@ -1 +0,0 @@ -!function(){var t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function e(t){var e={exports:{}};return t(e,e.exports),e.exports}var r,n,o=function(t){return t&&t.Math===Math&&t},i=o("object"==typeof globalThis&&globalThis)||o("object"==typeof window&&window)||o("object"==typeof self&&self)||o("object"==typeof t&&t)||o("object"==typeof t&&t)||function(){return this}()||Function("return this")(),a=function(t){try{return!!t()}catch(t){return!0}},u=!a(function(){return 7!==Object.defineProperty({},1,{get:function(){return 7}})[1]}),s=!a(function(){var t=function(){}.bind();return"function"!=typeof t||t.hasOwnProperty("prototype")}),c=Function.prototype.call,f=s?c.bind(c):function(){return c.apply(c,arguments)},l={}.propertyIsEnumerable,h=Object.getOwnPropertyDescriptor,p=h&&!l.call({1:2},1)?function(t){var e=h(this,t);return!!e&&e.enumerable}:l,v={f:p},d=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}},g=Function.prototype,y=g.call,m=s&&g.bind.bind(y,y),b=s?m:function(t){return function(){return y.apply(t,arguments)}},w=b({}.toString),S=b("".slice),E=function(t){return S(w(t),8,-1)},O=Object,x=b("".split),R=a(function(){return!O("z").propertyIsEnumerable(0)})?function(t){return"String"===E(t)?x(t,""):O(t)}:O,P=function(t){return null==t},A=TypeError,j=function(t){if(P(t))throw new A("Can't call method on "+t);return t},k=function(t){return R(j(t))},I="object"==typeof document&&document.all,T=void 0===I&&void 0!==I?function(t){return"function"==typeof t||t===I}:function(t){return"function"==typeof t},M=function(t){return"object"==typeof t?null!==t:T(t)},L=function(t,e){return arguments.length<2?T(r=i[t])?r:void 0:i[t]&&i[t][e];var r},U=b({}.isPrototypeOf),N=i.navigator,C=N&&N.userAgent,_=C?String(C):"",F=i.process,B=i.Deno,D=F&&F.versions||B&&B.version,z=D&&D.v8;z&&(n=(r=z.split("."))[0]>0&&r[0]<4?1:+(r[0]+r[1])),!n&&_&&(!(r=_.match(/Edge\/(\d+)/))||r[1]>=74)&&(r=_.match(/Chrome\/(\d+)/))&&(n=+r[1]);var W=n,q=i.String,H=!!Object.getOwnPropertySymbols&&!a(function(){var t=Symbol("symbol detection");return!q(t)||!(Object(t)instanceof Symbol)||!Symbol.sham&&W&&W<41}),$=H&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,K=Object,G=$?function(t){return"symbol"==typeof t}:function(t){var e=L("Symbol");return T(e)&&U(e.prototype,K(t))},V=String,Y=function(t){try{return V(t)}catch(t){return"Object"}},X=TypeError,J=function(t){if(T(t))return t;throw new X(Y(t)+" is not a function")},Q=function(t,e){var r=t[e];return P(r)?void 0:J(r)},Z=TypeError,tt=Object.defineProperty,et=function(t,e){try{tt(i,t,{value:e,configurable:!0,writable:!0})}catch(r){i[t]=e}return e},rt=e(function(t){var e="__core-js_shared__",r=t.exports=i[e]||et(e,{});(r.versions||(r.versions=[])).push({version:"3.38.1",mode:"global",copyright:"© 2014-2024 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.38.1/LICENSE",source:"https://github.com/zloirock/core-js"})}),nt=function(t,e){return rt[t]||(rt[t]=e||{})},ot=Object,it=function(t){return ot(j(t))},at=b({}.hasOwnProperty),ut=Object.hasOwn||function(t,e){return at(it(t),e)},st=0,ct=Math.random(),ft=b(1..toString),lt=function(t){return"Symbol("+(void 0===t?"":t)+")_"+ft(++st+ct,36)},ht=i.Symbol,pt=nt("wks"),vt=$?ht.for||ht:ht&&ht.withoutSetter||lt,dt=function(t){return ut(pt,t)||(pt[t]=H&&ut(ht,t)?ht[t]:vt("Symbol."+t)),pt[t]},gt=TypeError,yt=dt("toPrimitive"),mt=function(t,e){if(!M(t)||G(t))return t;var r,n=Q(t,yt);if(n){if(void 0===e&&(e="default"),r=f(n,t,e),!M(r)||G(r))return r;throw new gt("Can't convert object to primitive value")}return void 0===e&&(e="number"),function(t,e){var r,n;if("string"===e&&T(r=t.toString)&&!M(n=f(r,t)))return n;if(T(r=t.valueOf)&&!M(n=f(r,t)))return n;if("string"!==e&&T(r=t.toString)&&!M(n=f(r,t)))return n;throw new Z("Can't convert object to primitive value")}(t,e)},bt=function(t){var e=mt(t,"string");return G(e)?e:e+""},wt=i.document,St=M(wt)&&M(wt.createElement),Et=function(t){return St?wt.createElement(t):{}},Ot=!u&&!a(function(){return 7!==Object.defineProperty(Et("div"),"a",{get:function(){return 7}}).a}),xt=Object.getOwnPropertyDescriptor,Rt={f:u?xt:function(t,e){if(t=k(t),e=bt(e),Ot)try{return xt(t,e)}catch(t){}if(ut(t,e))return d(!f(v.f,t,e),t[e])}},Pt=u&&a(function(){return 42!==Object.defineProperty(function(){},"prototype",{value:42,writable:!1}).prototype}),At=String,jt=TypeError,kt=function(t){if(M(t))return t;throw new jt(At(t)+" is not an object")},It=TypeError,Tt=Object.defineProperty,Mt=Object.getOwnPropertyDescriptor,Lt="enumerable",Ut="configurable",Nt="writable",Ct={f:u?Pt?function(t,e,r){if(kt(t),e=bt(e),kt(r),"function"==typeof t&&"prototype"===e&&"value"in r&&Nt in r&&!r[Nt]){var n=Mt(t,e);n&&n[Nt]&&(t[e]=r.value,r={configurable:Ut in r?r[Ut]:n[Ut],enumerable:Lt in r?r[Lt]:n[Lt],writable:!1})}return Tt(t,e,r)}:Tt:function(t,e,r){if(kt(t),e=bt(e),kt(r),Ot)try{return Tt(t,e,r)}catch(t){}if("get"in r||"set"in r)throw new It("Accessors not supported");return"value"in r&&(t[e]=r.value),t}},_t=u?function(t,e,r){return Ct.f(t,e,d(1,r))}:function(t,e,r){return t[e]=r,t},Ft=Function.prototype,Bt=u&&Object.getOwnPropertyDescriptor,Dt=ut(Ft,"name"),zt={EXISTS:Dt,PROPER:Dt&&"something"===function(){}.name,CONFIGURABLE:Dt&&(!u||u&&Bt(Ft,"name").configurable)},Wt=b(Function.toString);T(rt.inspectSource)||(rt.inspectSource=function(t){return Wt(t)});var qt,Ht,$t,Kt=rt.inspectSource,Gt=i.WeakMap,Vt=T(Gt)&&/native code/.test(String(Gt)),Yt=nt("keys"),Xt=function(t){return Yt[t]||(Yt[t]=lt(t))},Jt={},Qt="Object already initialized",Zt=i.TypeError;if(Vt||rt.state){var te=rt.state||(rt.state=new(0,i.WeakMap));te.get=te.get,te.has=te.has,te.set=te.set,qt=function(t,e){if(te.has(t))throw new Zt(Qt);return e.facade=t,te.set(t,e),e},Ht=function(t){return te.get(t)||{}},$t=function(t){return te.has(t)}}else{var ee=Xt("state");Jt[ee]=!0,qt=function(t,e){if(ut(t,ee))throw new Zt(Qt);return e.facade=t,_t(t,ee,e),e},Ht=function(t){return ut(t,ee)?t[ee]:{}},$t=function(t){return ut(t,ee)}}var re,ne={set:qt,get:Ht,has:$t,enforce:function(t){return $t(t)?Ht(t):qt(t,{})},getterFor:function(t){return function(e){var r;if(!M(e)||(r=Ht(e)).type!==t)throw new Zt("Incompatible receiver, "+t+" required");return r}}},oe=e(function(t){var e=zt.CONFIGURABLE,r=ne.enforce,n=ne.get,o=String,i=Object.defineProperty,s=b("".slice),c=b("".replace),f=b([].join),l=u&&!a(function(){return 8!==i(function(){},"length",{value:8}).length}),h=String(String).split("String"),p=t.exports=function(t,n,a){"Symbol("===s(o(n),0,7)&&(n="["+c(o(n),/^Symbol\(([^)]*)\).*$/,"$1")+"]"),a&&a.getter&&(n="get "+n),a&&a.setter&&(n="set "+n),(!ut(t,"name")||e&&t.name!==n)&&(u?i(t,"name",{value:n,configurable:!0}):t.name=n),l&&a&&ut(a,"arity")&&t.length!==a.arity&&i(t,"length",{value:a.arity});try{a&&ut(a,"constructor")&&a.constructor?u&&i(t,"prototype",{writable:!1}):t.prototype&&(t.prototype=void 0)}catch(t){}var p=r(t);return ut(p,"source")||(p.source=f(h,"string"==typeof n?n:"")),t};Function.prototype.toString=p(function(){return T(this)&&n(this).source||Kt(this)},"toString")}),ie=function(t,e,r,n){n||(n={});var o=n.enumerable,i=void 0!==n.name?n.name:e;if(T(r)&&oe(r,i,n),n.global)o?t[e]=r:et(e,r);else{try{n.unsafe?t[e]&&(o=!0):delete t[e]}catch(t){}o?t[e]=r:Ct.f(t,e,{value:r,enumerable:!1,configurable:!n.nonConfigurable,writable:!n.nonWritable})}return t},ae=Math.ceil,ue=Math.floor,se=Math.trunc||function(t){var e=+t;return(e>0?ue:ae)(e)},ce=function(t){var e=+t;return e!=e||0===e?0:se(e)},fe=Math.max,le=Math.min,he=function(t,e){var r=ce(t);return r<0?fe(r+e,0):le(r,e)},pe=Math.min,ve=function(t){var e=ce(t);return e>0?pe(e,9007199254740991):0},de=function(t){return ve(t.length)},ge=function(t){return function(e,r,n){var o=k(e),i=de(o);if(0===i)return!t&&-1;var a,u=he(n,i);if(t&&r!=r){for(;i>u;)if((a=o[u++])!=a)return!0}else for(;i>u;u++)if((t||u in o)&&o[u]===r)return t||u||0;return!t&&-1}},ye={includes:ge(!0),indexOf:ge(!1)},me=ye.indexOf,be=b([].push),we=function(t,e){var r,n=k(t),o=0,i=[];for(r in n)!ut(Jt,r)&&ut(n,r)&&be(i,r);for(;e.length>o;)ut(n,r=e[o++])&&(~me(i,r)||be(i,r));return i},Se=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],Ee=Se.concat("length","prototype"),Oe={f:Object.getOwnPropertyNames||function(t){return we(t,Ee)}},xe={f:Object.getOwnPropertySymbols},Re=b([].concat),Pe=L("Reflect","ownKeys")||function(t){var e=Oe.f(kt(t)),r=xe.f;return r?Re(e,r(t)):e},Ae=function(t,e,r){for(var n=Pe(e),o=Ct.f,i=Rt.f,a=0;aa;)Ct.f(t,r=o[a++],n[r]);return t},Be={f:Fe},De=L("document","documentElement"),ze="prototype",We="script",qe=Xt("IE_PROTO"),He=function(){},$e=function(t){return"<"+We+">"+t+""},Ke=function(t){t.write($e("")),t.close();var e=t.parentWindow.Object;return t=null,e},Ge=function(){try{re=new ActiveXObject("htmlfile")}catch(t){}var t,e,r;Ge="undefined"!=typeof document?document.domain&&re?Ke(re):(e=Et("iframe"),r="java"+We+":",e.style.display="none",De.appendChild(e),e.src=String(r),(t=e.contentWindow.document).open(),t.write($e("document.F=Object")),t.close(),t.F):Ke(re);for(var n=Se.length;n--;)delete Ge[ze][Se[n]];return Ge()};Jt[qe]=!0;var Ve=Object.create||function(t,e){var r;return null!==t?(He[ze]=kt(t),r=new He,He[ze]=null,r[qe]=t):r=Ge(),void 0===e?r:Be.f(r,e)},Ye=Ct.f,Xe=dt("unscopables"),Je=Array.prototype;void 0===Je[Xe]&&Ye(Je,Xe,{configurable:!0,value:Ve(null)});var Qe=function(t){Je[Xe][t]=!0};Ce({target:"Array",proto:!0},{at:function(t){var e=it(this),r=de(e),n=ce(t),o=n>=0?n:r+n;return o<0||o>=r?void 0:e[o]}}),Qe("at");var Ze=function(t,e){return b(i[t].prototype[e])},tr=(Ze("Array","at"),TypeError),er=function(t,e){if(!delete t[e])throw new tr("Cannot delete property "+Y(e)+" of "+Y(t))},rr=Math.min,nr=[].copyWithin||function(t,e){var r=it(this),n=de(r),o=he(t,n),i=he(e,n),a=arguments.length>2?arguments[2]:void 0,u=rr((void 0===a?n:he(a,n))-i,n-o),s=1;for(i0;)i in r?r[o]=r[i]:er(r,o),o+=s,i+=s;return r};Ce({target:"Array",proto:!0},{copyWithin:nr}),Qe("copyWithin"),Ze("Array","copyWithin"),Ce({target:"Array",proto:!0},{fill:function(t){for(var e=it(this),r=de(e),n=arguments.length,o=he(n>1?arguments[1]:void 0,r),i=n>2?arguments[2]:void 0,a=void 0===i?r:he(i,r);a>o;)e[o++]=t;return e}}),Qe("fill"),Ze("Array","fill");var or=function(t){if("Function"===E(t))return b(t)},ir=or(or.bind),ar=function(t,e){return J(t),void 0===e?t:s?ir(t,e):function(){return t.apply(e,arguments)}},ur=Array.isArray||function(t){return"Array"===E(t)},sr={};sr[dt("toStringTag")]="z";var cr="[object z]"===String(sr),fr=dt("toStringTag"),lr=Object,hr="Arguments"===E(function(){return arguments}()),pr=cr?E:function(t){var e,r,n;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(r=function(t,e){try{return t[e]}catch(t){}}(e=lr(t),fr))?r:hr?E(e):"Object"===(n=E(e))&&T(e.callee)?"Arguments":n},vr=function(){},dr=L("Reflect","construct"),gr=/^\s*(?:class|function)\b/,yr=b(gr.exec),mr=!gr.test(vr),br=function(t){if(!T(t))return!1;try{return dr(vr,[],t),!0}catch(t){return!1}},wr=function(t){if(!T(t))return!1;switch(pr(t)){case"AsyncFunction":case"GeneratorFunction":case"AsyncGeneratorFunction":return!1}try{return mr||!!yr(gr,Kt(t))}catch(t){return!0}};wr.sham=!0;var Sr=!dr||a(function(){var t;return br(br.call)||!br(Object)||!br(function(){t=!0})||t})?wr:br,Er=dt("species"),Or=Array,xr=function(t,e){return new(function(t){var e;return ur(t)&&(Sr(e=t.constructor)&&(e===Or||ur(e.prototype))||M(e)&&null===(e=e[Er]))&&(e=void 0),void 0===e?Or:e}(t))(0===e?0:e)},Rr=b([].push),Pr=function(t){var e=1===t,r=2===t,n=3===t,o=4===t,i=6===t,a=7===t,u=5===t||i;return function(s,c,f,l){for(var h,p,v=it(s),d=R(v),g=de(d),y=ar(c,f),m=0,b=l||xr,w=e?b(s,g):r||a?b(s,0):void 0;g>m;m++)if((u||m in d)&&(p=y(h=d[m],m,v),t))if(e)w[m]=p;else if(p)switch(t){case 3:return!0;case 5:return h;case 6:return m;case 2:Rr(w,h)}else switch(t){case 4:return!1;case 7:Rr(w,h)}return i?-1:n||o?o:w}},Ar={forEach:Pr(0),map:Pr(1),filter:Pr(2),some:Pr(3),every:Pr(4),find:Pr(5),findIndex:Pr(6),filterReject:Pr(7)},jr=Ar.find,kr="find",Ir=!0;kr in[]&&Array(1)[kr](function(){Ir=!1}),Ce({target:"Array",proto:!0,forced:Ir},{find:function(t){return jr(this,t,arguments.length>1?arguments[1]:void 0)}}),Qe(kr),Ze("Array","find");var Tr=Ar.findIndex,Mr="findIndex",Lr=!0;Mr in[]&&Array(1)[Mr](function(){Lr=!1}),Ce({target:"Array",proto:!0,forced:Lr},{findIndex:function(t){return Tr(this,t,arguments.length>1?arguments[1]:void 0)}}),Qe(Mr),Ze("Array","findIndex");var Ur=TypeError,Nr=function(t){if(t>9007199254740991)throw Ur("Maximum allowed index exceeded");return t},Cr=function(t,e,r,n,o,i,a,u){for(var s,c,f=o,l=0,h=!!a&&ar(a,u);l0&&ur(s)?(c=de(s),f=Cr(t,e,s,c,f,i-1)-1):(Nr(f+1),t[f]=s),f++),l++;return f},_r=Cr;Ce({target:"Array",proto:!0},{flatMap:function(t){var e,r=it(this),n=de(r);return J(t),(e=xr(r,0)).length=_r(e,r,r,n,0,1,t,arguments.length>1?arguments[1]:void 0),e}}),Qe("flatMap"),Ze("Array","flatMap"),Ce({target:"Array",proto:!0},{flat:function(){var t=arguments.length?arguments[0]:void 0,e=it(this),r=de(e),n=xr(e,0);return n.length=_r(n,e,e,r,0,void 0===t?1:ce(t)),n}}),Qe("flat"),Ze("Array","flat");var Fr,Br,Dr,zr=String,Wr=function(t){if("Symbol"===pr(t))throw new TypeError("Cannot convert a Symbol value to a string");return zr(t)},qr=b("".charAt),Hr=b("".charCodeAt),$r=b("".slice),Kr=function(t){return function(e,r){var n,o,i=Wr(j(e)),a=ce(r),u=i.length;return a<0||a>=u?t?"":void 0:(n=Hr(i,a))<55296||n>56319||a+1===u||(o=Hr(i,a+1))<56320||o>57343?t?qr(i,a):n:t?$r(i,a,a+2):o-56320+(n-55296<<10)+65536}},Gr={codeAt:Kr(!1),charAt:Kr(!0)},Vr=!a(function(){function t(){}return t.prototype.constructor=null,Object.getPrototypeOf(new t)!==t.prototype}),Yr=Xt("IE_PROTO"),Xr=Object,Jr=Xr.prototype,Qr=Vr?Xr.getPrototypeOf:function(t){var e=it(t);if(ut(e,Yr))return e[Yr];var r=e.constructor;return T(r)&&e instanceof r?r.prototype:e instanceof Xr?Jr:null},Zr=dt("iterator"),tn=!1;[].keys&&("next"in(Dr=[].keys())?(Br=Qr(Qr(Dr)))!==Object.prototype&&(Fr=Br):tn=!0);var en=!M(Fr)||a(function(){var t={};return Fr[Zr].call(t)!==t});en&&(Fr={}),T(Fr[Zr])||ie(Fr,Zr,function(){return this});var rn={IteratorPrototype:Fr,BUGGY_SAFARI_ITERATORS:tn},nn=Ct.f,on=dt("toStringTag"),an=function(t,e,r){t&&!r&&(t=t.prototype),t&&!ut(t,on)&&nn(t,on,{configurable:!0,value:e})},un={},sn=rn.IteratorPrototype,cn=function(){return this},fn=function(t,e,r,n){var o=e+" Iterator";return t.prototype=Ve(sn,{next:d(+!n,r)}),an(t,o,!1),un[o]=cn,t},ln=function(t,e,r){try{return b(J(Object.getOwnPropertyDescriptor(t,e)[r]))}catch(t){}},hn=String,pn=TypeError,vn=function(t){if(function(t){return M(t)||null===t}(t))return t;throw new pn("Can't set "+hn(t)+" as a prototype")},dn=Object.setPrototypeOf||("__proto__"in{}?function(){var t,e=!1,r={};try{(t=ln(Object.prototype,"__proto__","set"))(r,[]),e=r instanceof Array}catch(t){}return function(r,n){return j(r),vn(n),M(r)?(e?t(r,n):r.__proto__=n,r):r}}():void 0),gn=zt.PROPER,yn=zt.CONFIGURABLE,mn=rn.IteratorPrototype,bn=rn.BUGGY_SAFARI_ITERATORS,wn=dt("iterator"),Sn="keys",En="values",On="entries",xn=function(){return this},Rn=function(t,e,r,n,o,i,a){fn(r,e,n);var u,s,c,l=function(t){if(t===o&&g)return g;if(!bn&&t&&t in v)return v[t];switch(t){case Sn:case En:case On:return function(){return new r(this,t)}}return function(){return new r(this)}},h=e+" Iterator",p=!1,v=t.prototype,d=v[wn]||v["@@iterator"]||o&&v[o],g=!bn&&d||l(o),y="Array"===e&&v.entries||d;if(y&&(u=Qr(y.call(new t)))!==Object.prototype&&u.next&&(Qr(u)!==mn&&(dn?dn(u,mn):T(u[wn])||ie(u,wn,xn)),an(u,h,!0)),gn&&o===En&&d&&d.name!==En&&(yn?_t(v,"name",En):(p=!0,g=function(){return f(d,this)})),o)if(s={values:l(En),keys:i?g:l(Sn),entries:l(On)},a)for(c in s)(bn||p||!(c in v))&&ie(v,c,s[c]);else Ce({target:e,proto:!0,forced:bn||p},s);return v[wn]!==g&&ie(v,wn,g,{name:o}),un[e]=g,s},Pn=function(t,e){return{value:t,done:e}},An=Gr.charAt,jn="String Iterator",kn=ne.set,In=ne.getterFor(jn);Rn(String,"String",function(t){kn(this,{type:jn,string:Wr(t),index:0})},function(){var t,e=In(this),r=e.string,n=e.index;return n>=r.length?Pn(void 0,!0):(t=An(r,n),e.index+=t.length,Pn(t,!1))});var Tn=function(t,e,r){var n,o;kt(t);try{if(!(n=Q(t,"return"))){if("throw"===e)throw r;return r}n=f(n,t)}catch(t){o=!0,n=t}if("throw"===e)throw r;if(o)throw n;return kt(n),r},Mn=function(t,e,r,n){try{return n?e(kt(r)[0],r[1]):e(r)}catch(e){Tn(t,"throw",e)}},Ln=dt("iterator"),Un=Array.prototype,Nn=function(t){return void 0!==t&&(un.Array===t||Un[Ln]===t)},Cn=function(t,e,r){u?Ct.f(t,e,d(0,r)):t[e]=r},_n=dt("iterator"),Fn=function(t){if(!P(t))return Q(t,_n)||Q(t,"@@iterator")||un[pr(t)]},Bn=TypeError,Dn=function(t,e){var r=arguments.length<2?Fn(t):e;if(J(r))return kt(f(r,t));throw new Bn(Y(t)+" is not iterable")},zn=Array,Wn=function(t){var e=it(t),r=Sr(this),n=arguments.length,o=n>1?arguments[1]:void 0,i=void 0!==o;i&&(o=ar(o,n>2?arguments[2]:void 0));var a,u,s,c,l,h,p=Fn(e),v=0;if(!p||this===zn&&Nn(p))for(a=de(e),u=r?new this(a):zn(a);a>v;v++)h=i?o(e[v],v):e[v],Cn(u,v,h);else for(u=r?new this:[],l=(c=Dn(e,p)).next;!(s=f(l,c)).done;v++)h=i?Mn(c,o,[s.value,v],!0):s.value,Cn(u,v,h);return u.length=v,u},qn=dt("iterator"),Hn=!1;try{var $n=0,Kn={next:function(){return{done:!!$n++}},return:function(){Hn=!0}};Kn[qn]=function(){return this},Array.from(Kn,function(){throw 2})}catch(t){}var Gn=function(t,e){try{if(!e&&!Hn)return!1}catch(t){return!1}var r=!1;try{var n={};n[qn]=function(){return{next:function(){return{done:r=!0}}}},t(n)}catch(t){}return r},Vn=!Gn(function(t){Array.from(t)});Ce({target:"Array",stat:!0,forced:Vn},{from:Wn});var Yn=i,Xn=ye.includes,Jn=a(function(){return!Array(1).includes()});Ce({target:"Array",proto:!0,forced:Jn},{includes:function(t){return Xn(this,t,arguments.length>1?arguments[1]:void 0)}}),Qe("includes"),Ze("Array","includes");var Qn=Ct.f,Zn="Array Iterator",to=ne.set,eo=ne.getterFor(Zn),ro=Rn(Array,"Array",function(t,e){to(this,{type:Zn,target:k(t),index:0,kind:e})},function(){var t=eo(this),e=t.target,r=t.index++;if(!e||r>=e.length)return t.target=null,Pn(void 0,!0);switch(t.kind){case"keys":return Pn(r,!1);case"values":return Pn(e[r],!1)}return Pn([r,e[r]],!1)},"values"),no=un.Arguments=un.Array;if(Qe("keys"),Qe("values"),Qe("entries"),u&&"values"!==no.name)try{Qn(no,"name",{value:"values"})}catch(t){}cr||ie(Object.prototype,"toString",cr?{}.toString:function(){return"[object "+pr(this)+"]"},{unsafe:!0}),Ze("Array","values");var oo=Array,io=a(function(){function t(){}return!(oo.of.call(t)instanceof t)});Ce({target:"Array",stat:!0,forced:io},{of:function(){for(var t=0,e=arguments.length,r=new(Sr(this)?this:oo)(e);e>t;)Cn(r,t,arguments[t++]);return r.length=e,r}});var ao=dt("hasInstance"),uo=Function.prototype;ao in uo||Ct.f(uo,ao,{value:oe(function(t){if(!T(this)||!M(t))return!1;var e=this.prototype;return M(e)?U(e,t):t instanceof this},ao)}),dt("hasInstance");var so=function(t,e,r){return r.get&&oe(r.get,e,{getter:!0}),r.set&&oe(r.set,e,{setter:!0}),Ct.f(t,e,r)},co=zt.EXISTS,fo=Function.prototype,lo=b(fo.toString),ho=/function\b(?:\s|\/\*[\S\s]*?\*\/|\/\/[^\n\r]*[\n\r]+)*([^\s(/]*)/,po=b(ho.exec);u&&!co&&so(fo,"name",{configurable:!0,get:function(){try{return po(ho,lo(this))[1]}catch(t){return""}}});var vo=b([].slice),go=Oe.f,yo="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[],mo={f:function(t){return yo&&"Window"===E(t)?function(t){try{return go(t)}catch(t){return vo(yo)}}(t):go(k(t))}},bo=a(function(){if("function"==typeof ArrayBuffer){var t=new ArrayBuffer(8);Object.isExtensible(t)&&Object.defineProperty(t,"a",{value:8})}}),wo=Object.isExtensible,So=a(function(){wo(1)})||bo?function(t){return!!M(t)&&(!bo||"ArrayBuffer"!==E(t))&&(!wo||wo(t))}:wo,Eo=!a(function(){return Object.isExtensible(Object.preventExtensions({}))}),Oo=e(function(t){var e=Ct.f,r=!1,n=lt("meta"),o=0,i=function(t){e(t,n,{value:{objectID:"O"+o++,weakData:{}}})},a=t.exports={enable:function(){a.enable=function(){},r=!0;var t=Oe.f,e=b([].splice),o={};o[n]=1,t(o).length&&(Oe.f=function(r){for(var o=t(r),i=0,a=o.length;ii;i++)if((u=y(t[i]))&&U(Po,u))return u;return new Ro(!1)}n=Dn(t,o)}for(s=h?t.next:n.next;!(c=f(s,n)).done;){try{u=y(c.value)}catch(t){Tn(n,"throw",t)}if("object"==typeof u&&u&&U(Po,u))return u}return new Ro(!1)},jo=TypeError,ko=function(t,e){if(U(e,t))return t;throw new jo("Incorrect invocation")},Io=function(t,e,r){var n,o;return dn&&T(n=e.constructor)&&n!==r&&M(o=n.prototype)&&o!==r.prototype&&dn(t,o),t},To=function(t,e,r){var n=-1!==t.indexOf("Map"),o=-1!==t.indexOf("Weak"),u=n?"set":"add",s=i[t],c=s&&s.prototype,f=s,l={},h=function(t){var e=b(c[t]);ie(c,t,"add"===t?function(t){return e(this,0===t?0:t),this}:"delete"===t?function(t){return!(o&&!M(t))&&e(this,0===t?0:t)}:"get"===t?function(t){return o&&!M(t)?void 0:e(this,0===t?0:t)}:"has"===t?function(t){return!(o&&!M(t))&&e(this,0===t?0:t)}:function(t,r){return e(this,0===t?0:t,r),this})};if(Ue(t,!T(s)||!(o||c.forEach&&!a(function(){(new s).entries().next()}))))f=r.getConstructor(e,t,n,u),Oo.enable();else if(Ue(t,!0)){var p=new f,v=p[u](o?{}:-0,1)!==p,d=a(function(){p.has(1)}),g=Gn(function(t){new s(t)}),y=!o&&a(function(){for(var t=new s,e=5;e--;)t[u](e,e);return!t.has(-0)});g||((f=e(function(t,e){ko(t,c);var r=Io(new s,t,f);return P(e)||Ao(e,r[u],{that:r,AS_ENTRIES:n}),r})).prototype=c,c.constructor=f),(d||y)&&(h("delete"),h("has"),n&&h("get")),(y||v)&&h(u),o&&c.clear&&delete c.clear}return l[t]=f,Ce({global:!0,constructor:!0,forced:f!==s},l),an(f,t),o||r.setStrong(f,t,n),f},Mo=function(t,e,r){for(var n in e)ie(t,n,e[n],r);return t},Lo=dt("species"),Uo=function(t){var e=L(t);u&&e&&!e[Lo]&&so(e,Lo,{configurable:!0,get:function(){return this}})},No=Oo.fastKey,Co=ne.set,_o=ne.getterFor,Fo={getConstructor:function(t,e,r,n){var o=t(function(t,o){ko(t,i),Co(t,{type:e,index:Ve(null),first:null,last:null,size:0}),u||(t.size=0),P(o)||Ao(o,t[n],{that:t,AS_ENTRIES:r})}),i=o.prototype,a=_o(e),s=function(t,e,r){var n,o,i=a(t),s=c(t,e);return s?s.value=r:(i.last=s={index:o=No(e,!0),key:e,value:r,previous:n=i.last,next:null,removed:!1},i.first||(i.first=s),n&&(n.next=s),u?i.size++:t.size++,"F"!==o&&(i.index[o]=s)),t},c=function(t,e){var r,n=a(t),o=No(e);if("F"!==o)return n.index[o];for(r=n.first;r;r=r.next)if(r.key===e)return r};return Mo(i,{clear:function(){for(var t=a(this),e=t.first;e;)e.removed=!0,e.previous&&(e.previous=e.previous.next=null),e=e.next;t.first=t.last=null,t.index=Ve(null),u?t.size=0:this.size=0},delete:function(t){var e=this,r=a(e),n=c(e,t);if(n){var o=n.next,i=n.previous;delete r.index[n.index],n.removed=!0,i&&(i.next=o),o&&(o.previous=i),r.first===n&&(r.first=o),r.last===n&&(r.last=i),u?r.size--:e.size--}return!!n},forEach:function(t){for(var e,r=a(this),n=ar(t,arguments.length>1?arguments[1]:void 0);e=e?e.next:r.first;)for(n(e.value,e.key,this);e&&e.removed;)e=e.previous},has:function(t){return!!c(this,t)}}),Mo(i,r?{get:function(t){var e=c(this,t);return e&&e.value},set:function(t,e){return s(this,0===t?0:t,e)}}:{add:function(t){return s(this,t=0===t?0:t,t)}}),u&&so(i,"size",{configurable:!0,get:function(){return a(this).size}}),o},setStrong:function(t,e,r){var n=e+" Iterator",o=_o(e),i=_o(n);Rn(t,e,function(t,e){Co(this,{type:n,target:t,state:o(t),kind:e,last:null})},function(){for(var t=i(this),e=t.kind,r=t.last;r&&r.removed;)r=r.previous;return t.target&&(t.last=r=r?r.next:t.state.first)?Pn("keys"===e?r.key:"values"===e?r.value:[r.key,r.value],!1):(t.target=null,Pn(void 0,!0))},r?"entries":"values",!r,!0),Uo(e)}};To("Map",function(t){return function(){return t(this,arguments.length?arguments[0]:void 0)}},Fo);var Bo=Map.prototype,Do={Map:Map,set:b(Bo.set),get:b(Bo.get),has:b(Bo.has),remove:b(Bo.delete),proto:Bo},zo=Do.Map,Wo=Do.has,qo=Do.get,Ho=Do.set,$o=b([].push),Ko=a(function(){return 1!==zo.groupBy("ab",function(t){return t}).get("a").length});Ce({target:"Map",stat:!0,forced:Ko},{groupBy:function(t,e){j(t),J(e);var r=new zo,n=0;return Ao(t,function(t){var o=e(t,n++);Wo(r,o)?$o(qo(r,o),t):Ho(r,o,[t])}),r}});var Go={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0},Vo=Et("span").classList,Yo=Vo&&Vo.constructor&&Vo.constructor.prototype,Xo=Yo===Object.prototype?void 0:Yo,Jo=dt("iterator"),Qo=ro.values,Zo=function(t,e){if(t){if(t[Jo]!==Qo)try{_t(t,Jo,Qo)}catch(e){t[Jo]=Qo}if(an(t,e,!0),Go[e])for(var r in ro)if(t[r]!==ro[r])try{_t(t,r,ro[r])}catch(e){t[r]=ro[r]}}};for(var ti in Go)Zo(i[ti]&&i[ti].prototype,ti);Zo(Xo,"DOMTokenList");var ei=function(t,e,r){return function(n){var o=it(n),i=arguments.length,a=i>1?arguments[1]:void 0,u=void 0!==a,s=u?ar(a,i>2?arguments[2]:void 0):void 0,c=new t,f=0;return Ao(o,function(t){var n=u?s(t,f++):t;r?e(c,kt(n)[0],n[1]):e(c,n)}),c}};Ce({target:"Map",stat:!0,forced:!0},{from:ei(Do.Map,Do.set,!0)});var ri=function(t,e,r){return function(){for(var n=new t,o=arguments.length,i=0;i1?arguments[1]:void 0);return!1!==di(e,function(t,n){if(!r(t,n,e))return!1},!0)}});var gi=Do.Map,yi=Do.set;Ce({target:"Map",proto:!0,real:!0,forced:!0},{filter:function(t){var e=oi(this),r=ar(t,arguments.length>1?arguments[1]:void 0),n=new gi;return di(e,function(t,o){r(t,o,e)&&yi(n,o,t)}),n}}),Ce({target:"Map",proto:!0,real:!0,forced:!0},{find:function(t){var e=oi(this),r=ar(t,arguments.length>1?arguments[1]:void 0),n=di(e,function(t,n){if(r(t,n,e))return{value:t}},!0);return n&&n.value}}),Ce({target:"Map",proto:!0,real:!0,forced:!0},{findKey:function(t){var e=oi(this),r=ar(t,arguments.length>1?arguments[1]:void 0),n=di(e,function(t,n){if(r(t,n,e))return{key:n}},!0);return n&&n.key}}),Ce({target:"Map",proto:!0,real:!0,forced:!0},{includes:function(t){return!0===di(oi(this),function(e){if((r=e)===(n=t)||r!=r&&n!=n)return!0;var r,n},!0)}});var mi=Do.Map;Ce({target:"Map",stat:!0,forced:!0},{keyBy:function(t,e){var r=new(T(this)?this:mi);J(e);var n=J(r.set);return Ao(t,function(t){f(n,r,e(t),t)}),r}}),Ce({target:"Map",proto:!0,real:!0,forced:!0},{keyOf:function(t){var e=di(oi(this),function(e,r){if(e===t)return{key:r}},!0);return e&&e.key}});var bi=Do.Map,wi=Do.set;Ce({target:"Map",proto:!0,real:!0,forced:!0},{mapKeys:function(t){var e=oi(this),r=ar(t,arguments.length>1?arguments[1]:void 0),n=new bi;return di(e,function(t,o){wi(n,r(t,o,e),t)}),n}});var Si=Do.Map,Ei=Do.set;Ce({target:"Map",proto:!0,real:!0,forced:!0},{mapValues:function(t){var e=oi(this),r=ar(t,arguments.length>1?arguments[1]:void 0),n=new Si;return di(e,function(t,o){Ei(n,o,r(t,o,e))}),n}});var Oi=Do.set;Ce({target:"Map",proto:!0,real:!0,arity:1,forced:!0},{merge:function(t){for(var e=oi(this),r=arguments.length,n=0;n1?arguments[1]:void 0);return!0===di(e,function(t,n){if(r(t,n,e))return!0},!0)}});var Ri=TypeError,Pi=Do.get,Ai=Do.has,ji=Do.set;Ce({target:"Map",proto:!0,real:!0,forced:!0},{update:function(t,e){var r=oi(this),n=arguments.length;J(e);var o=Ai(r,t);if(!o&&n<3)throw new Ri("Updating absent value");var i=o?Pi(r,t):J(n>2?arguments[2]:void 0)(t,r);return ji(r,t,e(i,t,r)),r}});var ki=TypeError,Ii=function(t,e){var r,n=kt(this),o=J(n.get),i=J(n.has),a=J(n.set),u=arguments.length>2?arguments[2]:void 0;if(!T(e)&&!T(u))throw new ki("At least one callback required");return f(i,n,t)?(r=f(o,n,t),T(e)&&(r=e(r),f(a,n,t,r))):T(u)&&(r=u(),f(a,n,t,r)),r};Ce({target:"Map",proto:!0,real:!0,forced:!0},{upsert:Ii}),Ce({target:"Map",proto:!0,real:!0,name:"upsert",forced:!0},{updateOrInsert:Ii});var Ti=b(1..valueOf),Mi="\t\n\v\f\r                 \u2028\u2029\ufeff",Li=b("".replace),Ui=RegExp("^["+Mi+"]+"),Ni=RegExp("(^|[^"+Mi+"])["+Mi+"]+$"),Ci=function(t){return function(e){var r=Wr(j(e));return 1&t&&(r=Li(r,Ui,"")),2&t&&(r=Li(r,Ni,"$1")),r}},_i={start:Ci(1),end:Ci(2),trim:Ci(3)},Fi=Oe.f,Bi=Rt.f,Di=Ct.f,zi=_i.trim,Wi="Number",qi=i[Wi],Hi=qi.prototype,$i=i.TypeError,Ki=b("".slice),Gi=b("".charCodeAt),Vi=Ue(Wi,!qi(" 0o1")||!qi("0b1")||qi("+0x1")),Yi=function(t){var e,r=arguments.length<1?0:qi(function(t){var e=mt(t,"number");return"bigint"==typeof e?e:function(t){var e,r,n,o,i,a,u,s,c=mt(t,"number");if(G(c))throw new $i("Cannot convert a Symbol value to a number");if("string"==typeof c&&c.length>2)if(c=zi(c),43===(e=Gi(c,0))||45===e){if(88===(r=Gi(c,2))||120===r)return NaN}else if(48===e){switch(Gi(c,1)){case 66:case 98:n=2,o=49;break;case 79:case 111:n=8,o=55;break;default:return+c}for(a=(i=Ki(c,2)).length,u=0;uo)return NaN;return parseInt(i,n)}return+c}(e)}(t));return U(Hi,e=this)&&a(function(){Ti(e)})?Io(Object(r),this,Yi):r};Yi.prototype=Hi,Vi&&(Hi.constructor=Yi),Ce({global:!0,constructor:!0,wrap:!0,forced:Vi},{Number:Yi}),Vi&&function(t,e){for(var r,n=u?Fi(e):"MAX_VALUE,MIN_VALUE,NaN,NEGATIVE_INFINITY,POSITIVE_INFINITY,EPSILON,MAX_SAFE_INTEGER,MIN_SAFE_INTEGER,isFinite,isInteger,isNaN,isSafeInteger,parseFloat,parseInt,fromString,range".split(","),o=0;n.length>o;o++)ut(e,r=n[o])&&!ut(t,r)&&Di(t,r,Bi(e,r))}(Yn[Wi],qi),Ce({target:"Number",stat:!0,nonConfigurable:!0,nonWritable:!0},{EPSILON:Math.pow(2,-52)});var Xi=i.isFinite;Ce({target:"Number",stat:!0},{isFinite:Number.isFinite||function(t){return"number"==typeof t&&Xi(t)}});var Ji=Math.floor,Qi=Number.isInteger||function(t){return!M(t)&&isFinite(t)&&Ji(t)===t};Ce({target:"Number",stat:!0},{isInteger:Qi}),Ce({target:"Number",stat:!0},{isNaN:function(t){return t!=t}});var Zi=Math.abs;Ce({target:"Number",stat:!0},{isSafeInteger:function(t){return Qi(t)&&Zi(t)<=9007199254740991}}),Ce({target:"Number",stat:!0,nonConfigurable:!0,nonWritable:!0},{MAX_SAFE_INTEGER:9007199254740991}),Ce({target:"Number",stat:!0,nonConfigurable:!0,nonWritable:!0},{MIN_SAFE_INTEGER:-9007199254740991});var ta=_i.trim,ea=b("".charAt),ra=i.parseFloat,na=i.Symbol,oa=na&&na.iterator,ia=1/ra(Mi+"-0")!=-Infinity||oa&&!a(function(){ra(Object(oa))})?function(t){var e=ta(Wr(t)),r=ra(e);return 0===r&&"-"===ea(e,0)?-0:r}:ra;Ce({target:"Number",stat:!0,forced:Number.parseFloat!==ia},{parseFloat:ia});var aa=_i.trim,ua=i.parseInt,sa=i.Symbol,ca=sa&&sa.iterator,fa=/^[+-]?0x/i,la=b(fa.exec),ha=8!==ua(Mi+"08")||22!==ua(Mi+"0x16")||ca&&!a(function(){ua(Object(ca))})?function(t,e){var r=aa(Wr(t));return ua(r,e>>>0||(la(fa,r)?16:10))}:ua;Ce({target:"Number",stat:!0,forced:Number.parseInt!==ha},{parseInt:ha});var pa=b(v.f),va=b([].push),da=u&&a(function(){var t=Object.create(null);return t[2]=2,!pa(t,2)}),ga=function(t){return function(e){for(var r,n=k(e),o=_e(n),i=da&&null===Qr(n),a=o.length,s=0,c=[];a>s;)r=o[s++],u&&!(i?r in n:pa(n,r))||va(c,t?[r,n[r]]:n[r]);return c}},ya={entries:ga(!0),values:ga(!1)},ma=ya.entries;Ce({target:"Object",stat:!0},{entries:function(t){return ma(t)}}),Ce({target:"Object",stat:!0,sham:!u},{getOwnPropertyDescriptors:function(t){for(var e,r,n=k(t),o=Rt.f,i=Pe(n),a={},u=0;i.length>u;)void 0!==(r=o(n,e=i[u++]))&&Cn(a,e,r);return a}});var ba=a(function(){_e(1)});Ce({target:"Object",stat:!0,forced:ba},{keys:function(t){return _e(it(t))}});var wa=Object.is||function(t,e){return t===e?0!==t||1/t==1/e:t!=t&&e!=e};Ce({target:"Object",stat:!0},{is:wa});var Sa=ya.values;Ce({target:"Object",stat:!0},{values:function(t){return Sa(t)}}),Ce({target:"Object",stat:!0},{hasOwn:ut});var Ea=Function.prototype,Oa=Ea.apply,xa=Ea.call,Ra="object"==typeof Reflect&&Reflect.apply||(s?xa.bind(Oa):function(){return xa.apply(Oa,arguments)}),Pa=!a(function(){Reflect.apply(function(){})});Ce({target:"Reflect",stat:!0,forced:Pa},{apply:function(t,e,r){return Ra(J(t),e,kt(r))}});var Aa=Function,ja=b([].concat),ka=b([].join),Ia={},Ta=s?Aa.bind:function(t){var e=J(this),r=e.prototype,n=vo(arguments,1),o=function(){var r=ja(n,vo(arguments));return this instanceof o?function(t,e,r){if(!ut(Ia,e)){for(var n=[],o=0;ob)","g");return"b"!==t.exec("b").groups.a||"bc"!=="b".replace(t,"$c")}),gs=Oe.f,ys=ne.enforce,ms=dt("match"),bs=i.RegExp,ws=bs.prototype,Ss=i.SyntaxError,Es=b(ws.exec),Os=b("".charAt),xs=b("".replace),Rs=b("".indexOf),Ps=b("".slice),As=/^\?<[^\s\d!#%&*+<=>@^][^\s!#%&*+<=>@^]*>/,js=/a/g,ks=/a/g,Is=new bs(js)!==js,Ts=cs.MISSED_STICKY,Ms=cs.UNSUPPORTED_Y,Ls=u&&(!Is||Ts||ps||ds||a(function(){return ks[ms]=!1,bs(js)!==js||bs(ks)===ks||"/a/i"!==String(bs(js,"i"))}));if(Ue("RegExp",Ls)){for(var Us=function(t,e){var r,n,o,i,a,u,s=U(ws,this),c=es(t),f=void 0===e,l=[],h=t;if(!s&&c&&f&&t.constructor===Us)return t;if((c||U(ws,t))&&(t=t.source,f&&(e=os(h))),t=void 0===t?"":Wr(t),e=void 0===e?"":Wr(e),h=t,ps&&"dotAll"in js&&(n=!!e&&Rs(e,"s")>-1)&&(e=xs(e,/s/g,"")),r=e,Ts&&"sticky"in js&&(o=!!e&&Rs(e,"y")>-1)&&Ms&&(e=xs(e,/y/g,"")),ds&&(i=function(t){for(var e,r=t.length,n=0,o="",i=[],a=Ve(null),u=!1,s=!1,c=0,f="";n<=r;n++){if("\\"===(e=Os(t,n)))e+=Os(t,++n);else if("]"===e)u=!1;else if(!u)switch(!0){case"["===e:u=!0;break;case"("===e:if(o+=e,"?:"===Ps(t,n+1,n+3))continue;Es(As,Ps(t,n+1))&&(n+=2,s=!0),c++;continue;case">"===e&&s:if(""===f||ut(a,f))throw new Ss("Invalid capture group name");a[f]=!0,i[i.length]=[f,c],s=!1,f="";continue}s?f+=e:o+=e}return[o,i]}(t),t=i[0],l=i[1]),a=Io(bs(t,e),s?this:ws,Us),(n||o||l.length)&&(u=ys(a),n&&(u.dotAll=!0,u.raw=Us(function(t){for(var e,r=t.length,n=0,o="",i=!1;n<=r;n++)"\\"!==(e=Os(t,n))?i||"."!==e?("["===e?i=!0:"]"===e&&(i=!1),o+=e):o+="[\\s\\S]":o+=e+Os(t,++n);return o}(t),r)),o&&(u.sticky=!0),l.length&&(u.groups=l)),t!==h)try{_t(a,"source",""===h?"(?:)":h)}catch(t){}return a},Ns=gs(bs),Cs=0;Ns.length>Cs;)ls(Us,bs,Ns[Cs++]);ws.constructor=Us,Us.prototype=ws,ie(i,"RegExp",Us,{constructor:!0})}Uo("RegExp");var _s=zt.PROPER,Fs="toString",Bs=RegExp.prototype,Ds=Bs[Fs];(a(function(){return"/a/b"!==Ds.call({source:"a",flags:"b"})})||_s&&Ds.name!==Fs)&&ie(Bs,Fs,function(){var t=kt(this);return"/"+Wr(t.source)+"/"+Wr(os(t))},{unsafe:!0});var zs=ne.get,Ws=RegExp.prototype,qs=TypeError;u&&ps&&so(Ws,"dotAll",{configurable:!0,get:function(){if(this!==Ws){if("RegExp"===E(this))return!!zs(this).dotAll;throw new qs("Incompatible receiver, RegExp required")}}});var Hs=ne.get,$s=nt("native-string-replace",String.prototype.replace),Ks=RegExp.prototype.exec,Gs=Ks,Vs=b("".charAt),Ys=b("".indexOf),Xs=b("".replace),Js=b("".slice),Qs=function(){var t=/a/,e=/b*/g;return f(Ks,t,"a"),f(Ks,e,"a"),0!==t.lastIndex||0!==e.lastIndex}(),Zs=cs.BROKEN_CARET,tc=void 0!==/()??/.exec("")[1];(Qs||tc||Zs||ps||ds)&&(Gs=function(t){var e,r,n,o,i,a,u,s=this,c=Hs(s),l=Wr(t),h=c.raw;if(h)return h.lastIndex=s.lastIndex,e=f(Gs,h,l),s.lastIndex=h.lastIndex,e;var p=c.groups,v=Zs&&s.sticky,d=f(rs,s),g=s.source,y=0,m=l;if(v&&(d=Xs(d,"y",""),-1===Ys(d,"g")&&(d+="g"),m=Js(l,s.lastIndex),s.lastIndex>0&&(!s.multiline||s.multiline&&"\n"!==Vs(l,s.lastIndex-1))&&(g="(?: "+g+")",m=" "+m,y++),r=new RegExp("^(?:"+g+")",d)),tc&&(r=new RegExp("^"+g+"$(?!\\s)",d)),Qs&&(n=s.lastIndex),o=f(Ks,v?r:s,m),v?o?(o.input=Js(o.input,y),o[0]=Js(o[0],y),o.index=s.lastIndex,s.lastIndex+=o[0].length):s.lastIndex=0:Qs&&o&&(s.lastIndex=s.global?o.index+o[0].length:n),tc&&o&&o.length>1&&f($s,o[0],r,function(){for(i=1;i]*>)/g,Oc=/\$([$&'`]|\d{1,2})/g,xc=function(t,e,r,n,o,i){var a=r+t.length,u=n.length,s=Oc;return void 0!==o&&(o=it(o),s=Ec),wc(i,s,function(i,s){var c;switch(bc(s,0)){case"$":return"$";case"&":return t;case"`":return Sc(e,0,r);case"'":return Sc(e,a);case"<":c=o[Sc(s,1,-1)];break;default:var f=+s;if(0===f)return i;if(f>u){var l=mc(f/10);return 0===l?i:l<=u?void 0===n[l-1]?bc(s,1):n[l-1]+bc(s,1):i}c=n[f-1]}return void 0===c?"":c})},Rc=dt("replace"),Pc=Math.max,Ac=Math.min,jc=b([].concat),kc=b([].push),Ic=b("".indexOf),Tc=b("".slice),Mc="$0"==="a".replace(/./,"$0"),Lc=!!/./[Rc]&&""===/./[Rc]("a","$0"),Uc=!a(function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$")});pc("replace",function(t,e,r){var n=Lc?"$":"$0";return[function(t,r){var n=j(this),o=P(t)?void 0:Q(t,Rc);return o?f(o,t,n,r):f(e,Wr(n),t,r)},function(t,o){var i=kt(this),a=Wr(t);if("string"==typeof o&&-1===Ic(o,n)&&-1===Ic(o,"$<")){var u=r(e,i,a,o);if(u.done)return u.value}var s=T(o);s||(o=Wr(o));var c,f=i.global;f&&(c=i.unicode,i.lastIndex=0);for(var l,h=[];null!==(l=yc(i,a))&&(kc(h,l),f);)""===Wr(l[0])&&(i.lastIndex=dc(a,ve(i.lastIndex),c));for(var p,v="",d=0,g=0;g=d&&(v+=Tc(a,d,b)+y,d=b+m.length)}return v+Tc(a,d)}]},!Uc||!Mc||Lc),pc("search",function(t,e,r){return[function(e){var r=j(this),n=P(e)?void 0:Q(e,t);return n?f(n,e,r):new RegExp(e)[t](Wr(r))},function(t){var n=kt(this),o=Wr(t),i=r(e,n,o);if(i.done)return i.value;var a=n.lastIndex;wa(a,0)||(n.lastIndex=0);var u=yc(n,o);return wa(n.lastIndex,a)||(n.lastIndex=a),null===u?-1:u.index}]});var Nc=dt("species"),Cc=function(t,e){var r,n=kt(t).constructor;return void 0===n||P(r=kt(n)[Nc])?e:La(r)},_c=cs.UNSUPPORTED_Y,Fc=Math.min,Bc=b([].push),Dc=b("".slice),zc=!a(function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var r="ab".split(t);return 2!==r.length||"a"!==r[0]||"b"!==r[1]}),Wc="c"==="abbc".split(/(b)*/)[1]||4!=="test".split(/(?:)/,-1).length||2!=="ab".split(/(?:ab)*/).length||4!==".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length;pc("split",function(t,e,r){var n="0".split(void 0,0).length?function(t,r){return void 0===t&&0===r?[]:f(e,this,t,r)}:e;return[function(e,r){var o=j(this),i=P(e)?void 0:Q(e,t);return i?f(i,e,o,r):f(n,Wr(o),e,r)},function(t,o){var i=kt(this),a=Wr(t);if(!Wc){var u=r(n,i,a,o,n!==e);if(u.done)return u.value}var s=Cc(i,RegExp),c=i.unicode,f=new s(_c?"^(?:"+i.source+")":i,(i.ignoreCase?"i":"")+(i.multiline?"m":"")+(i.unicode?"u":"")+(_c?"g":"y")),l=void 0===o?4294967295:o>>>0;if(0===l)return[];if(0===a.length)return null===yc(f,a)?[a]:[];for(var h=0,p=0,v=[];p0;(n>>>=1)&&(e+=e))1&n&&(r+=e);return r},Kc=b($c),Gc=b("".slice),Vc=Math.ceil,Yc=function(t){return function(e,r,n){var o,i,a=Wr(j(e)),u=ve(r),s=a.length,c=void 0===n?" ":Wr(n);return u<=s||""===c?a:((i=Kc(c,Vc((o=u-s)/c.length))).length>o&&(i=Gc(i,0,o)),t?a+i:i+a)}},Xc={start:Yc(!1),end:Yc(!0)},Jc=Xc.start,Qc=Array,Zc=RegExp.escape,tf=b("".charAt),ef=b("".charCodeAt),rf=b(1.1.toString),nf=b([].join),of=/^[0-9a-z]/i,af=/^[$()*+./?[\\\]^{|}]/,uf=RegExp("^[!\"#%&',\\-:;<=>@`~"+Mi+"]"),sf=b(of.exec),cf={"\t":"t","\n":"n","\v":"v","\f":"f","\r":"r"},ff=function(t){var e=rf(ef(t,0),16);return e.length<3?"\\x"+Jc(e,2,"0"):"\\u"+Jc(e,4,"0")},lf=!Zc||"\\x61b"!==Zc("ab");Ce({target:"RegExp",stat:!0,forced:lf},{escape:function(t){!function(t){if("string"==typeof t)return t;throw new qc("Argument is not a string")}(t);for(var e=t.length,r=Qc(e),n=0;n=56320||n+1>=e||56320!=(64512&ef(t,n+1))?r[n]=ff(o):(r[n]=o,r[++n]=tf(t,n))}}return nf(r,"")}}),To("Set",function(t){return function(){return t(this,arguments.length?arguments[0]:void 0)}},Fo);var hf=Set.prototype,pf={Set:Set,add:b(hf.add),has:b(hf.has),remove:b(hf.delete),proto:hf},vf=pf.has,df=function(t){return vf(t),t},gf=pf.Set,yf=pf.proto,mf=b(yf.forEach),bf=b(yf.keys),wf=bf(new gf).next,Sf=function(t,e,r){return r?ci({iterator:bf(t),next:wf},e):mf(t,e)},Ef=pf.Set,Of=pf.add,xf=function(t){var e=new Ef;return Sf(t,function(t){Of(e,t)}),e},Rf=ln(pf.proto,"size","get")||function(t){return t.size},Pf="Invalid size",Af=RangeError,jf=TypeError,kf=Math.max,If=function(t,e){this.set=t,this.size=kf(e,0),this.has=J(t.has),this.keys=J(t.keys)};If.prototype={getIterator:function(){return{iterator:t=kt(f(this.keys,this.set)),next:t.next,done:!1};var t},includes:function(t){return f(this.has,this.set,t)}};var Tf=function(t){kt(t);var e=+t.size;if(e!=e)throw new jf(Pf);var r=ce(e);if(r<0)throw new Af(Pf);return new If(t,r)},Mf=pf.has,Lf=pf.remove,Uf=function(t){var e=df(this),r=Tf(t),n=xf(e);return Rf(e)<=r.size?Sf(e,function(t){r.includes(t)&&Lf(n,t)}):ci(r.getIterator(),function(t){Mf(e,t)&&Lf(n,t)}),n},Nf=function(t){return{size:t,has:function(){return!1},keys:function(){return{next:function(){return{done:!0}}}}}},Cf=function(t){var e=L("Set");try{(new e)[t](Nf(0));try{return(new e)[t](Nf(-1)),!1}catch(t){return!0}}catch(t){return!1}};Ce({target:"Set",proto:!0,real:!0,forced:!Cf("difference")},{difference:Uf});var _f=pf.Set,Ff=pf.add,Bf=pf.has,Df=function(t){var e=df(this),r=Tf(t),n=new _f;return Rf(e)>r.size?ci(r.getIterator(),function(t){Bf(e,t)&&Ff(n,t)}):Sf(e,function(t){r.includes(t)&&Ff(n,t)}),n},zf=!Cf("intersection")||a(function(){return"3,2"!==String(Array.from(new Set([1,2,3]).intersection(new Set([3,2]))))});Ce({target:"Set",proto:!0,real:!0,forced:zf},{intersection:Df});var Wf=pf.has,qf=function(t){var e=df(this),r=Tf(t);if(Rf(e)<=r.size)return!1!==Sf(e,function(t){if(r.includes(t))return!1},!0);var n=r.getIterator();return!1!==ci(n,function(t){if(Wf(e,t))return Tn(n,"normal",!1)})};Ce({target:"Set",proto:!0,real:!0,forced:!Cf("isDisjointFrom")},{isDisjointFrom:qf});var Hf=function(t){var e=df(this),r=Tf(t);return!(Rf(e)>r.size)&&!1!==Sf(e,function(t){if(!r.includes(t))return!1},!0)};Ce({target:"Set",proto:!0,real:!0,forced:!Cf("isSubsetOf")},{isSubsetOf:Hf});var $f=pf.has,Kf=function(t){var e=df(this),r=Tf(t);if(Rf(e)1?arguments[1]:void 0);return!1!==Sf(e,function(t){if(!r(t,t,e))return!1},!0)}});var el=dt("iterator"),rl=Object,nl=L("Set"),ol=function(t){return function(t){return M(t)&&"number"==typeof t.size&&T(t.has)&&T(t.keys)}(t)?t:function(t){if(P(t))return!1;var e=rl(t);return void 0!==e[el]||"@@iterator"in e||ut(un,pr(e))}(t)?new nl(t):t};Ce({target:"Set",proto:!0,real:!0,forced:!0},{difference:function(t){return f(Uf,this,ol(t))}});var il=pf.Set,al=pf.add;Ce({target:"Set",proto:!0,real:!0,forced:!0},{filter:function(t){var e=df(this),r=ar(t,arguments.length>1?arguments[1]:void 0),n=new il;return Sf(e,function(t){r(t,t,e)&&al(n,t)}),n}}),Ce({target:"Set",proto:!0,real:!0,forced:!0},{find:function(t){var e=df(this),r=ar(t,arguments.length>1?arguments[1]:void 0),n=Sf(e,function(t){if(r(t,t,e))return{value:t}},!0);return n&&n.value}}),Ce({target:"Set",proto:!0,real:!0,forced:!0},{intersection:function(t){return f(Df,this,ol(t))}}),Ce({target:"Set",proto:!0,real:!0,forced:!0},{isDisjointFrom:function(t){return f(qf,this,ol(t))}}),Ce({target:"Set",proto:!0,real:!0,forced:!0},{isSubsetOf:function(t){return f(Hf,this,ol(t))}}),Ce({target:"Set",proto:!0,real:!0,forced:!0},{isSupersetOf:function(t){return f(Kf,this,ol(t))}});var ul=b([].join),sl=b([].push);Ce({target:"Set",proto:!0,real:!0,forced:!0},{join:function(t){var e=df(this),r=void 0===t?",":Wr(t),n=[];return Sf(e,function(t){sl(n,t)}),ul(n,r)}});var cl=pf.Set,fl=pf.add;Ce({target:"Set",proto:!0,real:!0,forced:!0},{map:function(t){var e=df(this),r=ar(t,arguments.length>1?arguments[1]:void 0),n=new cl;return Sf(e,function(t){fl(n,r(t,t,e))}),n}});var ll=TypeError;Ce({target:"Set",proto:!0,real:!0,forced:!0},{reduce:function(t){var e=df(this),r=arguments.length<2,n=r?void 0:arguments[1];if(J(t),Sf(e,function(o){r?(r=!1,n=o):n=t(n,o,o,e)}),r)throw new ll("Reduce of empty set with no initial value");return n}}),Ce({target:"Set",proto:!0,real:!0,forced:!0},{some:function(t){var e=df(this),r=ar(t,arguments.length>1?arguments[1]:void 0);return!0===Sf(e,function(t){if(r(t,t,e))return!0},!0)}}),Ce({target:"Set",proto:!0,real:!0,forced:!0},{symmetricDifference:function(t){return f(Xf,this,ol(t))}}),Ce({target:"Set",proto:!0,real:!0,forced:!0},{union:function(t){return f(Qf,this,ol(t))}});var hl=dt("species"),pl=dt("isConcatSpreadable"),vl=W>=51||!a(function(){var t=[];return t[pl]=!1,t.concat()[0]!==t}),dl=function(t){if(!M(t))return!1;var e=t[pl];return void 0!==e?!!e:ur(t)},gl=!(vl&&(W>=51||!a(function(){var t=[];return(t.constructor={})[hl]=function(){return{foo:1}},1!==t.concat(Boolean).foo})));Ce({target:"Array",proto:!0,arity:1,forced:gl},{concat:function(t){var e,r,n,o,i,a=it(this),u=xr(a,0),s=0;for(e=-1,n=arguments.length;e1?arguments[1]:void 0,n=e.length,o=void 0===r?n:ip(ve(r),n),i=Wr(t);return op(e,o-i.length,o)===i}}),Ze("String","endsWith");var sp=RangeError,cp=String.fromCharCode,fp=String.fromCodePoint,lp=b([].join);Ce({target:"String",stat:!0,arity:1,forced:!!fp&&1!==fp.length},{fromCodePoint:function(t){for(var e,r=[],n=arguments.length,o=0;n>o;){if(e=+arguments[o++],he(e,1114111)!==e)throw new sp(e+" is not a valid code point");r[o]=e<65536?cp(e):cp(55296+((e-=65536)>>10),e%1024+56320)}return lp(r,"")}});var hp=b("".indexOf);Ce({target:"String",proto:!0,forced:!rp("includes")},{includes:function(t){return!!~hp(Wr(j(this)),Wr(tp(t)),arguments.length>1?arguments[1]:void 0)}}),Ze("String","includes"),b(un.String);var pp=/Version\/10(?:\.\d+){1,2}(?: [\w./]+)?(?: Mobile\/\w+)? Safari\//.test(_),vp=Xc.start;Ce({target:"String",proto:!0,forced:pp},{padStart:function(t){return vp(this,t,arguments.length>1?arguments[1]:void 0)}}),Ze("String","padStart");var dp=Xc.end;Ce({target:"String",proto:!0,forced:pp},{padEnd:function(t){return dp(this,t,arguments.length>1?arguments[1]:void 0)}}),Ze("String","padEnd");var gp=b([].push),yp=b([].join);Ce({target:"String",stat:!0},{raw:function(t){var e=k(it(t).raw),r=de(e);if(!r)return"";for(var n=arguments.length,o=[],i=0;;){if(gp(o,Wr(e[i++])),i===r)return yp(o,"");i1?arguments[1]:void 0,e.length)),n=Wr(t);return bp(e,r,r+n.length)===n}}),Ze("String","startsWith");var Op=zt.PROPER,xp=function(t){return a(function(){return!!Mi[t]()||"​…᠎"!=="​…᠎"[t]()||Op&&Mi[t].name!==t})},Rp=_i.start,Pp=xp("trimStart")?function(){return Rp(this)}:"".trimStart;Ce({target:"String",proto:!0,name:"trimStart",forced:"".trimLeft!==Pp},{trimLeft:Pp}),Ce({target:"String",proto:!0,name:"trimStart",forced:"".trimStart!==Pp},{trimStart:Pp}),Ze("String","trimLeft");var Ap=_i.end,jp=xp("trimEnd")?function(){return Ap(this)}:"".trimEnd;Ce({target:"String",proto:!0,name:"trimEnd",forced:"".trimRight!==jp},{trimRight:jp}),Ce({target:"String",proto:!0,name:"trimEnd",forced:"".trimEnd!==jp},{trimEnd:jp}),Ze("String","trimRight");var kp=Object.getOwnPropertyDescriptor,Ip=function(t){if(!u)return i[t];var e=kp(i,t);return e&&e.value},Tp=dt("iterator"),Mp=!a(function(){var t=new URL("b?a=1&b=2&c=3","https://a"),e=t.searchParams,r=new URLSearchParams("a=1&a=2&b=3"),n="";return t.pathname="c%20d",e.forEach(function(t,r){e.delete("b"),n+=r+t}),r.delete("a",2),r.delete("b",void 0),!e.size&&!u||!e.sort||"https://a/c%20d?a=1&c=3"!==t.href||"3"!==e.get("c")||"a=1"!==String(new URLSearchParams("?a=1"))||!e[Tp]||"a"!==new URL("https://a@b").username||"b"!==new URLSearchParams(new URLSearchParams("a=b")).get("a")||"xn--e1aybc"!==new URL("https://тест").host||"#%D0%B1"!==new URL("https://a#б").hash||"a1c3"!==n||"x"!==new URL("https://x",void 0).host}),Lp=TypeError,Up=function(t,e){if(t0;)t[o]=t[--o];o!==i++&&(t[o]=n)}else for(var a=Np(r/2),u=Cp(vo(t,0,a),e),s=Cp(vo(t,a),e),c=u.length,f=s.length,l=0,h=0;l0&&0!=(t&r);r>>=1)e++;return e},pv=function(t){var e=null;switch(t.length){case 1:e=t[0];break;case 2:e=(31&t[0])<<6|63&t[1];break;case 3:e=(15&t[0])<<12|(63&t[1])<<6|63&t[2];break;case 4:e=(7&t[0])<<18|(63&t[1])<<12|(63&t[2])<<6|63&t[3]}return e>1114111?null:e},vv=function(t){for(var e=(t=nv(t,cv," ")).length,r="",n=0;ne){r+="%",n++;continue}var i=lv(t,n+1);if(i!=i){r+=o,n++;continue}n+=2;var a=hv(i);if(0===a)o=Jp(i);else{if(1===a||a>4){r+="�",n++;continue}for(var u=[i],s=1;se||"%"!==tv(t,n));){var c=lv(t,n+1);if(c!=c){n+=3;break}if(c>191||c<128)break;rv(u,c),n+=2,s++}if(u.length!==a){r+="�";continue}var f=pv(u);null===f?r+="�":o=Qp(f)}}r+=o,n++}return r},dv=/[!'()~]|%20/g,gv={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+"},yv=function(t){return gv[t]},mv=function(t){return nv(Xp(t),dv,yv)},bv=fn(function(t,e){zp(this,{type:Dp,target:Wp(t).entries,index:0,kind:e})},Bp,function(){var t=qp(this),e=t.target,r=t.index++;if(!e||r>=e.length)return t.target=null,Pn(void 0,!0);var n=e[r];switch(t.kind){case"keys":return Pn(n.key,!1);case"values":return Pn(n.value,!1)}return Pn([n.key,n.value],!1)},!0),wv=function(t){this.entries=[],this.url=null,void 0!==t&&(M(t)?this.parseObject(t):this.parseQuery("string"==typeof t?"?"===tv(t,0)?uv(t,1):t:Wr(t)))};wv.prototype={type:Bp,bindURL:function(t){this.url=t,this.update()},parseObject:function(t){var e,r,n,o,i,a,u,s=this.entries,c=Fn(t);if(c)for(r=(e=Dn(t,c)).next;!(n=f(r,e)).done;){if(o=Dn(kt(n.value)),(a=f(i=o.next,o)).done||(u=f(i,o)).done||!f(i,o).done)throw new Yp("Expected sequence with length 2");rv(s,{key:Wr(a.value),value:Wr(u.value)})}else for(var l in t)ut(t,l)&&rv(s,{key:l,value:Wr(t[l])})},parseQuery:function(t){if(t)for(var e,r,n=this.entries,o=av(t,"&"),i=0;i0?arguments[0]:void 0));u||(this.size=t.entries.length)},Ev=Sv.prototype;if(Mo(Ev,{append:function(t,e){var r=Wp(this);Up(arguments.length,2),rv(r.entries,{key:Wr(t),value:Wr(e)}),u||this.length++,r.updateURL()},delete:function(t){for(var e=Wp(this),r=Up(arguments.length,1),n=e.entries,o=Wr(t),i=r<2?void 0:arguments[1],a=void 0===i?i:Wr(i),s=0;se.key?1:-1}),t.updateURL()},forEach:function(t){for(var e,r=Wp(this).entries,n=ar(t,arguments.length>1?arguments[1]:void 0),o=0;o1?Rv(arguments[1]):{})}}),T($p)){var Pv=function(t){return ko(this,Gp),new $p(t,arguments.length>1?Rv(arguments[1]):{})};Gp.constructor=Pv,Pv.prototype=Gp,Ce({global:!0,constructor:!0,dontCallGetSet:!0,forced:!0},{Request:Pv})}}var Av={URLSearchParams:Sv,getState:Wp},jv=URLSearchParams,kv=jv.prototype,Iv=b(kv.append),Tv=b(kv.delete),Mv=b(kv.forEach),Lv=b([].push),Uv=new jv("a=1&a=2&b=3");Uv.delete("a",1),Uv.delete("b",void 0),Uv+""!="a=2"&&ie(kv,"delete",function(t){var e=arguments.length,r=e<2?void 0:arguments[1];if(e&&void 0===r)return Tv(this,t);var n=[];Mv(this,function(t,e){Lv(n,{key:e,value:t})}),Up(e,1);for(var o,i=Wr(t),a=Wr(r),u=0,s=0,c=!1,f=n.length;uo;)for(var s,c=R(arguments[o++]),l=i?$v(_e(c),i(c)):_e(c),h=l.length,p=0;h>p;)s=l[p++],u&&!f(a,c,s)||(r[s]=c[s]);return r}:qv,Gv=2147483647,Vv=/[^\0-\u007E]/,Yv=/[.\u3002\uFF0E\uFF61]/g,Xv="Overflow: input needs wider integers to process",Jv=RangeError,Qv=b(Yv.exec),Zv=Math.floor,td=String.fromCharCode,ed=b("".charCodeAt),rd=b([].join),nd=b([].push),od=b("".replace),id=b("".split),ad=b("".toLowerCase),ud=function(t){return t+22+75*(t<26)},sd=function(t,e,r){var n=0;for(t=r?Zv(t/700):t>>1,t+=Zv(t/e);t>455;)t=Zv(t/35),n+=36;return Zv(n+36*t/(t+38))},cd=function(t){var e=[];t=function(t){for(var e=[],r=0,n=t.length;r=55296&&o<=56319&&r=i&&nZv((Gv-a)/l))throw new Jv(Xv);for(a+=(f-i)*l,i=f,r=0;rGv)throw new Jv(Xv);if(n===i){for(var h=a,p=36;;){var v=p<=u?1:p>=u+26?26:p-u;if(h?@[\\\]^|]/,qd=/[\0\t\n\r #/:<>?@[\\\]^|]/,Hd=/^[\u0000-\u0020]+/,$d=/(^|[^\u0000-\u0020])[\u0000-\u0020]+$/,Kd=/[\t\n\r]/g,Gd=function(t){var e,r,n,o;if("number"==typeof t){for(e=[],r=0;r<4;r++)Td(e,t%256),t=md(t/256);return Ed(e,".")}if("object"==typeof t){for(e="",n=function(t){for(var e=null,r=1,n=null,o=0,i=0;i<8;i++)0!==t[i]?(o>r&&(e=n,r=o),n=null,o=0):(null===n&&(n=i),++o);return o>r?n:e}(t),r=0;r<8;r++)o&&0===t[r]||(o&&(o=!1),n===r?(e+=r?":":"::",o=!0):(e+=Od(t[r],16),r<7&&(e+=":")));return"["+e+"]"}return t},Vd={},Yd=Kv({},Vd,{" ":1,'"':1,"<":1,">":1,"`":1}),Xd=Kv({},Yd,{"#":1,"?":1,"{":1,"}":1}),Jd=Kv({},Xd,{"/":1,":":1,";":1,"=":1,"@":1,"[":1,"\\":1,"]":1,"^":1,"|":1}),Qd=function(t,e){var r=fd(t,0);return r>32&&r<127&&!ut(e,t)?t:encodeURIComponent(t)},Zd={ftp:21,file:null,http:80,https:443,ws:80,wss:443},tg=function(t,e){var r;return 2===t.length&&Sd(Nd,wd(t,0))&&(":"===(r=wd(t,1))||!e&&"|"===r)},eg=function(t){var e;return t.length>1&&tg(kd(t,0,2))&&(2===t.length||"/"===(e=wd(t,2))||"\\"===e||"?"===e||"#"===e)},rg=function(t){return"."===t||"%2e"===Id(t)},ng={},og={},ig={},ag={},ug={},sg={},cg={},fg={},lg={},hg={},pg={},vg={},dg={},gg={},yg={},mg={},bg={},wg={},Sg={},Eg={},Og={},xg=function(t,e,r){var n,o,i,a=Wr(t);if(e){if(o=this.parse(a))throw new gd(o);this.searchParams=null}else{if(void 0!==r&&(n=new xg(r,!0)),o=this.parse(a,null,n))throw new gd(o);(i=vd(new pd)).bindURL(this),this.searchParams=i}};xg.prototype={type:"URL",parse:function(t,e,r){var n,o,i,a,u,s=this,c=e||ng,f=0,l="",h=!1,p=!1,v=!1;for(t=Wr(t),e||(s.scheme="",s.username="",s.password="",s.host=null,s.port=null,s.path=[],s.query=null,s.fragment=null,s.cannotBeABaseURL=!1,t=Pd(t,Hd,""),t=Pd(t,$d,"$1")),t=Pd(t,Kd,""),n=Wn(t);f<=n.length;){switch(o=n[f],c){case ng:if(!o||!Sd(Nd,o)){if(e)return Md;c=ig;continue}l+=Id(o),c=og;break;case og:if(o&&(Sd(Cd,o)||"+"===o||"-"===o||"."===o))l+=Id(o);else{if(":"!==o){if(e)return Md;l="",c=ig,f=0;continue}if(e&&(s.isSpecial()!==ut(Zd,l)||"file"===l&&(s.includesCredentials()||null!==s.port)||"file"===s.scheme&&!s.host))return;if(s.scheme=l,e)return void(s.isSpecial()&&Zd[s.scheme]===s.port&&(s.port=null));l="","file"===s.scheme?c=gg:s.isSpecial()&&r&&r.scheme===s.scheme?c=ag:s.isSpecial()?c=fg:"/"===n[f+1]?(c=ug,f++):(s.cannotBeABaseURL=!0,Rd(s.path,""),c=Sg)}break;case ig:if(!r||r.cannotBeABaseURL&&"#"!==o)return Md;if(r.cannotBeABaseURL&&"#"===o){s.scheme=r.scheme,s.path=vo(r.path),s.query=r.query,s.fragment="",s.cannotBeABaseURL=!0,c=Og;break}c="file"===r.scheme?gg:sg;continue;case ag:if("/"!==o||"/"!==n[f+1]){c=sg;continue}c=lg,f++;break;case ug:if("/"===o){c=hg;break}c=wg;continue;case sg:if(s.scheme=r.scheme,o===Wv)s.username=r.username,s.password=r.password,s.host=r.host,s.port=r.port,s.path=vo(r.path),s.query=r.query;else if("/"===o||"\\"===o&&s.isSpecial())c=cg;else if("?"===o)s.username=r.username,s.password=r.password,s.host=r.host,s.port=r.port,s.path=vo(r.path),s.query="",c=Eg;else{if("#"!==o){s.username=r.username,s.password=r.password,s.host=r.host,s.port=r.port,s.path=vo(r.path),s.path.length--,c=wg;continue}s.username=r.username,s.password=r.password,s.host=r.host,s.port=r.port,s.path=vo(r.path),s.query=r.query,s.fragment="",c=Og}break;case cg:if(!s.isSpecial()||"/"!==o&&"\\"!==o){if("/"!==o){s.username=r.username,s.password=r.password,s.host=r.host,s.port=r.port,c=wg;continue}c=hg}else c=lg;break;case fg:if(c=lg,"/"!==o||"/"!==wd(l,f+1))continue;f++;break;case lg:if("/"!==o&&"\\"!==o){c=hg;continue}break;case hg:if("@"===o){h&&(l="%40"+l),h=!0,i=Wn(l);for(var d=0;d65535)return Ud;s.port=s.isSpecial()&&m===Zd[s.scheme]?null:m,l=""}if(e)return;c=bg;continue}return Ud}l+=o;break;case gg:if(s.scheme="file","/"===o||"\\"===o)c=yg;else{if(!r||"file"!==r.scheme){c=wg;continue}switch(o){case Wv:s.host=r.host,s.path=vo(r.path),s.query=r.query;break;case"?":s.host=r.host,s.path=vo(r.path),s.query="",c=Eg;break;case"#":s.host=r.host,s.path=vo(r.path),s.query=r.query,s.fragment="",c=Og;break;default:eg(Ed(vo(n,f),""))||(s.host=r.host,s.path=vo(r.path),s.shortenPath()),c=wg;continue}}break;case yg:if("/"===o||"\\"===o){c=mg;break}r&&"file"===r.scheme&&!eg(Ed(vo(n,f),""))&&(tg(r.path[0],!0)?Rd(s.path,r.path[0]):s.host=r.host),c=wg;continue;case mg:if(o===Wv||"/"===o||"\\"===o||"?"===o||"#"===o){if(!e&&tg(l))c=wg;else if(""===l){if(s.host="",e)return;c=bg}else{if(a=s.parseHost(l))return a;if("localhost"===s.host&&(s.host=""),e)return;l="",c=bg}continue}l+=o;break;case bg:if(s.isSpecial()){if(c=wg,"/"!==o&&"\\"!==o)continue}else if(e||"?"!==o)if(e||"#"!==o){if(o!==Wv&&(c=wg,"/"!==o))continue}else s.fragment="",c=Og;else s.query="",c=Eg;break;case wg:if(o===Wv||"/"===o||"\\"===o&&s.isSpecial()||!e&&("?"===o||"#"===o)){if(".."===(u=Id(u=l))||"%2e."===u||".%2e"===u||"%2e%2e"===u?(s.shortenPath(),"/"===o||"\\"===o&&s.isSpecial()||Rd(s.path,"")):rg(l)?"/"===o||"\\"===o&&s.isSpecial()||Rd(s.path,""):("file"===s.scheme&&!s.path.length&&tg(l)&&(s.host&&(s.host=""),l=wd(l,0)+":"),Rd(s.path,l)),l="","file"===s.scheme&&(o===Wv||"?"===o||"#"===o))for(;s.path.length>1&&""===s.path[0];)Ad(s.path);"?"===o?(s.query="",c=Eg):"#"===o&&(s.fragment="",c=Og)}else l+=Qd(o,Xd);break;case Sg:"?"===o?(s.query="",c=Eg):"#"===o?(s.fragment="",c=Og):o!==Wv&&(s.path[0]+=Qd(o,Vd));break;case Eg:e||"#"!==o?o!==Wv&&("'"===o&&s.isSpecial()?s.query+="%27":s.query+="#"===o?"%23":Qd(o,Vd)):(s.fragment="",c=Og);break;case Og:o!==Wv&&(s.fragment+=Qd(o,Yd))}f++}},parseHost:function(t){var e,r,n;if("["===wd(t,0)){if("]"!==wd(t,t.length-1))return Ld;if(e=function(t){var e,r,n,o,i,a,u,s=[0,0,0,0,0,0,0,0],c=0,f=null,l=0,h=function(){return wd(t,l)};if(":"===h()){if(":"!==wd(t,1))return;l+=2,f=++c}for(;h();){if(8===c)return;if(":"!==h()){for(e=r=0;r<4&&Sd(zd,h());)e=16*e+yd(h(),16),l++,r++;if("."===h()){if(0===r)return;if(l-=r,c>6)return;for(n=0;h();){if(o=null,n>0){if(!("."===h()&&n<4))return;l++}if(!Sd(_d,h()))return;for(;Sd(_d,h());){if(i=yd(h(),10),null===o)o=i;else{if(0===o)return;o=10*o+i}if(o>255)return;l++}s[c]=256*s[c]+o,2!=++n&&4!==n||c++}if(4!==n)return;break}if(":"===h()){if(l++,!h())return}else if(h())return;s[c++]=e}else{if(null!==f)return;l++,f=++c}}if(null!==f)for(a=c-f,c=7;0!==c&&a>0;)u=s[c],s[c--]=s[f+a-1],s[f+--a]=u;else if(8!==c)return;return s}(kd(t,1,-1)),!e)return Ld;this.host=e}else if(this.isSpecial()){if(t=function(t){var e,r,n=[],o=id(od(ad(t),Yv,"."),".");for(e=0;e4)return t;for(r=[],n=0;n1&&"0"===wd(o,0)&&(i=Sd(Fd,o)?16:8,o=kd(o,8===i?1:2)),""===o)a=0;else{if(!Sd(10===i?Dd:8===i?Bd:zd,o))return t;a=yd(o,i)}Rd(r,a)}for(n=0;n=bd(256,5-e))return null}else if(a>255)return null;for(u=xd(r),n=0;n1?arguments[1]:void 0,n=ld(e,new xg(t,!1,r));u||(e.href=n.serialize(),e.origin=n.getOrigin(),e.protocol=n.getProtocol(),e.username=n.getUsername(),e.password=n.getPassword(),e.host=n.getHost(),e.hostname=n.getHostname(),e.port=n.getPort(),e.pathname=n.getPathname(),e.search=n.getSearch(),e.searchParams=n.getSearchParams(),e.hash=n.getHash())},Pg=Rg.prototype,Ag=function(t,e){return{get:function(){return hd(this)[t]()},set:e&&function(t){return hd(this)[e](t)},configurable:!0,enumerable:!0}};if(u&&(so(Pg,"href",Ag("serialize","setHref")),so(Pg,"origin",Ag("getOrigin")),so(Pg,"protocol",Ag("getProtocol","setProtocol")),so(Pg,"username",Ag("getUsername","setUsername")),so(Pg,"password",Ag("getPassword","setPassword")),so(Pg,"host",Ag("getHost","setHost")),so(Pg,"hostname",Ag("getHostname","setHostname")),so(Pg,"port",Ag("getPort","setPort")),so(Pg,"pathname",Ag("getPathname","setPathname")),so(Pg,"search",Ag("getSearch","setSearch")),so(Pg,"searchParams",Ag("getSearchParams")),so(Pg,"hash",Ag("getHash","setHash"))),ie(Pg,"toJSON",function(){return hd(this).serialize()},{enumerable:!0}),ie(Pg,"toString",function(){return hd(this).serialize()},{enumerable:!0}),dd){var jg=dd.createObjectURL,kg=dd.revokeObjectURL;jg&&ie(Rg,"createObjectURL",ar(jg,dd)),kg&&ie(Rg,"revokeObjectURL",ar(kg,dd))}an(Rg,"URL"),Ce({global:!0,constructor:!0,forced:!Mp,sham:!u},{URL:Rg});var Ig=L("URL"),Tg=Mp&&a(function(){Ig.canParse()}),Mg=a(function(){return 1!==Ig.canParse.length});Ce({target:"URL",stat:!0,forced:!Tg||Mg},{canParse:function(t){var e=Up(arguments.length,1),r=Wr(t),n=e<2||void 0===arguments[1]?void 0:Wr(arguments[1]);try{return!!new Ig(r,n)}catch(t){return!1}}});var Lg=L("URL");Ce({target:"URL",stat:!0,forced:!Mp},{parse:function(t){var e=Up(arguments.length,1),r=Wr(t),n=e<2||void 0===arguments[1]?void 0:Wr(arguments[1]);try{return new Lg(r,n)}catch(t){return null}}}),Ce({target:"URL",proto:!0,enumerable:!0},{toJSON:function(){return f(URL.prototype.toString,this)}});var Ug=WeakMap.prototype,Ng={WeakMap:WeakMap,set:b(Ug.set),get:b(Ug.get),has:b(Ug.has),remove:b(Ug.delete)},Cg=Ng.has,_g=function(t){return Cg(t),t},Fg=Ng.get,Bg=Ng.has,Dg=Ng.set;Ce({target:"WeakMap",proto:!0,real:!0,forced:!0},{emplace:function(t,e){var r,n,o=_g(this);return Bg(o,t)?(r=Fg(o,t),"update"in e&&(r=e.update(r,t,o),Dg(o,t,r)),r):(n=e.insert(t,o),Dg(o,t,n),n)}}),Ce({target:"WeakMap",stat:!0,forced:!0},{from:ei(Ng.WeakMap,Ng.set,!0)}),Ce({target:"WeakMap",stat:!0,forced:!0},{of:ri(Ng.WeakMap,Ng.set,!0)});var zg=Ng.remove;Ce({target:"WeakMap",proto:!0,real:!0,forced:!0},{deleteAll:function(){for(var t,e=_g(this),r=!0,n=0,o=arguments.length;n2&&(n=r,M(o=arguments[2])&&"cause"in o&&_t(n,"cause",o.cause));var s=[];return Ao(t,ny,{that:s}),_t(r,"errors",s),r};dn?dn(oy,ry):Ae(oy,ry,{name:!0});var iy=oy.prototype=Ve(ry.prototype,{constructor:d(1,oy),message:d(1,""),name:d(1,"AggregateError")});Ce({global:!0,constructor:!0,arity:2},{AggregateError:oy});var ay,uy,sy,cy,fy=function(t){return _.slice(0,t.length)===t},ly=fy("Bun/")?"BUN":fy("Cloudflare-Workers")?"CLOUDFLARE":fy("Deno/")?"DENO":fy("Node.js/")?"NODE":i.Bun&&"string"==typeof Bun.version?"BUN":i.Deno&&"object"==typeof Deno.version?"DENO":"process"===E(i.process)?"NODE":i.window&&i.document?"BROWSER":"REST",hy="NODE"===ly,py=/(?:ipad|iphone|ipod).*applewebkit/i.test(_),vy=i.setImmediate,dy=i.clearImmediate,gy=i.process,yy=i.Dispatch,my=i.Function,by=i.MessageChannel,wy=i.String,Sy=0,Ey={},Oy="onreadystatechange";a(function(){ay=i.location});var xy=function(t){if(ut(Ey,t)){var e=Ey[t];delete Ey[t],e()}},Ry=function(t){return function(){xy(t)}},Py=function(t){xy(t.data)},Ay=function(t){i.postMessage(wy(t),ay.protocol+"//"+ay.host)};vy&&dy||(vy=function(t){Up(arguments.length,1);var e=T(t)?t:my(t),r=vo(arguments,1);return Ey[++Sy]=function(){Ra(e,void 0,r)},uy(Sy),Sy},dy=function(t){delete Ey[t]},hy?uy=function(t){gy.nextTick(Ry(t))}:yy&&yy.now?uy=function(t){yy.now(Ry(t))}:by&&!py?(cy=(sy=new by).port2,sy.port1.onmessage=Py,uy=ar(cy.postMessage,cy)):i.addEventListener&&T(i.postMessage)&&!i.importScripts&&ay&&"file:"!==ay.protocol&&!a(Ay)?(uy=Ay,i.addEventListener("message",Py,!1)):uy=Oy in Et("script")?function(t){De.appendChild(Et("script"))[Oy]=function(){De.removeChild(this),xy(t)}}:function(t){setTimeout(Ry(t),0)});var jy={set:vy,clear:dy},ky=function(){this.head=null,this.tail=null};ky.prototype={add:function(t){var e={item:t,next:null},r=this.tail;r?r.next=e:this.head=e,this.tail=e},get:function(){var t=this.head;if(t)return null===(this.head=t.next)&&(this.tail=null),t.item}};var Iy,Ty,My,Ly,Uy,Ny=ky,Cy=/ipad|iphone|ipod/i.test(_)&&"undefined"!=typeof Pebble,_y=/web0s(?!.*chrome)/i.test(_),Fy=jy.set,By=i.MutationObserver||i.WebKitMutationObserver,Dy=i.document,zy=i.process,Wy=i.Promise,qy=Ip("queueMicrotask");if(!qy){var Hy=new Ny,$y=function(){var t,e;for(hy&&(t=zy.domain)&&t.exit();e=Hy.get();)try{e()}catch(t){throw Hy.head&&Iy(),t}t&&t.enter()};py||hy||_y||!By||!Dy?!Cy&&Wy&&Wy.resolve?((Ly=Wy.resolve(void 0)).constructor=Wy,Uy=ar(Ly.then,Ly),Iy=function(){Uy($y)}):hy?Iy=function(){zy.nextTick($y)}:(Fy=ar(Fy,i),Iy=function(){Fy($y)}):(Ty=!0,My=Dy.createTextNode(""),new By($y).observe(My,{characterData:!0}),Iy=function(){My.data=Ty=!Ty}),qy=function(t){Hy.head||Iy(),Hy.add(t)}}var Ky,Gy,Vy,Yy=qy,Xy=function(t){try{return{error:!1,value:t()}}catch(t){return{error:!0,value:t}}},Jy=i.Promise,Qy=dt("species"),Zy=!1,tm=T(i.PromiseRejectionEvent),em=Ue("Promise",function(){var t=Kt(Jy),e=t!==String(Jy);if(!e&&66===W)return!0;if(!W||W<51||!/native code/.test(t)){var r=new Jy(function(t){t(1)}),n=function(t){t(function(){},function(){})};if((r.constructor={})[Qy]=n,!(Zy=r.then(function(){})instanceof n))return!0}return!(e||"BROWSER"!==ly&&"DENO"!==ly||tm)}),rm={CONSTRUCTOR:em,REJECTION_EVENT:tm,SUBCLASSING:Zy},nm=TypeError,om=function(t){var e,r;this.promise=new t(function(t,n){if(void 0!==e||void 0!==r)throw new nm("Bad Promise constructor");e=t,r=n}),this.resolve=J(e),this.reject=J(r)},im={f:function(t){return new om(t)}},am=jy.set,um="Promise",sm=rm.CONSTRUCTOR,cm=rm.REJECTION_EVENT,fm=rm.SUBCLASSING,lm=ne.getterFor(um),hm=ne.set,pm=Jy&&Jy.prototype,vm=Jy,dm=pm,gm=i.TypeError,ym=i.document,mm=i.process,bm=im.f,wm=bm,Sm=!!(ym&&ym.createEvent&&i.dispatchEvent),Em="unhandledrejection",Om=function(t){var e;return!(!M(t)||!T(e=t.then))&&e},xm=function(t,e){var r,n,o,i=e.value,a=1===e.state,u=a?t.ok:t.fail,s=t.resolve,c=t.reject,l=t.domain;try{u?(a||(2===e.rejection&&km(e),e.rejection=1),!0===u?r=i:(l&&l.enter(),r=u(i),l&&(l.exit(),o=!0)),r===t.promise?c(new gm("Promise-chain cycle")):(n=Om(r))?f(n,r,s,c):s(r)):c(i)}catch(t){l&&!o&&l.exit(),c(t)}},Rm=function(t,e){t.notified||(t.notified=!0,Yy(function(){for(var r,n=t.reactions;r=n.get();)xm(r,t);t.notified=!1,e&&!t.rejection&&Am(t)}))},Pm=function(t,e,r){var n,o;Sm?((n=ym.createEvent("Event")).promise=e,n.reason=r,n.initEvent(t,!1,!0),i.dispatchEvent(n)):n={promise:e,reason:r},!cm&&(o=i["on"+t])?o(n):t===Em&&function(t,e){try{1===arguments.length?console.error(t):console.error(t,e)}catch(t){}}("Unhandled promise rejection",r)},Am=function(t){f(am,i,function(){var e,r=t.facade,n=t.value;if(jm(t)&&(e=Xy(function(){hy?mm.emit("unhandledRejection",n,r):Pm(Em,r,n)}),t.rejection=hy||jm(t)?2:1,e.error))throw e.value})},jm=function(t){return 1!==t.rejection&&!t.parent},km=function(t){f(am,i,function(){var e=t.facade;hy?mm.emit("rejectionHandled",e):Pm("rejectionhandled",e,t.value)})},Im=function(t,e,r){return function(n){t(e,n,r)}},Tm=function(t,e,r){t.done||(t.done=!0,r&&(t=r),t.value=e,t.state=2,Rm(t,!0))},Mm=function(t,e,r){if(!t.done){t.done=!0,r&&(t=r);try{if(t.facade===e)throw new gm("Promise can't be resolved itself");var n=Om(e);n?Yy(function(){var r={done:!1};try{f(n,e,Im(Mm,r,t),Im(Tm,r,t))}catch(e){Tm(r,e,t)}}):(t.value=e,t.state=1,Rm(t,!1))}catch(e){Tm({done:!1},e,t)}}};if(sm&&(vm=function(t){ko(this,dm),J(t),f(Ky,this);var e=lm(this);try{t(Im(Mm,e),Im(Tm,e))}catch(t){Tm(e,t)}},(Ky=function(t){hm(this,{type:um,done:!1,notified:!1,parent:!1,reactions:new Ny,rejection:!1,state:0,value:null})}).prototype=ie(dm=vm.prototype,"then",function(t,e){var r=lm(this),n=bm(Cc(this,vm));return r.parent=!0,n.ok=!T(t)||t,n.fail=T(e)&&e,n.domain=hy?mm.domain:void 0,0===r.state?r.reactions.add(n):Yy(function(){xm(n,r)}),n.promise}),Gy=function(){var t=new Ky,e=lm(t);this.promise=t,this.resolve=Im(Mm,e),this.reject=Im(Tm,e)},im.f=bm=function(t){return t===vm||void 0===t?new Gy(t):wm(t)},T(Jy)&&pm!==Object.prototype)){Vy=pm.then,fm||ie(pm,"then",function(t,e){var r=this;return new vm(function(t,e){f(Vy,r,t,e)}).then(t,e)},{unsafe:!0});try{delete pm.constructor}catch(t){}dn&&dn(pm,dm)}Ce({global:!0,constructor:!0,wrap:!0,forced:sm},{Promise:vm}),an(vm,um,!1),Uo(um);var Lm=rm.CONSTRUCTOR||!Gn(function(t){Jy.all(t).then(void 0,function(){})});Ce({target:"Promise",stat:!0,forced:Lm},{all:function(t){var e=this,r=im.f(e),n=r.resolve,o=r.reject,i=Xy(function(){var r=J(e.resolve),i=[],a=0,u=1;Ao(t,function(t){var s=a++,c=!1;u++,f(r,e,t).then(function(t){c||(c=!0,i[s]=t,--u||n(i))},o)}),--u||n(i)});return i.error&&o(i.value),r.promise}});var Um=Jy&&Jy.prototype;if(Ce({target:"Promise",proto:!0,forced:rm.CONSTRUCTOR,real:!0},{catch:function(t){return this.then(void 0,t)}}),T(Jy)){var Nm=L("Promise").prototype.catch;Um.catch!==Nm&&ie(Um,"catch",Nm,{unsafe:!0})}Ce({target:"Promise",stat:!0,forced:Lm},{race:function(t){var e=this,r=im.f(e),n=r.reject,o=Xy(function(){var o=J(e.resolve);Ao(t,function(t){f(o,e,t).then(r.resolve,n)})});return o.error&&n(o.value),r.promise}}),Ce({target:"Promise",stat:!0,forced:rm.CONSTRUCTOR},{reject:function(t){var e=im.f(this);return(0,e.reject)(t),e.promise}});var Cm=function(t,e){if(kt(t),M(e)&&e.constructor===t)return e;var r=im.f(t);return(0,r.resolve)(e),r.promise};Ce({target:"Promise",stat:!0,forced:rm.CONSTRUCTOR},{resolve:function(t){return Cm(this,t)}}),Ce({target:"Promise",stat:!0,forced:Lm},{allSettled:function(t){var e=this,r=im.f(e),n=r.resolve,o=r.reject,i=Xy(function(){var r=J(e.resolve),o=[],i=0,a=1;Ao(t,function(t){var u=i++,s=!1;a++,f(r,e,t).then(function(t){s||(s=!0,o[u]={status:"fulfilled",value:t},--a||n(o))},function(t){s||(s=!0,o[u]={status:"rejected",reason:t},--a||n(o))})}),--a||n(o)});return i.error&&o(i.value),r.promise}});var _m="No one promise resolved";Ce({target:"Promise",stat:!0,forced:Lm},{any:function(t){var e=this,r=L("AggregateError"),n=im.f(e),o=n.resolve,i=n.reject,a=Xy(function(){var n=J(e.resolve),a=[],u=0,s=1,c=!1;Ao(t,function(t){var l=u++,h=!1;s++,f(n,e,t).then(function(t){h||c||(c=!0,o(t))},function(t){h||c||(h=!0,a[l]=t,--s||i(new r(a,_m)))})}),--s||i(new r(a,_m))});return a.error&&i(a.value),n.promise}}),Ce({target:"Promise",stat:!0},{withResolvers:function(){var t=im.f(this);return{promise:t.promise,resolve:t.resolve,reject:t.reject}}});var Fm=Jy&&Jy.prototype,Bm=!!Jy&&a(function(){Fm.finally.call({then:function(){}},function(){})});if(Ce({target:"Promise",proto:!0,real:!0,forced:Bm},{finally:function(t){var e=Cc(this,L("Promise")),r=T(t);return this.then(r?function(r){return Cm(e,t()).then(function(){return r})}:t,r?function(r){return Cm(e,t()).then(function(){throw r})}:t)}}),T(Jy)){var Dm=L("Promise").prototype.finally;Fm.finally!==Dm&&ie(Fm,"finally",Dm,{unsafe:!0})}var zm=i.Promise,Wm=!1,qm=!zm||!zm.try||Xy(function(){zm.try(function(t){Wm=8===t},8)}).error||!Wm;Ce({target:"Promise",stat:!0,forced:qm},{try:function(t){var e=arguments.length>1?vo(arguments,1):[],r=im.f(this),n=Xy(function(){return Ra(J(t),void 0,e)});return(n.error?r.reject:r.resolve)(n.value),r.promise}}),Ze("Promise","finally");var Hm="URLSearchParams"in self,$m="Symbol"in self&&"iterator"in Symbol,Km="FileReader"in self&&"Blob"in self&&function(){try{return new Blob,!0}catch(t){return!1}}(),Gm="FormData"in self,Vm="ArrayBuffer"in self;if(Vm)var Ym=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],Xm=ArrayBuffer.isView||function(t){return t&&Ym.indexOf(Object.prototype.toString.call(t))>-1};function Jm(t){if("string"!=typeof t&&(t=String(t)),/[^a-z0-9\-#$%&'*+.^_`|~]/i.test(t))throw new TypeError("Invalid character in header field name");return t.toLowerCase()}function Qm(t){return"string"!=typeof t&&(t=String(t)),t}function Zm(t){var e={next:function(){var e=t.shift();return{done:void 0===e,value:e}}};return $m&&(e[Symbol.iterator]=function(){return e}),e}function tb(t){this.map={},t instanceof tb?t.forEach(function(t,e){this.append(e,t)},this):Array.isArray(t)?t.forEach(function(t){this.append(t[0],t[1])},this):t&&Object.getOwnPropertyNames(t).forEach(function(e){this.append(e,t[e])},this)}function eb(t){if(t.bodyUsed)return Promise.reject(new TypeError("Already read"));t.bodyUsed=!0}function rb(t){return new Promise(function(e,r){t.onload=function(){e(t.result)},t.onerror=function(){r(t.error)}})}function nb(t){var e=new FileReader,r=rb(e);return e.readAsArrayBuffer(t),r}function ob(t){if(t.slice)return t.slice(0);var e=new Uint8Array(t.byteLength);return e.set(new Uint8Array(t)),e.buffer}function ib(){return this.bodyUsed=!1,this._initBody=function(t){var e;this._bodyInit=t,t?"string"==typeof t?this._bodyText=t:Km&&Blob.prototype.isPrototypeOf(t)?this._bodyBlob=t:Gm&&FormData.prototype.isPrototypeOf(t)?this._bodyFormData=t:Hm&&URLSearchParams.prototype.isPrototypeOf(t)?this._bodyText=t.toString():Vm&&Km&&(e=t)&&DataView.prototype.isPrototypeOf(e)?(this._bodyArrayBuffer=ob(t.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):Vm&&(ArrayBuffer.prototype.isPrototypeOf(t)||Xm(t))?this._bodyArrayBuffer=ob(t):this._bodyText=t=Object.prototype.toString.call(t):this._bodyText="",this.headers.get("content-type")||("string"==typeof t?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):Hm&&URLSearchParams.prototype.isPrototypeOf(t)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},Km&&(this.blob=function(){var t=eb(this);if(t)return t;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){return this._bodyArrayBuffer?eb(this)||Promise.resolve(this._bodyArrayBuffer):this.blob().then(nb)}),this.text=function(){var t=eb(this);if(t)return t;if(this._bodyBlob)return function(t){var e=new FileReader,r=rb(e);return e.readAsText(t),r}(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(function(t){for(var e=new Uint8Array(t),r=new Array(e.length),n=0;n-1?e:t}(e.method||this.method||"GET"),this.mode=e.mode||this.mode||null,this.signal=e.signal||this.signal,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&r)throw new TypeError("Body not allowed for GET or HEAD requests");this._initBody(r)}function sb(t){var e=new FormData;return t.trim().split("&").forEach(function(t){if(t){var r=t.split("="),n=r.shift().replace(/\+/g," "),o=r.join("=").replace(/\+/g," ");e.append(decodeURIComponent(n),decodeURIComponent(o))}}),e}function cb(t,e){e||(e={}),this.type="default",this.status=void 0===e.status?200:e.status,this.ok=this.status>=200&&this.status<300,this.statusText="statusText"in e?e.statusText:"OK",this.headers=new tb(e.headers),this.url=e.url||"",this._initBody(t)}ub.prototype.clone=function(){return new ub(this,{body:this._bodyInit})},ib.call(ub.prototype),ib.call(cb.prototype),cb.prototype.clone=function(){return new cb(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new tb(this.headers),url:this.url})},cb.error=function(){var t=new cb(null,{status:0,statusText:""});return t.type="error",t};var fb=[301,302,303,307,308];cb.redirect=function(t,e){if(-1===fb.indexOf(e))throw new RangeError("Invalid status code");return new cb(null,{status:e,headers:{location:t}})};var lb=self.DOMException;try{new lb}catch(t){(lb=function(t,e){this.message=t,this.name=e;var r=Error(t);this.stack=r.stack}).prototype=Object.create(Error.prototype),lb.prototype.constructor=lb}function hb(t,e){return new Promise(function(r,n){var o=new ub(t,e);if(o.signal&&o.signal.aborted)return n(new lb("Aborted","AbortError"));var i=new XMLHttpRequest;function a(){i.abort()}i.onload=function(){var t,e,n={status:i.status,statusText:i.statusText,headers:(t=i.getAllResponseHeaders()||"",e=new tb,t.replace(/\r?\n[\t ]+/g," ").split(/\r?\n/).forEach(function(t){var r=t.split(":"),n=r.shift().trim();if(n){var o=r.join(":").trim();e.append(n,o)}}),e)};n.url="responseURL"in i?i.responseURL:n.headers.get("X-Request-URL"),r(new cb("response"in i?i.response:i.responseText,n))},i.onerror=function(){n(new TypeError("Network request failed"))},i.ontimeout=function(){n(new TypeError("Network request failed"))},i.onabort=function(){n(new lb("Aborted","AbortError"))},i.open(o.method,o.url,!0),"include"===o.credentials?i.withCredentials=!0:"omit"===o.credentials&&(i.withCredentials=!1),"responseType"in i&&Km&&(i.responseType="blob"),o.headers.forEach(function(t,e){i.setRequestHeader(e,t)}),o.signal&&(o.signal.addEventListener("abort",a),i.onreadystatechange=function(){4===i.readyState&&o.signal.removeEventListener("abort",a)}),i.send(void 0===o._bodyInit?null:o._bodyInit)})}hb.polyfill=!0,self.fetch||(self.fetch=hb,self.Headers=tb,self.Request=ub,self.Response=cb);var pb=Object.getOwnPropertySymbols,vb=Object.prototype.hasOwnProperty,db=Object.prototype.propertyIsEnumerable,gb=function(){try{if(!Object.assign)return!1;var t=new String("abc");if(t[5]="de","5"===Object.getOwnPropertyNames(t)[0])return!1;for(var e={},r=0;r<10;r++)e["_"+String.fromCharCode(r)]=r;if("0123456789"!==Object.getOwnPropertyNames(e).map(function(t){return e[t]}).join(""))return!1;var n={};return"abcdefghijklmnopqrst".split("").forEach(function(t){n[t]=t}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},n)).join("")}catch(t){return!1}}()?Object.assign:function(t,e){for(var r,n,o=function(t){if(null==t)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(t)}(t),i=1;i{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"InvariantError",{enumerable:!0,get:function(){return n}});class n extends Error{constructor(e,t){super(`Invariant: ${e.endsWith(".")?e:e+"."} This is a bug in Next.js.`,t),this.name="InvariantError"}}},55682,(e,t,r)=>{"use strict";r._=function(e){return e&&e.__esModule?e:{default:e}}},32061,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={BailoutToCSRError:function(){return a},isBailoutToCSRError:function(){return i}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u="BAILOUT_TO_CLIENT_SIDE_RENDERING";class a extends Error{constructor(e){super(`Bail out to client-side rendering: ${e}`),this.reason=e,this.digest=u}}function i(e){return"object"==typeof e&&null!==e&&"digest"in e&&e.digest===u}},18800,(e,t,r)=>{"use strict";var n=e.r(71645);function o(e){var t="https://react.dev/errors/"+e;if(1{"use strict";!function e(){if("undefined"!=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&"function"==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(e){console.error(e)}}(),t.exports=e.r(18800)},64893,(e,t,r)=>{"use strict";var n=e.r(74080),o={stream:!0};function u(t){var r=e.r(t);return"function"!=typeof r.then||"fulfilled"===r.status?null:(r.then(function(e){r.status="fulfilled",r.value=e},function(e){r.status="rejected",r.reason=e}),r)}var a=new WeakSet,i=new WeakSet;function s(){}function l(t){for(var r=t[1],n=[],o=0;od||35===d||114===d||120===d?(p=d,d=3,c++):(p=0,d=3);continue;case 2:44===(b=s[c++])?d=4:_=_<<4|(96s.length&&(b=-1)}var g=s.byteOffset+c;if(-1{"use strict";t.exports=e.r(64893)},35326,(e,t,r)=>{"use strict";t.exports=e.r(21413)},54394,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={HTTPAccessErrorStatus:function(){return u},HTTP_ERROR_FALLBACK_ERROR_CODE:function(){return i},getAccessFallbackErrorTypeByStatus:function(){return c},getAccessFallbackHTTPStatus:function(){return l},isHTTPAccessFallbackError:function(){return s}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u={NOT_FOUND:404,FORBIDDEN:403,UNAUTHORIZED:401},a=new Set(Object.values(u)),i="NEXT_HTTP_ERROR_FALLBACK";function s(e){if("object"!=typeof e||null===e||!("digest"in e)||"string"!=typeof e.digest)return!1;let[t,r]=e.digest.split(";");return t===i&&a.has(Number(r))}function l(e){return Number(e.digest.split(";")[1])}function c(e){switch(e){case 401:return"unauthorized";case 403:return"forbidden";case 404:return"not-found";default:return}}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},76963,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"RedirectStatusCode",{enumerable:!0,get:function(){return o}});var n,o=((n={})[n.SeeOther=303]="SeeOther",n[n.TemporaryRedirect=307]="TemporaryRedirect",n[n.PermanentRedirect=308]="PermanentRedirect",n);("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},68391,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n,o={REDIRECT_ERROR_CODE:function(){return i},RedirectType:function(){return s},isRedirectError:function(){return l}};for(var u in o)Object.defineProperty(r,u,{enumerable:!0,get:o[u]});let a=e.r(76963),i="NEXT_REDIRECT";var s=((n={}).push="push",n.replace="replace",n);function l(e){if("object"!=typeof e||null===e||!("digest"in e)||"string"!=typeof e.digest)return!1;let t=e.digest.split(";"),[r,n]=t,o=t.slice(2,-2).join(";"),u=Number(t.at(-2));return r===i&&("replace"===n||"push"===n)&&"string"==typeof o&&!isNaN(u)&&u in a.RedirectStatusCode}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},65713,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"isNextRouterError",{enumerable:!0,get:function(){return u}});let n=e.r(54394),o=e.r(68391);function u(e){return(0,o.isRedirectError)(e)||(0,n.isHTTPAccessFallbackError)(e)}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},61994,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={NavigationPromisesContext:function(){return l},PathParamsContext:function(){return s},PathnameContext:function(){return i},SearchParamsContext:function(){return a},createDevToolsInstrumentedPromise:function(){return c}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(71645),a=(0,u.createContext)(null),i=(0,u.createContext)(null),s=(0,u.createContext)(null),l=(0,u.createContext)(null);function c(e,t){let r=Promise.resolve(t);return r.status="fulfilled",r.value=t,r.displayName=`${e} (SSR)`,r}},45955,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"workUnitAsyncStorageInstance",{enumerable:!0,get:function(){return n}});let n=(0,e.r(90317).createAsyncLocalStorage)()},21768,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={ACTION_HEADER:function(){return a},FLIGHT_HEADERS:function(){return _},NEXT_ACTION_NOT_FOUND_HEADER:function(){return E},NEXT_DID_POSTPONE_HEADER:function(){return b},NEXT_HMR_REFRESH_HASH_COOKIE:function(){return f},NEXT_HMR_REFRESH_HEADER:function(){return c},NEXT_HTML_REQUEST_ID_HEADER:function(){return O},NEXT_IS_PRERENDER_HEADER:function(){return m},NEXT_REQUEST_ID_HEADER:function(){return R},NEXT_REWRITTEN_PATH_HEADER:function(){return g},NEXT_REWRITTEN_QUERY_HEADER:function(){return v},NEXT_ROUTER_PREFETCH_HEADER:function(){return s},NEXT_ROUTER_SEGMENT_PREFETCH_HEADER:function(){return l},NEXT_ROUTER_STALE_TIME_HEADER:function(){return y},NEXT_ROUTER_STATE_TREE_HEADER:function(){return i},NEXT_RSC_UNION_QUERY:function(){return h},NEXT_URL:function(){return d},RSC_CONTENT_TYPE_HEADER:function(){return p},RSC_HEADER:function(){return u}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u="rsc",a="next-action",i="next-router-state-tree",s="next-router-prefetch",l="next-router-segment-prefetch",c="next-hmr-refresh",f="__next_hmr_refresh_hash__",d="next-url",p="text/x-component",_=[u,i,s,c,l],h="_rsc",y="x-nextjs-stale-time",b="x-nextjs-postponed",g="x-nextjs-rewritten-path",v="x-nextjs-rewritten-query",m="x-nextjs-prerender",E="x-nextjs-action-not-found",R="x-nextjs-request-id",O="x-nextjs-html-request-id";("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},62141,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={getCacheSignal:function(){return y},getDraftModeProviderForCacheScope:function(){return h},getHmrRefreshHash:function(){return d},getPrerenderResumeDataCache:function(){return c},getRenderResumeDataCache:function(){return f},getRuntimeStagePromise:function(){return b},getServerComponentsHmrCache:function(){return _},isHmrRefresh:function(){return p},throwForMissingRequestStore:function(){return s},throwInvariantForMissingStore:function(){return l},workUnitAsyncStorage:function(){return u.workUnitAsyncStorageInstance}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(45955),a=e.r(21768),i=e.r(12718);function s(e){throw Object.defineProperty(Error(`\`${e}\` was called outside a request scope. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context`),"__NEXT_ERROR_CODE",{value:"E251",enumerable:!1,configurable:!0})}function l(){throw Object.defineProperty(new i.InvariantError("Expected workUnitAsyncStorage to have a store."),"__NEXT_ERROR_CODE",{value:"E696",enumerable:!1,configurable:!0})}function c(e){switch(e.type){case"prerender":case"prerender-runtime":case"prerender-ppr":case"prerender-client":return e.prerenderResumeDataCache;case"request":if(e.prerenderResumeDataCache)return e.prerenderResumeDataCache;case"prerender-legacy":case"cache":case"private-cache":case"unstable-cache":return null;default:return e}}function f(e){switch(e.type){case"request":case"prerender":case"prerender-runtime":case"prerender-client":if(e.renderResumeDataCache)return e.renderResumeDataCache;case"prerender-ppr":return e.prerenderResumeDataCache??null;case"cache":case"private-cache":case"unstable-cache":case"prerender-legacy":return null;default:return e}}function d(e,t){if(e.dev)switch(t.type){case"cache":case"private-cache":case"prerender":case"prerender-runtime":return t.hmrRefreshHash;case"request":var r;return null==(r=t.cookies.get(a.NEXT_HMR_REFRESH_HASH_COOKIE))?void 0:r.value}}function p(e,t){if(e.dev)switch(t.type){case"cache":case"private-cache":case"request":return t.isHmrRefresh??!1}return!1}function _(e,t){if(e.dev)switch(t.type){case"cache":case"private-cache":case"request":return t.serverComponentsHmrCache}}function h(e,t){if(e.isDraftMode)switch(t.type){case"cache":case"private-cache":case"unstable-cache":case"prerender-runtime":case"request":return t.draftMode}}function y(e){switch(e.type){case"prerender":case"prerender-client":case"prerender-runtime":return e.cacheSignal;case"request":if(e.cacheSignal)return e.cacheSignal;case"prerender-ppr":case"prerender-legacy":case"cache":case"private-cache":case"unstable-cache":return null;default:return e}}function b(e){switch(e.type){case"prerender-runtime":case"private-cache":return e.runtimeStagePromise;case"prerender":case"prerender-client":case"prerender-ppr":case"prerender-legacy":case"request":case"cache":case"unstable-cache":return null;default:return e}}},90373,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"useUntrackedPathname",{enumerable:!0,get:function(){return u}});let n=e.r(71645),o=e.r(61994);function u(){return!function(){if("undefined"==typeof window){let{workUnitAsyncStorage:t}=e.r(62141),r=t.getStore();if(!r)return!1;switch(r.type){case"prerender":case"prerender-client":case"prerender-ppr":let n=r.fallbackRouteParams;return!!n&&n.size>0}}return!1}()?(0,n.useContext)(o.PathnameContext):null}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},51191,(e,t,r)=>{"use strict";function n(e,t=!0){return e.pathname+e.search+(t?e.hash:"")}Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"createHrefFromUrl",{enumerable:!0,get:function(){return n}}),("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},78377,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={handleHardNavError:function(){return a},useNavFailureHandler:function(){return i}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});e.r(71645);let u=e.r(51191);function a(e){return!!e&&"undefined"!=typeof window&&!!window.next.__pendingUrl&&(0,u.createHrefFromUrl)(new URL(window.location.href))!==(0,u.createHrefFromUrl)(window.next.__pendingUrl)&&(console.error("Error occurred during navigation, falling back to hard navigation",e),window.location.href=window.next.__pendingUrl.toString(),!0)}function i(){}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},26935,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"HTML_LIMITED_BOT_UA_RE",{enumerable:!0,get:function(){return n}});let n=/[\w-]+-Google|Google-[\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight/i},82604,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={HTML_LIMITED_BOT_UA_RE:function(){return u.HTML_LIMITED_BOT_UA_RE},HTML_LIMITED_BOT_UA_RE_STRING:function(){return i},getBotType:function(){return c},isBot:function(){return l}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(26935),a=/Googlebot(?!-)|Googlebot$/i,i=u.HTML_LIMITED_BOT_UA_RE.source;function s(e){return u.HTML_LIMITED_BOT_UA_RE.test(e)}function l(e){return a.test(e)||s(e)}function c(e){return a.test(e)?"dom":s(e)?"html":void 0}},72383,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={ErrorBoundary:function(){return _},ErrorBoundaryHandler:function(){return p}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(55682),a=e.r(43476),i=u._(e.r(71645)),s=e.r(90373),l=e.r(65713);e.r(78377);let c=e.r(12354),f=e.r(82604),d="undefined"!=typeof window&&(0,f.isBot)(window.navigator.userAgent);class p extends i.default.Component{constructor(e){super(e),this.reset=()=>{this.setState({error:null})},this.state={error:null,previousPathname:this.props.pathname}}static getDerivedStateFromError(e){if((0,l.isNextRouterError)(e))throw e;return{error:e}}static getDerivedStateFromProps(e,t){let{error:r}=t;return e.pathname!==t.previousPathname&&t.error?{error:null,previousPathname:e.pathname}:{error:t.error,previousPathname:e.pathname}}render(){return this.state.error&&!d?(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(c.HandleISRError,{error:this.state.error}),this.props.errorStyles,this.props.errorScripts,(0,a.jsx)(this.props.errorComponent,{error:this.state.error,reset:this.reset})]}):this.props.children}}function _({errorComponent:e,errorStyles:t,errorScripts:r,children:n}){let o=(0,s.useUntrackedPathname)();return e?(0,a.jsx)(p,{pathname:o,errorComponent:e,errorStyles:t,errorScripts:r,children:n}):(0,a.jsx)(a.Fragment,{children:n})}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},88540,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n,o={ACTION_HMR_REFRESH:function(){return c},ACTION_NAVIGATE:function(){return i},ACTION_REFRESH:function(){return a},ACTION_RESTORE:function(){return s},ACTION_SERVER_ACTION:function(){return f},ACTION_SERVER_PATCH:function(){return l},PrefetchKind:function(){return d}};for(var u in o)Object.defineProperty(r,u,{enumerable:!0,get:o[u]});let a="refresh",i="navigate",s="restore",l="server-patch",c="hmr-refresh",f="server-action";var d=((n={}).AUTO="auto",n.FULL="full",n.TEMPORARY="temporary",n);("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},90809,(e,t,r)=>{"use strict";function n(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(n=function(e){return e?r:t})(e)}r._=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var r=n(t);if(r&&r.has(e))return r.get(e);var o={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in e)if("default"!==a&&Object.prototype.hasOwnProperty.call(e,a)){var i=u?Object.getOwnPropertyDescriptor(e,a):null;i&&(i.get||i.set)?Object.defineProperty(o,a,i):o[a]=e[a]}return o.default=e,r&&r.set(e,o),o}},64245,(e,t,r)=>{"use strict";function n(e){return null!==e&&"object"==typeof e&&"then"in e&&"function"==typeof e.then}Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"isThenable",{enumerable:!0,get:function(){return n}})},41538,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={dispatchAppRouterAction:function(){return s},useActionQueue:function(){return l}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(90809)._(e.r(71645)),a=e.r(64245),i=null;function s(e){if(null===i)throw Object.defineProperty(Error("Internal Next.js error: Router action dispatched before initialization."),"__NEXT_ERROR_CODE",{value:"E668",enumerable:!1,configurable:!0});i(e)}function l(e){let[t,r]=u.default.useState(e.state);i=t=>e.dispatch(t,r);let n=(0,u.useMemo)(()=>{if((0,a.isThenable)(t)){let e=[],r=Promise.resolve(t).then(t=>(null!==t.debugInfo&&e.push(...t.debugInfo),t));return r._debugInfo=e,r}return t},[t]);return(0,a.isThenable)(n)?(0,u.use)(n):n}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},32120,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"callServer",{enumerable:!0,get:function(){return a}});let n=e.r(71645),o=e.r(88540),u=e.r(41538);async function a(e,t){return new Promise((r,a)=>{(0,n.startTransition)(()=>{(0,u.dispatchAppRouterAction)({type:o.ACTION_SERVER_ACTION,actionId:e,actionArgs:t,resolve:r,reject:a})})})}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},92245,(e,t,r)=>{"use strict";let n;Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"findSourceMapURL",{enumerable:!0,get:function(){return n}});("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},3372,(e,t,r)=>{"use strict";function n(e){return e.startsWith("/")?e:`/${e}`}Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"ensureLeadingSlash",{enumerable:!0,get:function(){return n}})},13258,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={DEFAULT_SEGMENT_KEY:function(){return f},PAGE_SEGMENT_KEY:function(){return c},addSearchParamsIfPageSegment:function(){return s},computeSelectedLayoutSegment:function(){return l},getSegmentValue:function(){return u},getSelectedLayoutSegmentPath:function(){return function e(t,r,n=!0,o=[]){let a;if(n)a=t[1][r];else{let e=t[1];a=e.children??Object.values(e)[0]}if(!a)return o;let i=u(a[0]);return!i||i.startsWith(c)?o:(o.push(i),e(a,r,!1,o))}},isGroupSegment:function(){return a},isParallelRouteSegment:function(){return i}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});function u(e){return Array.isArray(e)?e[1]:e}function a(e){return"("===e[0]&&e.endsWith(")")}function i(e){return e.startsWith("@")&&"@children"!==e}function s(e,t){if(e.includes(c)){let e=JSON.stringify(t);return"{}"!==e?c+"?"+e:c}return e}function l(e,t){if(!e||0===e.length)return null;let r="children"===t?e[0]:e[e.length-1];return r===f?null:r}let c="__PAGE__",f="__DEFAULT__"},74180,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={normalizeAppPath:function(){return i},normalizeRscURL:function(){return s}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(3372),a=e.r(13258);function i(e){return(0,u.ensureLeadingSlash)(e.split("/").reduce((e,t,r,n)=>!t||(0,a.isGroupSegment)(t)||"@"===t[0]||("page"===t||"route"===t)&&r===n.length-1?e:`${e}/${t}`,""))}function s(e){return e.replace(/\.rsc($|\?)/,"$1")}},91463,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={INTERCEPTION_ROUTE_MARKERS:function(){return a},extractInterceptionRouteInformation:function(){return s},isInterceptionRouteAppPath:function(){return i}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(74180),a=["(..)(..)","(.)","(..)","(...)"];function i(e){return void 0!==e.split("/").find(e=>a.find(t=>e.startsWith(t)))}function s(e){let t,r,n;for(let o of e.split("/"))if(r=a.find(e=>o.startsWith(e))){[t,n]=e.split(r,2);break}if(!t||!r||!n)throw Object.defineProperty(Error(`Invalid interception route: ${e}. Must be in the format //(..|...|..)(..)/`),"__NEXT_ERROR_CODE",{value:"E269",enumerable:!1,configurable:!0});switch(t=(0,u.normalizeAppPath)(t),r){case"(.)":n="/"===t?`/${n}`:t+"/"+n;break;case"(..)":if("/"===t)throw Object.defineProperty(Error(`Invalid interception route: ${e}. Cannot use (..) marker at the root level, use (.) instead.`),"__NEXT_ERROR_CODE",{value:"E207",enumerable:!1,configurable:!0});n=t.split("/").slice(0,-1).concat(n).join("/");break;case"(...)":n="/"+n;break;case"(..)(..)":let o=t.split("/");if(o.length<=2)throw Object.defineProperty(Error(`Invalid interception route: ${e}. Cannot use (..)(..) marker at the root level or one level up.`),"__NEXT_ERROR_CODE",{value:"E486",enumerable:!1,configurable:!0});n=o.slice(0,-2).concat(n).join("/");break;default:throw Object.defineProperty(Error("Invariant: unexpected marker"),"__NEXT_ERROR_CODE",{value:"E112",enumerable:!1,configurable:!0})}return{interceptingRoute:t,interceptedRoute:n}}},56019,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"matchSegment",{enumerable:!0,get:function(){return n}});let n=(e,t)=>"string"==typeof e?"string"==typeof t&&e===t:"string"!=typeof t&&e[0]===t[0]&&e[1]===t[1];("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},67764,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={ROOT_SEGMENT_CACHE_KEY:function(){return i},ROOT_SEGMENT_REQUEST_KEY:function(){return a},appendSegmentCacheKeyPart:function(){return f},appendSegmentRequestKeyPart:function(){return l},convertSegmentPathToStaticExportFilename:function(){return _},createSegmentCacheKeyPart:function(){return c},createSegmentRequestKeyPart:function(){return s}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(13258),a="",i="";function s(e){if("string"==typeof e)return e.startsWith(u.PAGE_SEGMENT_KEY)?u.PAGE_SEGMENT_KEY:"/_not-found"===e?"_not-found":p(e);let t=e[0];return"$"+e[2]+"$"+p(t)}function l(e,t,r){return e+"/"+("children"===t?r:`@${p(t)}/${r}`)}function c(e,t){return"string"==typeof t?e:e+"$"+p(t[1])}function f(e,t,r){return e+"/"+("children"===t?r:`@${p(t)}/${r}`)}let d=/^[a-zA-Z0-9\-_@]+$/;function p(e){return d.test(e)?e:"!"+btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function _(e){return`__next${e.replace(/\//g,".")}.txt`}},33906,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={doesStaticSegmentAppearInURL:function(){return f},getCacheKeyForDynamicParam:function(){return d},getParamValueFromCacheKey:function(){return _},getRenderedPathname:function(){return l},getRenderedSearch:function(){return s},parseDynamicParamFromURLPart:function(){return c},urlSearchParamsToParsedUrlQuery:function(){return h},urlToUrlWithoutFlightMarker:function(){return p}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(13258),a=e.r(67764),i=e.r(21768);function s(e){let t=e.headers.get(i.NEXT_REWRITTEN_QUERY_HEADER);return null!==t?""===t?"":"?"+t:p(new URL(e.url)).search}function l(e){return e.headers.get(i.NEXT_REWRITTEN_PATH_HEADER)??p(new URL(e.url)).pathname}function c(e,t,r){switch(e){case"c":case"ci":return rencodeURIComponent(e)):[];case"oc":return rencodeURIComponent(e)):null;case"d":case"di":if(r>=t.length)return"";return encodeURIComponent(t[r]);default:return""}}function f(e){return!(e===a.ROOT_SEGMENT_REQUEST_KEY||e.startsWith(u.PAGE_SEGMENT_KEY)||"("===e[0]&&e.endsWith(")"))&&e!==u.DEFAULT_SEGMENT_KEY&&"/_not-found"!==e}function d(e,t){return"string"==typeof e?(0,u.addSearchParamsIfPageSegment)(e,Object.fromEntries(new URLSearchParams(t))):null===e?"":e.join("/")}function p(e){let t=new URL(e);if(t.searchParams.delete(i.NEXT_RSC_UNION_QUERY),t.pathname.endsWith(".txt")){let{pathname:e}=t,r=e.endsWith("/index.txt")?10:4;t.pathname=e.slice(0,-r)}return t}function _(e,t){return"c"===t||"oc"===t?e.split("/"):e}function h(e){let t={};for(let[r,n]of e.entries())void 0===t[r]?t[r]=n:Array.isArray(t[r])?t[r].push(n):t[r]=[t[r],n];return t}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},50590,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={createInitialRSCPayloadFromFallbackPrerender:function(){return l},getFlightDataPartsFromPath:function(){return s},getNextFlightSegmentPath:function(){return c},normalizeFlightData:function(){return f},prepareFlightRouterStateForRequest:function(){return d}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(13258),a=e.r(33906),i=e.r(51191);function s(e){let[t,r,n,o]=e.slice(-4),u=e.slice(0,-4);return{pathToSegment:u.slice(0,-1),segmentPath:u,segment:u[u.length-1]??"",tree:t,seedData:r,head:n,isHeadPartial:o,isRootRender:4===e.length}}function l(e,t){let r=(0,a.getRenderedPathname)(e),n=(0,a.getRenderedSearch)(e),o=(0,i.createHrefFromUrl)(new URL(location.href)),u=t.f[0],s=u[0];return{b:t.b,c:o.split("/"),q:n,i:t.i,f:[[function e(t,r,n,o){let u,i,s=t[0];if("string"==typeof s)u=s,i=(0,a.doesStaticSegmentAppearInURL)(s);else{let e=s[0],t=s[2],l=(0,a.parseDynamicParamFromURLPart)(t,n,o);u=[e,(0,a.getCacheKeyForDynamicParam)(l,r),t],i=!0}let l=i?o+1:o,c=t[1],f={};for(let t in c){let o=c[t];f[t]=e(o,r,n,l)}return[u,f,null,t[3],t[4]]}(s,n,r.split("/").filter(e=>""!==e),0),u[1],u[2],u[2]]],m:t.m,G:t.G,s:t.s,S:t.S}}function c(e){return e.slice(2)}function f(e){return"string"==typeof e?e:e.map(e=>s(e))}function d(e,t){return t?encodeURIComponent(JSON.stringify(e)):encodeURIComponent(JSON.stringify(function e(t){var r,n;let[o,a,i,s,l,c]=t,f="string"==typeof(r=o)&&r.startsWith(u.PAGE_SEGMENT_KEY+"?")?u.PAGE_SEGMENT_KEY:r,d={};for(let[t,r]of Object.entries(a))d[t]=e(r);let p=[f,d,null,(n=s)&&"refresh"!==n?s:null];return void 0!==l&&(p[4]=l),void 0!==c&&(p[5]=c),p}(e)))}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},14297,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={getAppBuildId:function(){return i},setAppBuildId:function(){return a}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u="";function a(e){u=e}function i(){return u}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},19921,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={djb2Hash:function(){return u},hexHash:function(){return a}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});function u(e){let t=5381;for(let r=0;r>>0}function a(e){return u(e).toString(36).slice(0,5)}},86051,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"computeCacheBustingSearchParam",{enumerable:!0,get:function(){return o}});let n=e.r(19921);function o(e,t,r,o){return(void 0===e||"0"===e)&&void 0===t&&void 0===r&&void 0===o?"":(0,n.hexHash)([e||"0",t||"0",r||"0",o||"0"].join(","))}},88093,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={setCacheBustingSearchParam:function(){return i},setCacheBustingSearchParamWithHash:function(){return s}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(86051),a=e.r(21768),i=(e,t)=>{s(e,(0,u.computeCacheBustingSearchParam)(t[a.NEXT_ROUTER_PREFETCH_HEADER],t[a.NEXT_ROUTER_SEGMENT_PREFETCH_HEADER],t[a.NEXT_ROUTER_STATE_TREE_HEADER],t[a.NEXT_URL]))},s=(e,t)=>{let r=e.search,n=(r.startsWith("?")?r.slice(1):r).split("&").filter(e=>e&&!e.startsWith(`${a.NEXT_RSC_UNION_QUERY}=`));t.length>0?n.push(`${a.NEXT_RSC_UNION_QUERY}=${t}`):n.push(`${a.NEXT_RSC_UNION_QUERY}`),e.search=n.length?`?${n.join("&")}`:""};("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},87288,(e,t,r)=>{"use strict";let n;Object.defineProperty(r,"__esModule",{value:!0});var o={createFetch:function(){return m},createFromNextReadableStream:function(){return E},fetchServerResponse:function(){return v}};for(var u in o)Object.defineProperty(r,u,{enumerable:!0,get:o[u]});let a=e.r(35326),i=e.r(21768),s=e.r(32120),l=e.r(92245),c=e.r(88540),f=e.r(50590),d=e.r(14297),p=e.r(88093),_=e.r(33906),h=a.createFromReadableStream,y=a.createFromFetch;function b(e){return(0,_.urlToUrlWithoutFlightMarker)(new URL(e,location.origin)).toString()}let g=new AbortController;async function v(e,t){let{flightRouterState:r,nextUrl:n,prefetchKind:o}=t,u={[i.RSC_HEADER]:"1",[i.NEXT_ROUTER_STATE_TREE_HEADER]:(0,f.prepareFlightRouterStateForRequest)(r,t.isHmrRefresh)};o===c.PrefetchKind.AUTO&&(u[i.NEXT_ROUTER_PREFETCH_HEADER]="1"),n&&(u[i.NEXT_URL]=n);let a=e;try{let t=o?o===c.PrefetchKind.TEMPORARY?"high":"low":"auto";(e=new URL(e)).pathname.endsWith("/")?e.pathname+="index.txt":e.pathname+=".txt";let r=await m(e,u,t,!0,g.signal),n=(0,_.urlToUrlWithoutFlightMarker)(new URL(r.url)),s=r.redirected?n:a,l=r.headers.get("content-type")||"",p=!!r.headers.get("vary")?.includes(i.NEXT_URL),h=!!r.headers.get(i.NEXT_DID_POSTPONE_HEADER),y=r.headers.get(i.NEXT_ROUTER_STALE_TIME_HEADER),v=null!==y?1e3*parseInt(y,10):-1,R=l.startsWith(i.RSC_CONTENT_TYPE_HEADER);if(R||(R=l.startsWith("text/plain")),!R||!r.ok||!r.body)return e.hash&&(n.hash=e.hash),b(n.toString());let O=r.flightResponse;if(null===O){let e,t=h?(e=r.body.getReader(),new ReadableStream({async pull(t){for(;;){let{done:r,value:n}=await e.read();if(!r){t.enqueue(n);continue}return}}})):r.body;O=E(t,u)}let S=await O;if((0,d.getAppBuildId)()!==S.b)return b(r.url);let P=(0,f.normalizeFlightData)(S.f);if("string"==typeof P)return b(P);return{flightData:P,canonicalUrl:s,renderedSearch:(0,_.getRenderedSearch)(r),couldBeIntercepted:p,prerendered:S.S,postponed:h,staleTime:v,debugInfo:O._debugInfo??null}}catch(e){return g.signal.aborted||console.error(`Failed to fetch RSC payload for ${a}. Falling back to browser navigation.`,e),a.toString()}}async function m(e,t,r,o,u){var a,c;let f=new URL(e);(0,p.setCacheBustingSearchParam)(f,t);let d=fetch(f,{credentials:"same-origin",headers:t,priority:r||void 0,signal:u}),_=o?(a=d,c=t,y(a,{callServer:s.callServer,findSourceMapURL:l.findSourceMapURL,debugChannel:n&&n(c)})):null,h=await d,b=h.redirected,g=new URL(h.url,f);return g.searchParams.delete(i.NEXT_RSC_UNION_QUERY),{url:g.href,redirected:b,ok:h.ok,headers:h.headers,body:h.body,status:h.status,flightResponse:_}}function E(e,t){return h(e,{callServer:s.callServer,findSourceMapURL:l.findSourceMapURL,debugChannel:n&&n(t)})}"undefined"!=typeof window&&(window.addEventListener("pagehide",()=>{g.abort()}),window.addEventListener("pageshow",()=>{g=new AbortController})),("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},39470,(e,t,r)=>{"use strict";function n(){let e,t,r=new Promise((r,n)=>{e=r,t=n});return{resolve:e,reject:t,promise:r}}Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"createPromiseWithResolvers",{enumerable:!0,get:function(){return n}})},70725,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"createRouterCacheKey",{enumerable:!0,get:function(){return o}});let n=e.r(13258);function o(e,t=!1){return Array.isArray(e)?`${e[0]}|${e[1]}|${e[2]}`:t&&e.startsWith(n.PAGE_SEGMENT_KEY)?n.PAGE_SEGMENT_KEY:e}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},8372,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={AppRouterContext:function(){return a},GlobalLayoutRouterContext:function(){return s},LayoutRouterContext:function(){return i},MissingSlotContext:function(){return c},TemplateContext:function(){return l}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(55682)._(e.r(71645)),a=u.default.createContext(null),i=u.default.createContext(null),s=u.default.createContext(null),l=u.default.createContext(null),c=u.default.createContext(new Set)},3680,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"ReadonlyURLSearchParams",{enumerable:!0,get:function(){return o}});class n extends Error{constructor(){super("Method unavailable on `ReadonlyURLSearchParams`. Read more: https://nextjs.org/docs/app/api-reference/functions/use-search-params#updating-searchparams")}}class o extends URLSearchParams{append(){throw new n}delete(){throw new n}set(){throw new n}sort(){throw new n}}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},13957,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={ServerInsertedHTMLContext:function(){return a},useServerInsertedHTML:function(){return i}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(90809)._(e.r(71645)),a=u.default.createContext(null);function i(e){let t=(0,u.useContext)(a);t&&t(e)}},92838,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={UnrecognizedActionError:function(){return u},unstable_isUnrecognizedActionError:function(){return a}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});class u extends Error{constructor(...e){super(...e),this.name="UnrecognizedActionError"}}function a(e){return!!(e&&"object"==typeof e&&e instanceof u)}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},34457,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"actionAsyncStorageInstance",{enumerable:!0,get:function(){return n}});let n=(0,e.r(90317).createAsyncLocalStorage)()},62266,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"actionAsyncStorage",{enumerable:!0,get:function(){return n.actionAsyncStorageInstance}});let n=e.r(34457)},24063,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={getRedirectError:function(){return s},getRedirectStatusCodeFromError:function(){return p},getRedirectTypeFromError:function(){return d},getURLFromRedirectError:function(){return f},permanentRedirect:function(){return c},redirect:function(){return l}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(76963),a=e.r(68391),i="undefined"==typeof window?e.r(62266).actionAsyncStorage:void 0;function s(e,t,r=u.RedirectStatusCode.TemporaryRedirect){let n=Object.defineProperty(Error(a.REDIRECT_ERROR_CODE),"__NEXT_ERROR_CODE",{value:"E394",enumerable:!1,configurable:!0});return n.digest=`${a.REDIRECT_ERROR_CODE};${t};${e};${r};`,n}function l(e,t){throw s(e,t??=i?.getStore()?.isAction?a.RedirectType.push:a.RedirectType.replace,u.RedirectStatusCode.TemporaryRedirect)}function c(e,t=a.RedirectType.replace){throw s(e,t,u.RedirectStatusCode.PermanentRedirect)}function f(e){return(0,a.isRedirectError)(e)?e.digest.split(";").slice(2,-2).join(";"):null}function d(e){if(!(0,a.isRedirectError)(e))throw Object.defineProperty(Error("Not a redirect error"),"__NEXT_ERROR_CODE",{value:"E260",enumerable:!1,configurable:!0});return e.digest.split(";",2)[1]}function p(e){if(!(0,a.isRedirectError)(e))throw Object.defineProperty(Error("Not a redirect error"),"__NEXT_ERROR_CODE",{value:"E260",enumerable:!1,configurable:!0});return Number(e.digest.split(";").at(-2))}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},22783,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"notFound",{enumerable:!0,get:function(){return u}});let n=e.r(54394),o=`${n.HTTP_ERROR_FALLBACK_ERROR_CODE};404`;function u(){let e=Object.defineProperty(Error(o),"__NEXT_ERROR_CODE",{value:"E394",enumerable:!1,configurable:!0});throw e.digest=o,e}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},79854,(e,t,r)=>{"use strict";function n(){throw Object.defineProperty(Error("`forbidden()` is experimental and only allowed to be enabled when `experimental.authInterrupts` is enabled."),"__NEXT_ERROR_CODE",{value:"E488",enumerable:!1,configurable:!0})}Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"forbidden",{enumerable:!0,get:function(){return n}}),e.r(54394).HTTP_ERROR_FALLBACK_ERROR_CODE,("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},22683,(e,t,r)=>{"use strict";function n(){throw Object.defineProperty(Error("`unauthorized()` is experimental and only allowed to be used when `experimental.authInterrupts` is enabled."),"__NEXT_ERROR_CODE",{value:"E411",enumerable:!1,configurable:!0})}Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"unauthorized",{enumerable:!0,get:function(){return n}}),e.r(54394).HTTP_ERROR_FALLBACK_ERROR_CODE,("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},15507,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"unstable_rethrow",{enumerable:!0,get:function(){return function e(t){if((0,o.isNextRouterError)(t)||(0,n.isBailoutToCSRError)(t))throw t;t instanceof Error&&"cause"in t&&e(t.cause)}}});let n=e.r(32061),o=e.r(65713);("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},63138,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={isHangingPromiseRejectionError:function(){return u},makeDevtoolsIOAwarePromise:function(){return f},makeHangingPromise:function(){return l}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});function u(e){return"object"==typeof e&&null!==e&&"digest"in e&&e.digest===a}let a="HANGING_PROMISE_REJECTION";class i extends Error{constructor(e,t){super(`During prerendering, ${t} rejects when the prerender is complete. Typically these errors are handled by React but if you move ${t} to a different context by using \`setTimeout\`, \`after\`, or similar functions you may observe this error and you should handle it in that context. This occurred at route "${e}".`),this.route=e,this.expression=t,this.digest=a}}let s=new WeakMap;function l(e,t,r){if(e.aborted)return Promise.reject(new i(t,r));{let n=new Promise((n,o)=>{let u=o.bind(null,new i(t,r)),a=s.get(e);if(a)a.push(u);else{let t=[u];s.set(e,t),e.addEventListener("abort",()=>{for(let e=0;e{setTimeout(()=>{t(e)},0)})}},67287,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"isPostpone",{enumerable:!0,get:function(){return o}});let n=Symbol.for("react.postpone");function o(e){return"object"==typeof e&&null!==e&&e.$$typeof===n}},76353,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={DynamicServerError:function(){return a},isDynamicServerError:function(){return i}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u="DYNAMIC_SERVER_USAGE";class a extends Error{constructor(e){super(`Dynamic server usage: ${e}`),this.description=e,this.digest=u}}function i(e){return"object"==typeof e&&null!==e&&"digest"in e&&"string"==typeof e.digest&&e.digest===u}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},43248,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={StaticGenBailoutError:function(){return a},isStaticGenBailoutError:function(){return i}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u="NEXT_STATIC_GEN_BAILOUT";class a extends Error{constructor(...e){super(...e),this.code=u}}function i(e){return"object"==typeof e&&null!==e&&"code"in e&&e.code===u}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},54839,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={METADATA_BOUNDARY_NAME:function(){return u},OUTLET_BOUNDARY_NAME:function(){return i},ROOT_LAYOUT_BOUNDARY_NAME:function(){return s},VIEWPORT_BOUNDARY_NAME:function(){return a}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u="__next_metadata_boundary__",a="__next_viewport_boundary__",i="__next_outlet_boundary__",s="__next_root_layout_boundary__"},29419,(e,t,r)=>{"use strict";var n=e.i(47167);Object.defineProperty(r,"__esModule",{value:!0});var o={atLeastOneTask:function(){return s},scheduleImmediate:function(){return i},scheduleOnNextTick:function(){return a},waitAtLeastOneReactRenderTask:function(){return l}};for(var u in o)Object.defineProperty(r,u,{enumerable:!0,get:o[u]});let a=e=>{Promise.resolve().then(()=>{n.default.nextTick(e)})},i=e=>{setImmediate(e)};function s(){return new Promise(e=>i(e))}function l(){return new Promise(e=>setImmediate(e))}},42852,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n,o={RenderStage:function(){return s},StagedRenderingController:function(){return l}};for(var u in o)Object.defineProperty(r,u,{enumerable:!0,get:o[u]});let a=e.r(12718),i=e.r(39470);var s=((n={})[n.Static=1]="Static",n[n.Runtime=2]="Runtime",n[n.Dynamic=3]="Dynamic",n);class l{constructor(e=null){this.abortSignal=e,this.currentStage=1,this.runtimeStagePromise=(0,i.createPromiseWithResolvers)(),this.dynamicStagePromise=(0,i.createPromiseWithResolvers)(),e&&e.addEventListener("abort",()=>{let{reason:t}=e;this.currentStage<2&&(this.runtimeStagePromise.promise.catch(c),this.runtimeStagePromise.reject(t)),this.currentStage<3&&(this.dynamicStagePromise.promise.catch(c),this.dynamicStagePromise.reject(t))},{once:!0})}advanceStage(e){!(this.currentStage>=e)&&(this.currentStage=e,e>=2&&this.runtimeStagePromise.resolve(),e>=3&&this.dynamicStagePromise.resolve())}getStagePromise(e){switch(e){case 2:return this.runtimeStagePromise.promise;case 3:return this.dynamicStagePromise.promise;default:throw Object.defineProperty(new a.InvariantError(`Invalid render stage: ${e}`),"__NEXT_ERROR_CODE",{value:"E881",enumerable:!1,configurable:!0})}}waitForStage(e){return this.getStagePromise(e)}delayUntilStage(e,t,r){var n,o,u;let a,i=(n=this.getStagePromise(e),o=t,u=r,a=new Promise((e,t)=>{n.then(e.bind(null,u),t)}),void 0!==o&&(a.displayName=o),a);return this.abortSignal&&i.catch(c),i}}function c(){}},67673,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n,o,u={Postpone:function(){return A},PreludeState:function(){return J},abortAndThrowOnSynchronousRequestDataAccess:function(){return w},abortOnSynchronousPlatformIOAccess:function(){return T},accessedDynamicData:function(){return I},annotateDynamicAccess:function(){return B},consumeDynamicAccess:function(){return L},createDynamicTrackingState:function(){return v},createDynamicValidationState:function(){return m},createHangingInputAbortSignal:function(){return H},createRenderInBrowserAbortSignal:function(){return F},delayUntilRuntimeStage:function(){return ee},formatDynamicAPIAccesses:function(){return $},getFirstDynamicReason:function(){return E},isDynamicPostpone:function(){return N},isPrerenderInterruptedError:function(){return U},logDisallowedDynamicError:function(){return Q},markCurrentScopeAsDynamic:function(){return R},postponeWithTracking:function(){return M},throwIfDisallowedDynamic:function(){return Z},throwToInterruptStaticGeneration:function(){return O},trackAllowedDynamicAccess:function(){return V},trackDynamicDataInDynamicRender:function(){return S},trackSynchronousPlatformIOAccessInDev:function(){return j},useDynamicRouteParams:function(){return X},useDynamicSearchParams:function(){return W}};for(var a in u)Object.defineProperty(r,a,{enumerable:!0,get:u[a]});let i=(n=e.r(71645))&&n.__esModule?n:{default:n},s=e.r(76353),l=e.r(43248),c=e.r(62141),f=e.r(63599),d=e.r(63138),p=e.r(54839),_=e.r(29419),h=e.r(32061),y=e.r(12718),b=e.r(42852),g="function"==typeof i.default.unstable_postpone;function v(e){return{isDebugDynamicAccesses:e,dynamicAccesses:[],syncDynamicErrorWithStack:null}}function m(){return{hasSuspenseAboveBody:!1,hasDynamicMetadata:!1,hasDynamicViewport:!1,hasAllowedDynamic:!1,dynamicErrors:[]}}function E(e){var t;return null==(t=e.dynamicAccesses[0])?void 0:t.expression}function R(e,t,r){if(t)switch(t.type){case"cache":case"unstable-cache":case"private-cache":return}if(!e.forceDynamic&&!e.forceStatic){if(e.dynamicShouldError)throw Object.defineProperty(new l.StaticGenBailoutError(`Route ${e.route} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${r}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`),"__NEXT_ERROR_CODE",{value:"E553",enumerable:!1,configurable:!0});if(t)switch(t.type){case"prerender-ppr":return M(e.route,r,t.dynamicTracking);case"prerender-legacy":t.revalidate=0;let n=Object.defineProperty(new s.DynamicServerError(`Route ${e.route} couldn't be rendered statically because it used ${r}. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`),"__NEXT_ERROR_CODE",{value:"E550",enumerable:!1,configurable:!0});throw e.dynamicUsageDescription=r,e.dynamicUsageStack=n.stack,n}}}function O(e,t,r){let n=Object.defineProperty(new s.DynamicServerError(`Route ${t.route} couldn't be rendered statically because it used \`${e}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`),"__NEXT_ERROR_CODE",{value:"E558",enumerable:!1,configurable:!0});throw r.revalidate=0,t.dynamicUsageDescription=e,t.dynamicUsageStack=n.stack,n}function S(e){switch(e.type){case"cache":case"unstable-cache":case"private-cache":return}}function P(e,t,r){let n=k(`Route ${e} needs to bail out of prerendering at this point because it used ${t}.`);r.controller.abort(n);let o=r.dynamicTracking;o&&o.dynamicAccesses.push({stack:o.isDebugDynamicAccesses?Error().stack:void 0,expression:t})}function T(e,t,r,n){let o=n.dynamicTracking;P(e,t,n),o&&null===o.syncDynamicErrorWithStack&&(o.syncDynamicErrorWithStack=r)}function j(e){e.stagedRendering&&e.stagedRendering.advanceStage(b.RenderStage.Dynamic)}function w(e,t,r,n){if(!1===n.controller.signal.aborted){P(e,t,n);let o=n.dynamicTracking;o&&null===o.syncDynamicErrorWithStack&&(o.syncDynamicErrorWithStack=r)}throw k(`Route ${e} needs to bail out of prerendering at this point because it used ${t}.`)}function A({reason:e,route:t}){let r=c.workUnitAsyncStorage.getStore();M(t,e,r&&"prerender-ppr"===r.type?r.dynamicTracking:null)}function M(e,t,r){(function(){if(!g)throw Object.defineProperty(Error("Invariant: React.unstable_postpone is not defined. This suggests the wrong version of React was loaded. This is a bug in Next.js"),"__NEXT_ERROR_CODE",{value:"E224",enumerable:!1,configurable:!0})})(),r&&r.dynamicAccesses.push({stack:r.isDebugDynamicAccesses?Error().stack:void 0,expression:t}),i.default.unstable_postpone(D(e,t))}function D(e,t){return`Route ${e} needs to bail out of prerendering at this point because it used ${t}. React throws this special object to indicate where. It should not be caught by your own try/catch. Learn more: https://nextjs.org/docs/messages/ppr-caught-error`}function N(e){return"object"==typeof e&&null!==e&&"string"==typeof e.message&&C(e.message)}function C(e){return e.includes("needs to bail out of prerendering at this point because it used")&&e.includes("Learn more: https://nextjs.org/docs/messages/ppr-caught-error")}if(!1===C(D("%%%","^^^")))throw Object.defineProperty(Error("Invariant: isDynamicPostpone misidentified a postpone reason. This is a bug in Next.js"),"__NEXT_ERROR_CODE",{value:"E296",enumerable:!1,configurable:!0});let x="NEXT_PRERENDER_INTERRUPTED";function k(e){let t=Object.defineProperty(Error(e),"__NEXT_ERROR_CODE",{value:"E394",enumerable:!1,configurable:!0});return t.digest=x,t}function U(e){return"object"==typeof e&&null!==e&&e.digest===x&&"name"in e&&"message"in e&&e instanceof Error}function I(e){return e.length>0}function L(e,t){return e.dynamicAccesses.push(...t.dynamicAccesses),e.dynamicAccesses}function $(e){return e.filter(e=>"string"==typeof e.stack&&e.stack.length>0).map(({expression:e,stack:t})=>(t=t.split("\n").slice(4).filter(e=>!(e.includes("node_modules/next/")||e.includes(" ()")||e.includes(" (node:"))).join("\n"),`Dynamic API Usage Debug - ${e}: -${t}`))}function F(){let e=new AbortController;return e.abort(Object.defineProperty(new h.BailoutToCSRError("Render in Browser"),"__NEXT_ERROR_CODE",{value:"E721",enumerable:!1,configurable:!0})),e.signal}function H(e){switch(e.type){case"prerender":case"prerender-runtime":let t=new AbortController;if(e.cacheSignal)e.cacheSignal.inputReady().then(()=>{t.abort()});else{let r=(0,c.getRuntimeStagePromise)(e);r?r.then(()=>(0,_.scheduleOnNextTick)(()=>t.abort())):(0,_.scheduleOnNextTick)(()=>t.abort())}return t.signal;case"prerender-client":case"prerender-ppr":case"prerender-legacy":case"request":case"cache":case"private-cache":case"unstable-cache":return}}function B(e,t){let r=t.dynamicTracking;r&&r.dynamicAccesses.push({stack:r.isDebugDynamicAccesses?Error().stack:void 0,expression:e})}function X(e){let t=f.workAsyncStorage.getStore(),r=c.workUnitAsyncStorage.getStore();if(t&&r)switch(r.type){case"prerender-client":case"prerender":{let n=r.fallbackRouteParams;n&&n.size>0&&i.default.use((0,d.makeHangingPromise)(r.renderSignal,t.route,e));break}case"prerender-ppr":{let n=r.fallbackRouteParams;if(n&&n.size>0)return M(t.route,e,r.dynamicTracking);break}case"prerender-runtime":throw Object.defineProperty(new y.InvariantError(`\`${e}\` was called during a runtime prerender. Next.js should be preventing ${e} from being included in server components statically, but did not in this case.`),"__NEXT_ERROR_CODE",{value:"E771",enumerable:!1,configurable:!0});case"cache":case"private-cache":throw Object.defineProperty(new y.InvariantError(`\`${e}\` was called inside a cache scope. Next.js should be preventing ${e} from being included in server components statically, but did not in this case.`),"__NEXT_ERROR_CODE",{value:"E745",enumerable:!1,configurable:!0})}}function W(e){let t=f.workAsyncStorage.getStore(),r=c.workUnitAsyncStorage.getStore();if(t)switch(!r&&(0,c.throwForMissingRequestStore)(e),r.type){case"prerender-client":i.default.use((0,d.makeHangingPromise)(r.renderSignal,t.route,e));break;case"prerender-legacy":case"prerender-ppr":if(t.forceStatic)return;throw Object.defineProperty(new h.BailoutToCSRError(e),"__NEXT_ERROR_CODE",{value:"E394",enumerable:!1,configurable:!0});case"prerender":case"prerender-runtime":throw Object.defineProperty(new y.InvariantError(`\`${e}\` was called from a Server Component. Next.js should be preventing ${e} from being included in server components statically, but did not in this case.`),"__NEXT_ERROR_CODE",{value:"E795",enumerable:!1,configurable:!0});case"cache":case"unstable-cache":case"private-cache":throw Object.defineProperty(new y.InvariantError(`\`${e}\` was called inside a cache scope. Next.js should be preventing ${e} from being included in server components statically, but did not in this case.`),"__NEXT_ERROR_CODE",{value:"E745",enumerable:!1,configurable:!0});case"request":return}}let G=/\n\s+at Suspense \(\)/,Y=RegExp(`\\n\\s+at Suspense \\(\\)(?:(?!\\n\\s+at (?:body|div|main|section|article|aside|header|footer|nav|form|p|span|h1|h2|h3|h4|h5|h6) \\(\\))[\\s\\S])*?\\n\\s+at ${p.ROOT_LAYOUT_BOUNDARY_NAME} \\([^\\n]*\\)`),q=RegExp(`\\n\\s+at ${p.METADATA_BOUNDARY_NAME}[\\n\\s]`),K=RegExp(`\\n\\s+at ${p.VIEWPORT_BOUNDARY_NAME}[\\n\\s]`),z=RegExp(`\\n\\s+at ${p.OUTLET_BOUNDARY_NAME}[\\n\\s]`);function V(e,t,r,n){if(!z.test(t)){if(q.test(t)){r.hasDynamicMetadata=!0;return}if(K.test(t)){r.hasDynamicViewport=!0;return}if(Y.test(t)){r.hasAllowedDynamic=!0,r.hasSuspenseAboveBody=!0;return}else if(G.test(t)){r.hasAllowedDynamic=!0;return}else{var o,u;let a;if(n.syncDynamicErrorWithStack)return void r.dynamicErrors.push(n.syncDynamicErrorWithStack);let i=(o=`Route "${e.route}": Uncached data was accessed outside of . This delays the entire page from rendering, resulting in a slow user experience. Learn more: https://nextjs.org/docs/messages/blocking-route`,u=t,(a=Object.defineProperty(Error(o),"__NEXT_ERROR_CODE",{value:"E394",enumerable:!1,configurable:!0})).stack=a.name+": "+o+u,a);return void r.dynamicErrors.push(i)}}}var J=((o={})[o.Full=0]="Full",o[o.Empty=1]="Empty",o[o.Errored=2]="Errored",o);function Q(e,t){console.error(t),e.dev||(e.hasReadableErrorStacks?console.error(`To get a more detailed stack trace and pinpoint the issue, start the app in development mode by running \`next dev\`, then open "${e.route}" in your browser to investigate the error.`):console.error(`To get a more detailed stack trace and pinpoint the issue, try one of the following: - - Start the app in development mode by running \`next dev\`, then open "${e.route}" in your browser to investigate the error. - - Rerun the production build with \`next build --debug-prerender\` to generate better stack traces.`))}function Z(e,t,r,n){if(n.syncDynamicErrorWithStack)throw Q(e,n.syncDynamicErrorWithStack),new l.StaticGenBailoutError;if(0!==t){if(r.hasSuspenseAboveBody)return;let n=r.dynamicErrors;if(n.length>0){for(let t=0;tt):t}},91414,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"unstable_rethrow",{enumerable:!0,get:function(){return function e(t){if((0,a.isNextRouterError)(t)||(0,u.isBailoutToCSRError)(t)||(0,s.isDynamicServerError)(t)||(0,i.isDynamicPostpone)(t)||(0,o.isPostpone)(t)||(0,n.isHangingPromiseRejectionError)(t)||(0,i.isPrerenderInterruptedError)(t))throw t;t instanceof Error&&"cause"in t&&e(t.cause)}}});let n=e.r(63138),o=e.r(67287),u=e.r(32061),a=e.r(65713),i=e.r(67673),s=e.r(76353);("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},90508,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"unstable_rethrow",{enumerable:!0,get:function(){return n}});let n="undefined"==typeof window?e.r(91414).unstable_rethrow:e.r(15507).unstable_rethrow;("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},92805,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={ReadonlyURLSearchParams:function(){return u.ReadonlyURLSearchParams},RedirectType:function(){return i.RedirectType},forbidden:function(){return l.forbidden},notFound:function(){return s.notFound},permanentRedirect:function(){return a.permanentRedirect},redirect:function(){return a.redirect},unauthorized:function(){return c.unauthorized},unstable_isUnrecognizedActionError:function(){return d},unstable_rethrow:function(){return f.unstable_rethrow}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(3680),a=e.r(24063),i=e.r(68391),s=e.r(22783),l=e.r(79854),c=e.r(22683),f=e.r(90508);function d(){throw Object.defineProperty(Error("`unstable_isUnrecognizedActionError` can only be used on the client."),"__NEXT_ERROR_CODE",{value:"E776",enumerable:!1,configurable:!0})}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},76562,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={ReadonlyURLSearchParams:function(){return d.ReadonlyURLSearchParams},RedirectType:function(){return d.RedirectType},ServerInsertedHTMLContext:function(){return c.ServerInsertedHTMLContext},forbidden:function(){return d.forbidden},notFound:function(){return d.notFound},permanentRedirect:function(){return d.permanentRedirect},redirect:function(){return d.redirect},unauthorized:function(){return d.unauthorized},unstable_isUnrecognizedActionError:function(){return f.unstable_isUnrecognizedActionError},unstable_rethrow:function(){return d.unstable_rethrow},useParams:function(){return g},usePathname:function(){return y},useRouter:function(){return b},useSearchParams:function(){return h},useSelectedLayoutSegment:function(){return m},useSelectedLayoutSegments:function(){return v},useServerInsertedHTML:function(){return c.useServerInsertedHTML}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(90809)._(e.r(71645)),a=e.r(8372),i=e.r(61994),s=e.r(13258),l=e.r(3680),c=e.r(13957),f=e.r(92838),d=e.r(92805),p="undefined"==typeof window?e.r(67673).useDynamicRouteParams:void 0,_="undefined"==typeof window?e.r(67673).useDynamicSearchParams:void 0;function h(){_?.("useSearchParams()");let e=(0,u.useContext)(i.SearchParamsContext);return(0,u.useMemo)(()=>e?new l.ReadonlyURLSearchParams(e):null,[e])}function y(){return p?.("usePathname()"),(0,u.useContext)(i.PathnameContext)}function b(){let e=(0,u.useContext)(a.AppRouterContext);if(null===e)throw Object.defineProperty(Error("invariant expected app router to be mounted"),"__NEXT_ERROR_CODE",{value:"E238",enumerable:!1,configurable:!0});return e}function g(){return p?.("useParams()"),(0,u.useContext)(i.PathParamsContext)}function v(e="children"){p?.("useSelectedLayoutSegments()");let t=(0,u.useContext)(a.LayoutRouterContext);return t?(0,s.getSelectedLayoutSegmentPath)(t.parentTree,e):null}function m(e="children"){p?.("useSelectedLayoutSegment()"),(0,u.useContext)(i.NavigationPromisesContext);let t=v(e);return(0,s.computeSelectedLayoutSegment)(t,e)}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},58442,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={RedirectBoundary:function(){return p},RedirectErrorBoundary:function(){return d}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(90809),a=e.r(43476),i=u._(e.r(71645)),s=e.r(76562),l=e.r(24063),c=e.r(68391);function f({redirect:e,reset:t,redirectType:r}){let n=(0,s.useRouter)();return(0,i.useEffect)(()=>{i.default.startTransition(()=>{r===c.RedirectType.push?n.push(e,{}):n.replace(e,{}),t()})},[e,r,t,n]),null}class d extends i.default.Component{constructor(e){super(e),this.state={redirect:null,redirectType:null}}static getDerivedStateFromError(e){if((0,c.isRedirectError)(e))return{redirect:(0,l.getURLFromRedirectError)(e),redirectType:(0,l.getRedirectTypeFromError)(e)};throw e}render(){let{redirect:e,redirectType:t}=this.state;return null!==e&&null!==t?(0,a.jsx)(f,{redirect:e,redirectType:t,reset:()=>this.setState({redirect:null})}):this.props.children}}function p({children:e}){let t=(0,s.useRouter)();return(0,a.jsx)(d,{router:t,children:e})}("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},1244,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"unresolvedThenable",{enumerable:!0,get:function(){return n}});let n={then:()=>{}};("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)},97367,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n={MetadataBoundary:function(){return i},OutletBoundary:function(){return l},RootLayoutBoundary:function(){return c},ViewportBoundary:function(){return s}};for(var o in n)Object.defineProperty(r,o,{enumerable:!0,get:n[o]});let u=e.r(54839),a={[u.METADATA_BOUNDARY_NAME]:function({children:e}){return e},[u.VIEWPORT_BOUNDARY_NAME]:function({children:e}){return e},[u.OUTLET_BOUNDARY_NAME]:function({children:e}){return e},[u.ROOT_LAYOUT_BOUNDARY_NAME]:function({children:e}){return e}},i=a[u.METADATA_BOUNDARY_NAME.slice(0)],s=a[u.VIEWPORT_BOUNDARY_NAME.slice(0)],l=a[u.OUTLET_BOUNDARY_NAME.slice(0)],c=a[u.ROOT_LAYOUT_BOUNDARY_NAME.slice(0)]},84356,(e,t,r)=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0}),Object.defineProperty(r,"hasInterceptionRouteInCurrentTree",{enumerable:!0,get:function(){return function e([t,r]){if(Array.isArray(t)&&("di"===t[2]||"ci"===t[2])||"string"==typeof t&&(0,n.isInterceptionRouteAppPath)(t))return!0;if(r){for(let t in r)if(e(r[t]))return!0}return!1}}});let n=e.r(91463);("function"==typeof r.default||"object"==typeof r.default&&null!==r.default)&&void 0===r.default.__esModule&&(Object.defineProperty(r.default,"__esModule",{value:!0}),Object.assign(r.default,r),t.exports=r.default)}]); \ No newline at end of file diff --git a/static/frontend/login/_next/static/chunks/d98da620b14e9668.js b/static/frontend/login/_next/static/chunks/d98da620b14e9668.js deleted file mode 100644 index a3bb3a2f57605b6c1f3fbf3b1fd310c0aa3c7619..0000000000000000000000000000000000000000 --- a/static/frontend/login/_next/static/chunks/d98da620b14e9668.js +++ /dev/null @@ -1 +0,0 @@ -(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,17994,e=>{"use strict";let r,t;var o=e.i(43476),n=e.i(71645);function l(e,r){if("function"==typeof e)return e(r);null!=e&&(e.current=r)}var s=n.forwardRef((e,r)=>{let{children:t,...l}=e,s=n.Children.toArray(t),i=s.find(c);if(i){let e=i.props.children,t=s.map(r=>r!==i?r:n.Children.count(e)>1?n.Children.only(null):n.isValidElement(e)?e.props.children:null);return(0,o.jsx)(a,{...l,ref:r,children:n.isValidElement(e)?n.cloneElement(e,void 0,t):null})}return(0,o.jsx)(a,{...l,ref:r,children:t})});s.displayName="Slot";var a=n.forwardRef((e,r)=>{let{children:t,...o}=e;if(n.isValidElement(t)){var s;let e,a,i=(s=t,(a=(e=Object.getOwnPropertyDescriptor(s.props,"ref")?.get)&&"isReactWarning"in e&&e.isReactWarning)?s.ref:(a=(e=Object.getOwnPropertyDescriptor(s,"ref")?.get)&&"isReactWarning"in e&&e.isReactWarning)?s.props.ref:s.props.ref||s.ref);return n.cloneElement(t,{...function(e,r){let t={...r};for(let o in r){let n=e[o],l=r[o];/^on[A-Z]/.test(o)?n&&l?t[o]=(...e)=>{l(...e),n(...e)}:n&&(t[o]=n):"style"===o?t[o]={...n,...l}:"className"===o&&(t[o]=[n,l].filter(Boolean).join(" "))}return{...e,...t}}(o,t.props),ref:r?function(...e){return r=>{let t=!1,o=e.map(e=>{let o=l(e,r);return t||"function"!=typeof o||(t=!0),o});if(t)return()=>{for(let r=0;r1?n.Children.only(null):null});a.displayName="SlotClone";var i=({children:e})=>(0,o.jsx)(o.Fragment,{children:e});function c(e){return n.isValidElement(e)&&e.type===i}function d(){for(var e,r,t=0,o="",n=arguments.length;t"boolean"==typeof e?`${e}`:0===e?"0":e,u=(e,r)=>{if(0===e.length)return r.classGroupId;let t=e[0],o=r.nextPart.get(t),n=o?u(e.slice(1),o):void 0;if(n)return n;if(0===r.validators.length)return;let l=e.join("-");return r.validators.find(({validator:e})=>e(l))?.classGroupId},b=/^\[(.+)\]$/,h=(e,r,t,o)=>{e.forEach(e=>{if("string"==typeof e){(""===e?r:m(r,e)).classGroupId=t;return}"function"==typeof e?g(e)?h(e(o),r,t,o):r.validators.push({validator:e,classGroupId:t}):Object.entries(e).forEach(([e,n])=>{h(n,m(r,e),t,o)})})},m=(e,r)=>{let t=e;return r.split("-").forEach(e=>{t.nextPart.has(e)||t.nextPart.set(e,{nextPart:new Map,validators:[]}),t=t.nextPart.get(e)}),t},g=e=>e.isThemeGetter,f=(e,r)=>r?e.map(([e,t])=>[e,t.map(e=>"string"==typeof e?r+e:"object"==typeof e?Object.fromEntries(Object.entries(e).map(([e,t])=>[r+e,t])):e)]):e,w=e=>{if(e.length<=1)return e;let r=[],t=[];return e.forEach(e=>{"["===e[0]?(r.push(...t.sort(),e),t=[]):t.push(e)}),r.push(...t.sort()),r},x=/\s+/;function v(){let e,r,t=0,o="";for(;t{let r;if("string"==typeof e)return e;let t="";for(let o=0;o{let r=r=>r[e]||[];return r.isThemeGetter=!0,r},j=/^\[(?:([a-z-]+):)?(.+)\]$/i,A=/^\d+\/\d+$/,z=new Set(["px","full","screen"]),C=/^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/,N=/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/,S=/^(rgba?|hsla?|hwb|(ok)?(lab|lch))\(.+\)$/,M=/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/,_=/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/,L=e=>E(e)||z.has(e)||A.test(e),Q=e=>H(e,"length",U),E=e=>!!e&&!Number.isNaN(Number(e)),W=e=>H(e,"number",E),O=e=>!!e&&Number.isInteger(Number(e)),P=e=>e.endsWith("%")&&E(e.slice(0,-1)),I=e=>j.test(e),R=e=>C.test(e),$=new Set(["length","size","percentage"]),G=e=>H(e,$,F),B=e=>H(e,"position",F),V=new Set(["image","url"]),T=e=>H(e,V,K),Z=e=>H(e,"",D),q=()=>!0,H=(e,r,t)=>{let o=j.exec(e);return!!o&&(o[1]?"string"==typeof r?o[1]===r:r.has(o[1]):t(o[2]))},U=e=>N.test(e)&&!S.test(e),F=()=>!1,D=e=>M.test(e),K=e=>_.test(e),Y=function(e,...r){let t,o,n,l=function(a){let i;return o=(t={cache:(e=>{if(e<1)return{get:()=>void 0,set:()=>{}};let r=0,t=new Map,o=new Map,n=(n,l)=>{t.set(n,l),++r>e&&(r=0,o=t,t=new Map)};return{get(e){let r=t.get(e);return void 0!==r?r:void 0!==(r=o.get(e))?(n(e,r),r):void 0},set(e,r){t.has(e)?t.set(e,r):n(e,r)}}})((i=r.reduce((e,r)=>r(e),e())).cacheSize),parseClassName:(e=>{let{separator:r,experimentalParseClassName:t}=e,o=1===r.length,n=r[0],l=r.length,s=e=>{let t,s=[],a=0,i=0;for(let c=0;ci?t-i:void 0}};return t?e=>t({className:e,parseClassName:s}):s})(i),...(e=>{let r=(e=>{let{theme:r,prefix:t}=e,o={nextPart:new Map,validators:[]};return f(Object.entries(e.classGroups),t).forEach(([e,t])=>{h(t,o,e,r)}),o})(e),{conflictingClassGroups:t,conflictingClassGroupModifiers:o}=e;return{getClassGroupId:e=>{let t=e.split("-");return""===t[0]&&1!==t.length&&t.shift(),u(t,r)||(e=>{if(b.test(e)){let r=b.exec(e)[1],t=r?.substring(0,r.indexOf(":"));if(t)return"arbitrary.."+t}})(e)},getConflictingClassGroupIds:(e,r)=>{let n=t[e]||[];return r&&o[e]?[...n,...o[e]]:n}}})(i)}).cache.get,n=t.cache.set,l=s,s(a)};function s(e){let r=o(e);if(r)return r;let l=((e,r)=>{let{parseClassName:t,getClassGroupId:o,getConflictingClassGroupIds:n}=r,l=[],s=e.trim().split(x),a="";for(let e=s.length-1;e>=0;e-=1){let r=s[e],{modifiers:i,hasImportantModifier:c,baseClassName:d,maybePostfixModifierPosition:p}=t(r),u=!!p,b=o(u?d.substring(0,p):d);if(!b){if(!u||!(b=o(d))){a=r+(a.length>0?" "+a:a);continue}u=!1}let h=w(i).join(":"),m=c?h+"!":h,g=m+b;if(l.includes(g))continue;l.push(g);let f=n(b,u);for(let e=0;e0?" "+a:a)}return a})(e,t);return n(e,l),l}return function(){return l(v.apply(null,arguments))}}(()=>{let e=k("colors"),r=k("spacing"),t=k("blur"),o=k("brightness"),n=k("borderColor"),l=k("borderRadius"),s=k("borderSpacing"),a=k("borderWidth"),i=k("contrast"),c=k("grayscale"),d=k("hueRotate"),p=k("invert"),u=k("gap"),b=k("gradientColorStops"),h=k("gradientColorStopPositions"),m=k("inset"),g=k("margin"),f=k("opacity"),w=k("padding"),x=k("saturate"),v=k("scale"),y=k("sepia"),j=k("skew"),A=k("space"),z=k("translate"),C=()=>["auto","contain","none"],N=()=>["auto","hidden","clip","visible","scroll"],S=()=>["auto",I,r],M=()=>[I,r],_=()=>["",L,Q],$=()=>["auto",E,I],V=()=>["bottom","center","left","left-bottom","left-top","right","right-bottom","right-top","top"],H=()=>["solid","dashed","dotted","double","none"],U=()=>["normal","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity"],F=()=>["start","end","center","between","around","evenly","stretch"],D=()=>["","0",I],K=()=>["auto","avoid","all","avoid-page","page","left","right","column"],Y=()=>[E,I];return{cacheSize:500,separator:":",theme:{colors:[q],spacing:[L,Q],blur:["none","",R,I],brightness:Y(),borderColor:[e],borderRadius:["none","","full",R,I],borderSpacing:M(),borderWidth:_(),contrast:Y(),grayscale:D(),hueRotate:Y(),invert:D(),gap:M(),gradientColorStops:[e],gradientColorStopPositions:[P,Q],inset:S(),margin:S(),opacity:Y(),padding:M(),saturate:Y(),scale:Y(),sepia:D(),skew:Y(),space:M(),translate:M()},classGroups:{aspect:[{aspect:["auto","square","video",I]}],container:["container"],columns:[{columns:[R]}],"break-after":[{"break-after":K()}],"break-before":[{"break-before":K()}],"break-inside":[{"break-inside":["auto","avoid","avoid-page","avoid-column"]}],"box-decoration":[{"box-decoration":["slice","clone"]}],box:[{box:["border","content"]}],display:["block","inline-block","inline","flex","inline-flex","table","inline-table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row-group","table-row","flow-root","grid","inline-grid","contents","list-item","hidden"],float:[{float:["right","left","none","start","end"]}],clear:[{clear:["left","right","both","none","start","end"]}],isolation:["isolate","isolation-auto"],"object-fit":[{object:["contain","cover","fill","none","scale-down"]}],"object-position":[{object:[...V(),I]}],overflow:[{overflow:N()}],"overflow-x":[{"overflow-x":N()}],"overflow-y":[{"overflow-y":N()}],overscroll:[{overscroll:C()}],"overscroll-x":[{"overscroll-x":C()}],"overscroll-y":[{"overscroll-y":C()}],position:["static","fixed","absolute","relative","sticky"],inset:[{inset:[m]}],"inset-x":[{"inset-x":[m]}],"inset-y":[{"inset-y":[m]}],start:[{start:[m]}],end:[{end:[m]}],top:[{top:[m]}],right:[{right:[m]}],bottom:[{bottom:[m]}],left:[{left:[m]}],visibility:["visible","invisible","collapse"],z:[{z:["auto",O,I]}],basis:[{basis:S()}],"flex-direction":[{flex:["row","row-reverse","col","col-reverse"]}],"flex-wrap":[{flex:["wrap","wrap-reverse","nowrap"]}],flex:[{flex:["1","auto","initial","none",I]}],grow:[{grow:D()}],shrink:[{shrink:D()}],order:[{order:["first","last","none",O,I]}],"grid-cols":[{"grid-cols":[q]}],"col-start-end":[{col:["auto",{span:["full",O,I]},I]}],"col-start":[{"col-start":$()}],"col-end":[{"col-end":$()}],"grid-rows":[{"grid-rows":[q]}],"row-start-end":[{row:["auto",{span:[O,I]},I]}],"row-start":[{"row-start":$()}],"row-end":[{"row-end":$()}],"grid-flow":[{"grid-flow":["row","col","dense","row-dense","col-dense"]}],"auto-cols":[{"auto-cols":["auto","min","max","fr",I]}],"auto-rows":[{"auto-rows":["auto","min","max","fr",I]}],gap:[{gap:[u]}],"gap-x":[{"gap-x":[u]}],"gap-y":[{"gap-y":[u]}],"justify-content":[{justify:["normal",...F()]}],"justify-items":[{"justify-items":["start","end","center","stretch"]}],"justify-self":[{"justify-self":["auto","start","end","center","stretch"]}],"align-content":[{content:["normal",...F(),"baseline"]}],"align-items":[{items:["start","end","center","baseline","stretch"]}],"align-self":[{self:["auto","start","end","center","stretch","baseline"]}],"place-content":[{"place-content":[...F(),"baseline"]}],"place-items":[{"place-items":["start","end","center","baseline","stretch"]}],"place-self":[{"place-self":["auto","start","end","center","stretch"]}],p:[{p:[w]}],px:[{px:[w]}],py:[{py:[w]}],ps:[{ps:[w]}],pe:[{pe:[w]}],pt:[{pt:[w]}],pr:[{pr:[w]}],pb:[{pb:[w]}],pl:[{pl:[w]}],m:[{m:[g]}],mx:[{mx:[g]}],my:[{my:[g]}],ms:[{ms:[g]}],me:[{me:[g]}],mt:[{mt:[g]}],mr:[{mr:[g]}],mb:[{mb:[g]}],ml:[{ml:[g]}],"space-x":[{"space-x":[A]}],"space-x-reverse":["space-x-reverse"],"space-y":[{"space-y":[A]}],"space-y-reverse":["space-y-reverse"],w:[{w:["auto","min","max","fit","svw","lvw","dvw",I,r]}],"min-w":[{"min-w":[I,r,"min","max","fit"]}],"max-w":[{"max-w":[I,r,"none","full","min","max","fit","prose",{screen:[R]},R]}],h:[{h:[I,r,"auto","min","max","fit","svh","lvh","dvh"]}],"min-h":[{"min-h":[I,r,"min","max","fit","svh","lvh","dvh"]}],"max-h":[{"max-h":[I,r,"min","max","fit","svh","lvh","dvh"]}],size:[{size:[I,r,"auto","min","max","fit"]}],"font-size":[{text:["base",R,Q]}],"font-smoothing":["antialiased","subpixel-antialiased"],"font-style":["italic","not-italic"],"font-weight":[{font:["thin","extralight","light","normal","medium","semibold","bold","extrabold","black",W]}],"font-family":[{font:[q]}],"fvn-normal":["normal-nums"],"fvn-ordinal":["ordinal"],"fvn-slashed-zero":["slashed-zero"],"fvn-figure":["lining-nums","oldstyle-nums"],"fvn-spacing":["proportional-nums","tabular-nums"],"fvn-fraction":["diagonal-fractions","stacked-fractions"],tracking:[{tracking:["tighter","tight","normal","wide","wider","widest",I]}],"line-clamp":[{"line-clamp":["none",E,W]}],leading:[{leading:["none","tight","snug","normal","relaxed","loose",L,I]}],"list-image":[{"list-image":["none",I]}],"list-style-type":[{list:["none","disc","decimal",I]}],"list-style-position":[{list:["inside","outside"]}],"placeholder-color":[{placeholder:[e]}],"placeholder-opacity":[{"placeholder-opacity":[f]}],"text-alignment":[{text:["left","center","right","justify","start","end"]}],"text-color":[{text:[e]}],"text-opacity":[{"text-opacity":[f]}],"text-decoration":["underline","overline","line-through","no-underline"],"text-decoration-style":[{decoration:[...H(),"wavy"]}],"text-decoration-thickness":[{decoration:["auto","from-font",L,Q]}],"underline-offset":[{"underline-offset":["auto",L,I]}],"text-decoration-color":[{decoration:[e]}],"text-transform":["uppercase","lowercase","capitalize","normal-case"],"text-overflow":["truncate","text-ellipsis","text-clip"],"text-wrap":[{text:["wrap","nowrap","balance","pretty"]}],indent:[{indent:M()}],"vertical-align":[{align:["baseline","top","middle","bottom","text-top","text-bottom","sub","super",I]}],whitespace:[{whitespace:["normal","nowrap","pre","pre-line","pre-wrap","break-spaces"]}],break:[{break:["normal","words","all","keep"]}],hyphens:[{hyphens:["none","manual","auto"]}],content:[{content:["none",I]}],"bg-attachment":[{bg:["fixed","local","scroll"]}],"bg-clip":[{"bg-clip":["border","padding","content","text"]}],"bg-opacity":[{"bg-opacity":[f]}],"bg-origin":[{"bg-origin":["border","padding","content"]}],"bg-position":[{bg:[...V(),B]}],"bg-repeat":[{bg:["no-repeat",{repeat:["","x","y","round","space"]}]}],"bg-size":[{bg:["auto","cover","contain",G]}],"bg-image":[{bg:["none",{"gradient-to":["t","tr","r","br","b","bl","l","tl"]},T]}],"bg-color":[{bg:[e]}],"gradient-from-pos":[{from:[h]}],"gradient-via-pos":[{via:[h]}],"gradient-to-pos":[{to:[h]}],"gradient-from":[{from:[b]}],"gradient-via":[{via:[b]}],"gradient-to":[{to:[b]}],rounded:[{rounded:[l]}],"rounded-s":[{"rounded-s":[l]}],"rounded-e":[{"rounded-e":[l]}],"rounded-t":[{"rounded-t":[l]}],"rounded-r":[{"rounded-r":[l]}],"rounded-b":[{"rounded-b":[l]}],"rounded-l":[{"rounded-l":[l]}],"rounded-ss":[{"rounded-ss":[l]}],"rounded-se":[{"rounded-se":[l]}],"rounded-ee":[{"rounded-ee":[l]}],"rounded-es":[{"rounded-es":[l]}],"rounded-tl":[{"rounded-tl":[l]}],"rounded-tr":[{"rounded-tr":[l]}],"rounded-br":[{"rounded-br":[l]}],"rounded-bl":[{"rounded-bl":[l]}],"border-w":[{border:[a]}],"border-w-x":[{"border-x":[a]}],"border-w-y":[{"border-y":[a]}],"border-w-s":[{"border-s":[a]}],"border-w-e":[{"border-e":[a]}],"border-w-t":[{"border-t":[a]}],"border-w-r":[{"border-r":[a]}],"border-w-b":[{"border-b":[a]}],"border-w-l":[{"border-l":[a]}],"border-opacity":[{"border-opacity":[f]}],"border-style":[{border:[...H(),"hidden"]}],"divide-x":[{"divide-x":[a]}],"divide-x-reverse":["divide-x-reverse"],"divide-y":[{"divide-y":[a]}],"divide-y-reverse":["divide-y-reverse"],"divide-opacity":[{"divide-opacity":[f]}],"divide-style":[{divide:H()}],"border-color":[{border:[n]}],"border-color-x":[{"border-x":[n]}],"border-color-y":[{"border-y":[n]}],"border-color-s":[{"border-s":[n]}],"border-color-e":[{"border-e":[n]}],"border-color-t":[{"border-t":[n]}],"border-color-r":[{"border-r":[n]}],"border-color-b":[{"border-b":[n]}],"border-color-l":[{"border-l":[n]}],"divide-color":[{divide:[n]}],"outline-style":[{outline:["",...H()]}],"outline-offset":[{"outline-offset":[L,I]}],"outline-w":[{outline:[L,Q]}],"outline-color":[{outline:[e]}],"ring-w":[{ring:_()}],"ring-w-inset":["ring-inset"],"ring-color":[{ring:[e]}],"ring-opacity":[{"ring-opacity":[f]}],"ring-offset-w":[{"ring-offset":[L,Q]}],"ring-offset-color":[{"ring-offset":[e]}],shadow:[{shadow:["","inner","none",R,Z]}],"shadow-color":[{shadow:[q]}],opacity:[{opacity:[f]}],"mix-blend":[{"mix-blend":[...U(),"plus-lighter","plus-darker"]}],"bg-blend":[{"bg-blend":U()}],filter:[{filter:["","none"]}],blur:[{blur:[t]}],brightness:[{brightness:[o]}],contrast:[{contrast:[i]}],"drop-shadow":[{"drop-shadow":["","none",R,I]}],grayscale:[{grayscale:[c]}],"hue-rotate":[{"hue-rotate":[d]}],invert:[{invert:[p]}],saturate:[{saturate:[x]}],sepia:[{sepia:[y]}],"backdrop-filter":[{"backdrop-filter":["","none"]}],"backdrop-blur":[{"backdrop-blur":[t]}],"backdrop-brightness":[{"backdrop-brightness":[o]}],"backdrop-contrast":[{"backdrop-contrast":[i]}],"backdrop-grayscale":[{"backdrop-grayscale":[c]}],"backdrop-hue-rotate":[{"backdrop-hue-rotate":[d]}],"backdrop-invert":[{"backdrop-invert":[p]}],"backdrop-opacity":[{"backdrop-opacity":[f]}],"backdrop-saturate":[{"backdrop-saturate":[x]}],"backdrop-sepia":[{"backdrop-sepia":[y]}],"border-collapse":[{border:["collapse","separate"]}],"border-spacing":[{"border-spacing":[s]}],"border-spacing-x":[{"border-spacing-x":[s]}],"border-spacing-y":[{"border-spacing-y":[s]}],"table-layout":[{table:["auto","fixed"]}],caption:[{caption:["top","bottom"]}],transition:[{transition:["none","all","","colors","opacity","shadow","transform",I]}],duration:[{duration:Y()}],ease:[{ease:["linear","in","out","in-out",I]}],delay:[{delay:Y()}],animate:[{animate:["none","spin","ping","pulse","bounce",I]}],transform:[{transform:["","gpu","none"]}],scale:[{scale:[v]}],"scale-x":[{"scale-x":[v]}],"scale-y":[{"scale-y":[v]}],rotate:[{rotate:[O,I]}],"translate-x":[{"translate-x":[z]}],"translate-y":[{"translate-y":[z]}],"skew-x":[{"skew-x":[j]}],"skew-y":[{"skew-y":[j]}],"transform-origin":[{origin:["center","top","top-right","right","bottom-right","bottom","bottom-left","left","top-left",I]}],accent:[{accent:["auto",e]}],appearance:[{appearance:["none","auto"]}],cursor:[{cursor:["auto","default","pointer","wait","text","move","help","not-allowed","none","context-menu","progress","cell","crosshair","vertical-text","alias","copy","no-drop","grab","grabbing","all-scroll","col-resize","row-resize","n-resize","e-resize","s-resize","w-resize","ne-resize","nw-resize","se-resize","sw-resize","ew-resize","ns-resize","nesw-resize","nwse-resize","zoom-in","zoom-out",I]}],"caret-color":[{caret:[e]}],"pointer-events":[{"pointer-events":["none","auto"]}],resize:[{resize:["none","y","x",""]}],"scroll-behavior":[{scroll:["auto","smooth"]}],"scroll-m":[{"scroll-m":M()}],"scroll-mx":[{"scroll-mx":M()}],"scroll-my":[{"scroll-my":M()}],"scroll-ms":[{"scroll-ms":M()}],"scroll-me":[{"scroll-me":M()}],"scroll-mt":[{"scroll-mt":M()}],"scroll-mr":[{"scroll-mr":M()}],"scroll-mb":[{"scroll-mb":M()}],"scroll-ml":[{"scroll-ml":M()}],"scroll-p":[{"scroll-p":M()}],"scroll-px":[{"scroll-px":M()}],"scroll-py":[{"scroll-py":M()}],"scroll-ps":[{"scroll-ps":M()}],"scroll-pe":[{"scroll-pe":M()}],"scroll-pt":[{"scroll-pt":M()}],"scroll-pr":[{"scroll-pr":M()}],"scroll-pb":[{"scroll-pb":M()}],"scroll-pl":[{"scroll-pl":M()}],"snap-align":[{snap:["start","end","center","align-none"]}],"snap-stop":[{snap:["normal","always"]}],"snap-type":[{snap:["none","x","y","both"]}],"snap-strictness":[{snap:["mandatory","proximity"]}],touch:[{touch:["auto","none","manipulation"]}],"touch-x":[{"touch-pan":["x","left","right"]}],"touch-y":[{"touch-pan":["y","up","down"]}],"touch-pz":["touch-pinch-zoom"],select:[{select:["none","text","all","auto"]}],"will-change":[{"will-change":["auto","scroll","contents","transform",I]}],fill:[{fill:[e,"none"]}],"stroke-w":[{stroke:[L,Q,W]}],stroke:[{stroke:[e,"none"]}],sr:["sr-only","not-sr-only"],"forced-color-adjust":[{"forced-color-adjust":["auto","none"]}]},conflictingClassGroups:{overflow:["overflow-x","overflow-y"],overscroll:["overscroll-x","overscroll-y"],inset:["inset-x","inset-y","start","end","top","right","bottom","left"],"inset-x":["right","left"],"inset-y":["top","bottom"],flex:["basis","grow","shrink"],gap:["gap-x","gap-y"],p:["px","py","ps","pe","pt","pr","pb","pl"],px:["pr","pl"],py:["pt","pb"],m:["mx","my","ms","me","mt","mr","mb","ml"],mx:["mr","ml"],my:["mt","mb"],size:["w","h"],"font-size":["leading"],"fvn-normal":["fvn-ordinal","fvn-slashed-zero","fvn-figure","fvn-spacing","fvn-fraction"],"fvn-ordinal":["fvn-normal"],"fvn-slashed-zero":["fvn-normal"],"fvn-figure":["fvn-normal"],"fvn-spacing":["fvn-normal"],"fvn-fraction":["fvn-normal"],"line-clamp":["display","overflow"],rounded:["rounded-s","rounded-e","rounded-t","rounded-r","rounded-b","rounded-l","rounded-ss","rounded-se","rounded-ee","rounded-es","rounded-tl","rounded-tr","rounded-br","rounded-bl"],"rounded-s":["rounded-ss","rounded-es"],"rounded-e":["rounded-se","rounded-ee"],"rounded-t":["rounded-tl","rounded-tr"],"rounded-r":["rounded-tr","rounded-br"],"rounded-b":["rounded-br","rounded-bl"],"rounded-l":["rounded-tl","rounded-bl"],"border-spacing":["border-spacing-x","border-spacing-y"],"border-w":["border-w-s","border-w-e","border-w-t","border-w-r","border-w-b","border-w-l"],"border-w-x":["border-w-r","border-w-l"],"border-w-y":["border-w-t","border-w-b"],"border-color":["border-color-s","border-color-e","border-color-t","border-color-r","border-color-b","border-color-l"],"border-color-x":["border-color-r","border-color-l"],"border-color-y":["border-color-t","border-color-b"],"scroll-m":["scroll-mx","scroll-my","scroll-ms","scroll-me","scroll-mt","scroll-mr","scroll-mb","scroll-ml"],"scroll-mx":["scroll-mr","scroll-ml"],"scroll-my":["scroll-mt","scroll-mb"],"scroll-p":["scroll-px","scroll-py","scroll-ps","scroll-pe","scroll-pt","scroll-pr","scroll-pb","scroll-pl"],"scroll-px":["scroll-pr","scroll-pl"],"scroll-py":["scroll-pt","scroll-pb"],touch:["touch-x","touch-y","touch-pz"],"touch-x":["touch"],"touch-y":["touch"],"touch-pz":["touch"]},conflictingClassGroupModifiers:{"font-size":["leading"]}}}),J=(r="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",t={variants:{variant:{default:"bg-primary text-primary-foreground hover:bg-primary/90",destructive:"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",outline:"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",secondary:"bg-secondary text-secondary-foreground hover:bg-secondary/80",ghost:"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",link:"text-primary underline-offset-4 hover:underline"},size:{default:"h-9 px-4 py-2 has-[>svg]:px-3",sm:"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",lg:"h-10 rounded-md px-6 has-[>svg]:px-4",icon:"size-9","icon-sm":"size-8","icon-lg":"size-10"}},defaultVariants:{variant:"default",size:"default"}},e=>{var o;if((null==t?void 0:t.variants)==null)return d(r,null==e?void 0:e.class,null==e?void 0:e.className);let{variants:n,defaultVariants:l}=t,s=Object.keys(n).map(r=>{let t=null==e?void 0:e[r],o=null==l?void 0:l[r];if(null===t)return null;let s=p(t)||p(o);return n[r][s]}),a=e&&Object.entries(e).reduce((e,r)=>{let[t,o]=r;return void 0===o||(e[t]=o),e},{});return d(r,s,null==t||null==(o=t.compoundVariants)?void 0:o.reduce((e,r)=>{let{class:t,className:o,...n}=r;return Object.entries(n).every(e=>{let[r,t]=e;return Array.isArray(t)?t.includes({...l,...a}[r]):({...l,...a})[r]===t})?[...e,t,o]:e},[]),null==e?void 0:e.class,null==e?void 0:e.className)});function X({className:e,variant:r,size:t,asChild:n=!1,...l}){let a=n?s:"button";return(0,o.jsx)(a,{"data-slot":"button",className:function(...e){return Y(d(e))}(J({variant:r,size:t,className:e})),...l})}function ee({size:e="large"}){return(0,o.jsxs)("svg",{viewBox:"0 0 180 240",fill:"none",xmlns:"http://www.w3.org/2000/svg",className:`drop-shadow-sm ${"large"===e?"w-32 h-40 sm:w-40 sm:h-52 md:w-44 md:h-56":"w-16 h-20 sm:w-20 sm:h-24"}`,children:[(0,o.jsx)("path",{d:"M 70 85 Q 52 58, 58 28 Q 62 15, 68 18 Q 75 32, 80 55 Q 83 70, 82 85 Z",stroke:"#3A3A3A",strokeWidth:"0.7",fill:"rgba(255, 255, 255, 0.85)",strokeLinecap:"round",strokeLinejoin:"round",opacity:"0.95"}),(0,o.jsx)("path",{d:"M 82 85 Q 85 45, 88 20 Q 89 8, 92 12 Q 95 35, 93 65 Q 92 78, 93 85 Z",stroke:"#3A3A3A",strokeWidth:"0.7",fill:"rgba(255, 255, 255, 0.9)",strokeLinecap:"round",strokeLinejoin:"round",opacity:"0.95"}),(0,o.jsx)("path",{d:"M 93 85 Q 90 55, 92 35 Q 96 20, 102 25 Q 110 42, 115 60 Q 118 75, 108 85 Z",stroke:"#3A3A3A",strokeWidth:"0.7",fill:"rgba(255, 255, 255, 0.85)",strokeLinecap:"round",strokeLinejoin:"round",opacity:"0.95"}),(0,o.jsx)("path",{d:"M 72 75 Q 74 52, 76 32",stroke:"#3A3A3A",strokeWidth:"0.4",fill:"none",opacity:"0.25",strokeLinecap:"round"}),(0,o.jsx)("path",{d:"M 88 78 Q 89 53, 90 28",stroke:"#3A3A3A",strokeWidth:"0.4",fill:"none",opacity:"0.25",strokeLinecap:"round"}),(0,o.jsx)("path",{d:"M 104 75 Q 102 52, 100 32",stroke:"#3A3A3A",strokeWidth:"0.4",fill:"none",opacity:"0.25",strokeLinecap:"round"}),(0,o.jsx)("path",{d:"M 68 70 Q 70 55, 72 40",stroke:"#3A3A3A",strokeWidth:"0.3",fill:"none",opacity:"0.2",strokeLinecap:"round"}),(0,o.jsx)("path",{d:"M 108 70 Q 106 55, 104 40",stroke:"#3A3A3A",strokeWidth:"0.3",fill:"none",opacity:"0.2",strokeLinecap:"round"}),(0,o.jsx)("path",{d:"M 90 85 Q 88 125, 86 165 Q 85 185, 86 220",stroke:"#3A3A3A",strokeWidth:"1.2",fill:"none",strokeLinecap:"round",opacity:"0.9"}),(0,o.jsx)("path",{d:"M 86 145 Q 65 148, 50 156 Q 45 159, 47 162 Q 54 164, 72 160 Q 83 156, 86 152",stroke:"#3A3A3A",strokeWidth:"0.7",fill:"rgba(255, 255, 255, 0.7)",strokeLinecap:"round",strokeLinejoin:"round",opacity:"0.9"}),(0,o.jsx)("path",{d:"M 86 148 Q 72 152, 58 158",stroke:"#3A3A3A",strokeWidth:"0.4",fill:"none",opacity:"0.3",strokeLinecap:"round"}),(0,o.jsx)("path",{d:"M 86 185 Q 107 188, 122 196 Q 127 199, 125 202 Q 118 204, 100 200 Q 89 196, 86 192",stroke:"#3A3A3A",strokeWidth:"0.7",fill:"rgba(255, 255, 255, 0.7)",strokeLinecap:"round",strokeLinejoin:"round",opacity:"0.9"}),(0,o.jsx)("path",{d:"M 86 188 Q 100 192, 114 198",stroke:"#3A3A3A",strokeWidth:"0.4",fill:"none",opacity:"0.3",strokeLinecap:"round"})]})}let er=(...e)=>e.filter((e,r,t)=>!!e&&""!==e.trim()&&t.indexOf(e)===r).join(" ").trim();var et={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};let eo=(0,n.forwardRef)(({color:e="currentColor",size:r=24,strokeWidth:t=2,absoluteStrokeWidth:o,className:l="",children:s,iconNode:a,...i},c)=>(0,n.createElement)("svg",{ref:c,...et,width:r,height:r,stroke:e,strokeWidth:o?24*Number(t)/Number(r):t,className:er("lucide",l),...i},[...a.map(([e,r])=>(0,n.createElement)(e,r)),...Array.isArray(s)?s:[s]])),en=(e,r)=>{let t=(0,n.forwardRef)(({className:t,...o},l)=>(0,n.createElement)(eo,{ref:l,iconNode:r,className:er(`lucide-${e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}`,t),...o}));return t.displayName=`${e}`,t},el=en("Mic",[["path",{d:"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z",key:"131961"}],["path",{d:"M19 10v2a7 7 0 0 1-14 0v-2",key:"1vc78b"}],["line",{x1:"12",x2:"12",y1:"19",y2:"22",key:"x3vr5v"}]]),es=en("ExternalLink",[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]]);function ea(){let e=(0,n.useRef)(null),r=(0,n.useRef)(null),[t,l]=(0,n.useState)(!1);(0,n.useEffect)(()=>{try{l(window.self!==window.top)}catch{l(!0)}},[]);let s=(0,n.useCallback)(async(e,r,t)=>{try{console.log("📤 發送授權碼到後端...");let o=await fetch("/auth/google/callback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:e,state:r,code_verifier:t})}),n=await o.json();if(n.success)console.log("✅ 登入成功!"),localStorage.setItem("jwt_token",n.access_token),sessionStorage.removeItem("oauth_state"),sessionStorage.removeItem("oauth_code_verifier"),window.history.replaceState({},"",window.location.pathname),window.location.href="/static/";else throw Error(n.error||"登入失敗")}catch(e){console.error("❌ OAuth callback 處理失敗:",e),alert(`登入處理失敗: ${e}`),window.history.replaceState({},"",window.location.pathname)}},[]);(0,n.useEffect)(()=>{let r=r=>{if([window.location.origin,"https://xiaobai1221-bloom-ware.hf.space"].some(e=>r.origin.includes(e.replace("https://","").replace("http://","")))&&r.data?.type==="oauth_callback"){console.log("📨 收到 popup OAuth 回調");let{code:t,state:o}=r.data,n=sessionStorage.getItem("oauth_code_verifier")||"";if(o!==sessionStorage.getItem("oauth_state")){console.error("❌ State 參數不匹配"),alert("登入驗證失敗,請重試");return}e.current&&!e.current.closed&&e.current.close(),s(t,o,n)}};return window.addEventListener("message",r),()=>window.removeEventListener("message",r)},[s]),(0,n.useEffect)(()=>{let e=new URLSearchParams(window.location.search),r=e.get("code"),t=e.get("state"),o=e.get("error");if(o){console.error("❌ OAuth 錯誤:",o),window.opener?(window.opener.postMessage({type:"oauth_error",error:o},"*"),window.close()):(alert(`Google 登入失敗: ${o}`),window.history.replaceState({},"",window.location.pathname));return}if(r&&t)if(console.log("🔍 檢測到 OAuth callback"),window.opener)console.log("📤 在 popup 中,發送 postMessage 給主視窗"),window.opener.postMessage({type:"oauth_callback",code:r,state:t},"*"),window.close();else{console.log("📤 直接訪問模式,處理 OAuth callback");let e=sessionStorage.getItem("oauth_code_verifier")||"";t===sessionStorage.getItem("oauth_state")?s(r,t,e):(console.error("❌ State 參數不匹配"),alert("登入驗證失敗,請重試"),window.history.replaceState({},"",window.location.pathname))}},[s]),(0,n.useEffect)(()=>()=>{r.current&&clearInterval(r.current)},[]);let a=async()=>{if(t){console.log("📦 檢測到 iframe 環境,引導用戶在新分頁開啟"),window.open("https://xiaobai1221-bloom-ware.hf.space/login","_blank","noopener,noreferrer");return}try{console.log("🚀 開始 Google OAuth 登入流程(Popup 模式)...");let t=await fetch("/auth/google/url"),o=await t.json();if(!o.success)throw Error(o.error||"獲取授權 URL 失敗");console.log("✅ 獲取授權 URL 成功"),sessionStorage.setItem("oauth_state",o.state),sessionStorage.setItem("oauth_code_verifier",o.code_verifier),console.log("🔐 PKCE 參數已存儲");let n=window.screenX+(window.outerWidth-500)/2,l=window.screenY+(window.outerHeight-600)/2;if(console.log("🌐 在 popup 視窗中打開 Google 授權頁面..."),e.current=window.open(o.auth_url,"google_oauth_popup",`width=500,height=600,left=${n},top=${l},scrollbars=yes,resizable=yes`),!e.current){console.warn("⚠️ Popup 被阻擋,嘗試直接重定向..."),window.location.href=o.auth_url;return}r.current=setInterval(()=>{e.current&&e.current.closed&&(console.log("📪 Popup 視窗已關閉"),r.current&&clearInterval(r.current))},1e3)}catch(e){console.error("❌ OAuth 初始化失敗:",e),alert("Google 登入初始化失敗,請稍後再試")}};return(0,o.jsxs)("div",{className:"flex flex-col items-center space-y-6 sm:space-y-8",children:[(0,o.jsxs)("div",{className:"text-center space-y-1 sm:space-y-2",children:[(0,o.jsx)("h1",{className:"font-serif text-4xl sm:text-5xl md:text-6xl text-[#2C2C2C] tracking-wide text-balance",children:"Bloom Ware"}),(0,o.jsx)("p",{className:"text-[#4A4A4A] text-xs sm:text-sm tracking-widest uppercase",children:"MADE BY 槓上開發"})]}),(0,o.jsx)("div",{className:"my-4 sm:my-6 md:my-8",children:(0,o.jsx)(ee,{size:"large"})}),(0,o.jsxs)("div",{className:"w-full space-y-3 sm:space-y-4",children:[t&&(0,o.jsx)("p",{className:"text-[#8B7355] text-[11px] sm:text-xs text-center tracking-wide opacity-80",children:"點擊下方按鈕在新視窗中開啟"}),(0,o.jsxs)(X,{onClick:a,className:"w-full h-11 sm:h-12 bg-white hover:bg-gray-50 text-[#2C2C2C] shadow-md hover:shadow-lg transition-all duration-200 rounded-lg border border-gray-200 text-sm sm:text-base",variant:"outline",children:[(0,o.jsxs)("svg",{className:"w-4 h-4 sm:w-5 sm:h-5 mr-2 sm:mr-3",viewBox:"0 0 24 24",children:[(0,o.jsx)("path",{fill:"#4285F4",d:"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"}),(0,o.jsx)("path",{fill:"#34A853",d:"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"}),(0,o.jsx)("path",{fill:"#FBBC05",d:"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"}),(0,o.jsx)("path",{fill:"#EA4335",d:"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"})]}),(0,o.jsx)("span",{className:"font-medium",children:"Continue with Google"}),t&&(0,o.jsx)(es,{className:"w-3 h-3 ml-2 opacity-50"})]}),(0,o.jsxs)(X,{onClick:()=>{console.log("🎤 開始語音登入..."),localStorage.setItem("jwt_token","anonymous_voice_login"),window.location.href="/static/"},className:"w-full h-11 sm:h-12 bg-white hover:bg-gray-50 text-[#2C2C2C] shadow-md hover:shadow-lg transition-all duration-200 rounded-lg border border-gray-200 text-sm sm:text-base",variant:"outline",children:[(0,o.jsx)(el,{className:"w-4 h-4 sm:w-5 sm:h-5 mr-2 sm:mr-3"}),(0,o.jsx)("span",{className:"font-medium",children:"Voice Login"})]})]}),(0,o.jsx)("p",{className:"text-[#4A4A4A] text-[10px] sm:text-xs text-center mt-6 sm:mt-8 max-w-xs text-balance px-4",children:"By continuing, you agree to our Terms of Service and Privacy Policy"})]})}e.s(["LoginForm",()=>ea],17994)}]); \ No newline at end of file diff --git a/static/frontend/login/_next/static/chunks/turbopack-57ae785150f7b6ae.js b/static/frontend/login/_next/static/chunks/turbopack-57ae785150f7b6ae.js deleted file mode 100644 index eed7d94e955bb36d8512e635b5229ab927db48c6..0000000000000000000000000000000000000000 --- a/static/frontend/login/_next/static/chunks/turbopack-57ae785150f7b6ae.js +++ /dev/null @@ -1,3 +0,0 @@ -(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,{otherChunks:["static/chunks/752a4bd6387d6ef7.js","static/chunks/66925481c7f9f43e.js","static/chunks/c518a733bdcf3dee.js"],runtimeModuleIds:[94553]}]),(()=>{let e;if(!Array.isArray(globalThis.TURBOPACK))return;let t="/login/_next/",r=new WeakMap;function n(e,t){this.m=e,this.e=t}let o=n.prototype,l=Object.prototype.hasOwnProperty,i="undefined"!=typeof Symbol&&Symbol.toStringTag;function s(e,t,r){l.call(e,t)||Object.defineProperty(e,t,r)}function u(e,t){let r=e[t];return r||(r=a(t),e[t]=r),r}function a(e){return{exports:{},error:void 0,id:e,namespaceObject:void 0}}function c(e,t){s(e,"__esModule",{value:!0}),i&&s(e,i,{value:"Module"});let r=0;for(;rObject.getPrototypeOf(e):e=>e.__proto__,p=[null,f({}),f([]),f(f)];function h(e,t,r){let n=[],o=-1;for(let t=e;("object"==typeof t||"function"==typeof t)&&!p.includes(t);t=f(t))for(let r of Object.getOwnPropertyNames(t))n.push(r,function(e,t){return()=>e[t]}(e,r)),-1===o&&"default"===r&&(o=n.length-1);return r&&o>=0||(o>=0?n.splice(o,1,0,e):n.push("default",0,e)),c(t,n),t}function d(e){let t=N(e,this.m);if(t.namespaceObject)return t.namespaceObject;let r=t.exports;return t.namespaceObject=h(r,"function"==typeof r?function(...e){return r.apply(this,e)}:Object.create(null),r&&r.__esModule)}function m(){let e,t;return{promise:new Promise((r,n)=>{t=n,e=r}),resolve:e,reject:t}}o.i=d,o.A=function(e){return this.r(e)(d.bind(this))},o.t="function"==typeof require?require:function(){throw Error("Unexpected use of runtime require")},o.r=function(e){return N(e,this.m).exports},o.f=function(e){function t(t){if(l.call(e,t))return e[t].module();let r=Error(`Cannot find module '${t}'`);throw r.code="MODULE_NOT_FOUND",r}return t.keys=()=>Object.keys(e),t.resolve=t=>{if(l.call(e,t))return e[t].id();let r=Error(`Cannot find module '${t}'`);throw r.code="MODULE_NOT_FOUND",r},t.import=async e=>await t(e),t};let b=Symbol("turbopack queues"),y=Symbol("turbopack exports"),O=Symbol("turbopack error");function g(e){e&&1!==e.status&&(e.status=1,e.forEach(e=>e.queueCount--),e.forEach(e=>e.queueCount--?e.queueCount++:e()))}o.a=function(e,t){let r=this.m,n=t?Object.assign([],{status:-1}):void 0,o=new Set,{resolve:l,reject:i,promise:s}=m(),u=Object.assign(s,{[y]:r.exports,[b]:e=>{n&&e(n),o.forEach(e),u.catch(()=>{})}}),a={get:()=>u,set(e){e!==u&&(u[y]=e)}};Object.defineProperty(r,"exports",a),Object.defineProperty(r,"namespaceObject",a),e(function(e){let t=e.map(e=>{if(null!==e&&"object"==typeof e){if(b in e)return e;if(null!=e&&"object"==typeof e&&"then"in e&&"function"==typeof e.then){let t=Object.assign([],{status:0}),r={[y]:{},[b]:e=>e(t)};return e.then(e=>{r[y]=e,g(t)},e=>{r[O]=e,g(t)}),r}}return{[y]:e,[b]:()=>{}}}),r=()=>t.map(e=>{if(e[O])throw e[O];return e[y]}),{promise:l,resolve:i}=m(),s=Object.assign(()=>i(r),{queueCount:0});function u(e){e!==n&&!o.has(e)&&(o.add(e),e&&0===e.status&&(s.queueCount++,e.push(s)))}return t.map(e=>e[b](u)),s.queueCount?l:r()},function(e){e?i(u[O]=e):l(u[y]),g(n)}),n&&-1===n.status&&(n.status=0)};let w=function(e){let t=new URL(e,"x:/"),r={};for(let e in t)r[e]=t[e];for(let t in r.href=e,r.pathname=e.replace(/[?#].*/,""),r.origin=r.protocol="",r.toString=r.toJSON=(...t)=>e,r)Object.defineProperty(this,t,{enumerable:!0,configurable:!0,value:r[t]})};function j(e,t){throw Error(`Invariant: ${t(e)}`)}w.prototype=URL.prototype,o.U=w,o.z=function(e){throw Error("dynamic usage of require is not supported")},o.g=globalThis;let R=n.prototype;var C,U=((C=U||{})[C.Runtime=0]="Runtime",C[C.Parent=1]="Parent",C[C.Update=2]="Update",C);let k=new Map;o.M=k;let v=new Map,_=new Map;async function P(e,t,r){let n;if("string"==typeof r)return A(e,t,S(r));let o=r.included||[],l=o.map(e=>!!k.has(e)||v.get(e));if(l.length>0&&l.every(e=>e))return void await Promise.all(l);let i=r.moduleChunks||[],s=i.map(e=>_.get(e)).filter(e=>e);if(s.length>0){if(s.length===i.length)return void await Promise.all(s);let r=new Set;for(let e of i)_.has(e)||r.add(e);for(let n of r){let r=A(e,t,S(n));_.set(n,r),s.push(r)}n=Promise.all(s)}else{for(let o of(n=A(e,t,S(r.path)),i))_.has(o)||_.set(o,n)}for(let e of o)v.has(e)||v.set(e,n);await n}R.l=function(e){return P(1,this.m.id,e)};let $=Promise.resolve(void 0),T=new WeakMap;function A(t,r,n){let o=e.loadChunkCached(t,n),l=T.get(o);if(void 0===l){let e=T.set.bind(T,o,$);l=o.then(e).catch(e=>{let o;switch(t){case 0:o=`as a runtime dependency of chunk ${r}`;break;case 1:o=`from module ${r}`;break;case 2:o="from an HMR update";break;default:j(t,e=>`Unknown source type: ${e}`)}throw Error(`Failed to load chunk ${n} ${o}${e?`: ${e}`:""}`,e?{cause:e}:void 0)}),T.set(o,l)}return l}function S(e){return`${t}${e.split("/").map(e=>encodeURIComponent(e)).join("/")}`}R.L=function(e){return A(1,this.m.id,e)},R.R=function(e){let t=this.r(e);return t?.default??t},R.P=function(e){return`/ROOT/${e??""}`},R.b=function(e){let t=new Blob([`self.TURBOPACK_WORKER_LOCATION = ${JSON.stringify(location.origin)}; -self.TURBOPACK_NEXT_CHUNK_URLS = ${JSON.stringify(e.reverse().map(S),null,2)}; -importScripts(...self.TURBOPACK_NEXT_CHUNK_URLS.map(c => self.TURBOPACK_WORKER_LOCATION + c).reverse());`],{type:"text/javascript"});return URL.createObjectURL(t)};let E=/\.js(?:\?[^#]*)?(?:#.*)?$/,K=/\.css(?:\?[^#]*)?(?:#.*)?$/;function x(e){return K.test(e)}o.w=function(t,r,n){return e.loadWebAssembly(1,this.m.id,t,r,n)},o.u=function(t,r){return e.loadWebAssemblyModule(1,this.m.id,t,r)};let M={};o.c=M;let N=(e,t)=>{let r=M[e];if(r){if(r.error)throw r.error;return r}return L(e,U.Parent,t.id)};function L(e,t,r){let o=k.get(e);if("function"!=typeof o)throw Error(function(e,t,r){let n;switch(t){case 0:n=`as a runtime entry of chunk ${r}`;break;case 1:n=`because it was required from module ${r}`;break;case 2:n="because of an HMR update";break;default:j(t,e=>`Unknown source type: ${e}`)}return`Module ${e} was instantiated ${n}, but the module factory is not available.`}(e,t,r));let l=a(e),i=l.exports;M[e]=l;let s=new n(l,i);try{o(s,l,i)}catch(e){throw l.error=e,e}return l.namespaceObject&&l.exports!==l.namespaceObject&&h(l.exports,l.namespaceObject),l}function q(r){let n,o=function(e){if("string"==typeof e)return e;let r=decodeURIComponent(("undefined"!=typeof TURBOPACK_NEXT_CHUNK_URLS?TURBOPACK_NEXT_CHUNK_URLS.pop():e.getAttribute("src")).replace(/[?#].*$/,""));return r.startsWith(t)?r.slice(t.length):r}(r[0]);return 2===r.length?n=r[1]:(n=void 0,!function(e,t,r,n){let o=1;for(;o{r=e,n=t}),resolve:()=>{t.resolved=!0,r()},reject:n},B.set(e,t)}return t}e={async registerChunk(e,t){if(W(S(e)).resolve(),null!=t){for(let e of t.otherChunks)W(S("string"==typeof e?e:e.path));if(await Promise.all(t.otherChunks.map(t=>P(0,e,t))),t.runtimeModuleIds.length>0)for(let r of t.runtimeModuleIds)!function(e,t){let r=M[t];if(r){if(r.error)throw r.error;return}L(t,U.Runtime,e)}(e,r)}},loadChunkCached:(e,t)=>(function(e,t){let r=W(t);if(r.loadingStarted)return r.promise;if(e===U.Runtime)return r.loadingStarted=!0,x(t)&&r.resolve(),r.promise;if("function"==typeof importScripts)if(x(t));else if(E.test(t))self.TURBOPACK_NEXT_CHUNK_URLS.push(t),importScripts(TURBOPACK_WORKER_LOCATION+t);else throw Error(`can't infer type of chunk from URL ${t} in worker`);else{let e=decodeURI(t);if(x(t))if(document.querySelectorAll(`link[rel=stylesheet][href="${t}"],link[rel=stylesheet][href^="${t}?"],link[rel=stylesheet][href="${e}"],link[rel=stylesheet][href^="${e}?"]`).length>0)r.resolve();else{let e=document.createElement("link");e.rel="stylesheet",e.href=t,e.onerror=()=>{r.reject()},e.onload=()=>{r.resolve()},document.head.appendChild(e)}else if(E.test(t)){let n=document.querySelectorAll(`script[src="${t}"],script[src^="${t}?"],script[src="${e}"],script[src^="${e}?"]`);if(n.length>0)for(let e of Array.from(n))e.addEventListener("error",()=>{r.reject()});else{let e=document.createElement("script");e.src=t,e.onerror=()=>{r.reject()},document.head.appendChild(e)}}else throw Error(`can't infer type of chunk from URL ${t}`)}return r.loadingStarted=!0,r.promise})(e,t),async loadWebAssembly(e,t,r,n,o){let l=fetch(S(r)),{instance:i}=await WebAssembly.instantiateStreaming(l,o);return i.exports},async loadWebAssemblyModule(e,t,r,n){let o=fetch(S(r));return await WebAssembly.compileStreaming(o)}};let I=globalThis.TURBOPACK;globalThis.TURBOPACK={push:q},I.forEach(q)})(); \ No newline at end of file diff --git a/static/frontend/login/_next/static/media/14e23f9b59180572-s.9c448f3c.woff2 b/static/frontend/login/_next/static/media/14e23f9b59180572-s.9c448f3c.woff2 deleted file mode 100644 index eaa772a4ae310349982307c64d05cb194d89f423..0000000000000000000000000000000000000000 Binary files a/static/frontend/login/_next/static/media/14e23f9b59180572-s.9c448f3c.woff2 and /dev/null differ diff --git a/static/frontend/login/_next/static/media/2a65768255d6b625-s.p.d19752fb.woff2 b/static/frontend/login/_next/static/media/2a65768255d6b625-s.p.d19752fb.woff2 deleted file mode 100644 index 1636e5013e07fb418adcd0e2d9aa24c9aed2274f..0000000000000000000000000000000000000000 Binary files a/static/frontend/login/_next/static/media/2a65768255d6b625-s.p.d19752fb.woff2 and /dev/null differ diff --git a/static/frontend/login/_next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 b/static/frontend/login/_next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 deleted file mode 100644 index 46efdbdbc828839112404c8285ab7e516a6cec45..0000000000000000000000000000000000000000 Binary files a/static/frontend/login/_next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 and /dev/null differ diff --git a/static/frontend/login/_next/static/media/65c558afe41e89d6-s.e2c8389a.woff2 b/static/frontend/login/_next/static/media/65c558afe41e89d6-s.e2c8389a.woff2 deleted file mode 100644 index f4de91ef389d5474c505bc8d0058b6768d9aafb5..0000000000000000000000000000000000000000 Binary files a/static/frontend/login/_next/static/media/65c558afe41e89d6-s.e2c8389a.woff2 and /dev/null differ diff --git a/static/frontend/login/_next/static/media/7178b3e590c64307-s.b97b3418.woff2 b/static/frontend/login/_next/static/media/7178b3e590c64307-s.b97b3418.woff2 deleted file mode 100644 index e36649933f3a206057fe1068cff2e5078f043c59..0000000000000000000000000000000000000000 Binary files a/static/frontend/login/_next/static/media/7178b3e590c64307-s.b97b3418.woff2 and /dev/null differ diff --git a/static/frontend/login/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2 b/static/frontend/login/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2 deleted file mode 100644 index 68eeb7f4bd4a789bc1600f76e6ebac0a0c170d46..0000000000000000000000000000000000000000 Binary files a/static/frontend/login/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2 and /dev/null differ diff --git a/static/frontend/login/_next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 b/static/frontend/login/_next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 deleted file mode 100644 index eb8258cb183e80f57a9110c06e5dff44d15fc2c0..0000000000000000000000000000000000000000 Binary files a/static/frontend/login/_next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 and /dev/null differ diff --git a/static/frontend/login/_next/static/media/b49b0d9b851e4899-s.4f3fa681.woff2 b/static/frontend/login/_next/static/media/b49b0d9b851e4899-s.4f3fa681.woff2 deleted file mode 100644 index a3a82afa2d641b668ed76cb5430fe0178baf9219..0000000000000000000000000000000000000000 Binary files a/static/frontend/login/_next/static/media/b49b0d9b851e4899-s.4f3fa681.woff2 and /dev/null differ diff --git a/static/frontend/login/_next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 b/static/frontend/login/_next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 deleted file mode 100644 index 944424f972b9f9c985163da8096a25b214fa1983..0000000000000000000000000000000000000000 Binary files a/static/frontend/login/_next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 and /dev/null differ diff --git a/static/frontend/login/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 b/static/frontend/login/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 deleted file mode 100644 index aba2e8b521a38d3870ea04c7d9e9d550f76c2120..0000000000000000000000000000000000000000 Binary files a/static/frontend/login/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 and /dev/null differ diff --git a/static/frontend/login/_next/static/media/icon.db662264.svg b/static/frontend/login/_next/static/media/icon.db662264.svg deleted file mode 100644 index 11fad52b09d2398b9fbc4e98f0d8797271e1aaf1..0000000000000000000000000000000000000000 --- a/static/frontend/login/_next/static/media/icon.db662264.svg +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/static/frontend/login/_not-found/__next._full.txt b/static/frontend/login/_not-found/__next._full.txt deleted file mode 100644 index b1dc104d5da549f529535bffdf5f70109ee096a3..0000000000000000000000000000000000000000 --- a/static/frontend/login/_not-found/__next._full.txt +++ /dev/null @@ -1,15 +0,0 @@ -1:"$Sreact.fragment" -2:I[39756,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -3:I[37457,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -4:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"OutletBoundary"] -5:"$Sreact.suspense" -7:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"ViewportBoundary"] -9:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"MetadataBoundary"] -b:I[68027,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -:HL["/login/_next/static/chunks/1f600c83a5ecf168.css","style"] -:HL["/login/_next/static/chunks/0680eca589a5307c.css","style"] -0:{"P":null,"b":"8vUmiYF3_g4XnZ_PSXlm7","c":["","_not-found",""],"q":"","i":false,"f":[[["",{"children":["/_not-found",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/login/_next/static/chunks/1f600c83a5ecf168.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/login/_next/static/chunks/0680eca589a5307c.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"font-sans antialiased","children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],null,["$","$L4",null,{"children":["$","$5",null,{"name":"Next.MetadataOutlet","children":"$@6"}]}]]}],{},null,false,false]},null,false,false]},null,false,false],["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L7",null,{"children":"$@8"}],["$","div",null,{"hidden":true,"children":["$","$L9",null,{"children":["$","$5",null,{"name":"Next.Metadata","children":"$@a"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$b","$undefined"],"s":false,"S":true} -8:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]] -c:I[27201,["/login/_next/static/chunks/42879de7b8087bc9.js"],"IconMark"] -a:[["$","title","0",{"children":"Bloom Ware - 槓上開發"}],["$","meta","1",{"name":"description","content":"Elegant login experience for Bloom Ware"}],["$","meta","2",{"name":"generator","content":"v0.app"}],["$","link","3",{"rel":"icon","href":"/login/icon.svg?icon.db662264.svg","sizes":"any","type":"image/svg+xml"}],["$","$Lc","4",{}]] -6:null diff --git a/static/frontend/login/_not-found/__next._index.txt b/static/frontend/login/_not-found/__next._index.txt deleted file mode 100644 index 5fa9c71c3f7b76ee83d12fde5e6ba529159b175c..0000000000000000000000000000000000000000 --- a/static/frontend/login/_not-found/__next._index.txt +++ /dev/null @@ -1,6 +0,0 @@ -1:"$Sreact.fragment" -2:I[39756,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -3:I[37457,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -:HL["/login/_next/static/chunks/1f600c83a5ecf168.css","style"] -:HL["/login/_next/static/chunks/0680eca589a5307c.css","style"] -0:{"buildId":"8vUmiYF3_g4XnZ_PSXlm7","rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/login/_next/static/chunks/1f600c83a5ecf168.css","precedence":"next"}],["$","link","1",{"rel":"stylesheet","href":"/login/_next/static/chunks/0680eca589a5307c.css","precedence":"next"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"font-sans antialiased","children":["$","$L2",null,{"parallelRouterKey":"children","template":["$","$L3",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}]}]]}],"loading":null,"isPartial":false} diff --git a/static/frontend/login/_not-found/__next._not-found.__PAGE__.txt b/static/frontend/login/_not-found/__next._not-found.__PAGE__.txt deleted file mode 100644 index b849fe7f111116b9f0476a46343b1bc8aca0577c..0000000000000000000000000000000000000000 --- a/static/frontend/login/_not-found/__next._not-found.__PAGE__.txt +++ /dev/null @@ -1,5 +0,0 @@ -1:"$Sreact.fragment" -2:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"OutletBoundary"] -3:"$Sreact.suspense" -0:{"buildId":"8vUmiYF3_g4XnZ_PSXlm7","rsc":["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],null,["$","$L2",null,{"children":["$","$3",null,{"name":"Next.MetadataOutlet","children":"$@4"}]}]]}],"loading":null,"isPartial":false} -4:null diff --git a/static/frontend/login/_not-found/__next._not-found.txt b/static/frontend/login/_not-found/__next._not-found.txt deleted file mode 100644 index 8bf67cdef5581949966b2a912c451901375017a3..0000000000000000000000000000000000000000 --- a/static/frontend/login/_not-found/__next._not-found.txt +++ /dev/null @@ -1,4 +0,0 @@ -1:"$Sreact.fragment" -2:I[39756,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -3:I[37457,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -0:{"buildId":"8vUmiYF3_g4XnZ_PSXlm7","rsc":["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","template":["$","$L3",null,{}]}]]}],"loading":null,"isPartial":false} diff --git a/static/frontend/login/_not-found/__next._tree.txt b/static/frontend/login/_not-found/__next._tree.txt deleted file mode 100644 index 87ffa8fcc06af891c8964e4ff69af12b8407169c..0000000000000000000000000000000000000000 --- a/static/frontend/login/_not-found/__next._tree.txt +++ /dev/null @@ -1,10 +0,0 @@ -1:"$Sreact.fragment" -2:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"ViewportBoundary"] -4:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"MetadataBoundary"] -5:"$Sreact.suspense" -7:I[27201,["/login/_next/static/chunks/42879de7b8087bc9.js"],"IconMark"] -:HL["/login/_next/static/chunks/1f600c83a5ecf168.css","style"] -:HL["/login/_next/static/chunks/0680eca589a5307c.css","style"] -0:{"buildId":"8vUmiYF3_g4XnZ_PSXlm7","tree":{"name":"","paramType":null,"paramKey":"","hasRuntimePrefetch":false,"slots":{"children":{"name":"/_not-found","paramType":null,"paramKey":"/_not-found","hasRuntimePrefetch":false,"slots":{"children":{"name":"__PAGE__","paramType":null,"paramKey":"__PAGE__","hasRuntimePrefetch":false,"slots":null,"isRootLayout":false}},"isRootLayout":false}},"isRootLayout":true},"head":["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L2",null,{"children":"$@3"}],["$","div",null,{"hidden":true,"children":["$","$L4",null,{"children":["$","$5",null,{"name":"Next.Metadata","children":"$@6"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],"isHeadPartial":false,"staleTime":300} -3:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]] -6:[["$","title","0",{"children":"Bloom Ware - 槓上開發"}],["$","meta","1",{"name":"description","content":"Elegant login experience for Bloom Ware"}],["$","meta","2",{"name":"generator","content":"v0.app"}],["$","link","3",{"rel":"icon","href":"/login/icon.svg?icon.db662264.svg","sizes":"any","type":"image/svg+xml"}],["$","$L7","4",{}]] diff --git a/static/frontend/login/_not-found/index.html b/static/frontend/login/_not-found/index.html deleted file mode 100644 index bcd6b518c9001a54cd5e6af495ab82c2f590a787..0000000000000000000000000000000000000000 --- a/static/frontend/login/_not-found/index.html +++ /dev/null @@ -1 +0,0 @@ -404: This page could not be found.Bloom Ware - 槓上開發

404

This page could not be found.

\ No newline at end of file diff --git a/static/frontend/login/_not-found/index.txt b/static/frontend/login/_not-found/index.txt deleted file mode 100644 index b1dc104d5da549f529535bffdf5f70109ee096a3..0000000000000000000000000000000000000000 --- a/static/frontend/login/_not-found/index.txt +++ /dev/null @@ -1,15 +0,0 @@ -1:"$Sreact.fragment" -2:I[39756,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -3:I[37457,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -4:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"OutletBoundary"] -5:"$Sreact.suspense" -7:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"ViewportBoundary"] -9:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"MetadataBoundary"] -b:I[68027,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -:HL["/login/_next/static/chunks/1f600c83a5ecf168.css","style"] -:HL["/login/_next/static/chunks/0680eca589a5307c.css","style"] -0:{"P":null,"b":"8vUmiYF3_g4XnZ_PSXlm7","c":["","_not-found",""],"q":"","i":false,"f":[[["",{"children":["/_not-found",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/login/_next/static/chunks/1f600c83a5ecf168.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/login/_next/static/chunks/0680eca589a5307c.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"font-sans antialiased","children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],null,["$","$L4",null,{"children":["$","$5",null,{"name":"Next.MetadataOutlet","children":"$@6"}]}]]}],{},null,false,false]},null,false,false]},null,false,false],["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L7",null,{"children":"$@8"}],["$","div",null,{"hidden":true,"children":["$","$L9",null,{"children":["$","$5",null,{"name":"Next.Metadata","children":"$@a"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$b","$undefined"],"s":false,"S":true} -8:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]] -c:I[27201,["/login/_next/static/chunks/42879de7b8087bc9.js"],"IconMark"] -a:[["$","title","0",{"children":"Bloom Ware - 槓上開發"}],["$","meta","1",{"name":"description","content":"Elegant login experience for Bloom Ware"}],["$","meta","2",{"name":"generator","content":"v0.app"}],["$","link","3",{"rel":"icon","href":"/login/icon.svg?icon.db662264.svg","sizes":"any","type":"image/svg+xml"}],["$","$Lc","4",{}]] -6:null diff --git a/static/frontend/login/icon.svg b/static/frontend/login/icon.svg deleted file mode 100644 index 11fad52b09d2398b9fbc4e98f0d8797271e1aaf1..0000000000000000000000000000000000000000 --- a/static/frontend/login/icon.svg +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/static/frontend/login/index.html b/static/frontend/login/index.html deleted file mode 100644 index 7b0eb269f7d02b6273ca85b8261a43558cc9a6c5..0000000000000000000000000000000000000000 --- a/static/frontend/login/index.html +++ /dev/null @@ -1 +0,0 @@ -Bloom Ware - 槓上開發

Bloom Ware

MADE BY 槓上開發

By continuing, you agree to our Terms of Service and Privacy Policy

\ No newline at end of file diff --git a/static/frontend/login/index.txt b/static/frontend/login/index.txt deleted file mode 100644 index 26d437c8d84f2099c87fb6c13a78b8d827d16dba..0000000000000000000000000000000000000000 --- a/static/frontend/login/index.txt +++ /dev/null @@ -1,27 +0,0 @@ -1:"$Sreact.fragment" -2:I[39756,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -3:I[37457,["/login/_next/static/chunks/42879de7b8087bc9.js"],"default"] -c:I[68027,[],"default"] -:HL["/login/_next/static/chunks/1f600c83a5ecf168.css","style"] -:HL["/login/_next/static/chunks/0680eca589a5307c.css","style"] -:HL["/login/_next/static/media/2a65768255d6b625-s.p.d19752fb.woff2","font",{"crossOrigin":"","type":"font/woff2"}] -:HL["/login/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2","font",{"crossOrigin":"","type":"font/woff2"}] -:HL["/login/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2","font",{"crossOrigin":"","type":"font/woff2"}] -0:{"P":null,"b":"8vUmiYF3_g4XnZ_PSXlm7","c":["",""],"q":"","i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/login/_next/static/chunks/1f600c83a5ecf168.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/login/_next/static/chunks/0680eca589a5307c.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"font-sans antialiased","children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":[["$","$1","c",{"children":[["$","main",null,{"className":"min-h-screen bg-[#FFF8E7] flex items-center justify-center p-4 sm:p-6 lg:p-8 relative overflow-hidden","children":[["$","div",null,{"className":"hidden md:block absolute top-8 left-8 opacity-15","children":["$","svg",null,{"viewBox":"0 0 180 240","fill":"none","xmlns":"http://www.w3.org/2000/svg","className":"drop-shadow-sm w-16 h-20 sm:w-20 sm:h-24","children":[["$","path",null,{"d":"M 70 85 Q 52 58, 58 28 Q 62 15, 68 18 Q 75 32, 80 55 Q 83 70, 82 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 82 85 Q 85 45, 88 20 Q 89 8, 92 12 Q 95 35, 93 65 Q 92 78, 93 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.9)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 93 85 Q 90 55, 92 35 Q 96 20, 102 25 Q 110 42, 115 60 Q 118 75, 108 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 72 75 Q 74 52, 76 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 88 78 Q 89 53, 90 28","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 104 75 Q 102 52, 100 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 68 70 Q 70 55, 72 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 108 70 Q 106 55, 104 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 90 85 Q 88 125, 86 165 Q 85 185, 86 220","stroke":"#3A3A3A","strokeWidth":"1.2","fill":"none","strokeLinecap":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 145 Q 65 148, 50 156 Q 45 159, 47 162 Q 54 164, 72 160 Q 83 156, 86 152","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 148 Q 72 152, 58 158","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}],["$","path",null,{"d":"M 86 185 Q 107 188, 122 196 Q 127 199, 125 202 Q 118 204, 100 200 Q 89 196, 86 192","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],"$L4"]}]}],"$L5","$L6","$L7","$L8"]}],["$L9"],"$La"]}],{},null,false,false]},null,false,false],"$Lb",false]],"m":"$undefined","G":["$c",[]],"s":false,"S":true} -d:I[17994,["/login/_next/static/chunks/d98da620b14e9668.js"],"LoginForm"] -e:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"OutletBoundary"] -f:"$Sreact.suspense" -11:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"ViewportBoundary"] -13:I[97367,["/login/_next/static/chunks/42879de7b8087bc9.js"],"MetadataBoundary"] -4:["$","path",null,{"d":"M 86 188 Q 100 192, 114 198","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}] -5:["$","div",null,{"className":"hidden md:block absolute bottom-8 right-8 opacity-15 rotate-180","children":["$","svg",null,{"viewBox":"0 0 180 240","fill":"none","xmlns":"http://www.w3.org/2000/svg","className":"drop-shadow-sm w-16 h-20 sm:w-20 sm:h-24","children":[["$","path",null,{"d":"M 70 85 Q 52 58, 58 28 Q 62 15, 68 18 Q 75 32, 80 55 Q 83 70, 82 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 82 85 Q 85 45, 88 20 Q 89 8, 92 12 Q 95 35, 93 65 Q 92 78, 93 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.9)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 93 85 Q 90 55, 92 35 Q 96 20, 102 25 Q 110 42, 115 60 Q 118 75, 108 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 72 75 Q 74 52, 76 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 88 78 Q 89 53, 90 28","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 104 75 Q 102 52, 100 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 68 70 Q 70 55, 72 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 108 70 Q 106 55, 104 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 90 85 Q 88 125, 86 165 Q 85 185, 86 220","stroke":"#3A3A3A","strokeWidth":"1.2","fill":"none","strokeLinecap":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 145 Q 65 148, 50 156 Q 45 159, 47 162 Q 54 164, 72 160 Q 83 156, 86 152","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 148 Q 72 152, 58 158","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}],["$","path",null,{"d":"M 86 185 Q 107 188, 122 196 Q 127 199, 125 202 Q 118 204, 100 200 Q 89 196, 86 192","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 188 Q 100 192, 114 198","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}]]}]}] -6:["$","div",null,{"className":"hidden lg:block absolute top-1/4 right-12 opacity-10","children":["$","svg",null,{"viewBox":"0 0 180 240","fill":"none","xmlns":"http://www.w3.org/2000/svg","className":"drop-shadow-sm w-16 h-20 sm:w-20 sm:h-24","children":[["$","path",null,{"d":"M 70 85 Q 52 58, 58 28 Q 62 15, 68 18 Q 75 32, 80 55 Q 83 70, 82 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 82 85 Q 85 45, 88 20 Q 89 8, 92 12 Q 95 35, 93 65 Q 92 78, 93 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.9)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 93 85 Q 90 55, 92 35 Q 96 20, 102 25 Q 110 42, 115 60 Q 118 75, 108 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 72 75 Q 74 52, 76 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 88 78 Q 89 53, 90 28","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 104 75 Q 102 52, 100 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 68 70 Q 70 55, 72 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 108 70 Q 106 55, 104 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 90 85 Q 88 125, 86 165 Q 85 185, 86 220","stroke":"#3A3A3A","strokeWidth":"1.2","fill":"none","strokeLinecap":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 145 Q 65 148, 50 156 Q 45 159, 47 162 Q 54 164, 72 160 Q 83 156, 86 152","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 148 Q 72 152, 58 158","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}],["$","path",null,{"d":"M 86 185 Q 107 188, 122 196 Q 127 199, 125 202 Q 118 204, 100 200 Q 89 196, 86 192","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 188 Q 100 192, 114 198","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}]]}]}] -7:["$","div",null,{"className":"hidden lg:block absolute bottom-1/4 left-12 opacity-10 rotate-12","children":["$","svg",null,{"viewBox":"0 0 180 240","fill":"none","xmlns":"http://www.w3.org/2000/svg","className":"drop-shadow-sm w-16 h-20 sm:w-20 sm:h-24","children":[["$","path",null,{"d":"M 70 85 Q 52 58, 58 28 Q 62 15, 68 18 Q 75 32, 80 55 Q 83 70, 82 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 82 85 Q 85 45, 88 20 Q 89 8, 92 12 Q 95 35, 93 65 Q 92 78, 93 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.9)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 93 85 Q 90 55, 92 35 Q 96 20, 102 25 Q 110 42, 115 60 Q 118 75, 108 85 Z","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.85)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.95"}],["$","path",null,{"d":"M 72 75 Q 74 52, 76 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 88 78 Q 89 53, 90 28","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 104 75 Q 102 52, 100 32","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.25","strokeLinecap":"round"}],["$","path",null,{"d":"M 68 70 Q 70 55, 72 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 108 70 Q 106 55, 104 40","stroke":"#3A3A3A","strokeWidth":"0.3","fill":"none","opacity":"0.2","strokeLinecap":"round"}],["$","path",null,{"d":"M 90 85 Q 88 125, 86 165 Q 85 185, 86 220","stroke":"#3A3A3A","strokeWidth":"1.2","fill":"none","strokeLinecap":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 145 Q 65 148, 50 156 Q 45 159, 47 162 Q 54 164, 72 160 Q 83 156, 86 152","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 148 Q 72 152, 58 158","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}],["$","path",null,{"d":"M 86 185 Q 107 188, 122 196 Q 127 199, 125 202 Q 118 204, 100 200 Q 89 196, 86 192","stroke":"#3A3A3A","strokeWidth":"0.7","fill":"rgba(255, 255, 255, 0.7)","strokeLinecap":"round","strokeLinejoin":"round","opacity":"0.9"}],["$","path",null,{"d":"M 86 188 Q 100 192, 114 198","stroke":"#3A3A3A","strokeWidth":"0.4","fill":"none","opacity":"0.3","strokeLinecap":"round"}]]}]}] -8:["$","div",null,{"className":"w-full max-w-sm sm:max-w-md relative z-10","children":["$","$Ld",null,{}]}] -9:["$","script","script-0",{"src":"/login/_next/static/chunks/d98da620b14e9668.js","async":true,"nonce":"$undefined"}] -a:["$","$Le",null,{"children":["$","$f",null,{"name":"Next.MetadataOutlet","children":"$@10"}]}] -b:["$","$1","h",{"children":[null,["$","$L11",null,{"children":"$@12"}],["$","div",null,{"hidden":true,"children":["$","$L13",null,{"children":["$","$f",null,{"name":"Next.Metadata","children":"$@14"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}] -12:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]] -15:I[27201,["/login/_next/static/chunks/42879de7b8087bc9.js"],"IconMark"] -14:[["$","title","0",{"children":"Bloom Ware - 槓上開發"}],["$","meta","1",{"name":"description","content":"Elegant login experience for Bloom Ware"}],["$","meta","2",{"name":"generator","content":"v0.app"}],["$","link","3",{"rel":"icon","href":"/login/icon.svg?icon.db662264.svg","sizes":"any","type":"image/svg+xml"}],["$","$L15","4",{}]] -10:null diff --git a/static/frontend/login/placeholder-logo.png b/static/frontend/login/placeholder-logo.png deleted file mode 100644 index 8a792ac2ddfbe047639f7907c82f14c73e3de3d9..0000000000000000000000000000000000000000 Binary files a/static/frontend/login/placeholder-logo.png and /dev/null differ diff --git a/static/frontend/login/placeholder-logo.svg b/static/frontend/login/placeholder-logo.svg deleted file mode 100644 index b1695aafcd5925a0b3eb122b9e553b8a0f4a0d47..0000000000000000000000000000000000000000 --- a/static/frontend/login/placeholder-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/frontend/login/placeholder-user.jpg b/static/frontend/login/placeholder-user.jpg deleted file mode 100644 index 6fa7543d38ed8fdb38f03b5c0f40ad2d66827d4a..0000000000000000000000000000000000000000 Binary files a/static/frontend/login/placeholder-user.jpg and /dev/null differ diff --git a/static/frontend/login/placeholder.jpg b/static/frontend/login/placeholder.jpg deleted file mode 100644 index 6bfe96336dbe567f5f1e9129f5bccdc78b114bba..0000000000000000000000000000000000000000 Binary files a/static/frontend/login/placeholder.jpg and /dev/null differ diff --git a/static/frontend/login/placeholder.svg b/static/frontend/login/placeholder.svg deleted file mode 100644 index e763910b27fdd9ac872f56baede51bc839402347..0000000000000000000000000000000000000000 --- a/static/frontend/login/placeholder.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d4839a6b14c11e64143d1d200c2d4733595ffc6c --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Tests package diff --git a/tests/test_ai_client.py b/tests/test_ai_client.py new file mode 100644 index 0000000000000000000000000000000000000000..8f73aa708e9c539ceadb91aad6ba814b2f3a46c8 --- /dev/null +++ b/tests/test_ai_client.py @@ -0,0 +1,42 @@ +""" +測試 core/ai_client.py OpenAI 客戶端管理 +""" + +import pytest +from unittest.mock import patch, MagicMock + + +class TestAIClient: + """測試 AI 客戶端模組""" + + def test_get_openai_client_no_api_key(self): + """測試無 API Key 時返回 None""" + from core import ai_client + ai_client.reset_client() + + with patch.object(ai_client, 'settings') as mock_settings: + mock_settings.OPENAI_API_KEY = "" + mock_settings.OPENAI_TIMEOUT = 30 + + client = ai_client.get_openai_client() + # 無 API Key 應返回 None + assert client is None + + def test_reset_client(self): + """測試重置客戶端""" + from core import ai_client + + ai_client.reset_client() + assert ai_client._initialized is False + assert ai_client._openai_client is None + + def test_is_available_false(self): + """測試服務不可用""" + from core import ai_client + ai_client.reset_client() + + with patch.object(ai_client, 'settings') as mock_settings: + mock_settings.OPENAI_API_KEY = "" + mock_settings.OPENAI_TIMEOUT = 30 + + assert ai_client.is_available() is False diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000000000000000000000000000000000000..f982248878388039b9ccecab58094225960c967d --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,38 @@ +""" +測試 core/config.py 配置模組 +""" + +import pytest +from core.config import Settings + + +class TestSettings: + """測試 Settings 類別""" + + def test_cors_origins_default(self): + """測試 CORS 預設值""" + origins = Settings.get_cors_origins() + assert isinstance(origins, list) + + def test_constants_defined(self): + """測試常數已定義""" + assert hasattr(Settings, 'WEBSOCKET_SESSION_TIMEOUT') + assert hasattr(Settings, 'CLEANUP_INTERVAL') + assert hasattr(Settings, 'MEMORY_IMPORTANCE_THRESHOLD') + assert hasattr(Settings, 'INTENT_CACHE_TTL') + assert hasattr(Settings, 'CHAT_HISTORY_LIMIT') + assert hasattr(Settings, 'CARE_MODE_HISTORY_LIMIT') + + def test_security_constants(self): + """測試安全性常數""" + assert hasattr(Settings, 'FAILED_LOGIN_THRESHOLD') + assert hasattr(Settings, 'LOGIN_BLOCK_DURATION') + assert hasattr(Settings, 'JWT_SECRET_MIN_LENGTH') + assert Settings.JWT_SECRET_MIN_LENGTH >= 32 + + def test_default_values(self): + """測試預設值合理性""" + assert Settings.WEBSOCKET_SESSION_TIMEOUT > 0 + assert Settings.CLEANUP_INTERVAL > 0 + assert 0 <= Settings.MEMORY_IMPORTANCE_THRESHOLD <= 1 + assert Settings.INTENT_CACHE_TTL > 0 diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..5fcc1b9d6af57be695b1f7a8f25e2655fe346c1a --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,85 @@ +""" +測試 core/exceptions.py 異常處理 +""" + +import pytest +from core.exceptions import ( + BloomWareException, + AuthenticationError, + TokenExpiredError, + InvalidTokenError, + ResourceNotFoundError, + ChatNotFoundError, + UserNotFoundError, + ValidationError, + create_error_response, +) + + +class TestExceptions: + """測試異常類別""" + + def test_bloomware_exception(self): + """測試基礎異常""" + exc = BloomWareException("測試錯誤", code="TEST_ERROR", status_code=400) + assert exc.message == "測試錯誤" + assert exc.code == "TEST_ERROR" + assert exc.status_code == 400 + + def test_exception_to_dict(self): + """測試異常轉字典""" + exc = BloomWareException("測試", code="TEST") + result = exc.to_dict() + + assert result["success"] is False + assert result["error"]["code"] == "TEST" + assert result["error"]["message"] == "測試" + + def test_authentication_error(self): + """測試認證錯誤""" + exc = AuthenticationError() + assert exc.status_code == 401 + assert exc.code == "AUTHENTICATION_ERROR" + + def test_token_expired_error(self): + """測試 Token 過期錯誤""" + exc = TokenExpiredError() + assert exc.status_code == 401 + assert "過期" in exc.message + + def test_invalid_token_error(self): + """測試無效 Token 錯誤""" + exc = InvalidTokenError() + assert exc.status_code == 401 + + def test_resource_not_found_error(self): + """測試資源不存在錯誤""" + exc = ResourceNotFoundError("用戶", "user123") + assert exc.status_code == 404 + assert "用戶" in exc.message + + def test_chat_not_found_error(self): + """測試對話不存在錯誤""" + exc = ChatNotFoundError("chat123") + assert exc.status_code == 404 + assert exc.details["resource_id"] == "chat123" + + def test_user_not_found_error(self): + """測試用戶不存在錯誤""" + exc = UserNotFoundError("user123") + assert exc.status_code == 404 + + def test_validation_error(self): + """測試驗證錯誤""" + exc = ValidationError("email", "格式不正確") + assert exc.status_code == 400 + assert "email" in exc.message + + def test_create_error_response(self): + """測試建立錯誤回應""" + response = create_error_response( + code="TEST", + message="測試訊息", + status_code=400 + ) + assert response.status_code == 400 diff --git a/tests/test_heartbeat.py b/tests/test_heartbeat.py new file mode 100644 index 0000000000000000000000000000000000000000..837fa7c32f38a73feb35598d3fda932bd189a065 --- /dev/null +++ b/tests/test_heartbeat.py @@ -0,0 +1,92 @@ +""" +測試 websocket/heartbeat.py 心跳機制 +""" + +import pytest +import time +from websocket.heartbeat import HeartbeatManager, HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT + + +class TestHeartbeatManager: + """測試心跳管理器""" + + def test_init(self): + """測試初始化""" + manager = HeartbeatManager() + assert manager._last_heartbeat == {} + assert manager._heartbeat_tasks == {} + + def test_record_heartbeat(self): + """測試記錄心跳""" + manager = HeartbeatManager() + manager.record_heartbeat("user123") + + assert "user123" in manager._last_heartbeat + assert manager._last_heartbeat["user123"] <= time.time() + + def test_get_last_heartbeat(self): + """測試取得最後心跳時間""" + manager = HeartbeatManager() + + # 未記錄時返回 None + assert manager.get_last_heartbeat("user123") is None + + # 記錄後返回時間戳 + manager.record_heartbeat("user123") + last = manager.get_last_heartbeat("user123") + assert last is not None + assert last <= time.time() + + def test_is_alive_true(self): + """測試連線存活(剛記錄心跳)""" + manager = HeartbeatManager() + manager.record_heartbeat("user123") + + assert manager.is_alive("user123") is True + + def test_is_alive_false_no_heartbeat(self): + """測試連線不存活(無心跳記錄)""" + manager = HeartbeatManager() + assert manager.is_alive("user123") is False + + def test_is_alive_false_timeout(self): + """測試連線不存活(超時)""" + manager = HeartbeatManager() + # 模擬很久以前的心跳 + manager._last_heartbeat["user123"] = time.time() - 1000 + + assert manager.is_alive("user123", timeout=60) is False + + def test_get_stats(self): + """測試取得統計""" + manager = HeartbeatManager() + manager.record_heartbeat("user1") + manager.record_heartbeat("user2") + + stats = manager.get_stats() + assert stats["tracked_users"] == 2 + assert stats["active_heartbeats"] == 0 # 沒有啟動任務 + + def test_set_disconnect_callback(self): + """測試設定斷線回調""" + manager = HeartbeatManager() + + async def callback(user_id: str): + pass + + manager.set_disconnect_callback(callback) + assert manager._disconnect_callback is not None + + +class TestHeartbeatConstants: + """測試心跳常數""" + + def test_heartbeat_interval(self): + """測試心跳間隔""" + assert HEARTBEAT_INTERVAL > 0 + assert HEARTBEAT_INTERVAL <= 60 # 不應超過 1 分鐘 + + def test_heartbeat_timeout(self): + """測試心跳超時""" + assert HEARTBEAT_TIMEOUT > 0 + assert HEARTBEAT_TIMEOUT < HEARTBEAT_INTERVAL # 超時應小於間隔 diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 0000000000000000000000000000000000000000..96880841a9926f0b2790662fc0108db7d9630248 --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,48 @@ +""" +測試 core/logging.py 日誌配置 +""" + +import pytest +import logging + + +class TestLogging: + """測試日誌模組""" + + def test_get_logger(self): + """測試取得 logger""" + from core.logging import get_logger + + logger = get_logger("test_module") + assert logger is not None + assert logger.name == "test_module" + + def test_get_log_level(self): + """測試取得日誌等級""" + from core.logging import get_log_level + + level = get_log_level() + assert isinstance(level, int) + assert level in [ + logging.DEBUG, + logging.INFO, + logging.WARNING, + logging.ERROR, + logging.CRITICAL + ] + + def test_get_level_name(self): + """測試取得日誌等級名稱""" + from core.logging import get_level_name + + name = get_level_name() + assert isinstance(name, str) + assert name in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + + def test_setup_logging(self): + """測試設置日誌""" + from core.logging import setup_logging + + logger = setup_logging("test_setup") + assert logger is not None + assert len(logger.handlers) > 0 diff --git a/tests/test_prompts.py b/tests/test_prompts.py new file mode 100644 index 0000000000000000000000000000000000000000..51372a559f657ce4f6e67b8d62d59281c74ffdde --- /dev/null +++ b/tests/test_prompts.py @@ -0,0 +1,78 @@ +""" +測試 core/prompts 模組 +""" + +import pytest +from core.prompts.intent_detection import get_intent_prompt, TOOL_RULES +from core.prompts.care_mode import get_care_prompt, CARE_MODE_PROMPT + + +class TestIntentPrompt: + """測試意圖檢測 Prompt""" + + def test_get_intent_prompt_basic(self): + """測試基本 Prompt 生成""" + prompt = get_intent_prompt("工具列表") + assert "意圖解析" in prompt + assert "工具列表" in prompt + assert "is_tool_call" in prompt + + def test_get_intent_prompt_with_rules(self): + """測試帶規則的 Prompt""" + prompt = get_intent_prompt("工具", include_rules=["weather", "bus"]) + assert "天氣" in prompt or "Taipei" in prompt + assert "公車" in prompt or "route_name" in prompt + + def test_get_intent_prompt_empty_rules(self): + """測試空規則""" + prompt = get_intent_prompt("工具", include_rules=[]) + assert "意圖解析" in prompt + assert "emotion" in prompt + + def test_tool_rules_defined(self): + """測試工具規則已定義""" + assert "weather" in TOOL_RULES + assert "bus" in TOOL_RULES + assert "train" in TOOL_RULES + assert "youbike" in TOOL_RULES + assert "location" in TOOL_RULES + + def test_prompt_length_reduced(self): + """測試 Prompt 長度合理""" + prompt = get_intent_prompt("工具列表") + # 精簡後應少於 2000 字元 + assert len(prompt) < 2000 + + +class TestCarePrompt: + """測試關懷模式 Prompt""" + + def test_care_mode_prompt_exists(self): + """測試關懷 Prompt 存在""" + assert CARE_MODE_PROMPT is not None + assert "小花" in CARE_MODE_PROMPT + assert "傾聽" in CARE_MODE_PROMPT + + def test_get_care_prompt_basic(self): + """測試基本關懷 Prompt""" + prompt = get_care_prompt() + assert "小花" in prompt + assert "60 字" in prompt + + def test_get_care_prompt_with_emotion(self): + """測試帶情緒的關懷 Prompt""" + prompt = get_care_prompt(emotion="sad") + assert "sad" in prompt + assert "用戶情緒" in prompt + + def test_get_care_prompt_with_name(self): + """測試帶名稱的關懷 Prompt""" + prompt = get_care_prompt(user_name="小明") + assert "小明" in prompt + assert "用戶名稱" in prompt + + def test_get_care_prompt_full(self): + """測試完整關懷 Prompt""" + prompt = get_care_prompt(emotion="angry", user_name="小華") + assert "angry" in prompt + assert "小華" in prompt diff --git a/tests/test_retry.py b/tests/test_retry.py new file mode 100644 index 0000000000000000000000000000000000000000..5e2575f840d88b3b5f492f20a6bdc39bbc56a23c --- /dev/null +++ b/tests/test_retry.py @@ -0,0 +1,129 @@ +""" +測試 core/retry.py 重試機制 +""" + +import pytest +import asyncio +from core.retry import ( + RetryConfig, + retry_async, + with_retry, + default_retry_config, + api_retry_config, +) + + +class TestRetryConfig: + """測試重試配置""" + + def test_default_config(self): + """測試預設配置""" + config = RetryConfig() + assert config.max_retries == 3 + assert config.base_delay == 1.0 + assert config.max_delay == 30.0 + + def test_custom_config(self): + """測試自訂配置""" + config = RetryConfig(max_retries=5, base_delay=0.5) + assert config.max_retries == 5 + assert config.base_delay == 0.5 + + def test_calculate_delay(self): + """測試延遲計算""" + config = RetryConfig(base_delay=1.0, jitter=False) + + # 第一次重試 + delay0 = config.calculate_delay(0) + assert delay0 == 1.0 + + # 第二次重試(指數退避) + delay1 = config.calculate_delay(1) + assert delay1 == 2.0 + + def test_max_delay_cap(self): + """測試最大延遲上限""" + config = RetryConfig(base_delay=10.0, max_delay=15.0, jitter=False) + delay = config.calculate_delay(5) # 10 * 2^5 = 320,應被限制 + assert delay == 15.0 + + +class TestRetryAsync: + """測試異步重試""" + + @pytest.mark.asyncio + async def test_success_no_retry(self): + """測試成功時不重試""" + call_count = 0 + + async def success_func(): + nonlocal call_count + call_count += 1 + return "success" + + result = await retry_async(success_func) + assert result == "success" + assert call_count == 1 + + @pytest.mark.asyncio + async def test_retry_then_success(self): + """測試重試後成功""" + call_count = 0 + + async def fail_then_success(): + nonlocal call_count + call_count += 1 + if call_count < 3: + raise ValueError("temporary error") + return "success" + + config = RetryConfig(max_retries=3, base_delay=0.01) + result = await retry_async(fail_then_success, config=config) + assert result == "success" + assert call_count == 3 + + @pytest.mark.asyncio + async def test_all_retries_fail(self): + """測試所有重試都失敗""" + call_count = 0 + + async def always_fail(): + nonlocal call_count + call_count += 1 + raise ValueError("always fails") + + config = RetryConfig(max_retries=2, base_delay=0.01) + + with pytest.raises(ValueError): + await retry_async(always_fail, config=config) + + assert call_count == 3 # 初始 + 2 次重試 + + +class TestWithRetryDecorator: + """測試重試裝飾器""" + + @pytest.mark.asyncio + async def test_decorator_success(self): + """測試裝飾器成功""" + @with_retry(max_retries=2, base_delay=0.01) + async def decorated_func(): + return "decorated" + + result = await decorated_func() + assert result == "decorated" + + +class TestPredefinedConfigs: + """測試預定義配置""" + + def test_default_config_exists(self): + """測試預設配置存在""" + assert default_retry_config is not None + assert default_retry_config.max_retries == 3 + + def test_api_config_exists(self): + """測試 API 配置存在""" + assert api_retry_config is not None + assert api_retry_config.max_retries == 2 + assert api_retry_config.base_delay == 0.5 diff --git a/tests/test_schemas.py b/tests/test_schemas.py new file mode 100644 index 0000000000000000000000000000000000000000..5d1e7b170499ca3968645925796e924689f09911 --- /dev/null +++ b/tests/test_schemas.py @@ -0,0 +1,100 @@ +""" +測試 models/schemas.py Pydantic 模型 +""" + +import pytest +from datetime import datetime +from pydantic import ValidationError as PydanticValidationError + +from models.schemas import ( + UserCreate, + UserLogin, + ChatCreateRequest, + MessageCreateRequest, + FileAnalysisRequest, + SpeakerLabelBindRequest, +) + + +class TestUserSchemas: + """測試用戶相關模型""" + + def test_user_create_valid(self): + """測試有效的用戶建立請求""" + user = UserCreate( + name="測試用戶", + email="test@example.com", + password="password123" + ) + assert user.name == "測試用戶" + assert user.email == "test@example.com" + + def test_user_create_invalid_email(self): + """測試無效的 email""" + with pytest.raises(PydanticValidationError): + UserCreate( + name="測試", + email="invalid-email", + password="password123" + ) + + def test_user_create_short_password(self): + """測試過短的密碼""" + with pytest.raises(PydanticValidationError): + UserCreate( + name="測試", + email="test@example.com", + password="12345" # 少於 6 字元 + ) + + def test_user_login_valid(self): + """測試有效的登入請求""" + login = UserLogin( + email="test@example.com", + password="password123" + ) + assert login.email == "test@example.com" + + +class TestChatSchemas: + """測試對話相關模型""" + + def test_chat_create_request(self): + """測試建立對話請求""" + chat = ChatCreateRequest(user_id="user123") + assert chat.user_id == "user123" + assert chat.title == "新對話" # 預設值 + + def test_chat_create_with_title(self): + """測試帶標題的建立對話請求""" + chat = ChatCreateRequest(user_id="user123", title="我的對話") + assert chat.title == "我的對話" + + def test_message_create_request(self): + """測試建立訊息請求""" + msg = MessageCreateRequest(sender="user", content="你好") + assert msg.sender == "user" + assert msg.content == "你好" + + +class TestFileSchemas: + """測試檔案相關模型""" + + def test_file_analysis_request(self): + """測試檔案分析請求""" + req = FileAnalysisRequest( + filename="test.txt", + content="SGVsbG8gV29ybGQ=", + mime_type="text/plain" + ) + assert req.filename == "test.txt" + assert req.user_prompt == "請分析這個檔案的內容" # 預設值 + + +class TestVoiceSchemas: + """測試語音相關模型""" + + def test_speaker_label_bind_request(self): + """測試語音標籤綁定請求""" + req = SpeakerLabelBindRequest(speaker_label="speaker_001") + assert req.speaker_label == "speaker_001" diff --git a/tests/test_tool_registry_refactor.py b/tests/test_tool_registry_refactor.py new file mode 100644 index 0000000000000000000000000000000000000000..4d97cb751b0e85ad2872ffaf856943406415f5d5 --- /dev/null +++ b/tests/test_tool_registry_refactor.py @@ -0,0 +1,321 @@ +""" +測試重構後的工具註冊系統 +驗證 Pydantic Schema 自動生成和 OpenAI Function Calling 格式 +""" + +import pytest +import sys +import os + +# 添加專案根目錄到 path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from core.tool_schema import ( + ToolSchema, + ToolMetadata, + ToolSchemaRegistry, + extract_schema_from_mcp_tool, +) +from core.tool_registry import ToolRegistry, ToolDefinition + + +class TestToolSchema: + """測試 ToolSchema 類別""" + + def test_to_openai_tool_basic(self): + """測試基本的 OpenAI 工具格式轉換""" + schema = ToolSchema( + metadata=ToolMetadata( + name="test_tool", + description="測試工具", + category="test", + keywords=["測試", "test"], + ), + input_schema={ + "type": "object", + "properties": { + "query": {"type": "string", "description": "查詢字串"}, + "limit": {"type": "integer", "default": 10}, + }, + "required": ["query"], + }, + ) + + openai_tool = schema.to_openai_tool(strict=True) + + assert openai_tool["type"] == "function" + assert openai_tool["function"]["name"] == "test_tool" + assert "測試工具" in openai_tool["function"]["description"] + assert openai_tool["function"]["strict"] is True + + # 檢查 strict mode 要求 + params = openai_tool["function"]["parameters"] + assert params["additionalProperties"] is False + assert "query" in params["required"] + + def test_rich_description(self): + """測試豐富描述生成""" + schema = ToolSchema( + metadata=ToolMetadata( + name="weather", + description="查詢天氣", + keywords=["天氣", "氣溫", "下雨"], + examples=["台北天氣", "明天會下雨嗎"], + ), + input_schema={"type": "object", "properties": {}}, + ) + + openai_tool = schema.to_openai_tool() + desc = openai_tool["function"]["description"] + + assert "查詢天氣" in desc + assert "天氣" in desc + assert "台北天氣" in desc + + def test_get_summary(self): + """測試工具摘要""" + schema = ToolSchema( + metadata=ToolMetadata( + name="bus_query", + description="查詢公車到站時間", + category="transportation", + keywords=["公車", "bus"], + ), + input_schema={ + "type": "object", + "properties": { + "route": {"type": "string"}, + "stop": {"type": "string"}, + }, + }, + ) + + summary = schema.get_summary() + + assert summary["name"] == "bus_query" + assert summary["category"] == "transportation" + assert "route" in summary["params"] + assert "stop" in summary["params"] + + +class TestToolSchemaRegistry: + """測試 ToolSchemaRegistry 類別""" + + def test_register_and_get(self): + """測試註冊和取得""" + registry = ToolSchemaRegistry() + + schema = ToolSchema( + metadata=ToolMetadata(name="test", description="Test"), + input_schema={"type": "object", "properties": {}}, + ) + + registry.register(schema) + + retrieved = registry.get("test") + assert retrieved is not None + assert retrieved.metadata.name == "test" + + def test_disable_enable(self): + """測試停用和啟用""" + registry = ToolSchemaRegistry() + + schema = ToolSchema( + metadata=ToolMetadata(name="toggleable", description="Test"), + input_schema={"type": "object", "properties": {}}, + ) + + registry.register(schema) + + # 停用 + registry.disable("toggleable") + assert registry.get("toggleable") is None + + # 啟用 + registry.enable("toggleable") + assert registry.get("toggleable") is not None + + def test_get_openai_tools_filtering(self): + """測試 OpenAI 工具列表過濾""" + registry = ToolSchemaRegistry() + + # 註冊不同分類的工具 + registry.register(ToolSchema( + metadata=ToolMetadata(name="weather", description="天氣", category="weather"), + input_schema={"type": "object", "properties": {}}, + )) + registry.register(ToolSchema( + metadata=ToolMetadata(name="bus", description="公車", category="transportation"), + input_schema={"type": "object", "properties": {}}, + )) + registry.register(ToolSchema( + metadata=ToolMetadata( + name="geocode", + description="地理編碼", + category="location", + requires_location=True, + ), + input_schema={"type": "object", "properties": {}}, + )) + + # 測試分類過濾 + weather_tools = registry.get_openai_tools(categories=["weather"]) + assert len(weather_tools) == 1 + assert weather_tools[0]["function"]["name"] == "weather" + + # 測試位置過濾 + no_location_tools = registry.get_openai_tools(include_location_tools=False) + tool_names = [t["function"]["name"] for t in no_location_tools] + assert "geocode" not in tool_names + + def test_get_stats(self): + """測試統計資訊""" + registry = ToolSchemaRegistry() + + registry.register(ToolSchema( + metadata=ToolMetadata(name="t1", description="", category="a"), + input_schema={"type": "object", "properties": {}}, + )) + registry.register(ToolSchema( + metadata=ToolMetadata(name="t2", description="", category="a"), + input_schema={"type": "object", "properties": {}}, + )) + registry.register(ToolSchema( + metadata=ToolMetadata(name="t3", description="", category="b"), + input_schema={"type": "object", "properties": {}}, + )) + + registry.disable("t1") + + stats = registry.get_stats() + + assert stats["total"] == 3 + assert stats["disabled"] == 1 + assert stats["active"] == 2 + assert stats["categories"]["a"] == 2 + assert stats["categories"]["b"] == 1 + + +class TestToolRegistry: + """測試重構後的 ToolRegistry 類別""" + + def test_register_legacy(self): + """測試舊版註冊方式(向後兼容)""" + # 使用獨立的 schema registry 避免全域污染 + from core.tool_schema import ToolSchemaRegistry + isolated_schema_registry = ToolSchemaRegistry() + + registry = ToolRegistry() + registry._schema_registry = isolated_schema_registry + + registry.register( + name="legacy_tool", + description="舊版工具", + parameters={"type": "object", "properties": {"x": {"type": "string"}}}, + category="test", + keywords=["legacy"], + ) + + tool = registry.get_tool("legacy_tool") + assert tool is not None + assert tool.name == "legacy_tool" + assert tool.keywords == ["legacy"] + + def test_get_openai_tools_strict(self): + """測試 strict mode 的 OpenAI 工具格式""" + # 使用獨立的 schema registry 避免全域污染 + from core.tool_schema import ToolSchemaRegistry + isolated_schema_registry = ToolSchemaRegistry() + + registry = ToolRegistry() + registry._schema_registry = isolated_schema_registry + + registry.register( + name="strict_tool", + description="Strict 工具", + parameters={ + "type": "object", + "properties": { + "required_param": {"type": "string"}, + "optional_param": {"type": "integer", "default": 5}, + }, + "required": ["required_param"], + }, + ) + + tools = registry.get_openai_tools(strict=True) + + assert len(tools) == 1 + tool = tools[0] + + # 檢查 strict mode 格式 + assert tool["function"]["strict"] is True + params = tool["function"]["parameters"] + assert params["additionalProperties"] is False + + def test_get_summaries(self): + """測試取得工具摘要""" + # 使用獨立的 schema registry 避免全域污染 + from core.tool_schema import ToolSchemaRegistry + isolated_schema_registry = ToolSchemaRegistry() + + registry = ToolRegistry() + registry._schema_registry = isolated_schema_registry + + registry.register( + name="summary_tool", + description="摘要測試工具", + parameters={"type": "object", "properties": {"a": {"type": "string"}}}, + keywords=["摘要", "test"], + ) + + summaries = registry.get_summaries() + + assert len(summaries) == 1 + assert summaries[0]["name"] == "summary_tool" + assert "摘要" in summaries[0]["keywords"] + + +class TestExtractSchemaFromMCPTool: + """測試從 MCPTool 類別提取 Schema""" + + def test_extract_from_mock_tool(self): + """測試從模擬的 MCPTool 類別提取""" + + class MockTool: + NAME = "mock_tool" + DESCRIPTION = "模擬工具" + CATEGORY = "test" + KEYWORDS = ["mock", "test"] + USAGE_TIPS = ["使用範例"] + + @classmethod + def get_input_schema(cls): + return { + "type": "object", + "properties": { + "param1": {"type": "string"}, + }, + "required": ["param1"], + } + + @classmethod + def get_output_schema(cls): + return {"type": "object", "properties": {"result": {"type": "string"}}} + + @classmethod + async def execute(cls, arguments): + return {"result": "ok"} + + schema = extract_schema_from_mcp_tool(MockTool) + + assert schema is not None + assert schema.metadata.name == "mock_tool" + assert schema.metadata.description == "模擬工具" + assert schema.metadata.category == "test" + assert "mock" in schema.metadata.keywords + assert "param1" in schema.input_schema["properties"] + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_tool_router.py b/tests/test_tool_router.py new file mode 100644 index 0000000000000000000000000000000000000000..14a8920aa320fe617a1a0119ea371ef91f1752f7 --- /dev/null +++ b/tests/test_tool_router.py @@ -0,0 +1,118 @@ +""" +測試動態工具路由器 +""" + +import pytest +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from core.tool_router import ToolRouter + + +class TestToolRouter: + """測試 ToolRouter 類別""" + + def _create_mock_tools(self): + """建立模擬工具列表""" + return [ + {"type": "function", "function": {"name": "weather_query", "description": "天氣查詢"}}, + {"type": "function", "function": {"name": "tdx_bus_arrival", "description": "公車到站"}}, + {"type": "function", "function": {"name": "tdx_youbike", "description": "YouBike 查詢"}}, + {"type": "function", "function": {"name": "reverse_geocode", "description": "位置查詢"}}, + {"type": "function", "function": {"name": "news_query", "description": "新聞查詢"}}, + {"type": "function", "function": {"name": "exchange_query", "description": "匯率查詢"}}, + ] + + def test_detect_categories_weather(self): + """測試天氣分類檢測""" + router = ToolRouter() + + categories = router._detect_categories("台北天氣怎麼樣") + assert "weather" in categories + + categories = router._detect_categories("今天會下雨嗎") + assert "weather" in categories + + def test_detect_categories_transportation(self): + """測試交通分類檢測""" + router = ToolRouter() + + categories = router._detect_categories("附近的公車站") + assert "transportation" in categories + + categories = router._detect_categories("YouBike 在哪") + assert "transportation" in categories + + def test_detect_categories_location(self): + """測試位置分類檢測""" + router = ToolRouter() + + categories = router._detect_categories("我在哪裡") + assert "location" in categories + + categories = router._detect_categories("怎麼去台北車站") + assert "location" in categories + + def test_filter_tools_basic(self): + """測試基本工具過濾""" + router = ToolRouter() + tools = self._create_mock_tools() + + filtered = router.filter_tools(tools, "台北天氣", {}) + + # 應該返回工具(天氣相關優先) + assert len(filtered) > 0 + tool_names = [t["function"]["name"] for t in filtered] + assert "weather_query" in tool_names + + def test_filter_tools_night_exclusion(self): + """測試深夜工具排除""" + router = ToolRouter() + tools = self._create_mock_tools() + + # 深夜(凌晨 2 點) + filtered = router.filter_tools(tools, "公車", {"hour": 2}) + tool_names = [t["function"]["name"] for t in filtered] + + # 深夜應該排除公車和捷運 + assert "tdx_bus_arrival" not in tool_names + + def test_filter_tools_daytime(self): + """測試白天工具不排除""" + router = ToolRouter() + tools = self._create_mock_tools() + + # 白天(下午 2 點) + filtered = router.filter_tools(tools, "公車", {"hour": 14}) + tool_names = [t["function"]["name"] for t in filtered] + + # 白天應該包含公車 + assert "tdx_bus_arrival" in tool_names + + def test_sort_tools_priority(self): + """測試工具排序""" + router = ToolRouter() + tools = self._create_mock_tools() + + # 天氣查詢應該排在前面 + filtered = router.filter_tools(tools, "天氣", {"hour": 12}) + + # 第一個應該是天氣相關工具 + assert filtered[0]["function"]["name"] == "weather_query" + + def test_record_tool_usage(self): + """測試工具使用記錄""" + router = ToolRouter() + + router.record_tool_usage("user1", "weather_query") + router.record_tool_usage("user1", "weather_query") + router.record_tool_usage("user1", "news_query") + + assert router._user_preferences["user1"]["weather_query"] == 2 + assert router._user_preferences["user1"]["news_query"] == 1 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_voice_binding.py b/tests/test_voice_binding.py new file mode 100644 index 0000000000000000000000000000000000000000..b3133ddae881581eb674f580780b736f5f8cc806 --- /dev/null +++ b/tests/test_voice_binding.py @@ -0,0 +1,59 @@ +""" +測試 services/voice_binding.py 語音綁定狀態機 +""" + +import pytest +from services.voice_binding import VoiceBindingStateMachine + + +class TestVoiceBindingStateMachine: + """測試 VoiceBindingStateMachine 類別""" + + def test_init(self): + """測試初始化""" + fsm = VoiceBindingStateMachine() + assert fsm.user_states == {} + + def test_trigger_keywords(self): + """測試觸發關鍵字""" + fsm = VoiceBindingStateMachine() + + # 測試各種觸發關鍵字 + assert fsm.check_binding_trigger("user1", "我要綁定語音登入") == "TRIGGER" + assert fsm.check_binding_trigger("user2", "語音登入綁定") == "TRIGGER" + assert fsm.check_binding_trigger("user3", "綁定語音") == "TRIGGER" + assert fsm.check_binding_trigger("user4", "設定語音登入") == "TRIGGER" + + def test_no_trigger(self): + """測試非觸發訊息""" + fsm = VoiceBindingStateMachine() + + assert fsm.check_binding_trigger("user1", "你好") is None + assert fsm.check_binding_trigger("user1", "今天天氣如何") is None + assert fsm.check_binding_trigger("user1", "語音") is None + + def test_clear_state(self): + """測試清理狀態""" + fsm = VoiceBindingStateMachine() + + # 觸發後清理 + fsm.check_binding_trigger("user1", "綁定語音登入") + assert "user1" in fsm.user_states + + fsm.clear_state("user1") + assert "user1" not in fsm.user_states + + def test_is_awaiting_voice(self): + """測試等待語音狀態""" + fsm = VoiceBindingStateMachine() + + # 初始狀態 + assert fsm.is_awaiting_voice("user1") is False + + # 觸發後 + fsm.check_binding_trigger("user1", "綁定語音登入") + assert fsm.is_awaiting_voice("user1") is True + + # 清理後 + fsm.clear_state("user1") + assert fsm.is_awaiting_voice("user1") is False diff --git a/tests/test_websocket_manager.py b/tests/test_websocket_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..350325796bd6344a6ffe4cbcee65dd997702d1a9 --- /dev/null +++ b/tests/test_websocket_manager.py @@ -0,0 +1,47 @@ +""" +測試 websocket/manager.py 連線管理器 +""" + +import pytest +from datetime import datetime, timedelta +from websocket.manager import ConnectionManager + + +class TestConnectionManager: + """測試 ConnectionManager 類別""" + + def test_init(self): + """測試初始化""" + manager = ConnectionManager() + assert manager.active_connections == {} + assert manager.client_info == {} + assert manager.user_sessions == {} + + def test_is_connected_false(self): + """測試未連線狀態""" + manager = ConnectionManager() + assert manager.is_connected("user123") is False + + def test_get_active_user_count_empty(self): + """測試空連線數""" + manager = ConnectionManager() + assert manager.get_active_user_count() == 0 + + def test_client_info(self): + """測試客戶端資訊存取""" + manager = ConnectionManager() + manager.set_client_info("user123", {"device": "iPhone"}) + info = manager.get_client_info("user123") + assert info == {"device": "iPhone"} + + def test_get_client_info_not_found(self): + """測試取得不存在的客戶端資訊""" + manager = ConnectionManager() + info = manager.get_client_info("nonexistent") + assert info == {} + + def test_get_user_session_not_found(self): + """測試取得不存在的會話""" + manager = ConnectionManager() + session = manager.get_user_session("nonexistent") + assert session is None diff --git a/websocket/__init__.py b/websocket/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..53aa0ffa0787cadfa6d67aa27accf41c00c54d98 --- /dev/null +++ b/websocket/__init__.py @@ -0,0 +1,14 @@ +""" +WebSocket 模組 +統一管理 WebSocket 連線、會話和訊息處理 +""" + +from .manager import ConnectionManager, manager +from .heartbeat import HeartbeatManager, heartbeat_manager + +__all__ = [ + "ConnectionManager", + "manager", + "HeartbeatManager", + "heartbeat_manager", +] diff --git a/websocket/heartbeat.py b/websocket/heartbeat.py new file mode 100644 index 0000000000000000000000000000000000000000..fdecf6665b4cb9db7c5add9f90a670747d3483f4 --- /dev/null +++ b/websocket/heartbeat.py @@ -0,0 +1,153 @@ +""" +WebSocket 心跳機制 +保持連線穩定,自動檢測斷線 +""" + +import asyncio +import time +from typing import Dict, Optional, Callable, Awaitable + +from core.logging import get_logger +from core.config import settings + +logger = get_logger("websocket.heartbeat") + +# 心跳間隔(秒) +HEARTBEAT_INTERVAL = 30 + +# 心跳超時(秒) +HEARTBEAT_TIMEOUT = 10 + +# 最大重連次數 +MAX_RECONNECT_ATTEMPTS = 5 + + +class HeartbeatManager: + """ + WebSocket 心跳管理器 + + 功能: + 1. 定期發送心跳包 + 2. 檢測連線狀態 + 3. 觸發斷線回調 + """ + + def __init__(self): + # 用戶最後心跳時間 + self._last_heartbeat: Dict[str, float] = {} + # 心跳任務 + self._heartbeat_tasks: Dict[str, asyncio.Task] = {} + # 斷線回調 + self._disconnect_callback: Optional[Callable[[str], Awaitable[None]]] = None + + def set_disconnect_callback( + self, callback: Callable[[str], Awaitable[None]] + ) -> None: + """設定斷線回調函數""" + self._disconnect_callback = callback + + def record_heartbeat(self, user_id: str) -> None: + """記錄用戶心跳""" + self._last_heartbeat[user_id] = time.time() + logger.debug(f"💓 收到心跳: {user_id}") + + def get_last_heartbeat(self, user_id: str) -> Optional[float]: + """取得用戶最後心跳時間""" + return self._last_heartbeat.get(user_id) + + def is_alive(self, user_id: str, timeout: float = HEARTBEAT_TIMEOUT * 3) -> bool: + """檢查用戶連線是否存活""" + last = self._last_heartbeat.get(user_id) + if last is None: + return False + return (time.time() - last) < timeout + + async def start_heartbeat( + self, + user_id: str, + send_func: Callable[[dict], Awaitable[bool]], + ) -> None: + """ + 啟動心跳任務 + + Args: + user_id: 用戶 ID + send_func: 發送訊息的函數 + """ + # 取消舊任務 + if user_id in self._heartbeat_tasks: + self._heartbeat_tasks[user_id].cancel() + + # 記錄初始心跳 + self.record_heartbeat(user_id) + + # 建立新任務 + task = asyncio.create_task( + self._heartbeat_loop(user_id, send_func) + ) + self._heartbeat_tasks[user_id] = task + logger.info(f"💓 啟動心跳任務: {user_id}") + + async def stop_heartbeat(self, user_id: str) -> None: + """停止心跳任務""" + if user_id in self._heartbeat_tasks: + self._heartbeat_tasks[user_id].cancel() + del self._heartbeat_tasks[user_id] + logger.info(f"💔 停止心跳任務: {user_id}") + + if user_id in self._last_heartbeat: + del self._last_heartbeat[user_id] + + async def _heartbeat_loop( + self, + user_id: str, + send_func: Callable[[dict], Awaitable[bool]], + ) -> None: + """心跳循環""" + missed_count = 0 + + while True: + try: + await asyncio.sleep(HEARTBEAT_INTERVAL) + + # 發送心跳 + success = await send_func({ + "type": "heartbeat", + "timestamp": time.time(), + }) + + if success: + missed_count = 0 + else: + missed_count += 1 + logger.warning(f"⚠️ 心跳發送失敗: {user_id} (連續 {missed_count} 次)") + + # 檢查是否超時 + if not self.is_alive(user_id): + missed_count += 1 + logger.warning(f"⚠️ 心跳超時: {user_id}") + + # 連續失敗超過閾值,觸發斷線 + if missed_count >= 3: + logger.error(f"❌ 連線失效: {user_id}") + if self._disconnect_callback: + await self._disconnect_callback(user_id) + break + + except asyncio.CancelledError: + logger.debug(f"心跳任務被取消: {user_id}") + break + except Exception as e: + logger.error(f"心跳任務錯誤: {user_id} - {e}") + await asyncio.sleep(5) + + def get_stats(self) -> Dict[str, int]: + """取得心跳統計""" + return { + "active_heartbeats": len(self._heartbeat_tasks), + "tracked_users": len(self._last_heartbeat), + } + + +# 全域單例 +heartbeat_manager = HeartbeatManager() diff --git a/websocket/manager.py b/websocket/manager.py new file mode 100644 index 0000000000000000000000000000000000000000..0b885236ab98e5d7b6533ef3aabaff1cfe1f7249 --- /dev/null +++ b/websocket/manager.py @@ -0,0 +1,179 @@ +""" +WebSocket 連線管理器 +統一管理 WebSocket 連線、會話狀態和訊息發送 +""" + +import logging +import time +from datetime import datetime +from typing import Dict, Any, Optional + +from fastapi import WebSocket + +from core.logging import get_logger + +logger = get_logger("websocket.manager") + + +class ConnectionManager: + """WebSocket 連線管理器""" + + def __init__(self): + self.active_connections: Dict[str, WebSocket] = {} + self.client_info: Dict[str, dict] = {} + self.user_sessions: Dict[str, Dict[str, Any]] = {} + self.last_env: Dict[str, Dict[str, Any]] = {} + + async def connect( + self, + websocket: WebSocket, + user_id: str, + user_info: Dict[str, Any] + ) -> None: + """建立 WebSocket 連線""" + await websocket.accept() + self.active_connections[user_id] = websocket + self.user_sessions[user_id] = user_info + logger.info(f"新的 WebSocket 連接: {user_id}") + + def disconnect(self, user_id: str) -> None: + """關閉 WebSocket 連線""" + if user_id in self.active_connections: + del self.active_connections[user_id] + if user_id in self.user_sessions: + del self.user_sessions[user_id] + if user_id in self.client_info: + del self.client_info[user_id] + logger.info(f"WebSocket 連接關閉: {user_id}") + + async def send_message( + self, + message: str, + user_id: str, + message_type: str = "bot_message" + ) -> bool: + """發送訊息給指定用戶""" + if user_id not in self.active_connections: + logger.warning(f"用戶 {user_id} 不在線,無法發送訊息") + return False + + try: + payload = { + "type": message_type, + "message": message, + "timestamp": time.time() + } + await self.active_connections[user_id].send_json(payload) + + # 日誌記錄(截斷過長訊息) + preview = (str(message) or "").strip().replace("\n", " ") + if len(preview) > 120: + preview = preview[:120] + "..." + logger.debug( + f"WebSocket 已發送 → client={user_id} " + f"type={message_type} preview=\"{preview}\"" + ) + return True + + except Exception as e: + logger.error(f"發送訊息到客戶端 {user_id} 時出錯: {e}") + return False + + async def send_json( + self, + data: Dict[str, Any], + user_id: str + ) -> bool: + """發送 JSON 資料給指定用戶""" + if user_id not in self.active_connections: + return False + + try: + await self.active_connections[user_id].send_json(data) + return True + except Exception as e: + logger.error(f"發送 JSON 到客戶端 {user_id} 時出錯: {e}") + return False + + def set_client_info(self, user_id: str, info: dict) -> None: + """設定客戶端資訊""" + self.client_info[user_id] = info + + def get_client_info(self, user_id: str) -> dict: + """取得客戶端資訊""" + return self.client_info.get(user_id, {}) + + def get_user_session(self, user_id: str) -> Optional[Dict[str, Any]]: + """取得用戶會話資訊""" + return self.user_sessions.get(user_id) + + def update_last_activity(self, user_id: str) -> None: + """更新用戶最後活動時間""" + if user_id in self.user_sessions: + self.user_sessions[user_id]["last_activity"] = datetime.now() + + def is_connected(self, user_id: str) -> bool: + """檢查用戶是否在線""" + return user_id in self.active_connections + + def get_active_user_count(self) -> int: + """取得在線用戶數量""" + return len(self.active_connections) + + async def cleanup_expired_sessions(self, timeout_seconds: int = None) -> int: + """ + 清理過期的用戶會話 + + Args: + timeout_seconds: 超時時間(秒),預設使用配置值 + + Returns: + 清理的會話數量 + """ + if timeout_seconds is None: + from core.config import settings + timeout_seconds = settings.WEBSOCKET_SESSION_TIMEOUT + current_time = datetime.now() + expired_users = [] + + for user_id, session_info in self.user_sessions.items(): + last_activity = session_info.get("last_activity", current_time) + if (current_time - last_activity).total_seconds() > timeout_seconds: + expired_users.append(user_id) + + for user_id in expired_users: + logger.info(f"清理過期會話: {user_id}") + self.disconnect(user_id) + + return len(expired_users) + + async def broadcast( + self, + message: str, + message_type: str = "system", + exclude_users: Optional[list] = None + ) -> int: + """ + 廣播訊息給所有在線用戶 + + Args: + message: 訊息內容 + message_type: 訊息類型 + exclude_users: 排除的用戶列表 + + Returns: + 成功發送的數量 + """ + exclude_users = exclude_users or [] + success_count = 0 + + for user_id in list(self.active_connections.keys()): + if user_id not in exclude_users: + if await self.send_message(message, user_id, message_type): + success_count += 1 + + return success_count + + +# 全域單例 +manager = ConnectionManager() diff --git a/websocket/voice_binding.py b/websocket/voice_binding.py new file mode 100644 index 0000000000000000000000000000000000000000..eb6c15f5ba61ed10965b6745913d3c17a99126f8 --- /dev/null +++ b/websocket/voice_binding.py @@ -0,0 +1,149 @@ +""" +語音綁定狀態機 +管理語音帳號綁定流程(關鍵字匹配,無 GPT) +""" + +import logging +import time +from datetime import datetime +from typing import Dict, Any, Optional +from fastapi import WebSocket + +logger = logging.getLogger("websocket.voice_binding") + + +class VoiceBindingStateMachine: + """ + 語音帳號綁定狀態機(硬編碼關鍵字匹配) + + 流程: + 1. 用戶說「我要綁定語音登入」 + 2. Agent 回應「好的,你現在要綁定誰?」 + 3. 用戶提供名稱 + 4. 系統綁定 speaker_label 到用戶帳號 + 5. Agent 回應「綁定成功!」 + """ + + def __init__(self): + # 用戶狀態:{user_id: {state: str, speaker_label: str}} + self.user_states: Dict[str, Dict[str, Any]] = {} + + def check_binding_trigger(self, user_id: str, message: str) -> Optional[str]: + """ + 檢查是否觸發綁定流程 + + Returns: + - "TRIGGER": 觸發綁定流程 + - "AWAITING_NAME": 等待用戶提供名稱 + - None: 不是綁定相關訊息 + """ + message_lower = message.lower().replace(" ", "") + + # 檢測觸發關鍵字 + trigger_keywords = ["綁定語音登入", "語音登入綁定", "綁定語音", "設定語音登入"] + for keyword in trigger_keywords: + if keyword.replace(" ", "") in message_lower: + # 進入等待狀態 + self.user_states[user_id] = { + "state": "AWAITING_NAME", + "timestamp": datetime.now() + } + return "TRIGGER" + + # 檢查是否在等待名稱狀態 + if user_id in self.user_states: + state_info = self.user_states[user_id] + if state_info.get("state") == "AWAITING_NAME": + # 檢查是否超時(5分鐘) + if (datetime.now() - state_info.get("timestamp")).total_seconds() > 300: + del self.user_states[user_id] + return None + return "AWAITING_NAME" + + return None + + async def handle_binding_flow( + self, + user_id: str, + message: str, + websocket: WebSocket, + voice_service: Optional[Any] = None, + manager: Optional[Any] = None, + ) -> bool: + """ + 處理綁定流程 + + Returns: + True: 已處理(不要繼續到 Agent) + False: 未處理(繼續到 Agent) + """ + state = self.check_binding_trigger(user_id, message) + + if state == "TRIGGER": + # 用戶觸發綁定 - 先檢查是否已經綁定過 + logger.info(f"🎙️ 用戶 {user_id} 觸發語音綁定流程") + + # 檢查使用者是否已經綁定過 speaker_label + from core.database import get_user_by_id + try: + user_data = await get_user_by_id(user_id) + if user_data and user_data.get("speaker_label"): + # 已經綁定過了 + existing_label = user_data.get("speaker_label") + logger.info(f"⚠️ 用戶 {user_id} 已綁定 speaker_label: {existing_label}") + + await websocket.send_json({ + "type": "bot_message", + "message": f"你已經綁定過語音了!目前的聲紋標籤是:{existing_label}。如果需要重新綁定,請聯繫管理員。", + "timestamp": time.time() + }) + + # 清理 FSM 狀態 + self.clear_state(user_id) + return True + except Exception as e: + logger.error(f"❌ 檢查使用者綁定狀態失敗: {e}") + await websocket.send_json({ + "type": "error", + "message": "系統錯誤,無法檢查綁定狀態" + }) + return True + + # 未綁定,繼續綁定流程 + logger.info(f"✅ 用戶 {user_id} 尚未綁定,啟動綁定流程") + + # 標記用戶進入語音綁定等待狀態 + if manager: + user_session = manager.get_client_info(user_id) or {} + user_session["voice_binding_pending"] = True + user_session["voice_binding_started_at"] = datetime.now() + manager.set_client_info(user_id, user_session) + + await websocket.send_json({ + "type": "bot_message", + "message": "好的,請錄製一段語音(約3-5秒),用於建立你的聲紋特徵。系統會自動識別並綁定到你的帳號。", + "timestamp": time.time() + }) + await websocket.send_json({ + "type": "voice_binding_ready", + "message": "請點擊錄音按鈕開始錄製" + }) + return True + + elif state == "AWAITING_NAME": + # 這個狀態已不再使用,因為我們改為直接錄音綁定 + pass + + return False + + def clear_state(self, user_id: str): + """清理用戶狀態""" + self.user_states.pop(user_id, None) + + def is_in_binding_flow(self, user_id: str) -> bool: + """檢查用戶是否在綁定流程中""" + return user_id in self.user_states + + +# 全局語音綁定狀態機實例 +voice_binding_fsm = VoiceBindingStateMachine()