diff --git a/20251227_16h50m55s_grim.png b/20251227_16h50m55s_grim.png new file mode 100644 index 00000000..f71f4c5d Binary files /dev/null and b/20251227_16h50m55s_grim.png differ diff --git a/=1.57.0 b/=1.57.0 new file mode 100644 index 00000000..e69de29b diff --git a/README.md b/README.md index 9c9d3599..049b1e80 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,7 @@ MCP servers are configured in `data/mcp_servers.json` and hot-reloaded via API. - **PDF tools** — extract text and metadata - **Monarch Money** — personal finance data and transactions - **Calculator & utilities** — housekeeping helpers +- Local servers run over streamable HTTP using `http_port`; remote servers use `http_url`. - The canonical list of bundled servers lives in `src/backend/mcp_servers/__init__.py` (`BUILTIN_MCP_SERVER_DEFINITIONS`). The FastAPI factory consumes that list to generate default entries with the same enable/disable defaults, so updating the diff --git a/calendar_screenshot.png b/calendar_screenshot.png new file mode 100644 index 00000000..8047e234 Binary files /dev/null and b/calendar_screenshot.png differ diff --git a/data/client_profiles/cli-default.json b/data/client_profiles/cli-default.json new file mode 100644 index 00000000..8febc1ef --- /dev/null +++ b/data/client_profiles/cli-default.json @@ -0,0 +1,17 @@ +{ + "description": "Default CLI profile with all servers enabled", + "enabled_servers": [ + "custom-calendar", + "custom-gdrive", + "custom-gmail", + "custom-pdf", + "housekeeping", + "local-calculator", + "monarch-money", + "notes", + "playwright", + "shell-control", + "spotify" + ], + "profile_id": "cli-default" +} diff --git a/data/client_profiles/cli-minimal.json b/data/client_profiles/cli-minimal.json new file mode 100644 index 00000000..863a2961 --- /dev/null +++ b/data/client_profiles/cli-minimal.json @@ -0,0 +1,7 @@ +{ + "description": "Minimal CLI profile with only shell control", + "enabled_servers": [ + "shell-control" + ], + "profile_id": "cli-minimal" +} diff --git a/data/client_profiles/kiosk.json b/data/client_profiles/kiosk.json new file mode 100644 index 00000000..ebec3db5 --- /dev/null +++ b/data/client_profiles/kiosk.json @@ -0,0 +1,11 @@ +{ + "description": "Kiosk profile for voice assistant", + "enabled_servers": [ + "custom-calendar", + "custom-gmail", + "housekeeping", + "notes", + "spotify" + ], + "profile_id": "kiosk" +} diff --git a/data/client_profiles/svelte.json b/data/client_profiles/svelte.json new file mode 100644 index 00000000..3653d3ec --- /dev/null +++ b/data/client_profiles/svelte.json @@ -0,0 +1,16 @@ +{ + "description": "Default Svelte frontend profile with all servers enabled", + "enabled_servers": [ + "custom-calendar", + "custom-gdrive", + "custom-gmail", + "custom-pdf", + "housekeeping", + "local-calculator", + "monarch-money", + "notes", + "shell-control", + "spotify" + ], + "profile_id": "svelte" +} diff --git a/data/mcp_servers.json b/data/mcp_servers.json index 1961e28d..47eca58f 100644 --- a/data/mcp_servers.json +++ b/data/mcp_servers.json @@ -1,95 +1,163 @@ { "servers": [ { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": { - "HOST_PROFILE_ID": "xps13", - "HOST_ROOT_PATH": "/home/human/GoogleDrive/host_profiles" + "client_enabled": { + "backend": false, + "cli": true, + "kiosk": true, + "svelte": true }, - "id": "shell-control", - "module": "backend.mcp_servers.shell_control_server", + "contexts": [], + "enabled": false, + "env": {}, + "http_port": 9003, + "id": "local-calculator", + "module": "backend.mcp_servers.calculator_server", "tool_overrides": {} }, { + "client_enabled": { + "backend": false, + "cli": true, + "kiosk": true, + "svelte": true + }, "contexts": [], - "disabled_tools": [], - "enabled": false, + "enabled": true, "env": {}, - "id": "notes", - "module": "backend.mcp_servers.notes_server", + "http_port": 9002, + "id": "housekeeping", + "module": "backend.mcp_servers.housekeeping_server", "tool_overrides": {} }, { + "client_enabled": { + "cli": true, + "kiosk": true, + "svelte": false + }, "contexts": [], "enabled": false, "env": {}, - "id": "monarch-money", - "module": "backend.mcp_servers.monarch_server", + "http_port": 9004, + "id": "custom-calendar", + "module": "backend.mcp_servers.calendar_server", "tool_overrides": {} }, { + "client_enabled": { + "cli": true, + "kiosk": true, + "svelte": true + }, "contexts": [], - "disabled_tools": [], "enabled": false, "env": {}, + "http_port": 9005, "id": "custom-gmail", "module": "backend.mcp_servers.gmail_server", "tool_overrides": {} }, { + "client_enabled": { + "cli": true, + "kiosk": true, + "svelte": true + }, "contexts": [], - "disabled_tools": [], "enabled": false, "env": {}, + "http_port": 9006, "id": "custom-gdrive", "module": "backend.mcp_servers.gdrive_server", "tool_overrides": {} }, { + "client_enabled": { + "cli": true, + "kiosk": true, + "svelte": true + }, "contexts": [], - "disabled_tools": [], "enabled": false, "env": {}, + "http_port": 9007, "id": "custom-pdf", "module": "backend.mcp_servers.pdf_server", "tool_overrides": {} }, { + "client_enabled": { + "cli": true, + "kiosk": true, + "svelte": true + }, "contexts": [], - "disabled_tools": [], "enabled": false, "env": {}, - "id": "custom-calendar", - "module": "backend.mcp_servers.calendar_server", + "http_port": 9008, + "id": "monarch-money", + "module": "backend.mcp_servers.monarch_server", "tool_overrides": {} }, { + "client_enabled": { + "cli": true, + "kiosk": true, + "svelte": true + }, "contexts": [], - "disabled_tools": [], - "enabled": true, + "enabled": false, "env": {}, - "id": "spotify", - "module": "backend.mcp_servers.spotify_server", + "http_port": 9009, + "id": "notes", + "module": "backend.mcp_servers.notes_server", + "tool_overrides": {} + }, + { + "client_enabled": { + "backend": true, + "cli": true, + "kiosk": true, + "svelte": true + }, + "contexts": [], + "enabled": true, + "env": { + "HOST_PROFILE_ID": "xps13", + "HOST_ROOT_PATH": "/home/human/GoogleDrive/host_profiles" + }, + "http_port": 9001, + "id": "shell-control", + "module": "backend.mcp_servers.shell_control_server", "tool_overrides": {} }, { + "client_enabled": { + "cli": true, + "kiosk": true, + "svelte": true + }, "contexts": [], - "disabled_tools": [], "enabled": false, "env": {}, - "id": "local-calculator", - "module": "backend.mcp_servers.calculator_server", + "http_port": 9010, + "id": "spotify", + "module": "backend.mcp_servers.spotify_server", "tool_overrides": {} }, { + "client_enabled": { + "cli": true, + "kiosk": true, + "svelte": true + }, "contexts": [], - "disabled_tools": [], - "enabled": false, + "enabled": true, "env": {}, - "id": "housekeeping", - "module": "backend.mcp_servers.housekeeping_server", + "http_port": 9011, + "id": "playwright", + "module": "backend.mcp_servers.playwright_server", "tool_overrides": {} } ] diff --git a/data/mcp_servers.json.backup b/data/mcp_servers.json.backup new file mode 100644 index 00000000..f801cc56 --- /dev/null +++ b/data/mcp_servers.json.backup @@ -0,0 +1,116 @@ +{ + "servers": [ + { + "contexts": [], + "disabled_tools": [], + "enabled": false, + "env": { + "HOST_PROFILE_ID": "xps13", + "HOST_ROOT_PATH": "/home/human/GoogleDrive/host_profiles" + }, + "frontend_enabled": true, + "id": "shell-control", + "kiosk_enabled": true, + "module": "backend.mcp_servers.shell_control_server", + "tool_overrides": {} + }, + { + "contexts": [], + "disabled_tools": [], + "enabled": false, + "env": {}, + "frontend_enabled": false, + "id": "notes", + "kiosk_enabled": true, + "module": "backend.mcp_servers.notes_server", + "tool_overrides": {} + }, + { + "contexts": [], + "enabled": false, + "env": {}, + "frontend_enabled": true, + "id": "monarch-money", + "kiosk_enabled": false, + "module": "backend.mcp_servers.monarch_server", + "tool_overrides": {} + }, + { + "contexts": [], + "disabled_tools": [], + "enabled": false, + "env": {}, + "frontend_enabled": true, + "id": "custom-gmail", + "kiosk_enabled": false, + "module": "backend.mcp_servers.gmail_server", + "tool_overrides": {} + }, + { + "contexts": [], + "disabled_tools": [], + "enabled": false, + "env": {}, + "frontend_enabled": false, + "id": "custom-gdrive", + "kiosk_enabled": true, + "module": "backend.mcp_servers.gdrive_server", + "tool_overrides": {} + }, + { + "contexts": [], + "disabled_tools": [], + "enabled": false, + "env": {}, + "frontend_enabled": true, + "id": "custom-pdf", + "kiosk_enabled": false, + "module": "backend.mcp_servers.pdf_server", + "tool_overrides": {} + }, + { + "contexts": [], + "disabled_tools": [], + "enabled": false, + "env": {}, + "frontend_enabled": true, + "id": "custom-calendar", + "kiosk_enabled": false, + "module": "backend.mcp_servers.calendar_server", + "tool_overrides": {} + }, + { + "contexts": [], + "disabled_tools": [], + "enabled": false, + "env": {}, + "frontend_enabled": true, + "id": "spotify", + "kiosk_enabled": false, + "module": "backend.mcp_servers.spotify_server", + "tool_overrides": {} + }, + { + "contexts": [], + "disabled_tools": [], + "enabled": false, + "env": {}, + "frontend_enabled": false, + "id": "local-calculator", + "kiosk_enabled": true, + "module": "backend.mcp_servers.calculator_server", + "tool_overrides": {} + }, + { + "contexts": [], + "disabled_tools": [], + "enabled": true, + "env": {}, + "frontend_enabled": true, + "id": "housekeeping", + "kiosk_enabled": false, + "module": "backend.mcp_servers.housekeeping_server", + "tool_overrides": {} + } + ] +} diff --git a/data/presets.json b/data/presets.json deleted file mode 100644 index 242a2792..00000000 --- a/data/presets.json +++ /dev/null @@ -1,922 +0,0 @@ -{ - "presets": [ - { - "created_at": "2025-10-10T13:11:30.688032Z", - "is_default": false, - "mcp_servers": [ - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "local-calculator", - "module": "backend.mcp_servers.calculator_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "housekeeping", - "module": "backend.mcp_servers.housekeeping_server", - "tool_overrides": {} - } - ], - "model": "anthropic/claude-3-opus", - "name": "new", - "system_prompt": "You are a helpful but funny assistant", - "updated_at": "2025-10-18T15:03:46.468919Z" - }, - { - "created_at": "2025-10-10T13:55:19.113657Z", - "is_default": false, - "mcp_servers": [ - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "notes", - "module": "backend.mcp_servers.notes_server", - "tool_overrides": {} - }, - { - "contexts": [], - "enabled": false, - "env": {}, - "id": "monarch-money", - "module": "backend.mcp_servers.monarch_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-gmail", - "module": "backend.mcp_servers.gmail_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-gdrive", - "module": "backend.mcp_servers.gdrive_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-pdf", - "module": "backend.mcp_servers.pdf_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-calendar", - "module": "backend.mcp_servers.calendar_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "spotify", - "module": "backend.mcp_servers.spotify_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "local-calculator", - "module": "backend.mcp_servers.calculator_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "housekeeping", - "module": "backend.mcp_servers.housekeeping_server", - "tool_overrides": {} - } - ], - "model": "openai/gpt-4o:extended", - "model_filters": { - "series": { - "exclude": [], - "include": [ - "gpt" - ] - }, - "sort": "newness" - }, - "name": "open", - "system_prompt": "You are a helpful but funny assistant", - "updated_at": "2025-11-26T10:38:10.911612Z" - }, - { - "created_at": "2025-10-24T01:44:35.184462Z", - "is_default": false, - "mcp_servers": [ - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "custom-gmail", - "module": "backend.mcp_servers.gmail_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "custom-gdrive", - "module": "backend.mcp_servers.gdrive_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "custom-pdf", - "module": "backend.mcp_servers.pdf_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "custom-calendar", - "module": "backend.mcp_servers.calendar_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "local-calculator", - "module": "backend.mcp_servers.calculator_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "housekeeping", - "module": "backend.mcp_servers.housekeeping_server", - "tool_overrides": {} - } - ], - "model": "google/gemini-2.5-flash-image", - "name": "image", - "suggestions": [ - { - "label": "apple", - "text": "make an image of an apple" - } - ], - "system_prompt": "You are a helpful but funny assistant. You have access to tools. User is Jack and live in Olrlando, Fl", - "updated_at": "2025-11-05T05:36:19.318281Z" - }, - { - "created_at": "2025-11-19T05:52:17.581157Z", - "is_default": false, - "mcp_servers": [ - { - "contexts": [], - "enabled": true, - "env": {}, - "id": "monarch-money", - "module": "backend.mcp_servers.monarch_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-gmail", - "module": "backend.mcp_servers.gmail_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "custom-gdrive", - "module": "backend.mcp_servers.gdrive_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-pdf", - "module": "backend.mcp_servers.pdf_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-calendar", - "module": "backend.mcp_servers.calendar_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "local-calculator", - "module": "backend.mcp_servers.calculator_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "housekeeping", - "module": "backend.mcp_servers.housekeeping_server", - "tool_overrides": {} - } - ], - "model": "openai/gpt-5.1", - "model_filters": { - "series": { - "exclude": [], - "include": [ - "gpt" - ] - }, - "sort": "newness" - }, - "name": "finances", - "suggestions": [ - { - "label": "financial aggregates", - "text": "Give me a breakdown of my financial aggregates" - }, - { - "label": "This month's savings", - "text": "How much have I saved this month" - }, - { - "label": "This month spending", - "text": "show spending by category this month" - }, - { - "label": "30 days cashflow summary", - "text": "Show me a summary of my cashflow for the last 30 days" - } - ], - "supports_tools": true, - "system_prompt": "You are a helpful but funny assistant. You have access to tools. Users are in a family, Dad is Jack and Mom is Sanja. Be inquisitive to help financially.", - "updated_at": "2025-11-20T03:47:37.740434Z" - }, - { - "created_at": "2025-11-22T06:23:53.919638Z", - "is_default": false, - "mcp_servers": [ - { - "contexts": [], - "enabled": false, - "env": {}, - "id": "monarch-money", - "module": "backend.mcp_servers.monarch_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-gmail", - "module": "backend.mcp_servers.gmail_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-gdrive", - "module": "backend.mcp_servers.gdrive_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-pdf", - "module": "backend.mcp_servers.pdf_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-calendar", - "module": "backend.mcp_servers.calendar_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "spotify", - "module": "backend.mcp_servers.spotify_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "local-calculator", - "module": "backend.mcp_servers.calculator_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "housekeeping", - "module": "backend.mcp_servers.housekeeping_server", - "tool_overrides": {} - } - ], - "model": "openai/gpt-5.1", - "model_filters": { - "sort": "newness" - }, - "name": "only spotify", - "suggestions": [ - { - "label": "song hx", - "text": "tell me the last 10 songs I listened to" - } - ], - "supports_tools": true, - "system_prompt": "You are a helpful but funny assistant. You are an expert in music and giving advice as a dj and suggesting tracks to play.", - "updated_at": "2025-11-26T02:12:00.830730Z" - }, - { - "created_at": "2025-11-23T01:42:45.393832Z", - "is_default": false, - "mcp_servers": [ - { - "contexts": [], - "enabled": false, - "env": {}, - "id": "monarch-money", - "module": "backend.mcp_servers.monarch_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-gmail", - "module": "backend.mcp_servers.gmail_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "custom-gdrive", - "module": "backend.mcp_servers.gdrive_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "custom-pdf", - "module": "backend.mcp_servers.pdf_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-calendar", - "module": "backend.mcp_servers.calendar_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "spotify", - "module": "backend.mcp_servers.spotify_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "local-calculator", - "module": "backend.mcp_servers.calculator_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "housekeeping", - "module": "backend.mcp_servers.housekeeping_server", - "tool_overrides": {} - } - ], - "model": "openai/gpt-5.1", - "model_filters": { - "series": { - "exclude": [], - "include": [ - "gpt" - ] - }, - "sort": "newness" - }, - "name": "medical", - "supports_tools": true, - "system_prompt": "You are a helpful assistant that helps with medical issues for a pharmacist working in a pediatric and women's hospital", - "updated_at": "2025-11-26T01:45:11.282076Z" - }, - { - "created_at": "2025-11-26T10:39:50.398659Z", - "is_default": false, - "mcp_servers": [ - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "notes", - "module": "backend.mcp_servers.notes_server", - "tool_overrides": {} - }, - { - "contexts": [], - "enabled": false, - "env": {}, - "id": "monarch-money", - "module": "backend.mcp_servers.monarch_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-gmail", - "module": "backend.mcp_servers.gmail_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-gdrive", - "module": "backend.mcp_servers.gdrive_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-pdf", - "module": "backend.mcp_servers.pdf_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-calendar", - "module": "backend.mcp_servers.calendar_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "spotify", - "module": "backend.mcp_servers.spotify_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "local-calculator", - "module": "backend.mcp_servers.calculator_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "housekeeping", - "module": "backend.mcp_servers.housekeeping_server", - "tool_overrides": {} - } - ], - "model": "openai/gpt-4o:extended", - "model_filters": { - "series": { - "exclude": [], - "include": [ - "gpt" - ] - }, - "sort": "newness" - }, - "name": "notes", - "system_prompt": "You are a helpful but funny assistant", - "updated_at": "2025-11-26T10:39:50.398659Z" - }, - { - "created_at": "2025-11-29T17:57:16.844539Z", - "is_default": false, - "mcp_servers": [ - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "notes", - "module": "backend.mcp_servers.notes_server", - "tool_overrides": {} - }, - { - "contexts": [], - "enabled": false, - "env": {}, - "id": "monarch-money", - "module": "backend.mcp_servers.monarch_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-gmail", - "module": "backend.mcp_servers.gmail_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-gdrive", - "module": "backend.mcp_servers.gdrive_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-pdf", - "module": "backend.mcp_servers.pdf_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "custom-calendar", - "module": "backend.mcp_servers.calendar_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "spotify", - "module": "backend.mcp_servers.spotify_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "local-calculator", - "module": "backend.mcp_servers.calculator_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "housekeeping", - "module": "backend.mcp_servers.housekeeping_server", - "tool_overrides": {} - } - ], - "model": "openai/gpt-5.1", - "model_filters": { - "series": { - "exclude": [], - "include": [ - "gpt" - ] - }, - "sort": "newness" - }, - "name": "calendar only", - "supports_tools": true, - "system_prompt": "You are a helpful but funny assistant", - "updated_at": "2025-11-29T17:57:49.874961Z" - }, - { - "created_at": "2025-12-02T17:03:20.940841Z", - "is_default": false, - "mcp_servers": [ - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": { - "HOST_PROFILE_ID": "xps13", - "HOST_ROOT_PATH": "/home/human/GoogleDrive/host_profiles" - }, - "id": "shell-control", - "module": "backend.mcp_servers.shell_control_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "notes", - "module": "backend.mcp_servers.notes_server", - "tool_overrides": {} - }, - { - "contexts": [], - "enabled": false, - "env": {}, - "id": "monarch-money", - "module": "backend.mcp_servers.monarch_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-gmail", - "module": "backend.mcp_servers.gmail_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-gdrive", - "module": "backend.mcp_servers.gdrive_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-pdf", - "module": "backend.mcp_servers.pdf_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-calendar", - "module": "backend.mcp_servers.calendar_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "spotify", - "module": "backend.mcp_servers.spotify_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "local-calculator", - "module": "backend.mcp_servers.calculator_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "housekeeping", - "module": "backend.mcp_servers.housekeeping_server", - "tool_overrides": {} - } - ], - "model": "openai/gpt-5.1", - "model_filters": { - "series": { - "exclude": [], - "include": [ - "gpt" - ] - }, - "sort": "newness" - }, - "name": "shell executor", - "suggestions": [ - { - "label": "update profile", - "text": "lets update host profile" - } - ], - "supports_tools": true, - "system_prompt": "You are a funny but helpful assistant with shell access to an Arch Linux system.\n\nAll tasks are delegated to you to control the system.\n\n1. Explain what you are doing\n\n2. Notify the user if it will take a long time\n\n3. If you encounter an error, explain what went wrong and what options we have", - "updated_at": "2025-12-06T00:20:10.734334Z" - }, - { - "created_at": "2025-12-06T00:45:00Z", - "is_default": true, - "mcp_servers": [ - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": { - "HOST_PROFILE_ID": "xps13", - "HOST_ROOT_PATH": "/home/human/GoogleDrive/host_profiles" - }, - "id": "shell-control", - "module": "backend.mcp_servers.shell_control_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "notes", - "module": "backend.mcp_servers.notes_server", - "tool_overrides": {} - }, - { - "contexts": [], - "enabled": false, - "env": {}, - "id": "monarch-money", - "module": "backend.mcp_servers.monarch_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-gmail", - "module": "backend.mcp_servers.gmail_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-gdrive", - "module": "backend.mcp_servers.gdrive_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-pdf", - "module": "backend.mcp_servers.pdf_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "custom-calendar", - "module": "backend.mcp_servers.calendar_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": true, - "env": {}, - "id": "spotify", - "module": "backend.mcp_servers.spotify_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "local-calculator", - "module": "backend.mcp_servers.calculator_server", - "tool_overrides": {} - }, - { - "contexts": [], - "disabled_tools": [], - "enabled": false, - "env": {}, - "id": "housekeeping", - "module": "backend.mcp_servers.housekeeping_server", - "tool_overrides": {} - } - ], - "model": "openai/gpt-5.1", - "model_filters": { - "series": { - "exclude": [], - "include": [ - "gpt" - ] - }, - "sort": "newness" - }, - "name": "action-first shell", - "suggestions": [ - { - "label": "update profile", - "text": "lets update host profile" - }, - { - "label": "bluetooth connect", - "text": "connect to my bluetooth headphones" - } - ], - "supports_tools": true, - "system_prompt": "You control the user's system via shell. Call host_get_profile first - it tells you everything: OS, paths, tools, and quirks.\n\nPRINCIPLES:\n1. ACT, don't instruct. Execute commands directly. Never tell the user to click, navigate, or check something themselves - do it yourself.\n2. FETCH, don't defer. If answering requires live data (weather, IPs, system state, etc.), get it via shell - don't say you lack access.\n3. READ before WRITE. Query current state before changing anything. Never assume.\n4. VERIFY outcomes. Don't trust exit codes alone - check that the expected result actually exists.\n5. BACKUP before destructive edits. Verify the backup exists before proceeding.\n6. BRIEF STATUS. When calling tools, include a short sentence explaining what you're doing.\n\nWHEN THINGS FAIL:\n- Read both stdout and stderr carefully - they usually explain why.\n- Explain the root cause briefly.\n- Offer what YOU can try next (retry, workaround, different approach).\n- If you need user input (e.g., password, confirmation), ask once and wait - do not dump instructions.\n\nWorkflow: get profile \u2192 query state \u2192 act \u2192 verify \u2192 report.", - "updated_at": "2025-12-07T02:21:05.668923Z" - } - ] -} diff --git a/data/suggestions.json b/data/suggestions.json index 79bc5b88..ba8b0ffc 100644 --- a/data/suggestions.json +++ b/data/suggestions.json @@ -1,12 +1,3 @@ { - "suggestions": [ - { - "label": "update profile", - "text": "lets update host profile" - }, - { - "label": "bluetooth connect", - "text": "connect to my bluetooth headphones" - } - ] + "suggestions": [] } diff --git a/docs/HTTP_MCP_SERVERS.md b/docs/HTTP_MCP_SERVERS.md index db6cae6b..51db3e9d 100644 --- a/docs/HTTP_MCP_SERVERS.md +++ b/docs/HTTP_MCP_SERVERS.md @@ -1,14 +1,13 @@ # HTTP MCP Servers Configuration Guide -This document explains how to configure and use HTTP/SSE-based MCP servers in addition to the standard Python module-based servers. +This document explains how to configure and use HTTP/SSE-based MCP servers, including local module launches and remote endpoints. ## Overview -The MCP client supports three transport methods: +The backend now uses **streamable HTTP** for all MCP servers. You can either: -1. **Python Module** - Launch MCP server as a Python module (default) -2. **Custom Command** - Launch MCP server with arbitrary command -3. **HTTP/SSE** - Connect to remote HTTP MCP server via Server-Sent Events +1. **Local module/command servers** - Launched by the backend with an assigned `http_port` +2. **Remote HTTP/SSE servers** - Connected via `http_url` HTTP transport is ideal for: - Remote MCP servers running on different machines @@ -19,9 +18,29 @@ HTTP transport is ideal for: ## Configuration -### Basic HTTP Server Configuration +### Local Module Server (HTTP) -Add an HTTP server to your `data/mcp_servers.json`: +Launch a bundled Python module over HTTP by supplying `http_port`: + +```json +{ + "servers": [ + { + "id": "housekeeping", + "module": "backend.mcp_servers.housekeeping_server", + "http_port": 9002, + "enabled": true + } + ] +} +``` + +The backend starts the module with: +`python -m --transport streamable-http --host 127.0.0.1 --port ` + +### Basic HTTP Server Configuration (Remote) + +Add a remote HTTP server to your `data/mcp_servers.json`: ```json { @@ -113,7 +132,7 @@ If a connection takes longer than 30 seconds, it will fail with a timeout error. ### Reconnection Logic -HTTP servers support automatic reconnection (unlike subprocess-based servers): +HTTP servers support automatic reconnection: - **Max Attempts**: 3 reconnection attempts - **Delay**: 2 seconds between attempts diff --git a/docs/SCHEMA_GENERATION_EXPLAINED.md b/docs/SCHEMA_GENERATION_EXPLAINED.md index ee1e9ee2..4299b6cb 100644 --- a/docs/SCHEMA_GENERATION_EXPLAINED.md +++ b/docs/SCHEMA_GENERATION_EXPLAINED.md @@ -6,7 +6,7 @@ ```python # In src/backend/mcp_servers/calendar_server.py -from mcp.server.fastmcp import FastMCP +from fastmcp import FastMCP DEFAULT_USER_EMAIL = "jck411@gmail.com" mcp: FastMCP = FastMCP("custom-calendar") diff --git a/frontend-cli/README.md b/frontend-cli/README.md new file mode 100644 index 00000000..01fb3470 --- /dev/null +++ b/frontend-cli/README.md @@ -0,0 +1,61 @@ +# Shell Chat CLI + +A rich terminal client for Backend_FastAPI that provides the same chat capabilities as the web frontends. + +## Features + +- **Streaming responses** via Server-Sent Events (SSE) +- **Session persistence** - conversations are cached locally +- **Preset management** - switch between different configurations +- **MCP tool control** - enable/disable MCP servers on the fly +- **Rich markdown rendering** in the terminal + +## Usage + +```bash +# Connect to localhost:8000 (default) +shell-chat + +# Connect to a remote server +shell-chat --server http://pi:8000 + +# Apply a preset on startup +shell-chat --preset coding +``` + +## Commands + +| Command | Description | +|---------|-------------| +| `/help` | Show available commands | +| `/clear` | Clear session (start new conversation) | +| `/model` | Show current model | +| `/model ` | Set model (e.g., `/model anthropic/claude-3-opus`) | +| `/presets` | List available presets | +| `/preset ` | Apply a preset | +| `/tools` | List MCP servers and status | +| `/tools on` | Enable an MCP server | +| `/tools off` | Disable an MCP server | +| `/system` | Show system prompt | +| `/quit` | Exit shell-chat | + +## Shortcuts + +- `Ctrl+C` - Cancel current request +- `Ctrl+D` - Exit shell-chat + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `SHELLCHAT_SERVER` | Default server URL (alternative to `--server` flag) | + +## Client Settings + +The CLI uses the unified client settings API at `/api/clients/cli/`. Settings are stored on the backend in: + +``` +src/backend/data/clients/cli/ +├── llm.json # LLM configuration +└── presets.json # Preset configurations +``` diff --git a/frontend-cli/__init__.py b/frontend-cli/__init__.py new file mode 100644 index 00000000..f5cd53b2 --- /dev/null +++ b/frontend-cli/__init__.py @@ -0,0 +1,2 @@ +# Frontend CLI for Backend_FastAPI +# Terminal client that connects to the backend via HTTP/SSE diff --git a/src/backend/cli/shell_chat.py b/frontend-cli/shell_chat.py similarity index 79% rename from src/backend/cli/shell_chat.py rename to frontend-cli/shell_chat.py index 0582afd3..5eb0d8d4 100644 --- a/src/backend/cli/shell_chat.py +++ b/frontend-cli/shell_chat.py @@ -38,9 +38,13 @@ class ShellChat: """Terminal chat client for Backend_FastAPI.""" - def __init__(self, server_url: str, preset: Optional[str] = None): + CLIENT_ID = "cli" # Client identifier for settings isolation + + def __init__(self, server_url: str, preset: Optional[str] = None, profile: Optional[str] = None): self.server_url = server_url.rstrip("/") + self.client_api = f"{self.server_url}/api/clients/{self.CLIENT_ID}" self.preset = preset + self.profile = profile or os.environ.get("SHELL_CHAT_PROFILE", "cli-default") self.session_id: Optional[str] = None self.console = Console() self.running = True @@ -87,8 +91,14 @@ async def _check_health(self) -> bool: if resp.status_code == 200: data = resp.json() model = data.get("active_model", "unknown") + try: + llm_resp = await client.get(f"{self.client_api}/llm") + if llm_resp.status_code == 200: + model = llm_resp.json().get("model", model) + except Exception: + pass self.console.print( - f"[dim]Connected to backend. Model: {model}[/dim]" + f"[dim]Connected to backend. CLI model: {model}[/dim]" ) return True except Exception as e: @@ -97,19 +107,37 @@ async def _check_health(self) -> bool: ) return False - async def _apply_preset(self, name: str) -> bool: - """Apply a preset configuration.""" + async def _apply_preset(self, index_or_name: str) -> bool: + """Apply a preset configuration by index.""" try: + # Try to parse as index first + try: + index = int(index_or_name) + except ValueError: + # If not an index, search by name + presets = await self._get_presets() + if presets is None: + return False + index = next( + (i for i, p in enumerate(presets) if p.get("name", "").lower() == index_or_name.lower()), + -1, + ) + if index < 0: + self.console.print( + f"[error]Preset '{index_or_name}' not found[/error]", style=ERROR_STYLE + ) + return False + async with httpx.AsyncClient(timeout=10.0) as client: - resp = await client.post(f"{self.server_url}/api/presets/{name}/apply") + resp = await client.post(f"{self.client_api}/presets/{index}/activate") if resp.status_code == 200: self.console.print( - f"[info]Applied preset: {name}[/info]", style=INFO_STYLE + f"[info]Applied preset: {index_or_name}[/info]", style=INFO_STYLE ) return True elif resp.status_code == 404: self.console.print( - f"[error]Preset '{name}' not found[/error]", style=ERROR_STYLE + f"[error]Preset '{index_or_name}' not found[/error]", style=ERROR_STYLE ) else: self.console.print( @@ -122,23 +150,37 @@ async def _apply_preset(self, name: str) -> bool: ) return False + async def _get_presets(self) -> Optional[list]: + """Get list of presets.""" + try: + async with httpx.AsyncClient(timeout=10.0) as client: + resp = await client.get(f"{self.client_api}/presets") + if resp.status_code == 200: + data = resp.json() + return data.get("presets", []) + except Exception: + pass + return None + async def _list_presets(self) -> None: """List available presets.""" try: async with httpx.AsyncClient(timeout=10.0) as client: - resp = await client.get(f"{self.server_url}/api/presets/") + resp = await client.get(f"{self.client_api}/presets") if resp.status_code == 200: - presets = resp.json() + data = resp.json() + presets = data.get("presets", []) + active_index = data.get("active_index") if not presets: self.console.print("[dim]No presets configured[/dim]") return self.console.print("\n[bold]Available Presets:[/bold]") - for p in presets: + for i, p in enumerate(presets): name = p.get("name", "?") - model = p.get("model", "?") - is_default = p.get("is_default", False) - marker = " [default]" if is_default else "" - self.console.print(f" • {name}{marker} - {model}") + llm = p.get("llm", {}) + model = llm.get("model", "?") + marker = " [active]" if i == active_index else "" + self.console.print(f" {i}. {name}{marker} - {model}") self.console.print() except Exception as e: self.console.print( @@ -149,7 +191,7 @@ async def _show_model(self) -> None: """Show current model.""" try: async with httpx.AsyncClient(timeout=10.0) as client: - resp = await client.get(f"{self.server_url}/api/settings/model") + resp = await client.get(f"{self.client_api}/llm") if resp.status_code == 200: data = resp.json() model = data.get("model", "unknown") @@ -166,8 +208,8 @@ async def _set_model(self, model_id: str) -> None: try: async with httpx.AsyncClient(timeout=10.0) as client: resp = await client.put( - f"{self.server_url}/api/settings/model", - json={"model_id": model_id}, + f"{self.client_api}/llm", + json={"model": model_id}, ) if resp.status_code == 200: self.console.print( @@ -243,18 +285,19 @@ async def _show_system_prompt(self) -> None: """Show current system prompt.""" try: async with httpx.AsyncClient(timeout=10.0) as client: - resp = await client.get(f"{self.server_url}/api/settings/system-prompt") + resp = await client.get(f"{self.client_api}/llm") if resp.status_code == 200: data = resp.json() - prompt = data.get("content", "") + prompt = data.get("system_prompt", "") self.console.print( - Panel(prompt, title="System Prompt", border_style="dim") + Panel(prompt or "(no system prompt set)", title="System Prompt", border_style="dim") ) except Exception as e: self.console.print( f"[error]Failed to get system prompt: {e}[/error]", style=ERROR_STYLE ) + def _show_help(self) -> None: """Show available commands.""" help_text = """ @@ -330,12 +373,17 @@ async def _handle_command(self, cmd: str) -> bool: await self._show_system_prompt() return True + return False async def _stream_chat(self, message: str) -> None: """Send message and stream response via SSE.""" payload: dict[str, Any] = { "messages": [{"role": "user", "content": message}], + "metadata": { + "client_id": self.CLIENT_ID, + "profile_id": self.profile, + }, } if self.session_id: payload["session_id"] = self.session_id @@ -422,6 +470,24 @@ async def _stream_chat(self, message: str) -> None: tool_panels.append( f"✓ {name}: {result[:100]}" ) + elif status == "hop_limit": + # Show pause message - LLM hit tool call limit + hop_msg = parsed.get("message", "") + hop_count = parsed.get("hop_count", 0) + limit = parsed.get("limit", 20) + live.update( + Text( + f"⏸️ {hop_msg}", + style="bold cyan", + ) + ) + # Also print it permanently so user sees it + self.console.print( + f"\n[bold cyan]⏸️ Completed {hop_count} tool calls (limit: {limit})[/bold cyan]" + ) + self.console.print( + "[cyan]Reply 'continue' or 'yes' to keep going, or ask something else.[/cyan]\n" + ) elif event_type == "metadata": # Could show usage stats here @@ -511,6 +577,11 @@ def main() -> None: default=None, help="Apply a preset on startup", ) + parser.add_argument( + "--profile", + default=os.environ.get("SHELL_CHAT_PROFILE"), + help="Client profile for MCP server filtering (default: cli-default or $SHELL_CHAT_PROFILE)", + ) args = parser.parse_args() @@ -523,7 +594,7 @@ def signal_handler(sig, frame): signal.signal(signal.SIGTERM, signal_handler) # Run the chat - chat = ShellChat(server_url=args.server, preset=args.preset) + chat = ShellChat(server_url=args.server, preset=args.preset, profile=args.profile) try: asyncio.run(chat.run()) except KeyboardInterrupt: diff --git a/frontend-kiosk/index.html b/frontend-kiosk/index.html new file mode 100644 index 00000000..06642f34 --- /dev/null +++ b/frontend-kiosk/index.html @@ -0,0 +1,13 @@ + + + + + + + Kiosk + + +
+ + + diff --git a/frontend-kiosk/package-lock.json b/frontend-kiosk/package-lock.json new file mode 100644 index 00000000..696a1656 --- /dev/null +++ b/frontend-kiosk/package-lock.json @@ -0,0 +1,2759 @@ +{ + "name": "frontend-kiosk", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend-kiosk", + "version": "0.0.0", + "dependencies": { + "framer-motion": "^11.15.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-use-websocket": "^4.9.0" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "vite": "^6.0.5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz", + "integrity": "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-use-websocket": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.13.0.tgz", + "integrity": "sha512-anMuVoV//g2N76Wxqvqjjo1X48r9Np3y1/gMl7arX84tAPXdy5R7sB5lO5hvCzQRYjqXwV8XMAiEBOUbyrZFrw==", + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend-kiosk/package.json b/frontend-kiosk/package.json new file mode 100644 index 00000000..47f59b65 --- /dev/null +++ b/frontend-kiosk/package.json @@ -0,0 +1,26 @@ +{ + "name": "frontend-kiosk", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "framer-motion": "^11.15.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-use-websocket": "^4.9.0" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "vite": "^6.0.5" + } +} diff --git a/frontend-kiosk/postcss.config.js b/frontend-kiosk/postcss.config.js new file mode 100644 index 00000000..d41ad635 --- /dev/null +++ b/frontend-kiosk/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend-kiosk/public/vite.svg b/frontend-kiosk/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/frontend-kiosk/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-kiosk/src/App.jsx b/frontend-kiosk/src/App.jsx new file mode 100644 index 00000000..99f13f1d --- /dev/null +++ b/frontend-kiosk/src/App.jsx @@ -0,0 +1,368 @@ +import { AnimatePresence, motion } from 'framer-motion'; +import { useEffect, useRef, useState } from 'react'; +import useWebSocket, { ReadyState } from 'react-use-websocket'; +import Clock from './components/Clock'; +import PhotoFrame from './components/PhotoFrame'; +import TranscriptionScreen from './components/TranscriptionScreen'; + +export default function App() { + // 0 = Clock, 1 = Photos, 2 = Transcription + const [currentScreen, setCurrentScreen] = useState(0); + + // WebSocket Connection - connects to backend + const wsUrl = `ws://${window.location.hostname}:8000/api/voice/connect?client_id=frontend_gui`; + + const [messages, setMessages] = useState([]); + const [liveTranscript, setLiveTranscript] = useState(""); + const [agentState, setAgentState] = useState("IDLE"); + const [toolStatus, setToolStatus] = useState(null); // { name: string, status: 'started' | 'finished' | 'error' } + const [idleReturnDelay, setIdleReturnDelay] = useState(10000); // Default 10s + const messagesEndRef = useRef(null); + + // Web Audio API for streaming TTS playback + // Using refs to persist across re-renders without triggering updates + const audioContextRef = useRef(null); + const nextPlayTimeRef = useRef(0); + const ttsSampleRateRef = useRef(24000); // Default to 24kHz (OpenAI default) + const isPlayingRef = useRef(false); + const hasStartedRef = useRef(false); + const scheduledSourcesRef = useRef([]); // Track scheduled audio sources for cancellation + + // Fetch UI settings on mount + useEffect(() => { + const fetchSettings = async () => { + try { + const response = await fetch(`http://${window.location.hostname}:8000/api/clients/kiosk/ui`); + if (response.ok) { + const data = await response.json(); + setIdleReturnDelay(data.idle_return_delay_ms); + } + } catch (e) { + console.error("Failed to fetch UI settings", e); + } + }; + fetchSettings(); + }, []); + + const { + sendMessage, + lastMessage, + readyState + } = useWebSocket(wsUrl, { + onOpen: () => console.log('WebSocket Connected'), + shouldReconnect: () => true, + reconnectAttempts: 10, + reconnectInterval: 3000, + }); + + /** + * Initialize or get the AudioContext. + * Must be called after user interaction (e.g., on first audio chunk). + */ + const getAudioContext = () => { + if (!audioContextRef.current || audioContextRef.current.state === 'closed') { + audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)({ + sampleRate: ttsSampleRateRef.current + }); + console.log(`Created AudioContext with sample rate: ${ttsSampleRateRef.current}`); + } + // Resume if suspended (required after user interaction in some browsers) + if (audioContextRef.current.state === 'suspended') { + audioContextRef.current.resume(); + } + return audioContextRef.current; + }; + + /** + * Play a PCM audio chunk immediately using Web Audio API. + * Schedules the audio buffer for gapless playback. + */ + const playAudioChunk = (base64Audio) => { + try { + const ctx = getAudioContext(); + + // Decode base64 to binary + const binaryString = atob(base64Audio); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + // Convert 16-bit PCM to Float32 for Web Audio API + const samples = new Int16Array(bytes.buffer); + const floatSamples = new Float32Array(samples.length); + for (let i = 0; i < samples.length; i++) { + floatSamples[i] = samples[i] / 32768.0; // Normalize to [-1, 1] + } + + // Create audio buffer + const audioBuffer = ctx.createBuffer(1, floatSamples.length, ttsSampleRateRef.current); + audioBuffer.getChannelData(0).set(floatSamples); + + // Create source node + const source = ctx.createBufferSource(); + source.buffer = audioBuffer; + source.connect(ctx.destination); + + // Calculate when to play this chunk (gapless scheduling) + const currentTime = ctx.currentTime; + const startTime = Math.max(nextPlayTimeRef.current, currentTime); + + // Schedule playback + source.start(startTime); + scheduledSourcesRef.current.push(source); + + // Update next play time for gapless playback + nextPlayTimeRef.current = startTime + audioBuffer.duration; + + // Send playback start event on first chunk + if (!hasStartedRef.current) { + hasStartedRef.current = true; + isPlayingRef.current = true; + console.log("🎵 Audio playback started (streaming)"); + sendMessage(JSON.stringify({ type: "tts_playback_start" })); + } + } catch (e) { + console.error("Failed to play audio chunk:", e); + } + }; + + /** + * Stop all audio playback immediately (for barge-in). + * Stops all scheduled sources and resets state. + */ + const stopAudio = () => { + console.log("🛑 Stopping TTS audio (barge-in)"); + + // Stop all scheduled audio sources + for (const source of scheduledSourcesRef.current) { + try { + source.stop(); + } catch (e) { + // Source may have already finished + } + } + scheduledSourcesRef.current = []; + + // Close and recreate audio context for clean slate + if (audioContextRef.current && audioContextRef.current.state !== 'closed') { + audioContextRef.current.close(); + audioContextRef.current = null; + } + + // Reset playback state + nextPlayTimeRef.current = 0; + + // Only send end event if we were playing + if (isPlayingRef.current || hasStartedRef.current) { + sendMessage(JSON.stringify({ type: "tts_playback_end" })); + isPlayingRef.current = false; + hasStartedRef.current = false; + } + }; + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages, liveTranscript]); + + useEffect(() => { + if (lastMessage !== null) { + try { + const data = JSON.parse(lastMessage.data); + if (data.type === 'transcript') { + if (data.is_final) { + setMessages(prev => [...prev, { role: 'user', text: data.text }]); + setLiveTranscript(""); + } else { + setLiveTranscript(data.text); + } + } else if (data.type === 'assistant_response_start') { + // Start of streaming response - add pending message + console.log('Streaming response starting'); + setMessages(prev => [...prev, { role: 'assistant', text: '', pending: true }]); + } else if (data.type === 'assistant_response_chunk') { + // Streaming text chunk - append to last assistant message + setMessages(prev => { + const updated = [...prev]; + const lastIdx = updated.length - 1; + if (lastIdx >= 0 && updated[lastIdx]?.role === 'assistant') { + updated[lastIdx] = { + ...updated[lastIdx], + text: updated[lastIdx].text + data.text + }; + } + return updated; + }); + } else if (data.type === 'assistant_response_end') { + // End of streaming - mark message as complete + console.log('Streaming response complete'); + setMessages(prev => { + const updated = [...prev]; + const lastIdx = updated.length - 1; + if (lastIdx >= 0 && updated[lastIdx]?.role === 'assistant') { + updated[lastIdx] = { + ...updated[lastIdx], + pending: false + }; + } + return updated; + }); + } else if (data.type === 'assistant_response') { + // Legacy: full response at once (backward compatibility) + console.log('Received assistant_response:', data.text); + setMessages(prev => [...prev, { role: 'assistant', text: data.text }]); + } else if (data.type === 'interrupt_tts') { + // Barge-in: stop TTS immediately + console.log('🛑 Received interrupt_tts - stopping audio playback'); + stopAudio(); + } else if (data.type === 'tts_audio_start') { + // Start of TTS audio stream - reset state and store sample rate + console.log('🎵 TTS stream starting, sample_rate:', data.sample_rate); + + // Reset state for new audio stream + nextPlayTimeRef.current = 0; + hasStartedRef.current = false; + isPlayingRef.current = false; + scheduledSourcesRef.current = []; + + // Store sample rate for AudioContext creation + ttsSampleRateRef.current = data.sample_rate || 24000; + + // Close existing context if sample rate changed + if (audioContextRef.current && audioContextRef.current.state !== 'closed') { + audioContextRef.current.close(); + audioContextRef.current = null; + } + } else if (data.type === 'tts_audio_chunk') { + // Play audio chunk IMMEDIATELY using Web Audio API + playAudioChunk(data.data); + } else if (data.type === 'tts_audio_end') { + // Audio stream complete - send playback end after all scheduled audio finishes + console.log('TTS stream complete'); + + // Calculate when all audio will finish + if (audioContextRef.current && hasStartedRef.current) { + const timeUntilDone = Math.max(0, nextPlayTimeRef.current - audioContextRef.current.currentTime); + console.log(`All audio scheduled, will complete in ${(timeUntilDone * 1000).toFixed(0)}ms`); + + // Send playback end event after all audio finishes + setTimeout(() => { + console.log("Audio playback finished"); + sendMessage(JSON.stringify({ type: "tts_playback_end" })); + isPlayingRef.current = false; + hasStartedRef.current = false; + scheduledSourcesRef.current = []; + }, timeUntilDone * 1000 + 100); // +100ms buffer + } + } else if (data.type === 'tts_audio_cancelled') { + // TTS was cancelled on backend - clean up + console.log('TTS cancelled by backend'); + stopAudio(); + } else if (data.type === 'tts_audio') { + // Legacy: single audio message (backward compatibility) + // Play it as a single chunk + console.log('Received TTS audio (legacy), playing immediately'); + playAudioChunk(data.data); + } else if (data.type === 'state') { + setAgentState(data.state); + + if (data.state === 'LISTENING' || data.state === 'THINKING' || data.state === 'SPEAKING') { + setCurrentScreen(2); // Auto-jump to transcription screen + // Do NOT clear messages + } + + // Clear tool status when transitioning to IDLE + if (data.state === 'IDLE') { + setToolStatus(null); + } + } else if (data.type === 'tool_status') { + // Handle tool status updates + console.log('Tool status:', data.status, data.name); + setToolStatus({ name: data.name, status: data.status }); + + // Auto-clear after tool finishes (with small delay for visibility) + if (data.status === 'finished' || data.status === 'error') { + setTimeout(() => setToolStatus(null), 2000); + } + } + } catch (e) { + console.error("Failed to parse WS message", e); + } + } + }, [lastMessage]); + + // Idle Timeout Logic - Return to clock after delay + useEffect(() => { + if (agentState === 'IDLE') { + const timer = setTimeout(() => { + setCurrentScreen(0); + setMessages([]); + }, idleReturnDelay); + + // Cleanup: cancel timer if state changes before delay completes + return () => clearTimeout(timer); + } + }, [agentState, idleReturnDelay]); + + const handleSwipe = (direction) => { + if (direction > 0) { + setCurrentScreen((prev) => (prev + 1) % 3); + } else { + setCurrentScreen((prev) => (prev === 0 ? 2 : prev - 1)); + } + }; + + const screens = [ + , + , + + ]; + + return ( +
+ {/* Web Audio API is used for TTS playback - no HTML audio element needed */} + + + + {screens[currentScreen]} + + + + {/* Page Indicators */} +
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+ + {/* Connection Indicator */} +
+
+ ); +} diff --git a/frontend-kiosk/src/components/Clock.jsx b/frontend-kiosk/src/components/Clock.jsx new file mode 100644 index 00000000..108811da --- /dev/null +++ b/frontend-kiosk/src/components/Clock.jsx @@ -0,0 +1,45 @@ +import { useEffect, useState } from 'react'; + +export default function Clock() { + const [time, setTime] = useState(new Date()); + + useEffect(() => { + const timer = setInterval(() => setTime(new Date()), 1000); + return () => clearInterval(timer); + }, []); + + const formatTime = (date) => { + return date.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + hour12: true, + }); + }; + + const formatDate = (date) => { + return date.toLocaleDateString('en-US', { + weekday: 'long', + month: 'long', + day: 'numeric', + }); + }; + + return ( +
+ {/* Time */} +
+ {formatTime(time)} +
+ + {/* Date */} +
+ {formatDate(time)} +
+ + {/* Subtle ambient glow */} +
+
+
+
+ ); +} diff --git a/frontend-kiosk/src/components/PhotoFrame.jsx b/frontend-kiosk/src/components/PhotoFrame.jsx new file mode 100644 index 00000000..d76d6312 --- /dev/null +++ b/frontend-kiosk/src/components/PhotoFrame.jsx @@ -0,0 +1,46 @@ +import { AnimatePresence, motion } from 'framer-motion'; +import { useEffect, useState } from 'react'; + +// Placeholder images - in production, fetch from API or local storage +const SAMPLE_PHOTOS = [ + 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200', + 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=1200', + 'https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=1200', + 'https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=1200', +]; + +export default function PhotoFrame() { + const [currentIndex, setCurrentIndex] = useState(0); + + useEffect(() => { + const timer = setInterval(() => { + setCurrentIndex((prev) => (prev + 1) % SAMPLE_PHOTOS.length); + }, 8000); // Change photo every 8 seconds + return () => clearInterval(timer); + }, []); + + return ( +
+ + + + + {/* Subtle vignette overlay */} +
+ + {/* Photo counter */} +
+ {currentIndex + 1} / {SAMPLE_PHOTOS.length} +
+
+ ); +} diff --git a/frontend-kiosk/src/components/TranscriptionScreen.jsx b/frontend-kiosk/src/components/TranscriptionScreen.jsx new file mode 100644 index 00000000..e85bd8ec --- /dev/null +++ b/frontend-kiosk/src/components/TranscriptionScreen.jsx @@ -0,0 +1,102 @@ +import { AnimatePresence } from 'framer-motion'; +import { useEffect } from 'react'; + +export default function TranscriptionScreen({ messages, liveTranscript, isListening, agentState, toolStatus, messagesEndRef }) { + // Auto-scroll effect + useEffect(() => { + if (messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [messages, liveTranscript]); + + return ( +
+ + {/* Tool Status Indicator (Top Left) */} + {toolStatus && ( +
+
+ 🔧 + + {toolStatus.status === 'started' ? 'Running' : toolStatus.status === 'finished' ? 'Done' : 'Error'}: {toolStatus.name} + + {toolStatus.status === 'started' && ( +
+ )} +
+
+ )} + + {/* Status Indicator (Top Right) */} +
+ + {isListening ? ( +
+
+ LISTENING +
+ ) : agentState !== 'IDLE' ? ( +
+
+ {agentState} +
+ ) : null} + +
+ + {/* Content Area */} +
+ +
{/* Spacer */} + +
+ {/* History Messages */} + {messages.map((msg, idx) => ( +
+
+

+ {msg.text} +

+
+
+ ))} + + {/* Live Transcript (Active Input) */} + {liveTranscript && ( +
+
+

+ {liveTranscript} +

+
+
+ )} + +
+
+
+ + {/* Empty State */} + {messages.length === 0 && !liveTranscript && ( +
+

Hi there.

+

Say "Hey Jarvis"

+
+ )} +
+ ); +} diff --git a/frontend-kiosk/src/index.css b/frontend-kiosk/src/index.css new file mode 100644 index 00000000..814758f4 --- /dev/null +++ b/frontend-kiosk/src/index.css @@ -0,0 +1,31 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer utilities { + .no-scrollbar::-webkit-scrollbar { + display: none; + } + .no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + } +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body, #root { + width: 100%; + height: 100%; + overflow: hidden; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + -webkit-font-smoothing: antialiased; + background: #000; +} diff --git a/frontend-kiosk/src/main.jsx b/frontend-kiosk/src/main.jsx new file mode 100644 index 00000000..b4296e8a --- /dev/null +++ b/frontend-kiosk/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/frontend-kiosk/tailwind.config.js b/frontend-kiosk/tailwind.config.js new file mode 100644 index 00000000..ceb0351a --- /dev/null +++ b/frontend-kiosk/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/frontend-kiosk/vite.config.js b/frontend-kiosk/vite.config.js new file mode 100644 index 00000000..15d40c2d --- /dev/null +++ b/frontend-kiosk/vite.config.js @@ -0,0 +1,19 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [react()], + server: { + host: '0.0.0.0', + port: 5174, + strictPort: true, + proxy: { + '/api': 'http://localhost:8000', + '/health': 'http://localhost:8000', + }, + }, + build: { + outDir: '../src/backend/static', + emptyOutDir: true, + }, +}) diff --git a/frontend/mcp.html b/frontend/mcp.html new file mode 100644 index 00000000..2c1285e0 --- /dev/null +++ b/frontend/mcp.html @@ -0,0 +1,12 @@ + + + + + + MCP Settings + + +
+ + + diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index d1add375..f14ab37d 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -14,10 +14,12 @@ import MessageEditor from "./lib/components/chat/MessageEditor.svelte"; import MessageList from "./lib/components/chat/MessageList.svelte"; import ModelSettingsModal from "./lib/components/chat/ModelSettingsModal.svelte"; - import PresetsModal from "./lib/components/chat/PresetsModal.svelte"; import QuickPrompts from "./lib/components/chat/QuickPrompts.svelte"; - import SpeechSettingsModal from "./lib/components/chat/SpeechSettingsModal.svelte"; import SystemSettingsModal from "./lib/components/chat/SystemSettingsModal.svelte"; + + import KioskSettingsModal from "./lib/components/chat/KioskSettingsModal.svelte"; + import CliSettingsModal from "./lib/components/chat/CliSettingsModal.svelte"; + import McpServersModal from "./lib/components/chat/McpServersModal.svelte"; import ModelExplorer from "./lib/components/model-explorer/ModelExplorer.svelte"; import { clearPendingSubmit, @@ -57,8 +59,10 @@ let generationModalOpen = false; let modelSettingsOpen = false; let systemSettingsOpen = false; - let speechSettingsOpen = false; - let presetsOpen = false; + let mcpServersOpen = false; + + let kioskSettingsOpen = false; + let cliSettingsOpen = false; let lastSpeechPromptVersion = 0; let generationModalLoading = false; let generationModalError: string | null = null; @@ -86,6 +90,14 @@ // Reload suggestions after applying preset await suggestionsStore.load(); } + + // Check for URL params to open modals directly + const params = new URLSearchParams(window.location.search); + if (params.get("mcp") === "1") { + mcpServersOpen = true; + // Clean up URL + window.history.replaceState({}, "", window.location.pathname); + } }); async function handleSuggestionAdd(): Promise { @@ -110,7 +122,9 @@ generationModalOpen || modelSettingsOpen || systemSettingsOpen || - speechSettingsOpen, + mcpServersOpen || + kioskSettingsOpen || + cliSettingsOpen, ); } } @@ -307,8 +321,9 @@ on:modelChange={(event) => handleModelChange(event.detail.id)} on:openModelSettings={() => (modelSettingsOpen = true)} on:openSystemSettings={() => (systemSettingsOpen = true)} - on:openSpeechSettings={() => (speechSettingsOpen = true)} - on:openPresets={() => (presetsOpen = true)} + on:openMcpServers={() => (mcpServersOpen = true)} + on:openKioskSettings={() => (kioskSettingsOpen = true)} + on:openCliSettings={() => (cliSettingsOpen = true)} /> {#if !$chatStore.messages.length} @@ -382,11 +397,20 @@ on:close={() => (systemSettingsOpen = false)} /> - (speechSettingsOpen = false)} + (mcpServersOpen = false)} + /> + + (kioskSettingsOpen = false)} + /> + + (cliSettingsOpen = false)} /> - (presetsOpen = false)} /> diff --git a/frontend/src/McpStandalone.svelte b/frontend/src/McpStandalone.svelte new file mode 100644 index 00000000..b16f6ddf --- /dev/null +++ b/frontend/src/McpStandalone.svelte @@ -0,0 +1,28 @@ + + +
+ +
+ + diff --git a/frontend/src/lib/api/cli.ts b/frontend/src/lib/api/cli.ts new file mode 100644 index 00000000..dde2a72d --- /dev/null +++ b/frontend/src/lib/api/cli.ts @@ -0,0 +1,119 @@ +import { API_BASE_URL } from './config'; +import type { ClientSettings, LlmSettings, LlmSettingsUpdate } from './types'; + +export const CLI_CLIENT_ID = 'cli'; + +export interface ClientPreset { + name: string; + llm: LlmSettings; + [key: string]: unknown; +} + +export interface ClientPresets { + presets: ClientPreset[]; + active_index: number | null; +} + + +/** + * Get current CLI LLM settings + */ +export async function getCliLlmSettings(): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/cli/llm`); + if (!response.ok) { + throw new Error(`Failed to fetch CLI LLM settings: ${response.statusText}`); + } + return response.json(); +} + +/** + * Update CLI LLM settings + */ +export async function updateCliLlmSettings( + update: LlmSettingsUpdate +): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/cli/llm`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(update), + }); + + if (!response.ok) { + throw new Error(`Failed to update CLI LLM settings: ${response.statusText}`); + } + return response.json(); +} + +/** + * Reset CLI settings to defaults (optional, but good to have) + */ +export async function resetCliSettings(): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/cli/reset`, { + method: 'POST', + }); + + if (!response.ok) { + throw new Error(`Failed to reset CLI settings: ${response.statusText}`); + } + return response.json(); +} + +/** + * Get all CLI presets + */ +export async function getCliPresets(): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/cli/presets`); + if (!response.ok) { + throw new Error(`Failed to fetch CLI presets: ${response.statusText}`); + } + return response.json(); +} + +/** + * Create a new CLI preset + */ +export async function createCliPreset(preset: ClientPreset): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/cli/presets`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(preset), + }); + + if (!response.ok) { + throw new Error(`Failed to create CLI preset: ${response.statusText}`); + } + return response.json(); +} + +/** + * Delete a CLI preset + */ +export async function deleteCliPreset(name: string): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/cli/presets/by-name/${name}`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error(`Failed to delete CLI preset: ${response.statusText}`); + } +} + +/** + * Apply a CLI preset by name + * Returns the applied settings + */ +export async function applyCliPresetByName(name: string): Promise { + + const response = await fetch(`${API_BASE_URL}/api/clients/cli/presets/by-name/${name}/apply`, { + method: 'POST', + }); + + if (!response.ok) { + throw new Error(`Failed to apply CLI preset: ${response.statusText}`); + } + return response.json(); +} diff --git a/frontend/src/lib/api/client.ts b/frontend/src/lib/api/client.ts index dde83037..ccb48ec1 100644 --- a/frontend/src/lib/api/client.ts +++ b/frontend/src/lib/api/client.ts @@ -1,33 +1,34 @@ import { resolveApiPath } from './config'; import { parseSsePayload } from './sse'; import type { - ActiveModelSettingsPayload, - ActiveModelSettingsResponse, - AttachmentUploadResponse, - ChatCompletionChunk, - ChatCompletionRequest, - DeepgramTokenResponse, - GenerationDetailsResponse, - GoogleAuthAuthorizeRequest, - GoogleAuthAuthorizeResponse, - GoogleAuthStatusResponse, - McpServersCollectionPayload, - McpServersResponse, - McpServerUpdatePayload, - ModelListResponse, - MonarchCredentials, - MonarchStatusResponse, - PresetConfig, - PresetCreatePayload, - // Presets - PresetListItem, - PresetSaveSnapshotPayload, - SpotifyAuthAuthorizeRequest, - SpotifyAuthAuthorizeResponse, - SpotifyAuthStatusResponse, - SseEvent, - SystemPromptPayload, - SystemPromptResponse, + ActiveModelSettingsPayload, + ActiveModelSettingsResponse, + AttachmentUploadResponse, + ChatCompletionChunk, + ChatCompletionRequest, + DeepgramTokenResponse, + GenerationDetailsResponse, + GoogleAuthAuthorizeRequest, + GoogleAuthAuthorizeResponse, + GoogleAuthStatusResponse, + McpServersCollectionPayload, + McpServersResponse, + McpServerUpdatePayload, + ModelListResponse, + MonarchCredentials, + MonarchStatusResponse, + PresetConfig, + PresetCreatePayload, + // Presets + PresetListItem, + PresetModelFilters, + PresetSaveSnapshotPayload, + SpotifyAuthAuthorizeRequest, + SpotifyAuthAuthorizeResponse, + SpotifyAuthStatusResponse, + SseEvent, + SystemPromptPayload, + SystemPromptResponse, } from './types'; class ApiError extends Error { @@ -113,31 +114,36 @@ export async function deleteChatMessage( } export async function fetchModelSettings(): Promise { - return requestJson(resolveApiPath('/api/settings/model')); + return requestJson(resolveApiPath('/api/clients/svelte/llm')); } export async function updateModelSettings( payload: ActiveModelSettingsPayload, ): Promise { - return requestJson(resolveApiPath('/api/settings/model'), { + return requestJson(resolveApiPath('/api/clients/svelte/llm'), { method: 'PUT', body: JSON.stringify(payload), }); } export async function fetchSystemPrompt(): Promise { - return requestJson(resolveApiPath('/api/settings/system-prompt')); + // System prompt is part of LLM settings in the per-client API + const llm = await requestJson<{ system_prompt: string | null }>(resolveApiPath('/api/clients/svelte/llm')); + return { system_prompt: llm.system_prompt }; } export async function updateSystemPrompt( payload: SystemPromptPayload, ): Promise { - return requestJson(resolveApiPath('/api/settings/system-prompt'), { + // Update via LLM settings endpoint + const updated = await requestJson<{ system_prompt: string | null }>(resolveApiPath('/api/clients/svelte/llm'), { method: 'PUT', - body: JSON.stringify(payload), + body: JSON.stringify({ system_prompt: payload.system_prompt }), }); + return { system_prompt: updated.system_prompt }; } + export async function fetchMcpServers(): Promise { return requestJson(resolveApiPath('/api/mcp/servers/')); } @@ -169,6 +175,17 @@ export async function refreshMcpServers(): Promise { }); } +export async function setMcpServerClientEnabled( + serverId: string, + clientId: string, + enabled: boolean, +): Promise { + const path = `/api/mcp/servers/${encodeURIComponent(serverId)}/clients/${encodeURIComponent(clientId)}?enabled=${enabled}`; + return requestJson(resolveApiPath(path), { + method: 'PATCH', + }); +} + export async function fetchGoogleAuthStatus(): Promise { return requestJson(resolveApiPath('/api/google-auth/status')); } @@ -395,60 +412,185 @@ export async function streamChat( } } -/* Preset API */ +/* Preset API - using per-client svelte presets */ + +interface BackendPresetLlm { + model?: string; + system_prompt?: string | null; + supports_tools?: boolean | null; + [key: string]: unknown; +} + +interface BackendPreset { + name: string; + llm?: BackendPresetLlm | null; + model_filters?: PresetModelFilters | null; + created_at?: string | null; + updated_at?: string | null; + [key: string]: unknown; +} + +interface BackendPresetsResponse { + presets: BackendPreset[]; + active_index: number | null; +} + +function mapPresetConfig(preset: BackendPreset, isDefault: boolean): PresetConfig { + return { + name: preset.name, + model: preset.llm?.model ?? 'unknown', + provider: null, + parameters: null, + supports_tools: preset.llm?.supports_tools ?? null, + system_prompt: preset.llm?.system_prompt ?? null, + suggestions: null, + model_filters: preset.model_filters ?? null, + is_default: isDefault, + created_at: preset.created_at ?? new Date().toISOString(), + updated_at: preset.updated_at ?? new Date().toISOString(), + }; +} + +function mapPresetListItem(preset: BackendPreset, isDefault: boolean): PresetListItem { + return { + name: preset.name, + model: preset.llm?.model ?? 'unknown', + is_default: isDefault, + has_filters: Boolean(preset.model_filters), + created_at: preset.created_at ?? new Date().toISOString(), + updated_at: preset.updated_at ?? new Date().toISOString(), + }; +} export async function fetchPresets(): Promise { - return requestJson(resolveApiPath('/api/presets/')); + const response = await requestJson( + resolveApiPath('/api/clients/svelte/presets'), + ); + + return response.presets.map((preset, index) => + mapPresetListItem(preset, index === response.active_index), + ); } + export async function fetchPreset(name: string): Promise { - const path = `/api/presets/${encodeURIComponent(name)}`; - return requestJson(resolveApiPath(path)); + const response = await requestJson( + resolveApiPath('/api/clients/svelte/presets'), + ); + const preset = response.presets.find((entry) => entry.name === name); + if (!preset) throw new ApiError(404, `Preset not found: ${name}`); + + const index = response.presets.indexOf(preset); + return mapPresetConfig(preset, index === response.active_index); } + export async function createPreset(payload: PresetCreatePayload): Promise { - return requestJson(resolveApiPath('/api/presets/'), { - method: 'POST', - body: JSON.stringify(payload), - }); + // Get current LLM settings (includes model and system_prompt) + const currentLlm = await requestJson>(resolveApiPath('/api/clients/svelte/llm')); + + // Create preset via POST with LLM settings only (MCP is separate) + const presetPayload = { + name: payload.name, + llm: currentLlm, + model_filters: payload.model_filters, + }; + + const result = await requestJson( + resolveApiPath('/api/clients/svelte/presets'), + { + method: 'POST', + body: JSON.stringify(presetPayload), + } + ); + + // Return the newly created preset (last in list) + const newPreset = result.presets.find((entry) => entry.name === payload.name); + if (!newPreset) throw new ApiError(500, 'Failed to create preset'); + const index = result.presets.indexOf(newPreset); + return mapPresetConfig(newPreset, index === result.active_index); } + + export async function savePresetSnapshot( name: string, - payload?: PresetSaveSnapshotPayload | null, + _payload?: PresetSaveSnapshotPayload | null, ): Promise { - const path = `/api/presets/${encodeURIComponent(name)}`; - return requestJson(resolveApiPath(path), { - method: 'PUT', - body: JSON.stringify(payload ?? {}), - }); + // Get current presets to find the index + const presets = await requestJson( + resolveApiPath('/api/clients/svelte/presets') + ); + + const index = presets.presets.findIndex((entry) => entry.name === name); + if (index === -1) throw new ApiError(404, `Preset not found: ${name}`); + + // Get current LLM settings (includes model and system_prompt) + const currentLlm = await requestJson>(resolveApiPath('/api/clients/svelte/llm')); + + // Update preset at index with current LLM settings only (MCP is separate) + const result = await requestJson( + resolveApiPath(`/api/clients/svelte/presets/${index}`), + { + method: 'PUT', + body: JSON.stringify({ + llm: currentLlm, + model_filters: _payload?.model_filters ?? null, + }), + } + ); + + const updatedPreset = result.presets[index]; + if (!updatedPreset) throw new ApiError(500, 'Failed to save preset snapshot'); + return mapPresetConfig(updatedPreset, index === result.active_index); } + + + export async function deletePreset(name: string): Promise<{ deleted: boolean }> { - const path = `/api/presets/${encodeURIComponent(name)}`; - return requestJson<{ deleted: boolean }>(resolveApiPath(path), { + // Delete preset by name + await requestVoid(resolveApiPath(`/api/clients/svelte/presets/by-name/${encodeURIComponent(name)}`), { method: 'DELETE', }); + return { deleted: true }; } export async function applyPreset(name: string): Promise { - const path = `/api/presets/${encodeURIComponent(name)}/apply`; - return requestJson(resolveApiPath(path), { + // Apply preset by name + const path = `/api/clients/svelte/presets/by-name/${encodeURIComponent(name)}/apply`; + await requestJson<{ llm: Record }>(resolveApiPath(path), { method: 'POST', body: JSON.stringify({}), }); + // Fetch the full preset to restore filters + prompt metadata on the client. + return fetchPreset(name); } export async function setDefaultPreset(name: string): Promise { - const path = `/api/presets/${encodeURIComponent(name)}/set-default`; - return requestJson(resolveApiPath(path), { + // Set preset as active by name + const path = `/api/clients/svelte/presets/by-name/${encodeURIComponent(name)}/set-active`; + const result = await requestJson(resolveApiPath(path), { method: 'POST', body: JSON.stringify({}), }); + // Return the preset that was made active + const preset = result.presets.find((entry) => entry.name === name); + if (!preset) throw new ApiError(404, `Preset not found: ${name}`); + const index = result.presets.indexOf(preset); + return mapPresetConfig(preset, index === result.active_index); } + export async function fetchDefaultPreset(): Promise { - return requestJson(resolveApiPath('/api/presets/default')); + const response = await requestJson( + resolveApiPath('/api/clients/svelte/presets'), + ); + if (response.active_index !== null && response.presets[response.active_index]) { + return mapPresetConfig(response.presets[response.active_index], true); + } + return null; } + export { ApiError }; diff --git a/frontend/src/lib/api/kiosk.ts b/frontend/src/lib/api/kiosk.ts new file mode 100644 index 00000000..a9b408c2 --- /dev/null +++ b/frontend/src/lib/api/kiosk.ts @@ -0,0 +1,379 @@ +/** + * Kiosk API client for STT and TTS settings. + * Uses the unified /api/clients/kiosk/* endpoints. + */ + +import { API_BASE_URL } from './config'; + +// ============== STT Settings ============== + +export interface KioskSttSettings { + eot_threshold: number; + eot_timeout_ms: number; + eager_eot_threshold?: number | null; + keyterms: string[]; +} + +export interface KioskSttSettingsUpdate { + eot_threshold?: number; + eot_timeout_ms?: number; + eager_eot_threshold?: number | null; + keyterms?: string[]; +} + +/** + * Fetch current kiosk STT settings from the backend. + */ +export async function fetchKioskSttSettings(): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/stt`); + if (!response.ok) { + throw new Error(`Failed to fetch kiosk STT settings: ${response.statusText}`); + } + return response.json(); +} + +/** + * Update kiosk STT settings on the backend. + */ +export async function updateKioskSttSettings( + update: KioskSttSettingsUpdate +): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/stt`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(update), + }); + if (!response.ok) { + throw new Error(`Failed to update kiosk STT settings: ${response.statusText}`); + } + return response.json(); +} + +/** + * Reset kiosk STT settings to defaults on the backend. + */ +export async function resetKioskSttSettings(): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/stt/reset`, { + method: 'POST', + }); + if (!response.ok) { + throw new Error(`Failed to reset kiosk STT settings: ${response.statusText}`); + } + return response.json(); +} + +// ============== TTS Settings ============== + +export interface KioskTtsSettings { + enabled: boolean; + provider: string; + model: string; + voice: string; + speed: number; + response_format: string; + sample_rate: number; + use_segmentation: boolean; + delimiters: string[]; + character_maximum: number; +} + +export interface KioskTtsSettingsUpdate { + enabled?: boolean; + provider?: string; + model?: string; + voice?: string; + speed?: number; + response_format?: string; + sample_rate?: number; + use_segmentation?: boolean; + delimiters?: string[]; + character_maximum?: number; +} + +/** + * Fetch current kiosk TTS settings from the backend. + */ +export async function fetchKioskTtsSettings(): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/tts`); + if (!response.ok) { + throw new Error(`Failed to fetch kiosk TTS settings: ${response.statusText}`); + } + return response.json(); +} + +/** + * Update kiosk TTS settings on the backend. + */ +export async function updateKioskTtsSettings( + update: KioskTtsSettingsUpdate +): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/tts`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(update), + }); + if (!response.ok) { + throw new Error(`Failed to update kiosk TTS settings: ${response.statusText}`); + } + return response.json(); +} + +/** + * Reset kiosk TTS settings to defaults on the backend. + */ +export async function resetKioskTtsSettings(): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/tts/reset`, { + method: 'POST', + }); + if (!response.ok) { + throw new Error(`Failed to reset kiosk TTS settings: ${response.statusText}`); + } + return response.json(); +} + +/** + * Voice object returned by the TTS voices endpoint. + */ +export interface TtsVoice { + id: string; + name: string; +} + +/** + * Fetch available TTS voice models for the specified provider. + */ +export async function fetchTtsVoices(provider: string = 'openai'): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/tts/voices?provider=${encodeURIComponent(provider)}`); + if (!response.ok) { + throw new Error(`Failed to fetch TTS voices: ${response.statusText}`); + } + return response.json(); +} + +// ============== UI Settings ============== + +export interface KioskUiSettings { + idle_return_delay_ms: number; +} + +export interface KioskUiSettingsUpdate { + idle_return_delay_ms?: number; +} + +/** + * Fetch current kiosk UI settings from the backend. + */ +export async function fetchKioskUiSettings(): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/ui`); + if (!response.ok) { + throw new Error(`Failed to fetch kiosk UI settings: ${response.statusText}`); + } + return response.json(); +} + +/** + * Update kiosk UI settings on the backend. + */ +export async function updateKioskUiSettings( + update: KioskUiSettingsUpdate +): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/ui`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(update), + }); + if (!response.ok) { + throw new Error(`Failed to update kiosk UI settings: ${response.statusText}`); + } + return response.json(); +} + +// ============== MCP Settings ============== + +export interface KioskMcpServerInfo { + id: string; + enabled: boolean; + tool_count: number; + kiosk_enabled: boolean; +} + +/** + * Fetch all available MCP servers with their kiosk enabled status. + */ +export async function fetchKioskMcpServers(): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/mcp-servers`); + if (!response.ok) { + throw new Error(`Failed to fetch kiosk MCP servers: ${response.statusText}`); + } + return response.json(); +} + +/** + * Update kiosk_enabled for a specific MCP server. + * Uses the main MCP API endpoint. + */ +export async function updateServerKioskEnabled( + serverId: string, + kioskEnabled: boolean +): Promise { + const response = await fetch(`${API_BASE_URL}/api/mcp/servers/${serverId}/clients/kiosk?enabled=${kioskEnabled}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (!response.ok) { + throw new Error(`Failed to update kiosk_enabled for ${serverId}: ${response.statusText}`); + } +} + +// ============== LLM Settings ============== + +export interface KioskLlmSettings { + model: string; + system_prompt: string | null; + temperature: number; + max_tokens: number; + conversation_mode: boolean; + conversation_timeout_seconds: number; +} + +export interface KioskLlmSettingsUpdate { + model?: string; + system_prompt?: string | null; + temperature?: number; + max_tokens?: number; + conversation_mode?: boolean; + conversation_timeout_seconds?: number; +} + +/** + * Fetch current kiosk LLM settings from the backend. + */ +export async function fetchKioskLlmSettings(): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/llm`); + if (!response.ok) { + throw new Error(`Failed to fetch kiosk LLM settings: ${response.statusText}`); + } + return response.json(); +} + +/** + * Update kiosk LLM settings on the backend. + */ +export async function updateKioskLlmSettings( + update: KioskLlmSettingsUpdate +): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/llm`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(update), + }); + if (!response.ok) { + throw new Error(`Failed to update kiosk LLM settings: ${response.statusText}`); + } + return response.json(); +} + +/** + * Reset kiosk LLM settings to defaults on the backend. + */ +export async function resetKioskLlmSettings(): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/llm/reset`, { + method: 'POST', + }); + if (!response.ok) { + throw new Error(`Failed to reset kiosk LLM settings: ${response.statusText}`); + } + return response.json(); +} + +// ============== Presets ============== + +export interface KioskPreset { + name: string; + // LLM settings + model: string; + system_prompt: string; + temperature: number; + max_tokens: number; + // STT settings + eot_threshold: number; + eot_timeout_ms: number; + keyterms: string[]; + // TTS settings + tts_enabled: boolean; + tts_voice: string; + tts_model: string; + tts_sample_rate: number; +} + +export interface KioskPresets { + presets: KioskPreset[]; + active_index: number; +} + +/** + * Fetch all kiosk presets from the backend. + */ +export async function fetchKioskPresets(): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/presets`); + if (!response.ok) { + throw new Error(`Failed to fetch kiosk presets: ${response.statusText}`); + } + return response.json(); +} + +/** + * Activate a preset by index. This also applies its settings. + */ +export async function activateKioskPreset(index: number): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/presets/${index}/activate`, { + method: 'POST', + }); + if (!response.ok) { + throw new Error(`Failed to activate kiosk preset: ${response.statusText}`); + } + return response.json(); +} + +/** + * Update a single preset by index. + */ +export async function updateKioskPreset( + index: number, + preset: Partial +): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/presets/${index}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(preset), + }); + if (!response.ok) { + throw new Error(`Failed to update kiosk preset: ${response.statusText}`); + } + return response.json(); +} + +/** + * Reset all presets to defaults. + */ +export async function resetKioskPresets(): Promise { + const response = await fetch(`${API_BASE_URL}/api/clients/kiosk/presets/reset`, { + method: 'POST', + }); + if (!response.ok) { + throw new Error(`Failed to reset kiosk presets: ${response.statusText}`); + } + return response.json(); +} diff --git a/frontend/src/lib/api/types.ts b/frontend/src/lib/api/types.ts index 37db9047..f5e4b000 100644 --- a/frontend/src/lib/api/types.ts +++ b/frontend/src/lib/api/types.ts @@ -338,12 +338,15 @@ export interface McpServerStatus { connected: boolean; module?: string | null; command?: string[] | null; + http_url?: string | null; + http_port?: number | null; cwd?: string | null; env?: Record; tool_prefix?: string | null; disabled_tools: string[]; tool_count: number; tools: McpServerToolStatus[]; + client_enabled?: Record; } export interface McpServersResponse { @@ -356,6 +359,8 @@ export interface McpServerDefinition { enabled?: boolean; module?: string | null; command?: string[] | null; + http_url?: string | null; + http_port?: number | null; cwd?: string | null; env?: Record; tool_prefix?: string | null; @@ -371,9 +376,12 @@ export interface McpServerUpdatePayload { disabled_tools?: string[]; module?: string | null; command?: string[] | null; + http_url?: string | null; + http_port?: number | null; cwd?: string | null; env?: Record | null; tool_prefix?: string | null; + client_enabled?: Record; } /* Suggestions */ @@ -423,7 +431,6 @@ export interface PresetConfig { parameters?: ModelHyperparameters | null; supports_tools?: boolean | null; system_prompt?: string | null; - mcp_servers?: McpServerDefinition[] | null; suggestions?: Suggestion[] | null; model_filters?: PresetModelFilters | null; is_default: boolean; @@ -440,3 +447,19 @@ export interface PresetSaveSnapshotPayload { note?: string | null; model_filters?: PresetModelFilters | null; } + +/* Client Settings */ + +export interface LlmSettings { + model: string; + system_prompt: string | null; + temperature: number; + max_tokens: number; +} + +export interface LlmSettingsUpdate extends Partial { } + +export interface ClientSettings { + llm: LlmSettings; + [key: string]: unknown; +} diff --git a/frontend/src/lib/components/chat/ChatHeader.svelte b/frontend/src/lib/components/chat/ChatHeader.svelte index 38735286..4f8e5f56 100644 --- a/frontend/src/lib/components/chat/ChatHeader.svelte +++ b/frontend/src/lib/components/chat/ChatHeader.svelte @@ -17,8 +17,9 @@ modelChange: { id: string }; openModelSettings: void; openSystemSettings: void; - openSpeechSettings: void; - openPresets: void; + openKioskSettings: void; + openCliSettings: void; + openMcpServers: void; }>(); export let selectableModels: SelectableModel[] = []; @@ -82,12 +83,16 @@ dispatch("openSystemSettings"); } - function forwardOpenSpeechSettings(): void { - dispatch("openSpeechSettings"); + function forwardOpenKioskSettings(): void { + dispatch("openKioskSettings"); } - function forwardOpenPresets(): void { - dispatch("openPresets"); + function forwardOpenCliSettings(): void { + dispatch("openCliSettings"); + } + + function forwardOpenMcpServers(): void { + dispatch("openMcpServers"); } @@ -131,7 +136,11 @@ data-loading={webSearchMenuLoading} aria-hidden="true" > - - - {#if $presetsStore.applying} {/if} - + + - + + - + + + + + +
@@ -319,6 +383,11 @@ .controls > * { flex: 0 0 auto; } + .icon-row { + display: flex; + align-items: center; + gap: 0.75rem; + } :global(.chat-header .controls .model-picker) { display: flex; } @@ -432,7 +501,7 @@ :global(.chat-header .controls .btn) { white-space: nowrap; } - :global(.chat-header .controls .btn.system-settings svg) { + :global(.chat-header .controls .btn.settings-icon svg) { width: 1.05rem; height: 1.05rem; } @@ -491,6 +560,14 @@ .preset-badge { justify-content: center; } + .icon-row { + grid-column: 1 / -1; + justify-content: center; + flex-wrap: nowrap; + } + .icon-row :global(.btn) { + width: auto; + } :global(.chat-header .controls select) { min-width: 0; text-align: left; @@ -532,10 +609,13 @@ :global(.chat-header .controls select) { padding: 0.55rem 0.9rem; } - :global(.chat-header .controls .btn.system-settings svg) { + :global(.chat-header .controls .btn.settings-icon svg) { width: 1rem; height: 1rem; } + .icon-row { + gap: 0.5rem; + } } @media (max-width: 480px) { .topbar { diff --git a/frontend/src/lib/components/chat/CliSettingsModal.svelte b/frontend/src/lib/components/chat/CliSettingsModal.svelte new file mode 100644 index 00000000..a7c6404b --- /dev/null +++ b/frontend/src/lib/components/chat/CliSettingsModal.svelte @@ -0,0 +1,418 @@ + + +{#if open} + void closeModal()} + > + +

CLI Settings

+

+ Configure the command-line assistant. +

+
+ +
+ + +
+ + {#if loading} +

Loading settings…

+ {:else} +
+ +
+
+ Language Model + Configure the AI model and behavior for the CLI. +
+ +
+ +
+ + +
+ + +
+ + +
+ + +
+
+
+ Temperature + {draft.temperature?.toFixed(1) ?? "0.7"} +
+ { + draft = { + ...draft, + temperature: parseFloat( + (e.target as HTMLInputElement).value, + ), + }; + markDirty(); + }} + /> +
+ 0 (focused) + 2 (creative) +
+
+
+ + +
+
+
+ Max Tokens + {draft.max_tokens ?? 1000} +
+ { + draft = { + ...draft, + max_tokens: parseInt( + (e.target as HTMLInputElement).value, + 10, + ), + }; + markDirty(); + }} + /> +
+ 50 + 4000 +
+
+
+
+
+
+ {/if} + +
+ {#if statusMessage} + {statusMessage} + {:else if saving} + Saving changes… + {:else if dirty} + Pending changes; closing this modal will save. + {:else} + Changes save on close. "Set as default" updates the reset target. + {/if} +
+
+{/if} + + diff --git a/frontend/src/lib/components/chat/KioskSettingsModal.svelte b/frontend/src/lib/components/chat/KioskSettingsModal.svelte new file mode 100644 index 00000000..11589e2b --- /dev/null +++ b/frontend/src/lib/components/chat/KioskSettingsModal.svelte @@ -0,0 +1,948 @@ + + +{#if open} + void closeModal()} + > + +

Kiosk settings

+

+ Configure the voice assistant kiosk. +

+
+ + + + {#if loading} +

Loading settings…

+ {:else} +
+ +
+
+ Language Model + Configure the AI model and behavior. +
+ +
+ +
+ +
+ + +
+ + + {#if presets} +
+ +
+ + +
+
+ {/if} +
+ + +
+ + +
+ + +
+
+
+ Temperature + {draft.temperature.toFixed(1)} +
+ { + draft = { + ...draft, + temperature: parseFloat( + (e.target as HTMLInputElement).value, + ), + }; + markDirty(); + }} + /> +
+ 0 (focused) + 2 (creative) +
+
+
+ + +
+
+
+ Max Tokens + {draft.max_tokens} +
+ { + draft = { + ...draft, + max_tokens: parseInt( + (e.target as HTMLInputElement).value, + 10, + ), + }; + markDirty(); + }} + /> +
+ 50 (short) + 2000 (long) +
+
+
+
+
+ + +
+
+ Speech Recognition + Deepgram Flux end-of-turn detection settings. +
+ +
+ +
+
+
+ EOT Threshold + {draft.eot_threshold.toFixed(2)} +
+ { + draft = { + ...draft, + eot_threshold: parseFloat( + (e.target as HTMLInputElement).value, + ), + }; + markDirty(); + }} + /> +
+ 0.5 (fast) + 0.9 (reliable) +
+
+
+ + +
+
+
+ EOT Timeout + {draft.eot_timeout_ms}ms +
+ { + draft = { + ...draft, + eot_timeout_ms: parseInt( + (e.target as HTMLInputElement).value, + 10, + ), + }; + markDirty(); + }} + /> +
+ 500ms + 10s +
+
+
+
+
+ + +
+
+ Advanced + Optional fine-tuning options. +
+ + + + {#if draft.eager_eot_threshold !== null} +
+
+ Eager EOT Threshold + {draft.eager_eot_threshold?.toFixed(2) ?? "—"} +
+ { + draft = { + ...draft, + eager_eot_threshold: parseFloat( + (e.target as HTMLInputElement).value, + ), + }; + markDirty(); + }} + /> +
+ 0.3 (eager) + 0.9 (conservative) +
+
+ {/if} +
+ + +
+
+ Display + Kiosk screen behavior settings. +
+ +
+ + + + +
+
+
+ Return to Clock + {(draft.idle_return_delay_ms / 1000).toFixed(0)}s +
+ { + draft = { + ...draft, + idle_return_delay_ms: parseInt( + (e.target as HTMLInputElement).value, + 10, + ), + }; + markDirty(); + }} + /> +
+ 1s + 60s +
+
+
+ + +
+
+
+ Conversation Timeout + {draft.conversation_timeout_seconds.toFixed(0)}s +
+ { + draft = { + ...draft, + conversation_timeout_seconds: parseFloat( + (e.target as HTMLInputElement).value, + ), + }; + markDirty(); + }} + /> +
+ 1s + 60s +
+
+
+
+
+ + +
+
+ Text-to-Speech + Voice synthesis settings for assistant responses. +
+ +
+ + + + {#if draft.enabled} + +
+
+ + +
+
+ + +
+
+
+ Speed + {draft.speed.toFixed(1)}x +
+ { + draft = { + ...draft, + speed: parseFloat( + (e.target as HTMLInputElement).value, + ), + }; + markDirty(); + }} + /> +
+ 0.5x (slow) + 2x (fast) +
+
+
+ {/if} +
+
+ + +
+
+ Keyterms + Words to boost recognition (one per line, up to 100). +
+ +
+
+ {/if} + +
+ {#if statusMessage} + {statusMessage} + {:else if saving} + Saving changes… + {:else if dirty} + Pending changes; closing this modal will save. + {:else} + Changes save when you close this modal. + {/if} +
+
+{/if} + + diff --git a/frontend/src/lib/components/chat/McpServersModal.svelte b/frontend/src/lib/components/chat/McpServersModal.svelte new file mode 100644 index 00000000..61e42401 --- /dev/null +++ b/frontend/src/lib/components/chat/McpServersModal.svelte @@ -0,0 +1,781 @@ + + +{#if open} + void closeModal()} + > + +

MCP servers

+

+ Manage MCP server availability, tools, and client access. +

+
+ +
+
+
+

Google services

+

+ Connect Calendar, Tasks, Gmail, and Drive with a single consent. +

+
+
+ +
+
+ +
+ {#if $googleAuth.loading} +

Checking Google authorization...

+ {:else if $googleAuth.error} +

{$googleAuth.error}

+
+ +
+ {:else if $googleAuth.authorized} +

+ Connected as {$googleAuth.userEmail}. +

+ {#if $googleAuth.expiresAt} +

+ Current token expires {formatUpdatedAt($googleAuth.expiresAt) ?? + "soon"}. +

+ {:else} +

Access will refresh automatically.

+ {/if} + {:else} +

Google services are not connected.

+ {/if} + +
    + {#each $googleAuth.services as service} +
  • {service}
  • + {/each} +
+ +

+ Click "Connect Google Services" to authorize these integrations for + the assistant. +

+
+
+ +
+
+
+

Spotify

+

+ Connect Spotify for music control and playback. +

+
+
+ +
+
+ +
+ {#if $spotifyAuth.loading} +

Checking Spotify authorization...

+ {:else if $spotifyAuth.error} +

{$spotifyAuth.error}

+
+ +
+ {:else if $spotifyAuth.authorized} +

+ Connected as {$spotifyAuth.userEmail}. +

+

Access will refresh automatically.

+ {:else} +

Spotify is not connected.

+ {/if} + +

+ Click "Connect Spotify" to authorize music control and playback + features. +

+
+
+ +
+
+
+

Monarch Money

+

Connect your Monarch Money account.

+
+
+ {#if $monarchAuth.configured} + + {:else} + + {/if} +
+
+ +
+ {#if $monarchAuth.loading} +

Checking Monarch status...

+ {:else if $monarchAuth.configured} +

+ Connected as {$monarchAuth.email}. +

+ {:else} +
+ + + +

+ If you already use an app, you must reset MFA in Monarch + Settings to see the secret key again. Enter the new key here AND in + your app. +

+ {#if $monarchAuth.error} +

{$monarchAuth.error}

+ {/if} +
+ {/if} +
+
+ +
+
+
+

Server status

+ {#if $mcpServers.updatedAt} +

+ Last updated {formatUpdatedAt($mcpServers.updatedAt) ?? ""} +

+ {:else} +

+ Toggle integrations available to the assistant. +

+ {/if} +
+
+ +
+
+ +
+ {#if $mcpServers.loading} +

Loading MCP servers...

+ {:else if $mcpServers.error} +

{$mcpServers.error}

+ {:else} + {#if !$mcpServers.servers.length} +

No MCP servers configured.

+ {:else} +
    + {#each $mcpServers.servers as server} +
  • +
    +
    +

    {server.id}

    +
    + + {server.connected ? "Connected" : "Offline"} + + {#if server.module} + + Module: {server.module} + {:else if server.command?.length} + + Command: {server.command.join(" ")} + {/if} +
    +
    +
    + + + + + +
    +
    + +
    + {#if server.id === "shell-control"} +
    +
    +
    + This machine + + Select which computer you're on. Sets host profile + and storage path. + +
    + +
    +
    + {/if} + + + + {#if expandedServers.has(server.id)} +
    + {#if server.tools.length} +
      + {#each server.tools as tool} +
    • +
      + {tool.name} + {tool.qualified_name} +
      + +
    • + {/each} +
    + {:else} +

    Tool list unavailable.

    + {/if} + + {#if server.disabled_tools.length} +

    + Disabled tool ids: {server.disabled_tools.join( + ", ", + )} +

    + {/if} +
    + {/if} +
    +
  • + {/each} +
+ {/if} + {#if $mcpServers.saveError} +

{$mcpServers.saveError}

+ {/if} + {#if $mcpServers.dirty} +

+ Pending changes save when you close this modal. +

+ {/if} + {/if} +
+
+ +
+ {#if $mcpServers.saveError} +

Resolve the errors above before closing.

+ {:else if $mcpServers.saving} +

Saving changes...

+ {:else if $mcpServers.dirty} +

Pending changes; closing this modal will save.

+ {:else} +

Changes save when you close this modal.

+ {/if} +
+
+{/if} diff --git a/frontend/src/lib/components/chat/PresetsModal.svelte b/frontend/src/lib/components/chat/PresetsModal.svelte index 5821bbe1..c1d1e96f 100644 --- a/frontend/src/lib/components/chat/PresetsModal.svelte +++ b/frontend/src/lib/components/chat/PresetsModal.svelte @@ -6,6 +6,7 @@ import { presetsStore } from "../../stores/presets"; import { suggestionsStore } from "../../stores/suggestions"; import ModelSettingsDialog from "./model-settings/ModelSettingsDialog.svelte"; + import "./presets-settings.css"; export let open = false; @@ -329,97 +330,3 @@ {/if} - - diff --git a/frontend/src/lib/components/chat/SystemSettingsModal.svelte b/frontend/src/lib/components/chat/SystemSettingsModal.svelte index 21204a64..5f741522 100644 --- a/frontend/src/lib/components/chat/SystemSettingsModal.svelte +++ b/frontend/src/lib/components/chat/SystemSettingsModal.svelte @@ -1,52 +1,52 @@ {#if open} @@ -212,14 +254,13 @@ layerClass="system-settings-layer" closeLabel="Close system settings" closeDisabled={$systemPrompt.saving || - $mcpServers.saving || - $googleAuth.authorizing} + speechSaving} on:close={() => void closeModal()} >

System settings

- Configure orchestration defaults and MCP servers. + Configure orchestration defaults, presets, and speech settings.

@@ -237,8 +278,7 @@ class="btn btn-ghost btn-small" on:click={() => systemPrompt.reset()} disabled={!$systemPrompt.dirty || - $systemPrompt.saving || - $mcpServers.saving} + $systemPrompt.saving} > Reset @@ -258,502 +298,436 @@ on:input={handlePromptInput} placeholder="Provide guidance for the assistant to follow at the start of new conversations." disabled={$systemPrompt.saving} + use:autoSize={$systemPrompt.value} > {#if $systemPrompt.saveError}

{$systemPrompt.saveError}

- {:else if $systemPrompt.dirty} -

Pending changes; closing this modal will save.

- {:else} -

Changes save when you close this modal.

{/if} {/if}
+
-

Google services

+

Presets

- Connect Calendar, Tasks, Gmail, and Drive with a single consent. + Save and manage snapshots of the current configuration.

-
+
+ +
+
+ + event.key === "Enter" ? handleCreatePreset() : null} + />
- -
- {#if $googleAuth.loading} -

Checking Google authorization…

- {:else if $googleAuth.error} -

{$googleAuth.error}

-
- -
- {:else if $googleAuth.authorized} -

- Connected as {$googleAuth.userEmail}. -

- {#if $googleAuth.expiresAt} -

- Current token expires {formatUpdatedAt($googleAuth.expiresAt) ?? - "soon"}. -

- {:else} -

Access will refresh automatically.

- {/if} - {:else} -

Google services are not connected.

+ {#if $presetsStore.error} +

{$presetsStore.error}

{/if} -
    - {#each $googleAuth.services as service} -
  • {service}
  • - {/each} -
+ {#if $presetsStore.loading} +

Loading presets…

+ {:else if !$presetsStore.items.length} +

No presets saved yet.

+ {:else} +
    + {#each $presetsStore.items as item (item.name)} +
  • +
    +
    + {item.name} + {#if item.is_default} + Default + {/if} + {#if item.has_filters} + + + + + Filters + + {/if} +
    +
    + {item.model} + + {new Date(item.created_at).toLocaleString()} + + {new Date(item.updated_at).toLocaleString()} + +
    +
    +
    + + + {#if !item.is_default} + + {/if} + +
    +
  • + {/each} +
+ {/if} -

- Click "Connect Google Services" to authorize these integrations for - the assistant. -

+ {#if $presetsStore.lastApplied} +

+ Applied preset: {$presetsStore.lastApplied} +

+ {:else if $presetsStore.lastResult} +

Saved: {$presetsStore.lastResult.name}

+ {:else} +

Create, update, or apply a preset.

+ {/if}
-

Spotify

+

Speech settings

- Connect Spotify for music control and playback. + Configure Deepgram speech-to-text dictation settings.

-
- {#if $spotifyAuth.loading} -

Checking Spotify authorization…

- {:else if $spotifyAuth.error} -

{$spotifyAuth.error}

-
- +
+
+
+

Deepgram model

+

Choose the speech-to-text engine and helper features.

- {:else if $spotifyAuth.authorized} -

- Connected as {$spotifyAuth.userEmail}. -

-

Access will refresh automatically.

- {:else} -

Spotify is not connected.

- {/if} - -

- Click "Connect Spotify" to authorize music control and playback - features. -

-
-
- -
-
-
-

Monarch Money

-

Connect your Monarch Money account.

-
-
- {#if $monarchAuth.configured} - - {:else} - - {/if} -
-
-
- {#if $monarchAuth.loading} -

Checking Monarch status…

- {:else if $monarchAuth.configured} -

- Connected as {$monarchAuth.email}. -

- {:else} -
- -
- -
-
-
-

MCP servers

- {#if $mcpServers.updatedAt} -

- Last updated {formatUpdatedAt($mcpServers.updatedAt) ?? ""} -

- {:else} -

- Toggle integrations available to the assistant. -

- {/if}
-
- -
-
-
- {#if $mcpServers.loading} -

Loading MCP servers…

- {:else if $mcpServers.error} -

{$mcpServers.error}

- {:else} - {#if !$mcpServers.servers.length} -

No MCP servers configured.

- {:else} -
    - {#each $mcpServers.servers as server} -
  • -
    -
    -

    {server.id}

    -
    - - {server.connected ? "Connected" : "Offline"} - - {#if server.module} - - Module: {server.module} - {:else if server.command?.length} - - Command: {server.command.join(" ")} - {/if} -
    -
    - -
    +
    +
    +

    Timing & auto-submit

    +

    Tune silence detection and message submission.

    +
    -
    - {#if server.id === "shell-control"} -
    -
    -
    - This machine - - Select which computer you're on. Sets host profile and storage path. - -
    - -
    -
    - {/if} +
    +
    + Endpointing window (ms) + + handleSpeechNumberInput(event, (value) => + updateSpeechStt("endpointing", value), + )} + /> +

    + How long Deepgram waits after silence before finalizing the + transcript. +

    +
    +
    + Utterance gap (ms) + + handleSpeechNumberInput(event, (value) => + updateSpeechStt("utteranceEndMs", value), + )} + /> +

    + Silence between words before Deepgram starts a new interim + segment. +

    +
    +
    - +
    +
    + +

    + Send automatically when speech ends. Delay waits after + endpointing. +

    +
    +
    + + + handleSpeechNumberInput(event, (value) => + updateSpeechStt("autoSubmitDelayMs", value), + )} + /> +
    +
    +
    - {#if expandedServers.has(server.id)} -
    - {#if server.tools.length} -
      - {#each server.tools as tool} -
    • -
      - {tool.name} - {tool.qualified_name} -
      - -
    • - {/each} -
    - {:else} -

    Tool list unavailable.

    - {/if} - - {#if server.disabled_tools.length} -

    - Disabled tool ids: {server.disabled_tools.join( - ", ", - )} -

    - {/if} -
    - {/if} -
    -
  • - {/each} -
- {/if} - {#if $mcpServers.saveError} -

{$mcpServers.saveError}

- {/if} - {#if $mcpServers.dirty} -

- Pending changes save when you close this modal. -

- {/if} - {/if}