diff --git a/.github/DISCUSSION_TEMPLATE/skills.yml b/.github/DISCUSSION_TEMPLATE/skills.yml new file mode 100644 index 000000000..3e2a6d86e --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/skills.yml @@ -0,0 +1,48 @@ +title: "[Skill Name]: Brief Description" +body: + - type: markdown + attributes: + value: | + ## Share Your Skill! + Fill out the form below to showcase what you have built. + + - type: input + id: webhost-path + attributes: + label: Skill Webhost Path + description: The live URL where your skill is hosted. Users will use this link to import your skill directly into the AI Edge Gallery app. + placeholder: https://{username}.github.io/{repo}/path/to/skill_folder + validations: + required: true + + - type: markdown + attributes: + value: | + > 💡 **Tip**: To ensure your path is correct, try loading `https://{username}.github.io/{repo}/path/to/skill_folder/SKILL.md` file in your browser. If it displays the raw file content, you are good to go! + - type: markdown + attributes: + value: | + > ⚠️ **Important**: This must be a live web deployment (e.g., GitHub Pages, Vercel, Cloudflare, etc). Do not use a standard repository link or a `raw.githubusercontent.com` URL, as these will not work in the app. + + - type: markdown + attributes: + value: "
" + + - type: input + id: repo-link + attributes: + label: Skill Source Repository + description: Link to your source code repository. + placeholder: https://github.com/{username}/{repo}/path/to/skill_folder + + - type: markdown + attributes: + value: "
" + + - type: textarea + id: skill-details + attributes: + label: Skill Description + description: Tell us what your skill does! Include examples, specific instructions (e.g. how to get token/api key if your skill needs it), or screenshots. + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..9e38ca2ae --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: 🐛 Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug:** +A clear and concise description of what the bug is. + +**To Reproduce:** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior:** +A clear and concise description of what you expected to happen. + +**Screenshots:** +If applicable, add screenshots to help explain your problem. + +**Device & App Information (Please complete the following):** +- Device: [e.g., Samsung Galaxy S23, Google Pixel 7] +- Android Version: [e.g., Android 12, Android 13] +- App Version: [e.g., 1.0.1, v1.0.2] + +**Additional context:** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..ae26d6f2b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: ✨ Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/support_request.md b/.github/ISSUE_TEMPLATE/support_request.md new file mode 100644 index 000000000..0119f9273 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support_request.md @@ -0,0 +1,46 @@ +--- +name: 🆘 Support Request +about: Ask a question or get help with usage. +title: "[Support]: " +labels: ["support", "question"] +assignees: [] +--- + + + +**What do you need help with?** + +Is this a question about how to do something, a configuration problem, or a general issue you can't solve? + +**Describe the issue/question:** + +Clearly describe what you are trying to achieve, what problem you are facing, or what question you have. + +**What have you tried so far? (Optional):** + +List any steps you've already taken to troubleshoot, find information, or attempt a solution. + +**Expected outcome (Optional):** + +If applicable, what did you hope would happen, or what solution are you looking for? + +**Screenshots/Videos (Optional):** + +If applicable, add screenshots or a short video that might help explain your situation. + +**Environment & Details:** + +Please provide details about your operating environment, relevant URLs, or any messages you see. + +- **Operating System:** +- **Browser & Version (if applicable):** +- **Any relevant messages (e.g., from UI, console):** + ``` + PASTE_ANY_MESSAGES_HERE + ``` + +**Any additional context?:** + +Is there anything else that might be useful for us to know? diff --git a/.github/workflows/build_android.yaml b/.github/workflows/build_android.yaml new file mode 100644 index 000000000..1515255fa --- /dev/null +++ b/.github/workflows/build_android.yaml @@ -0,0 +1,29 @@ +name: Build Android APK + +on: + workflow_dispatch: + push: + branches: [ "main" ] + paths: + - 'Android/**' + pull_request: + branches: [ "main" ] + paths: + - 'Android/**' + +jobs: + build_apk: + name: Build Android APK + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./Android/src + steps: + - name: Checkout the source code + uses: actions/checkout@v3 + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + - name: Build + run: ./gradlew assembleRelease diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 000000000..d887f6e82 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,51 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + paths: + - 'skills/**' + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + # Create the nested directory structure + - name: Prepare for nested deployment + run: | + mkdir -p staging/skills + # Move all files (except the staging folder itself) into the subdirectory + rsync -rr --exclude='staging' ./skills/ staging/skills/ + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: 'staging/' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/Android/.gitignore b/Android/.gitignore index 1ce5acbd6..3c5bc8e2b 100644 --- a/Android/.gitignore +++ b/Android/.gitignore @@ -1,3 +1,19 @@ +# @license +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + # Gradle files .gradle/ build/ diff --git a/Android/README.md b/Android/README.md index 30c24d9af..64790d0c8 100644 --- a/Android/README.md +++ b/Android/README.md @@ -1 +1 @@ -# AI Edge Gallery (Android) +# Google AI Edge Gallery (Android) diff --git a/Android/src/.gitignore b/Android/src/.gitignore index aa724b770..8b6602cff 100644 --- a/Android/src/.gitignore +++ b/Android/src/.gitignore @@ -1,4 +1,20 @@ +# @license +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== *.iml + .gradle /local.properties /.idea/caches diff --git a/Android/src/app/.gitignore b/Android/src/app/.gitignore index 956c004dc..61b3747c8 100644 --- a/Android/src/app/.gitignore +++ b/Android/src/app/.gitignore @@ -1,2 +1,18 @@ +# @license +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + /build /release \ No newline at end of file diff --git a/Android/src/app/build.gradle.kts b/Android/src/app/build.gradle.kts index d21b0092e..ff6e6e2cf 100644 --- a/Android/src/app/build.gradle.kts +++ b/Android/src/app/build.gradle.kts @@ -16,24 +16,35 @@ plugins { alias(libs.plugins.android.application) + // Note: set apply to true to enable google-services (requires google-services.json). + alias(libs.plugins.google.services) apply false alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.protobuf) + alias(libs.plugins.hilt.application) + alias(libs.plugins.oss.licenses) + alias(libs.plugins.ksp) + kotlin("kapt") } android { - namespace = "com.google.aiedge.gallery" + namespace = "com.google.ai.edge.gallery" compileSdk = 35 defaultConfig { applicationId = "com.google.aiedge.gallery" - minSdk = 24 + minSdk = 31 targetSdk = 35 - versionCode = 1 - versionName = "20250428" + versionCode = 30 + versionName = "1.0.13" // Needed for HuggingFace auth workflows. - manifestPlaceholders["appAuthRedirectScheme"] = "com.google.aiedge.gallery.oauth" + // Use the scheme of the "Redirect URLs" in HuggingFace app. + manifestPlaceholders["appAuthRedirectScheme"] = + "REPLACE_WITH_YOUR_REDIRECT_SCHEME_IN_HUGGINGFACE_APP" + manifestPlaceholders["applicationName"] = "com.google.ai.edge.gallery.GalleryApplication" + manifestPlaceholders["appIcon"] = "@mipmap/ic_launcher" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -41,10 +52,7 @@ android { buildTypes { release { isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") signingConfig = signingConfigs.getByName("debug") } } @@ -73,14 +81,15 @@ dependencies { implementation(libs.androidx.material3) implementation(libs.androidx.compose.navigation) implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlin.reflect) implementation(libs.material.icon.extended) implementation(libs.androidx.work.runtime) - implementation(libs.androidx.datastore.preferences) + implementation(libs.androidx.datastore) implementation(libs.com.google.code.gson) implementation(libs.androidx.lifecycle.process) - implementation(libs.mediapipe.tasks.text) - implementation(libs.mediapipe.tasks.genai) - implementation(libs.mediapipe.tasks.imagegen) + implementation(libs.androidx.security.crypto) + implementation(libs.androidx.webkit) + implementation(libs.litertlm) implementation(libs.commonmark) implementation(libs.richtext) implementation(libs.tflite) @@ -92,11 +101,29 @@ dependencies { implementation(libs.camerax.view) implementation(libs.openid.appauth) implementation(libs.androidx.splashscreen) + implementation(libs.protobuf.javalite) + implementation(libs.hilt.android) + implementation(libs.hilt.navigation.compose) + implementation(libs.play.services.oss.licenses) + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.analytics) + implementation(libs.firebase.messaging) + implementation(libs.androidx.exifinterface) + implementation(libs.moshi.kotlin) + kapt(libs.hilt.android.compiler) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.ui.test.junit4) + androidTestImplementation(libs.hilt.android.testing) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) -} \ No newline at end of file + ksp(libs.moshi.kotlin.codegen) + implementation(libs.mlkit.genai.prompt) +} + +protobuf { + protoc { artifact = "com.google.protobuf:protoc:4.26.1" } + generateProtoTasks { all().forEach { it.plugins { create("java") { option("lite") } } } } +} diff --git a/Android/src/app/proguard-rules.pro b/Android/src/app/proguard-rules.pro deleted file mode 100644 index 481bb4348..000000000 --- a/Android/src/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/Android/src/app/src/androidTest/java/com/google/aiedge/gallery/ExampleInstrumentedTest.kt b/Android/src/app/src/androidTest/java/com/google/aiedge/gallery/ExampleInstrumentedTest.kt deleted file mode 100644 index f26352af1..000000000 --- a/Android/src/app/src/androidTest/java/com/google/aiedge/gallery/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.aiedge.gallery - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.google.aiedge.gallery", appContext.packageName) - } -} \ No newline at end of file diff --git a/Android/src/app/src/main/AndroidManifest.xml b/Android/src/app/src/main/AndroidManifest.xml index 4d3c83203..cec1b2edf 100644 --- a/Android/src/app/src/main/AndroidManifest.xml +++ b/Android/src/app/src/main/AndroidManifest.xml @@ -16,32 +16,68 @@ --> + + + + + - + + + + + + + + + + + + + + + + + + + + + android:screenOrientation="portrait" + android:windowSoftInputMode="adjustResize" + android:configChanges="uiMode" + tools:ignore="DiscouragedApi,LockedOrientationActivity"> @@ -55,19 +91,19 @@ - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/Android/src/app/src/main/assets/skills/calculate-hash/SKILL.md b/Android/src/app/src/main/assets/skills/calculate-hash/SKILL.md new file mode 100644 index 000000000..f1545e07b --- /dev/null +++ b/Android/src/app/src/main/assets/skills/calculate-hash/SKILL.md @@ -0,0 +1,21 @@ +--- +name: calculate-hash +description: Calculate the hash of a given text. +--- + +# Calculate hash + +This skill calculates the hash of a given text. + +## Examples + +* "Calculate hash of..." +* "What is the hash of..." + +## Instructions + +Call the `run_js` tool with the following exact parameters: + +- script name: `index.html` +- data: A JSON string with the following field + - text: the text to calculate hash for diff --git a/Android/src/app/src/main/assets/skills/calculate-hash/scripts/index.html b/Android/src/app/src/main/assets/skills/calculate-hash/scripts/index.html new file mode 100644 index 000000000..f8b25e6fe --- /dev/null +++ b/Android/src/app/src/main/assets/skills/calculate-hash/scripts/index.html @@ -0,0 +1,30 @@ + + + + + + + + + Calculate hash + + + + + diff --git a/Android/src/app/src/main/assets/skills/calculate-hash/scripts/index.js b/Android/src/app/src/main/assets/skills/calculate-hash/scripts/index.js new file mode 100644 index 000000000..7dd46b47f --- /dev/null +++ b/Android/src/app/src/main/assets/skills/calculate-hash/scripts/index.js @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +async function digestMessage(message) { + const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array + const hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8); // hash the message + const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); // convert bytes to hex string + return {result: hashHex}; +} + +window['ai_edge_gallery_get_result'] = async (data) => { + try { + const jsonData = JSON.parse(data); + return JSON.stringify(await digestMessage(jsonData['text'])); + } catch (e) { + console.error(e); + return JSON.stringify({error: `Failed to calculate hash: ${e.message}`}); + } +}; diff --git a/Android/src/app/src/main/assets/skills/create-calendar-event/SKILL.md b/Android/src/app/src/main/assets/skills/create-calendar-event/SKILL.md new file mode 100644 index 000000000..6144c0b2b --- /dev/null +++ b/Android/src/app/src/main/assets/skills/create-calendar-event/SKILL.md @@ -0,0 +1,22 @@ +--- +name: create-calendar-event +description: Create a calendar event. +--- + +# Create calendar event + +## Instructions +To schedule an event, you must follow these exact steps: +1. First, call the `run_intent` tool with `intent` as `get_current_date_and_time` and `parameters` as `{}` to get the user's local date, time, and the current day of the week. +2. Before creating the event, you must explicitly calculate the exact date in your response. Write out: + - Today's exact date and day of the week. + - The target day or relative time requested by the user (e.g., "tomorrow", "this Friday"). + - The exact number of days you need to add to today's date. + - The final calculated dates, ensuring you correctly roll over to the next month or year if the added days exceed the days in the current month. +3. Once you have calculated the correct dates, call the `run_intent` tool with the following exact parameters: + - `intent`: create_calendar_event + - `parameters`: A JSON string with the following fields: + - `title`: the title of the event. String. + - `description`: the description of the event. String. + - `begin_time`: the start time of the event in YYYY-MM-DDTHH:MM:SS format. String. + - `end_time`: the end time of the event in YYYY-MM-DDTHH:MM:SS format. String. diff --git a/Android/src/app/src/main/assets/skills/interactive-map/SKILL.md b/Android/src/app/src/main/assets/skills/interactive-map/SKILL.md new file mode 100644 index 000000000..e55c33bc8 --- /dev/null +++ b/Android/src/app/src/main/assets/skills/interactive-map/SKILL.md @@ -0,0 +1,18 @@ +--- +name: interactive-map +description: Show an interactive map view for the given location. +--- + +# Interactive map + +## Examples + +- "Show [a place] on interactive map" +- "Find [a place] on interactive map" + +## Instructions + +Call the `run_js` tool with the following exact parameters: + +- data: A JSON string with the following field + - location: The location to show on the map. diff --git a/Android/src/app/src/main/assets/skills/interactive-map/scripts/index.html b/Android/src/app/src/main/assets/skills/interactive-map/scripts/index.html new file mode 100644 index 000000000..69fa121bf --- /dev/null +++ b/Android/src/app/src/main/assets/skills/interactive-map/scripts/index.html @@ -0,0 +1,48 @@ + + + + + + + + + Interactive map + + + + + + diff --git a/Android/src/app/src/main/assets/skills/kitchen-adventure/SKILL.md b/Android/src/app/src/main/assets/skills/kitchen-adventure/SKILL.md new file mode 100644 index 000000000..958b0673e --- /dev/null +++ b/Android/src/app/src/main/assets/skills/kitchen-adventure/SKILL.md @@ -0,0 +1,55 @@ +--- +name: kitchen-adventure +description: Act as a dungeon master for a text-based adventure set in a world where everyone is a sentient kitchen appliance. Trigger when user says "start kitchen adventure". +--- + +# Kitchen Adventure + +## Instructions + +When the user initiates a session, you must transform into the +**Head Chef (DM)**. Follow these operational rules to maintain the +"Micro-Cosmos" immersion: + +* **World-Building (The Kitchen-Scale):** Every location is a kitchen zone + reimagined as an epic landscape. + * The "Stainless Steel Plains" (the countertop). + * The "Tundra of the Sub-Zero" (the freezer). + * The "Caverns of the Under-Sink" (storage). +* +* **Appliance Physics:** Characters move and interact based on their real-world + functions. + * A Toaster "dashes" by popping up. + * A Blender "rages" by spinning its blades. + * A Fridge is a lumbering, cold-hearted giant. + +* **The Narrative Boundary:** **Never** write the player's dialogue or actions. + Describe the world's reaction to their input, then stop and wait for their + turn. + +* **Dynamic Stakes:** Scale household hazards into high-level threats. A spilled + glass of juice is a "Citrus Flash Flood"; a stray fork is a "Fallen Titan's + Spear." + +* **Tone:** Maintain a "Serious-Whimsical" tone. Treat a quest for the "Sacred + Sourdough Starter" with the same gravity as a quest for the Holy Grail. + +## Output Format + +Every DM response must use the following structure to ensure gameplay clarity: + +### [Current Location Name] + +*A vivid, sensory description of the area (e.g., "The air here smells of burnt +toast and ancient grease").* +*Limited to only 1 short sentence* + +--- + +**The Situation:** +(Describe the immediate scene, any NPCs present, and any obstacles or threats.) +*Limited to only 1-2 short sentences* + +**What do you do?** +(Provide a brief prompt or 3 suggested actions to keep the momentum going.) +After user replies, continue the adventure. diff --git a/Android/src/app/src/main/assets/skills/mood-tracker/SKILL.md b/Android/src/app/src/main/assets/skills/mood-tracker/SKILL.md new file mode 100644 index 000000000..0b9fee01e --- /dev/null +++ b/Android/src/app/src/main/assets/skills/mood-tracker/SKILL.md @@ -0,0 +1,123 @@ +--- +name: mood-tracker +description: A simple mood tracking skill that stores your daily mood and comments. Use this when the user wants to log their mood, track how they feel, or see their mood history. +--- + +# Mood Tracker + +## Instructions + +The `mood-tracker` skill helps you keep track of your daily emotional well-being. You can log your mood on a scale of 1 to 10 and add a short comment about how you're feeling. + +### Actions + +#### 1. Log Mood +When a user wants to log their mood, call the `run_js` tool with: +- **script name**: `index.html` +- **data**: A JSON string with: + - `action`: "log_mood" + - `score`: Number (1-10) + - `comment`: String (Optional) + - `date`: String. **IMPORTANT**: Identify the date for the entry. + - If user says "today", pass "today". + - If user says "yesterday", pass "yesterday". + - If user gives a specific date (e.g., "March 18"), format it as **YYYY-MM-DD** or pass the original date string. + - If no date is mentioned, default to "today". + +#### 2. Get Mood for a Specific Date +When a user asks what their mood was on a specific date, call the `run_js` tool with: +- **script name**: `index.html` +- **data**: A JSON string with: + - `action`: "get_mood" + - `date`: String (Identify the date from the user's request) + +#### 3. Get History / Show Dashboard +When a user wants to see their mood history ("last week", "past 10 days") or the dashboard, call the `run_js` tool with: +- **script name**: `index.html` +- **data**: A JSON string with: + - `action`: "get_history" + - `days`: Number (Optional, default 7. E.g., for "last week" use 7) + - `show_dashboard`: Boolean (Optional) + +#### 4. Plot Mood Trends (Line Chart) +When a user wants to visualize their mood trends with a chart (e.g., "Plot my mood for 7 days"), call the `run_js` tool with: +- **script name**: `index.html` +- **data**: A JSON string with: + - `action`: "get_history" + - `days`: Number (Optional, default 7) + - `show_dashboard`: `true` + - **TIP**: This will trigger the plotting view in the dashboard. + +#### 5. Analyze Trends and Patterns +When a user asks for an analysis of their mood (e.g., "Are there any trends?", "Am I feeling better?"), follow these steps: +1. Call `run_js` with `action: "get_history"` and an appropriate `days` count (e.g., 30 for a monthly analysis). +2. Once you receive the JSON history, analyze the scores and comments. +3. Provide a thoughtful response to the user covering: + - General trend (improving, declining, stable). + - Any clusters of particularly good or bad days. + - Themes or patterns found in the comments. + +#### 6. Delete Mood for a Specific Date +When a user wants to delete only a single day's entry (e.g., "Delete my mood for today"), call the `run_js` tool with: +- **script name**: `index.html` +- **data**: A JSON string with: + - `action`: "delete_mood" + - `date`: String (Identify the date) + +#### 7. Export Data (Backup) +When a user wants to backup or export their data, call the `run_js` tool with: +- **script name**: `index.html` +- **data**: A JSON string with: + - `action`: "export_data" + +#### 8. Wipe All Data +When a user wants to clear their entire mood history and start fresh, call the `run_js` tool with: +- **script name**: `index.html` +- **data**: A JSON string with: + - `action`: "wipe_data" + +### Sample Commands + +You can use these samples to interact with the mood tracker: + +- **Logging Mood:** + - "Log my mood as 8 today, feeling great!" + - "Set my mood yesterday as a 2" + - "Set my mood on March 18, 2026 as a 1" + - "I'm feeling like a 5 today, a bit tired." + - "Last Friday I felt like a 7." + - "Record a mood of 9 for me." + +- **Viewing History:** + - "Show me my mood history." + - "Get my mood from last week." + - "How have I been feeling lately?" + - "Show my mood for the last 10 days." + - "Open the mood dashboard." + - "What was my mood on March 18?" + - "What was my mood yesterday?" + +- **Analyzing Trends:** + - "Analyze my mood for the last 30 days — are there any patterns?" + - "Am I generally feeling better or worse over time?" + - "Are there any clusters of bad days in my history?" + - "What do my recent comments suggest about my well-being?" + +- **Wiping & Deleting:** + - "Delete my mood for today." + - "Remove my mood log for yesterday." + - "Delete the entry for March 18." + - "Clear my mood history." (Use `wipe_data` for this) + - "Wipe my data." (Use `wipe_data` for this) + +- **Charting Trends:** + - "Plot my mood for the last 7 days." + - "Show me a chart of my mood this month." + - "Visualize my scores for the past 14 days." + - "Graph my mood progress." + +### Rules +- **Privacy**: All data is stored locally on your device. +- **No Entry**: If no mood entry exists for a specific date requested, explicitly inform the user that no entry was found for that date. +- **Updates**: Logging a mood for a date that already has an entry will update that entry. +- **Dashboard**: The dashboard is only shown when you explicitly ask to see your history or the dashboard itself. diff --git a/Android/src/app/src/main/assets/skills/mood-tracker/assets/dashboard.html b/Android/src/app/src/main/assets/skills/mood-tracker/assets/dashboard.html new file mode 100644 index 000000000..927f15d27 --- /dev/null +++ b/Android/src/app/src/main/assets/skills/mood-tracker/assets/dashboard.html @@ -0,0 +1,609 @@ + + + + + + + + Mood Tracker Dashboard + + + + + +
+
+

Mood Tracker

+
+ +
+ +
...
+ +
+ +
+
+
-
+
Score
+
+
No notes for today.
+
+ + +
+
Mood Trend Chart
+ +
+ +
+
+ + Your Mood Journey +
+ +
+ + +
+ + +
+
+ + + + diff --git a/Android/src/app/src/main/assets/skills/mood-tracker/scripts/index.html b/Android/src/app/src/main/assets/skills/mood-tracker/scripts/index.html new file mode 100644 index 000000000..0873040d0 --- /dev/null +++ b/Android/src/app/src/main/assets/skills/mood-tracker/scripts/index.html @@ -0,0 +1,150 @@ + + + + + + + + diff --git a/Android/src/app/src/main/assets/skills/qr-code/SKILL.md b/Android/src/app/src/main/assets/skills/qr-code/SKILL.md new file mode 100644 index 000000000..98817b04f --- /dev/null +++ b/Android/src/app/src/main/assets/skills/qr-code/SKILL.md @@ -0,0 +1,11 @@ +--- +name: qr-code +description: Generates a QR code for the given url. +--- + +# Instructions + +You MUST use the `run_js` tool with the following exact parameters: + +- data: A JSON string with the following fields: + - url: String - the url to create QR code for diff --git a/Android/src/app/src/main/assets/skills/qr-code/scripts/index.html b/Android/src/app/src/main/assets/skills/qr-code/scripts/index.html new file mode 100644 index 000000000..12ba2017c --- /dev/null +++ b/Android/src/app/src/main/assets/skills/qr-code/scripts/index.html @@ -0,0 +1,108 @@ + + + + + + + + QR Code Generator + + + + + + + + + + diff --git a/Android/src/app/src/main/assets/skills/query-wikipedia/SKILL.md b/Android/src/app/src/main/assets/skills/query-wikipedia/SKILL.md new file mode 100644 index 000000000..2d3db0110 --- /dev/null +++ b/Android/src/app/src/main/assets/skills/query-wikipedia/SKILL.md @@ -0,0 +1,17 @@ +--- +name: query-wikipedia +description: Query summary from Wikipedia for a given topic. +--- + +# Query Wiki + +## Instructions + +Call the `run_js` tool using `index.html` and a JSON string for `data` with the following fields: +- **topic**: Required. Extract ONLY the primary entity, person, or event (e.g., "2026 Oscars", "Albert Einstein"). You MUST REMOVE all specific question details, action words, or conversational text (e.g., do NOT include words like "winner", "best picture", "who won", "history of"). Search for the broad subject so the tool can return the main article. +- **lang**: Required. The 2-letter language code. This code MUST match the language of the keywords you provided in the `topic` field. Use standard codes, e.g., "en" (English), "es" (Spanish), "zh" (Chinese), "fr" (French), "de" (German), "ja" (Japanese), "ko" (Korean), "it" (Italian), "pt" (Portuguese), "ru" (Russian), "ar" (Arabic), "hi" (Hindi). + +**Constraints:** +- Provide a concise summary (1-3 complete sentences) to conserve context. Always ensure your response ends with a finished sentence. your response MUST BE written in the SAME language as the user's original prompt. +- For recurring events or time-sensitive facts, query the specific iteration (e.g., "2026 Oscars"). If the user omits the year, default to the current year. +- If the exact answer to the user's question is not found in the extract, briefly state this, then proactively offer a related piece of information that *was* found in the text. \ No newline at end of file diff --git a/Android/src/app/src/main/assets/skills/query-wikipedia/scripts/index.html b/Android/src/app/src/main/assets/skills/query-wikipedia/scripts/index.html new file mode 100644 index 000000000..3bf01c630 --- /dev/null +++ b/Android/src/app/src/main/assets/skills/query-wikipedia/scripts/index.html @@ -0,0 +1,143 @@ + + + + + + + Query Wiki + + + + + diff --git a/Android/src/app/src/main/assets/skills/schedule-notification/SKILL.md b/Android/src/app/src/main/assets/skills/schedule-notification/SKILL.md new file mode 100644 index 000000000..ab45112d7 --- /dev/null +++ b/Android/src/app/src/main/assets/skills/schedule-notification/SKILL.md @@ -0,0 +1,27 @@ +--- +name: schedule-notification +description: Schedule a notification for a specific date or repeating daily. +--- + +# Schedule Notification + +## Instructions + +To schedule a notification, you must follow these exact steps: +1. First, if the notification doesn't need to repeat daily, call the `run_intent` tool with `intent` as `get_current_date_and_time` and `parameters` as `{}` to get the user's local date and time. Then explicitly calculate the scheduling date and time in your response. Write out: +- Today's exact date. +- The target day or relative time requested by the user (e.g., "tomorrow", "this Friday"). +- The exact number of days you need to add to today's date. +- The final calculated dates, ensuring you correctly roll over to the next month or year if the added days exceed the days in the current month. +2. Call the `run_intent` tool with the following exact parameters: +- intent: schedule_notification +- parameters: A JSON string with the following fields: + - title: the title of the notification. String. + - message: the message content of the notification. String. + - hour: the hour of the day (0-23) for the notification. Integer. + - minute: the minute of the hour (0-59) for the notification. Integer. + - deeplink: (optional) the deeplink URI to open when the notification is tapped. String. + - year: (optional) the year for the notification. Integer. + - month: (optional) the month (1-12) for the notification. Integer. + - day: (optional) the day of the month (1-31) for the notification. Integer. + - repeat_daily: (optional) true if the notification should repeat daily at this time. Boolean. diff --git a/Android/src/app/src/main/assets/skills/send-email/SKILL.md b/Android/src/app/src/main/assets/skills/send-email/SKILL.md new file mode 100644 index 000000000..97f2b8cec --- /dev/null +++ b/Android/src/app/src/main/assets/skills/send-email/SKILL.md @@ -0,0 +1,16 @@ +--- +name: send-email +description: Send an email. +--- + +# Send email + +## Instructions + +Call the `run_intent` tool with the following exact parameters: + +- intent: send_email +- parameters: A JSON string with the following fields: + - extra_email: the email address to send the email to. String. + - extra_subject: the subject of the email. String. + - extra_text: the body of the email. String. \ No newline at end of file diff --git a/Android/src/app/src/main/assets/skills/text-spinner/SKILL.md b/Android/src/app/src/main/assets/skills/text-spinner/SKILL.md new file mode 100644 index 000000000..558ab6195 --- /dev/null +++ b/Android/src/app/src/main/assets/skills/text-spinner/SKILL.md @@ -0,0 +1,11 @@ +--- +name: text-spinner +description: Spin the given text on my head. +--- + +# Instructions + +You MUST use the `run_js` tool with the following exact parameters: + +- data: A JSON string with the following fields: + - label: The text string to spin on my head. diff --git a/Android/src/app/src/main/assets/skills/text-spinner/assets/webview.html b/Android/src/app/src/main/assets/skills/text-spinner/assets/webview.html new file mode 100644 index 000000000..97e6da4c5 --- /dev/null +++ b/Android/src/app/src/main/assets/skills/text-spinner/assets/webview.html @@ -0,0 +1,157 @@ + + + + + + + Full-Screen 3D Head Tracker + + + + +
Syncing Neural Engine...
+ +
+ + +
Label
+
+ + + + diff --git a/Android/src/app/src/main/assets/skills/text-spinner/scripts/index.html b/Android/src/app/src/main/assets/skills/text-spinner/scripts/index.html new file mode 100644 index 000000000..07504285f --- /dev/null +++ b/Android/src/app/src/main/assets/skills/text-spinner/scripts/index.html @@ -0,0 +1,39 @@ + + + + + + + + + Create github issue + + + + + diff --git a/Android/src/app/src/main/assets/tinygarden/04B_03.ttf b/Android/src/app/src/main/assets/tinygarden/04B_03.ttf new file mode 100644 index 000000000..fe4328b6a Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/04B_03.ttf differ diff --git a/Android/src/app/src/main/assets/tinygarden/atlas.png b/Android/src/app/src/main/assets/tinygarden/atlas.png new file mode 100644 index 000000000..b4facf7b9 Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/atlas.png differ diff --git a/Android/src/app/src/main/assets/tinygarden/blue_svg.svg b/Android/src/app/src/main/assets/tinygarden/blue_svg.svg new file mode 100644 index 000000000..2ac3eea89 --- /dev/null +++ b/Android/src/app/src/main/assets/tinygarden/blue_svg.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/src/app/src/main/assets/tinygarden/check_status.mp3 b/Android/src/app/src/main/assets/tinygarden/check_status.mp3 new file mode 100644 index 000000000..703db75e4 Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/check_status.mp3 differ diff --git a/Android/src/app/src/main/assets/tinygarden/close.mp3 b/Android/src/app/src/main/assets/tinygarden/close.mp3 new file mode 100644 index 000000000..5ada317c0 Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/close.mp3 differ diff --git a/Android/src/app/src/main/assets/tinygarden/daisy.png b/Android/src/app/src/main/assets/tinygarden/daisy.png new file mode 100644 index 000000000..760d69443 Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/daisy.png differ diff --git a/Android/src/app/src/main/assets/tinygarden/dirt.mp3 b/Android/src/app/src/main/assets/tinygarden/dirt.mp3 new file mode 100644 index 000000000..6bf67f315 Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/dirt.mp3 differ diff --git a/Android/src/app/src/main/assets/tinygarden/error.mp3 b/Android/src/app/src/main/assets/tinygarden/error.mp3 new file mode 100644 index 000000000..9ce8ecf7b Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/error.mp3 differ diff --git a/Android/src/app/src/main/assets/tinygarden/gallery.svg b/Android/src/app/src/main/assets/tinygarden/gallery.svg new file mode 100644 index 000000000..481125594 --- /dev/null +++ b/Android/src/app/src/main/assets/tinygarden/gallery.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Android/src/app/src/main/assets/tinygarden/gallery_with_border.svg b/Android/src/app/src/main/assets/tinygarden/gallery_with_border.svg new file mode 100644 index 000000000..7da3caf1f --- /dev/null +++ b/Android/src/app/src/main/assets/tinygarden/gallery_with_border.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/Android/src/app/src/main/assets/tinygarden/goal.mp3 b/Android/src/app/src/main/assets/tinygarden/goal.mp3 new file mode 100644 index 000000000..52d2b1c2b Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/goal.mp3 differ diff --git a/Android/src/app/src/main/assets/tinygarden/goal_gallery.mp3 b/Android/src/app/src/main/assets/tinygarden/goal_gallery.mp3 new file mode 100644 index 000000000..f8702fa5d Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/goal_gallery.mp3 differ diff --git a/Android/src/app/src/main/assets/tinygarden/green_svg.svg b/Android/src/app/src/main/assets/tinygarden/green_svg.svg new file mode 100644 index 000000000..f3e0dd3ea --- /dev/null +++ b/Android/src/app/src/main/assets/tinygarden/green_svg.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/Android/src/app/src/main/assets/tinygarden/index.html b/Android/src/app/src/main/assets/tinygarden/index.html new file mode 100644 index 000000000..39c102da1 --- /dev/null +++ b/Android/src/app/src/main/assets/tinygarden/index.html @@ -0,0 +1,30 @@ + + + + + + + Tiny Garden + + + + + + + diff --git a/Android/src/app/src/main/assets/tinygarden/inventory.mp3 b/Android/src/app/src/main/assets/tinygarden/inventory.mp3 new file mode 100644 index 000000000..699d97f43 Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/inventory.mp3 differ diff --git a/Android/src/app/src/main/assets/tinygarden/main-K5DSW5YL.js b/Android/src/app/src/main/assets/tinygarden/main-K5DSW5YL.js new file mode 100644 index 000000000..3587add80 --- /dev/null +++ b/Android/src/app/src/main/assets/tinygarden/main-K5DSW5YL.js @@ -0,0 +1,19 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var qg=Object.defineProperty,Yg=Object.defineProperties;var Zg=Object.getOwnPropertyDescriptors;var wu=Object.getOwnPropertySymbols;var Kg=Object.prototype.hasOwnProperty,Qg=Object.prototype.propertyIsEnumerable;var Tu=(e,t,n)=>t in e?qg(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,y=(e,t)=>{for(var n in t||={})Kg.call(t,n)&&Tu(e,n,t[n]);if(wu)for(var n of wu(t))Qg.call(t,n)&&Tu(e,n,t[n]);return e},P=(e,t)=>Yg(e,Zg(t));var Qs;function Oo(){return Qs}function qe(e){let t=Qs;return Qs=e,t}var Mu=Symbol("NotFound");function Sn(e){return e===Mu||e?.name==="\u0275NotFound"}function jo(e,t){return Object.is(e,t)}var de=null,Po=!1,Xs=1,Xg=null,oe=Symbol("SIGNAL");function _(e){let t=de;return de=e,t}function Ho(){return de}var $t={version:0,lastCleanEpoch:0,dirty:!1,producers:void 0,producersTail:void 0,consumers:void 0,consumersTail:void 0,recomputing:!1,consumerAllowSignalWrites:!1,consumerIsAlwaysLive:!1,kind:"unknown",producerMustRecompute:()=>!1,producerRecomputeValue:()=>{},consumerMarkedDirty:()=>{},consumerOnSignalRead:()=>{}};function bn(e){if(Po)throw new Error("");if(de===null)return;de.consumerOnSignalRead(e);let t=de.producersTail;if(t!==void 0&&t.producer===e)return;let n,r=de.recomputing;if(r&&(n=t!==void 0?t.nextProducer:de.producers,n!==void 0&&n.producer===e)){de.producersTail=n,n.lastReadVersion=e.version;return}let o=e.consumersTail;if(o!==void 0&&o.consumer===de&&(!r||em(o,de)))return;let i=Tn(de),s={producer:e,consumer:de,nextProducer:n,prevConsumer:o,lastReadVersion:e.version,nextConsumer:void 0};de.producersTail=s,t!==void 0?t.nextProducer=s:de.producers=s,i&&Ru(e,s)}function xu(){Xs++}function Bo(e){if(!(Tn(e)&&!e.dirty)&&!(!e.dirty&&e.lastCleanEpoch===Xs)){if(!e.producerMustRecompute(e)&&!wn(e)){Fo(e);return}e.producerRecomputeValue(e),Fo(e)}}function Js(e){if(e.consumers===void 0)return;let t=Po;Po=!0;try{for(let n=e.consumers;n!==void 0;n=n.nextConsumer){let r=n.consumer;r.dirty||Jg(r)}}finally{Po=t}}function ea(){return de?.consumerAllowSignalWrites!==!1}function Jg(e){e.dirty=!0,Js(e),e.consumerMarkedDirty?.(e)}function Fo(e){e.dirty=!1,e.lastCleanEpoch=Xs}function Gt(e){return e&&Nu(e),_(e)}function Nu(e){e.producersTail=void 0,e.recomputing=!0}function _n(e,t){_(t),e&&Au(e)}function Au(e){e.recomputing=!1;let t=e.producersTail,n=t!==void 0?t.nextProducer:e.producers;if(n!==void 0){if(Tn(e))do n=ta(n);while(n!==void 0);t!==void 0?t.nextProducer=void 0:e.producers=void 0}}function wn(e){for(let t=e.producers;t!==void 0;t=t.nextProducer){let n=t.producer,r=t.lastReadVersion;if(r!==n.version||(Bo(n),r!==n.version))return!0}return!1}function zt(e){if(Tn(e)){let t=e.producers;for(;t!==void 0;)t=ta(t)}e.producers=void 0,e.producersTail=void 0,e.consumers=void 0,e.consumersTail=void 0}function Ru(e,t){let n=e.consumersTail,r=Tn(e);if(n!==void 0?(t.nextConsumer=n.nextConsumer,n.nextConsumer=t):(t.nextConsumer=void 0,e.consumers=t),t.prevConsumer=n,e.consumersTail=t,!r)for(let o=e.producers;o!==void 0;o=o.nextProducer)Ru(o.producer,o)}function ta(e){let t=e.producer,n=e.nextProducer,r=e.nextConsumer,o=e.prevConsumer;if(e.nextConsumer=void 0,e.prevConsumer=void 0,r!==void 0?r.prevConsumer=o:t.consumersTail=o,o!==void 0)o.nextConsumer=r;else if(t.consumers=r,!Tn(t)){let i=t.producers;for(;i!==void 0;)i=ta(i)}return n}function Tn(e){return e.consumerIsAlwaysLive||e.consumers!==void 0}function Vo(e){Xg?.(e)}function em(e,t){let n=t.producersTail;if(n!==void 0){let r=t.producers;do{if(r===e)return!0;if(r===n)break;r=r.nextProducer}while(r!==void 0)}return!1}function Tr(e,t){let n=Object.create(tm);n.computation=e,t!==void 0&&(n.equal=t);let r=()=>{if(Bo(n),bn(n),n.value===wr)throw n.error;return n.value};return r[oe]=n,Vo(n),r}var ko=Symbol("UNSET"),Lo=Symbol("COMPUTING"),wr=Symbol("ERRORED"),tm=P(y({},$t),{value:ko,dirty:!0,error:null,equal:jo,kind:"computed",producerMustRecompute(e){return e.value===ko||e.value===Lo},producerRecomputeValue(e){if(e.value===Lo)throw new Error("");let t=e.value;e.value=Lo;let n=Gt(e),r,o=!1;try{r=e.computation(),_(null),o=t!==ko&&t!==wr&&r!==wr&&e.equal(t,r)}catch(i){r=wr,e.error=i}finally{_n(e,n)}if(o){e.value=t;return}e.value=r,e.version++}});function nm(){throw new Error}var Ou=nm;function Pu(e){Ou(e)}function na(e){Ou=e}var rm=null;function ra(e,t){let n=Object.create(Uo);n.value=e,t!==void 0&&(n.equal=t);let r=()=>ku(n);return r[oe]=n,Vo(n),[r,s=>Mn(n,s),s=>oa(n,s)]}function ku(e){return bn(e),e.value}function Mn(e,t){ea()||Pu(e),e.equal(e.value,t)||(e.value=t,om(e))}function oa(e,t){ea()||Pu(e),Mn(e,t(e.value))}var Uo=P(y({},$t),{equal:jo,value:void 0,kind:"signal"});function om(e){e.version++,xu(),Js(e),rm?.(e)}function x(e){return typeof e=="function"}function xn(e){let n=e(r=>{Error.call(r),r.stack=new Error().stack});return n.prototype=Object.create(Error.prototype),n.prototype.constructor=n,n}var $o=xn(e=>function(n){e(this),this.message=n?`${n.length} errors occurred during unsubscription: +${n.map((r,o)=>`${o+1}) ${r.toString()}`).join(` + `)}`:"",this.name="UnsubscriptionError",this.errors=n});function Mr(e,t){if(e){let n=e.indexOf(t);0<=n&&e.splice(n,1)}}var q=class e{constructor(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}unsubscribe(){let t;if(!this.closed){this.closed=!0;let{_parentage:n}=this;if(n)if(this._parentage=null,Array.isArray(n))for(let i of n)i.remove(this);else n.remove(this);let{initialTeardown:r}=this;if(x(r))try{r()}catch(i){t=i instanceof $o?i.errors:[i]}let{_finalizers:o}=this;if(o){this._finalizers=null;for(let i of o)try{Lu(i)}catch(s){t=t??[],s instanceof $o?t=[...t,...s.errors]:t.push(s)}}if(t)throw new $o(t)}}add(t){var n;if(t&&t!==this)if(this.closed)Lu(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(n=this._finalizers)!==null&&n!==void 0?n:[]).push(t)}}_hasParent(t){let{_parentage:n}=this;return n===t||Array.isArray(n)&&n.includes(t)}_addParent(t){let{_parentage:n}=this;this._parentage=Array.isArray(n)?(n.push(t),n):n?[n,t]:t}_removeParent(t){let{_parentage:n}=this;n===t?this._parentage=null:Array.isArray(n)&&Mr(n,t)}remove(t){let{_finalizers:n}=this;n&&Mr(n,t),t instanceof e&&t._removeParent(this)}};q.EMPTY=(()=>{let e=new q;return e.closed=!0,e})();var ia=q.EMPTY;function Go(e){return e instanceof q||e&&"closed"in e&&x(e.remove)&&x(e.add)&&x(e.unsubscribe)}function Lu(e){x(e)?e():e.unsubscribe()}var He={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Nn={setTimeout(e,t,...n){let{delegate:r}=Nn;return r?.setTimeout?r.setTimeout(e,t,...n):setTimeout(e,t,...n)},clearTimeout(e){let{delegate:t}=Nn;return(t?.clearTimeout||clearTimeout)(e)},delegate:void 0};function zo(e){Nn.setTimeout(()=>{let{onUnhandledError:t}=He;if(t)t(e);else throw e})}function xr(){}var Fu=sa("C",void 0,void 0);function ju(e){return sa("E",void 0,e)}function Hu(e){return sa("N",e,void 0)}function sa(e,t,n){return{kind:e,value:t,error:n}}var Wt=null;function An(e){if(He.useDeprecatedSynchronousErrorHandling){let t=!Wt;if(t&&(Wt={errorThrown:!1,error:null}),e(),t){let{errorThrown:n,error:r}=Wt;if(Wt=null,n)throw r}}else e()}function Bu(e){He.useDeprecatedSynchronousErrorHandling&&Wt&&(Wt.errorThrown=!0,Wt.error=e)}var qt=class extends q{constructor(t){super(),this.isStopped=!1,t?(this.destination=t,Go(t)&&t.add(this)):this.destination=am}static create(t,n,r){return new Rn(t,n,r)}next(t){this.isStopped?ca(Hu(t),this):this._next(t)}error(t){this.isStopped?ca(ju(t),this):(this.isStopped=!0,this._error(t))}complete(){this.isStopped?ca(Fu,this):(this.isStopped=!0,this._complete())}unsubscribe(){this.closed||(this.isStopped=!0,super.unsubscribe(),this.destination=null)}_next(t){this.destination.next(t)}_error(t){try{this.destination.error(t)}finally{this.unsubscribe()}}_complete(){try{this.destination.complete()}finally{this.unsubscribe()}}},im=Function.prototype.bind;function aa(e,t){return im.call(e,t)}var la=class{constructor(t){this.partialObserver=t}next(t){let{partialObserver:n}=this;if(n.next)try{n.next(t)}catch(r){Wo(r)}}error(t){let{partialObserver:n}=this;if(n.error)try{n.error(t)}catch(r){Wo(r)}else Wo(t)}complete(){let{partialObserver:t}=this;if(t.complete)try{t.complete()}catch(n){Wo(n)}}},Rn=class extends qt{constructor(t,n,r){super();let o;if(x(t)||!t)o={next:t??void 0,error:n??void 0,complete:r??void 0};else{let i;this&&He.useDeprecatedNextContext?(i=Object.create(t),i.unsubscribe=()=>this.unsubscribe(),o={next:t.next&&aa(t.next,i),error:t.error&&aa(t.error,i),complete:t.complete&&aa(t.complete,i)}):o=t}this.destination=new la(o)}};function Wo(e){He.useDeprecatedSynchronousErrorHandling?Bu(e):zo(e)}function sm(e){throw e}function ca(e,t){let{onStoppedNotification:n}=He;n&&Nn.setTimeout(()=>n(e,t))}var am={closed:!0,next:xr,error:sm,complete:xr};var On=typeof Symbol=="function"&&Symbol.observable||"@@observable";function be(e){return e}function ua(...e){return da(e)}function da(e){return e.length===0?be:e.length===1?e[0]:function(n){return e.reduce((r,o)=>o(r),n)}}var V=(()=>{class e{constructor(n){n&&(this._subscribe=n)}lift(n){let r=new e;return r.source=this,r.operator=n,r}subscribe(n,r,o){let i=lm(n)?n:new Rn(n,r,o);return An(()=>{let{operator:s,source:a}=this;i.add(s?s.call(i,a):a?this._subscribe(i):this._trySubscribe(i))}),i}_trySubscribe(n){try{return this._subscribe(n)}catch(r){n.error(r)}}forEach(n,r){return r=Vu(r),new r((o,i)=>{let s=new Rn({next:a=>{try{n(a)}catch(c){i(c),s.unsubscribe()}},error:i,complete:o});this.subscribe(s)})}_subscribe(n){var r;return(r=this.source)===null||r===void 0?void 0:r.subscribe(n)}[On](){return this}pipe(...n){return da(n)(this)}toPromise(n){return n=Vu(n),new n((r,o)=>{let i;this.subscribe(s=>i=s,s=>o(s),()=>r(i))})}}return e.create=t=>new e(t),e})();function Vu(e){var t;return(t=e??He.Promise)!==null&&t!==void 0?t:Promise}function cm(e){return e&&x(e.next)&&x(e.error)&&x(e.complete)}function lm(e){return e&&e instanceof qt||cm(e)&&Go(e)}function fa(e){return x(e?.lift)}function j(e){return t=>{if(fa(t))return t.lift(function(n){try{return e(n,this)}catch(r){this.error(r)}});throw new TypeError("Unable to lift unknown Observable type")}}function H(e,t,n,r,o){return new pa(e,t,n,r,o)}var pa=class extends qt{constructor(t,n,r,o,i,s){super(t),this.onFinalize=i,this.shouldUnsubscribe=s,this._next=n?function(a){try{n(a)}catch(c){t.error(c)}}:super._next,this._error=o?function(a){try{o(a)}catch(c){t.error(c)}finally{this.unsubscribe()}}:super._error,this._complete=r?function(){try{r()}catch(a){t.error(a)}finally{this.unsubscribe()}}:super._complete}unsubscribe(){var t;if(!this.shouldUnsubscribe||this.shouldUnsubscribe()){let{closed:n}=this;super.unsubscribe(),!n&&((t=this.onFinalize)===null||t===void 0||t.call(this))}}};function Pn(){return j((e,t)=>{let n=null;e._refCount++;let r=H(t,void 0,void 0,void 0,()=>{if(!e||e._refCount<=0||0<--e._refCount){n=null;return}let o=e._connection,i=n;n=null,o&&(!i||o===i)&&o.unsubscribe(),t.unsubscribe()});e.subscribe(r),r.closed||(n=e.connect())})}var kn=class extends V{constructor(t,n){super(),this.source=t,this.subjectFactory=n,this._subject=null,this._refCount=0,this._connection=null,fa(t)&&(this.lift=t.lift)}_subscribe(t){return this.getSubject().subscribe(t)}getSubject(){let t=this._subject;return(!t||t.isStopped)&&(this._subject=this.subjectFactory()),this._subject}_teardown(){this._refCount=0;let{_connection:t}=this;this._subject=this._connection=null,t?.unsubscribe()}connect(){let t=this._connection;if(!t){t=this._connection=new q;let n=this.getSubject();t.add(this.source.subscribe(H(n,void 0,()=>{this._teardown(),n.complete()},r=>{this._teardown(),n.error(r)},()=>this._teardown()))),t.closed&&(this._connection=null,t=q.EMPTY)}return t}refCount(){return Pn()(this)}};var Uu=xn(e=>function(){e(this),this.name="ObjectUnsubscribedError",this.message="object unsubscribed"});var X=(()=>{class e extends V{constructor(){super(),this.closed=!1,this.currentObservers=null,this.observers=[],this.isStopped=!1,this.hasError=!1,this.thrownError=null}lift(n){let r=new qo(this,this);return r.operator=n,r}_throwIfClosed(){if(this.closed)throw new Uu}next(n){An(()=>{if(this._throwIfClosed(),!this.isStopped){this.currentObservers||(this.currentObservers=Array.from(this.observers));for(let r of this.currentObservers)r.next(n)}})}error(n){An(()=>{if(this._throwIfClosed(),!this.isStopped){this.hasError=this.isStopped=!0,this.thrownError=n;let{observers:r}=this;for(;r.length;)r.shift().error(n)}})}complete(){An(()=>{if(this._throwIfClosed(),!this.isStopped){this.isStopped=!0;let{observers:n}=this;for(;n.length;)n.shift().complete()}})}unsubscribe(){this.isStopped=this.closed=!0,this.observers=this.currentObservers=null}get observed(){var n;return((n=this.observers)===null||n===void 0?void 0:n.length)>0}_trySubscribe(n){return this._throwIfClosed(),super._trySubscribe(n)}_subscribe(n){return this._throwIfClosed(),this._checkFinalizedStatuses(n),this._innerSubscribe(n)}_innerSubscribe(n){let{hasError:r,isStopped:o,observers:i}=this;return r||o?ia:(this.currentObservers=null,i.push(n),new q(()=>{this.currentObservers=null,Mr(i,n)}))}_checkFinalizedStatuses(n){let{hasError:r,thrownError:o,isStopped:i}=this;r?n.error(o):i&&n.complete()}asObservable(){let n=new V;return n.source=this,n}}return e.create=(t,n)=>new qo(t,n),e})(),qo=class extends X{constructor(t,n){super(),this.destination=t,this.source=n}next(t){var n,r;(r=(n=this.destination)===null||n===void 0?void 0:n.next)===null||r===void 0||r.call(n,t)}error(t){var n,r;(r=(n=this.destination)===null||n===void 0?void 0:n.error)===null||r===void 0||r.call(n,t)}complete(){var t,n;(n=(t=this.destination)===null||t===void 0?void 0:t.complete)===null||n===void 0||n.call(t)}_subscribe(t){var n,r;return(r=(n=this.source)===null||n===void 0?void 0:n.subscribe(t))!==null&&r!==void 0?r:ia}};var ie=class extends X{constructor(t){super(),this._value=t}get value(){return this.getValue()}_subscribe(t){let n=super._subscribe(t);return!n.closed&&t.next(this._value),n}getValue(){let{hasError:t,thrownError:n,_value:r}=this;if(t)throw n;return this._throwIfClosed(),r}next(t){super.next(this._value=t)}};var ve=new V(e=>e.complete());function $u(e){return e&&x(e.schedule)}function Gu(e){return e[e.length-1]}function zu(e){return x(Gu(e))?e.pop():void 0}function Mt(e){return $u(Gu(e))?e.pop():void 0}function qu(e,t,n,r){function o(i){return i instanceof n?i:new n(function(s){s(i)})}return new(n||(n=Promise))(function(i,s){function a(u){try{l(r.next(u))}catch(f){s(f)}}function c(u){try{l(r.throw(u))}catch(f){s(f)}}function l(u){u.done?i(u.value):o(u.value).then(a,c)}l((r=r.apply(e,t||[])).next())})}function Wu(e){var t=typeof Symbol=="function"&&Symbol.iterator,n=t&&e[t],r=0;if(n)return n.call(e);if(e&&typeof e.length=="number")return{next:function(){return e&&r>=e.length&&(e=void 0),{value:e&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function Yt(e){return this instanceof Yt?(this.v=e,this):new Yt(e)}function Yu(e,t,n){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var r=n.apply(e,t||[]),o,i=[];return o=Object.create((typeof AsyncIterator=="function"?AsyncIterator:Object).prototype),a("next"),a("throw"),a("return",s),o[Symbol.asyncIterator]=function(){return this},o;function s(h){return function(E){return Promise.resolve(E).then(h,f)}}function a(h,E){r[h]&&(o[h]=function(S){return new Promise(function(O,F){i.push([h,S,O,F])>1||c(h,S)})},E&&(o[h]=E(o[h])))}function c(h,E){try{l(r[h](E))}catch(S){m(i[0][3],S)}}function l(h){h.value instanceof Yt?Promise.resolve(h.value.v).then(u,f):m(i[0][2],h)}function u(h){c("next",h)}function f(h){c("throw",h)}function m(h,E){h(E),i.shift(),i.length&&c(i[0][0],i[0][1])}}function Zu(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],n;return t?t.call(e):(e=typeof Wu=="function"?Wu(e):e[Symbol.iterator](),n={},r("next"),r("throw"),r("return"),n[Symbol.asyncIterator]=function(){return this},n);function r(i){n[i]=e[i]&&function(s){return new Promise(function(a,c){s=e[i](s),o(a,c,s.done,s.value)})}}function o(i,s,a,c){Promise.resolve(c).then(function(l){i({value:l,done:a})},s)}}var Yo=e=>e&&typeof e.length=="number"&&typeof e!="function";function Zo(e){return x(e?.then)}function Ko(e){return x(e[On])}function Qo(e){return Symbol.asyncIterator&&x(e?.[Symbol.asyncIterator])}function Xo(e){return new TypeError(`You provided ${e!==null&&typeof e=="object"?"an invalid object":`'${e}'`} where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`)}function um(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Jo=um();function ei(e){return x(e?.[Jo])}function ti(e){return Yu(this,arguments,function*(){let n=e.getReader();try{for(;;){let{value:r,done:o}=yield Yt(n.read());if(o)return yield Yt(void 0);yield yield Yt(r)}}finally{n.releaseLock()}})}function ni(e){return x(e?.getReader)}function J(e){if(e instanceof V)return e;if(e!=null){if(Ko(e))return dm(e);if(Yo(e))return fm(e);if(Zo(e))return pm(e);if(Qo(e))return Ku(e);if(ei(e))return hm(e);if(ni(e))return gm(e)}throw Xo(e)}function dm(e){return new V(t=>{let n=e[On]();if(x(n.subscribe))return n.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function fm(e){return new V(t=>{for(let n=0;n{e.then(n=>{t.closed||(t.next(n),t.complete())},n=>t.error(n)).then(null,zo)})}function hm(e){return new V(t=>{for(let n of e)if(t.next(n),t.closed)return;t.complete()})}function Ku(e){return new V(t=>{mm(e,t).catch(n=>t.error(n))})}function gm(e){return Ku(ti(e))}function mm(e,t){var n,r,o,i;return qu(this,void 0,void 0,function*(){try{for(n=Zu(e);r=yield n.next(),!r.done;){let s=r.value;if(t.next(s),t.closed)return}}catch(s){o={error:s}}finally{try{r&&!r.done&&(i=n.return)&&(yield i.call(n))}finally{if(o)throw o.error}}t.complete()})}function ye(e,t,n,r=0,o=!1){let i=t.schedule(function(){n(),o?e.add(this.schedule(null,r)):this.unsubscribe()},r);if(e.add(i),!o)return i}function ri(e,t=0){return j((n,r)=>{n.subscribe(H(r,o=>ye(r,e,()=>r.next(o),t),()=>ye(r,e,()=>r.complete(),t),o=>ye(r,e,()=>r.error(o),t)))})}function oi(e,t=0){return j((n,r)=>{r.add(e.schedule(()=>n.subscribe(r),t))})}function Qu(e,t){return J(e).pipe(oi(t),ri(t))}function Xu(e,t){return J(e).pipe(oi(t),ri(t))}function Ju(e,t){return new V(n=>{let r=0;return t.schedule(function(){r===e.length?n.complete():(n.next(e[r++]),n.closed||this.schedule())})})}function ed(e,t){return new V(n=>{let r;return ye(n,t,()=>{r=e[Jo](),ye(n,t,()=>{let o,i;try{({value:o,done:i}=r.next())}catch(s){n.error(s);return}i?n.complete():n.next(o)},0,!0)}),()=>x(r?.return)&&r.return()})}function ii(e,t){if(!e)throw new Error("Iterable cannot be null");return new V(n=>{ye(n,t,()=>{let r=e[Symbol.asyncIterator]();ye(n,t,()=>{r.next().then(o=>{o.done?n.complete():n.next(o.value)})},0,!0)})})}function td(e,t){return ii(ti(e),t)}function nd(e,t){if(e!=null){if(Ko(e))return Qu(e,t);if(Yo(e))return Ju(e,t);if(Zo(e))return Xu(e,t);if(Qo(e))return ii(e,t);if(ei(e))return ed(e,t);if(ni(e))return td(e,t)}throw Xo(e)}function Y(e,t){return t?nd(e,t):J(e)}function w(...e){let t=Mt(e);return Y(e,t)}function Ln(e,t){let n=x(e)?e:()=>e,r=o=>o.error(n());return new V(t?o=>t.schedule(r,0,o):r)}function ha(e){return!!e&&(e instanceof V||x(e.lift)&&x(e.subscribe))}var ot=xn(e=>function(){e(this),this.name="EmptyError",this.message="no elements in sequence"});function B(e,t){return j((n,r)=>{let o=0;n.subscribe(H(r,i=>{r.next(e.call(t,i,o++))}))})}var{isArray:vm}=Array;function ym(e,t){return vm(t)?e(...t):e(t)}function rd(e){return B(t=>ym(e,t))}var{isArray:Em}=Array,{getPrototypeOf:Dm,prototype:Cm,keys:Im}=Object;function od(e){if(e.length===1){let t=e[0];if(Em(t))return{args:t,keys:null};if(Sm(t)){let n=Im(t);return{args:n.map(r=>t[r]),keys:n}}}return{args:e,keys:null}}function Sm(e){return e&&typeof e=="object"&&Dm(e)===Cm}function id(e,t){return e.reduce((n,r,o)=>(n[r]=t[o],n),{})}function si(...e){let t=Mt(e),n=zu(e),{args:r,keys:o}=od(e);if(r.length===0)return Y([],t);let i=new V(bm(r,t,o?s=>id(o,s):be));return n?i.pipe(rd(n)):i}function bm(e,t,n=be){return r=>{sd(t,()=>{let{length:o}=e,i=new Array(o),s=o,a=o;for(let c=0;c{let l=Y(e[c],t),u=!1;l.subscribe(H(r,f=>{i[c]=f,u||(u=!0,a--),a||r.next(n(i.slice()))},()=>{--s||r.complete()}))},r)},r)}}function sd(e,t,n){e?ye(n,e,t):t()}function ad(e,t,n,r,o,i,s,a){let c=[],l=0,u=0,f=!1,m=()=>{f&&!c.length&&!l&&t.complete()},h=S=>l{i&&t.next(S),l++;let O=!1;J(n(S,u++)).subscribe(H(t,F=>{o?.(F),i?h(F):t.next(F)},()=>{O=!0},void 0,()=>{if(O)try{for(l--;c.length&&lE(F)):E(F)}m()}catch(F){t.error(F)}}))};return e.subscribe(H(t,h,()=>{f=!0,m()})),()=>{a?.()}}function Z(e,t,n=1/0){return x(t)?Z((r,o)=>B((i,s)=>t(r,i,o,s))(J(e(r,o))),n):(typeof t=="number"&&(n=t),j((r,o)=>ad(r,o,e,n)))}function cd(e=1/0){return Z(be,e)}function ld(){return cd(1)}function Fn(...e){return ld()(Y(e,Mt(e)))}function Nr(e){return new V(t=>{J(e()).subscribe(t)})}function Ae(e,t){return j((n,r)=>{let o=0;n.subscribe(H(r,i=>e.call(t,i,o++)&&r.next(i)))})}function xt(e){return j((t,n)=>{let r=null,o=!1,i;r=t.subscribe(H(n,void 0,void 0,s=>{i=J(e(s,xt(e)(t))),r?(r.unsubscribe(),r=null,i.subscribe(n)):o=!0})),o&&(r.unsubscribe(),r=null,i.subscribe(n))})}function ud(e,t,n,r,o){return(i,s)=>{let a=n,c=t,l=0;i.subscribe(H(s,u=>{let f=l++;c=a?e(c,u,f):(a=!0,u),r&&s.next(c)},o&&(()=>{a&&s.next(c),s.complete()})))}}function jn(e,t){return x(t)?Z(e,t,1):Z(e,1)}function Nt(e){return j((t,n)=>{let r=!1;t.subscribe(H(n,o=>{r=!0,n.next(o)},()=>{r||n.next(e),n.complete()}))})}function it(e){return e<=0?()=>ve:j((t,n)=>{let r=0;t.subscribe(H(n,o=>{++r<=e&&(n.next(o),e<=r&&n.complete())}))})}function ai(e=_m){return j((t,n)=>{let r=!1;t.subscribe(H(n,o=>{r=!0,n.next(o)},()=>r?n.complete():n.error(e())))})}function _m(){return new ot}function Ar(e){return j((t,n)=>{try{t.subscribe(n)}finally{n.add(e)}})}function st(e,t){let n=arguments.length>=2;return r=>r.pipe(e?Ae((o,i)=>e(o,i,r)):be,it(1),n?Nt(t):ai(()=>new ot))}function Hn(e){return e<=0?()=>ve:j((t,n)=>{let r=[];t.subscribe(H(n,o=>{r.push(o),e{for(let o of r)n.next(o);n.complete()},void 0,()=>{r=null}))})}function ga(e,t){let n=arguments.length>=2;return r=>r.pipe(e?Ae((o,i)=>e(o,i,r)):be,Hn(1),n?Nt(t):ai(()=>new ot))}function ma(e,t){return j(ud(e,t,arguments.length>=2,!0))}function va(...e){let t=Mt(e);return j((n,r)=>{(t?Fn(e,n,t):Fn(e,n)).subscribe(r)})}function Ee(e,t){return j((n,r)=>{let o=null,i=0,s=!1,a=()=>s&&!o&&r.complete();n.subscribe(H(r,c=>{o?.unsubscribe();let l=0,u=i++;J(e(c,u)).subscribe(o=H(r,f=>r.next(t?t(c,f,u,l++):f),()=>{o=null,a()}))},()=>{s=!0,a()}))})}function ci(e){return j((t,n)=>{J(e).subscribe(H(n,()=>n.complete(),xr)),!n.closed&&t.subscribe(n)})}function ee(e,t,n){let r=x(e)||t||n?{next:e,error:t,complete:n}:e;return r?j((o,i)=>{var s;(s=r.subscribe)===null||s===void 0||s.call(r);let a=!0;o.subscribe(H(i,c=>{var l;(l=r.next)===null||l===void 0||l.call(r,c),i.next(c)},()=>{var c;a=!1,(c=r.complete)===null||c===void 0||c.call(r),i.complete()},c=>{var l;a=!1,(l=r.error)===null||l===void 0||l.call(r,c),i.error(c)},()=>{var c,l;a&&((c=r.unsubscribe)===null||c===void 0||c.call(r)),(l=r.finalize)===null||l===void 0||l.call(r)}))}):be}function dd(e){let t=_(null);try{return e()}finally{_(t)}}var fd=P(y({},$t),{consumerIsAlwaysLive:!0,consumerAllowSignalWrites:!0,dirty:!0,hasRun:!1,kind:"effect"});function pd(e){if(e.dirty=!1,e.hasRun&&!wn(e))return;e.hasRun=!0;let t=Gt(e);try{e.cleanup(),e.fn()}finally{_n(e,t)}}var Ma="https://angular.dev/best-practices/security#preventing-cross-site-scripting-xss",C=class extends Error{code;constructor(t,n){super(pi(t,n)),this.code=t}};function wm(e){return`NG0${Math.abs(e)}`}function pi(e,t){return`${wm(e)}${t?": "+t:""}`}function U(e){for(let t in e)if(e[t]===U)return t;throw Error("")}function ct(e){if(typeof e=="string")return e;if(Array.isArray(e))return`[${e.map(ct).join(", ")}]`;if(e==null)return""+e;let t=e.overriddenName||e.name;if(t)return`${t}`;let n=e.toString();if(n==null)return""+n;let r=n.indexOf(` +`);return r>=0?n.slice(0,r):n}function xa(e,t){return e?t?`${e} ${t}`:e:t||""}var Tm=U({__forward_ref__:U});function hi(e){return e.__forward_ref__=hi,e.toString=function(){return ct(this())},e}function De(e){return Na(e)?e():e}function Na(e){return typeof e=="function"&&e.hasOwnProperty(Tm)&&e.__forward_ref__===hi}function I(e){return{token:e.token,providedIn:e.providedIn||null,factory:e.factory,value:void 0}}function Vn(e){return{providers:e.providers||[],imports:e.imports||[]}}function Lr(e){return Mm(e,gi)}function Aa(e){return Lr(e)!==null}function Mm(e,t){return e.hasOwnProperty(t)&&e[t]||null}function xm(e){let t=e?.[gi]??null;return t||null}function Ea(e){return e&&e.hasOwnProperty(ui)?e[ui]:null}var gi=U({\u0275prov:U}),ui=U({\u0275inj:U}),b=class{_desc;ngMetadataName="InjectionToken";\u0275prov;constructor(t,n){this._desc=t,this.\u0275prov=void 0,typeof n=="number"?this.__NG_ELEMENT_ID__=n:n!==void 0&&(this.\u0275prov=I({token:this,providedIn:n.providedIn||"root",factory:n.factory}))}get multi(){return this}toString(){return`InjectionToken ${this._desc}`}};function Ra(e){return e&&!!e.\u0275providers}var Oa=U({\u0275cmp:U}),Pa=U({\u0275dir:U}),ka=U({\u0275pipe:U}),La=U({\u0275mod:U}),Or=U({\u0275fac:U}),Jt=U({__NG_ELEMENT_ID__:U}),hd=U({__NG_ENV_ID__:U});function mi(e){return typeof e=="string"?e:e==null?"":String(e)}function md(e){return typeof e=="function"?e.name||e.toString():typeof e=="object"&&e!=null&&typeof e.type=="function"?e.type.name||e.type.toString():mi(e)}var vd=U({ngErrorCode:U}),Nm=U({ngErrorMessage:U}),Am=U({ngTokenPath:U});function Fa(e,t){return yd("",-200,t)}function vi(e,t){throw new C(-201,!1)}function yd(e,t,n){let r=new C(t,e);return r[vd]=t,r[Nm]=e,n&&(r[Am]=n),r}function Rm(e){return e[vd]}var Da;function Ed(){return Da}function _e(e){let t=Da;return Da=e,t}function ja(e,t,n){let r=Lr(e);if(r&&r.providedIn=="root")return r.value===void 0?r.value=r.factory():r.value;if(n&8)return null;if(t!==void 0)return t;vi(e,"Injector")}var Om={},Zt=Om,Pm="__NG_DI_FLAG__",Ca=class{injector;constructor(t){this.injector=t}retrieve(t,n){let r=Kt(n)||0;try{return this.injector.get(t,r&8?null:Zt,r)}catch(o){if(Sn(o))return o;throw o}}};function km(e,t=0){let n=Oo();if(n===void 0)throw new C(-203,!1);if(n===null)return ja(e,void 0,t);{let r=Lm(t),o=n.retrieve(e,r);if(Sn(o)){if(r.optional)return null;throw o}return o}}function N(e,t=0){return(Ed()||km)(De(e),t)}function v(e,t){return N(e,Kt(t))}function Kt(e){return typeof e>"u"||typeof e=="number"?e:0|(e.optional&&8)|(e.host&&1)|(e.self&&2)|(e.skipSelf&&4)}function Lm(e){return{optional:!!(e&8),host:!!(e&1),self:!!(e&2),skipSelf:!!(e&4)}}function Ia(e){let t=[];for(let n=0;nArray.isArray(n)?yi(n,t):t(n))}function Ha(e,t,n){t>=e.length?e.push(n):e.splice(t,0,n)}function Fr(e,t){return t>=e.length-1?e.pop():e.splice(t,1)[0]}function Id(e,t,n,r){let o=e.length;if(o==t)e.push(n,r);else if(o===1)e.push(r,e[0]),e[0]=n;else{for(o--,e.push(e[o-1],e[o]);o>t;){let i=o-2;e[o]=e[i],o--}e[t]=n,e[t+1]=r}}function Sd(e,t,n){let r=Un(e,t);return r>=0?e[r|1]=n:(r=~r,Id(e,r,t,n)),r}function Ei(e,t){let n=Un(e,t);if(n>=0)return e[n|1]}function Un(e,t){return jm(e,t,1)}function jm(e,t,n){let r=0,o=e.length>>n;for(;o!==r;){let i=r+(o-r>>1),s=e[i<t?o=i:r=i+1}return~(o<{n.push(s)};return yi(t,s=>{let a=s;di(a,i,[],r)&&(o||=[],o.push(a))}),o!==void 0&&Td(o,i),n}function Td(e,t){for(let n=0;n{t(i,r)})}}function di(e,t,n,r){if(e=De(e),!e)return!1;let o=null,i=Ea(e),s=!i&&Rt(e);if(!i&&!s){let c=e.ngModule;if(i=Ea(c),i)o=c;else return!1}else{if(s&&!s.standalone)return!1;o=e}let a=r.has(o);if(s){if(a)return!1;if(r.add(o),s.dependencies){let c=typeof s.dependencies=="function"?s.dependencies():s.dependencies;for(let l of c)di(l,t,n,r)}}else if(i){if(i.imports!=null&&!a){r.add(o);let l;try{yi(i.imports,u=>{di(u,t,n,r)&&(l||=[],l.push(u))})}finally{}l!==void 0&&Td(l,t)}if(!a){let l=Qt(o)||(()=>new o);t({provide:o,useFactory:l,deps:we},o),t({provide:Va,useValue:o,multi:!0},o),t({provide:lt,useValue:()=>N(o),multi:!0},o)}let c=i.providers;if(c!=null&&!a){let l=e;za(c,u=>{t(u,l)})}}else return!1;return o!==e&&e.providers!==void 0}function za(e,t){for(let n of e)Ra(n)&&(n=n.\u0275providers),Array.isArray(n)?za(n,t):t(n)}var Hm=U({provide:String,useValue:U});function Md(e){return e!==null&&typeof e=="object"&&Hm in e}function Bm(e){return!!(e&&e.useExisting)}function Vm(e){return!!(e&&e.useFactory)}function fi(e){return typeof e=="function"}var jr=new b(""),li={},gd={},ya;function Hr(){return ya===void 0&&(ya=new Pr),ya}var K=class{},Xt=class extends K{parent;source;scopes;records=new Map;_ngOnDestroyHooks=new Set;_onDestroyHooks=[];get destroyed(){return this._destroyed}_destroyed=!1;injectorDefTypes;constructor(t,n,r,o){super(),this.parent=n,this.source=r,this.scopes=o,ba(t,s=>this.processProvider(s)),this.records.set(Ba,Bn(void 0,this)),o.has("environment")&&this.records.set(K,Bn(void 0,this));let i=this.records.get(jr);i!=null&&typeof i.value=="string"&&this.scopes.add(i.value),this.injectorDefTypes=new Set(this.get(Va,we,{self:!0}))}retrieve(t,n){let r=Kt(n)||0;try{return this.get(t,Zt,r)}catch(o){if(Sn(o))return o;throw o}}destroy(){Rr(this),this._destroyed=!0;let t=_(null);try{for(let r of this._ngOnDestroyHooks)r.ngOnDestroy();let n=this._onDestroyHooks;this._onDestroyHooks=[];for(let r of n)r()}finally{this.records.clear(),this._ngOnDestroyHooks.clear(),this.injectorDefTypes.clear(),_(t)}}onDestroy(t){return Rr(this),this._onDestroyHooks.push(t),()=>this.removeOnDestroy(t)}runInContext(t){Rr(this);let n=qe(this),r=_e(void 0),o;try{return t()}finally{qe(n),_e(r)}}get(t,n=Zt,r){if(Rr(this),t.hasOwnProperty(hd))return t[hd](this);let o=Kt(r),i,s=qe(this),a=_e(void 0);try{if(!(o&4)){let l=this.records.get(t);if(l===void 0){let u=Wm(t)&&Lr(t);u&&this.injectableDefInScope(u)?l=Bn(Sa(t),li):l=null,this.records.set(t,l)}if(l!=null)return this.hydrate(t,l,o)}let c=o&2?Hr():this.parent;return n=o&8&&n===Zt?null:n,c.get(t,n)}catch(c){let l=Rm(c);throw l===-200||l===-201?new C(l,null):c}finally{_e(a),qe(s)}}resolveInjectorInitializers(){let t=_(null),n=qe(this),r=_e(void 0),o;try{let i=this.get(lt,we,{self:!0});for(let s of i)s()}finally{qe(n),_e(r),_(t)}}toString(){let t=[],n=this.records;for(let r of n.keys())t.push(ct(r));return`R3Injector[${t.join(", ")}]`}processProvider(t){t=De(t);let n=fi(t)?t:De(t&&t.provide),r=$m(t);if(!fi(t)&&t.multi===!0){let o=this.records.get(n);o||(o=Bn(void 0,li,!0),o.factory=()=>Ia(o.multi),this.records.set(n,o)),n=t,o.multi.push(t)}this.records.set(n,r)}hydrate(t,n,r){let o=_(null);try{if(n.value===gd)throw Fa(ct(t));return n.value===li&&(n.value=gd,n.value=n.factory(void 0,r)),typeof n.value=="object"&&n.value&&zm(n.value)&&this._ngOnDestroyHooks.add(n.value),n.value}finally{_(o)}}injectableDefInScope(t){if(!t.providedIn)return!1;let n=De(t.providedIn);return typeof n=="string"?n==="any"||this.scopes.has(n):this.injectorDefTypes.has(n)}removeOnDestroy(t){let n=this._onDestroyHooks.indexOf(t);n!==-1&&this._onDestroyHooks.splice(n,1)}};function Sa(e){let t=Lr(e),n=t!==null?t.factory:Qt(e);if(n!==null)return n;if(e instanceof b)throw new C(204,!1);if(e instanceof Function)return Um(e);throw new C(204,!1)}function Um(e){if(e.length>0)throw new C(204,!1);let n=xm(e);return n!==null?()=>n.factory(e):()=>new e}function $m(e){if(Md(e))return Bn(void 0,e.useValue);{let t=xd(e);return Bn(t,li)}}function xd(e,t,n){let r;if(fi(e)){let o=De(e);return Qt(o)||Sa(o)}else if(Md(e))r=()=>De(e.useValue);else if(Vm(e))r=()=>e.useFactory(...Ia(e.deps||[]));else if(Bm(e))r=(o,i)=>N(De(e.useExisting),i!==void 0&&i&8?8:void 0);else{let o=De(e&&(e.useClass||e.provide));if(Gm(e))r=()=>new o(...Ia(e.deps));else return Qt(o)||Sa(o)}return r}function Rr(e){if(e.destroyed)throw new C(205,!1)}function Bn(e,t,n=!1){return{factory:e,value:t,multi:n?[]:void 0}}function Gm(e){return!!e.deps}function zm(e){return e!==null&&typeof e=="object"&&typeof e.ngOnDestroy=="function"}function Wm(e){return typeof e=="function"||typeof e=="object"&&e.ngMetadataName==="InjectionToken"}function ba(e,t){for(let n of e)Array.isArray(n)?ba(n,t):n&&Ra(n)?ba(n.\u0275providers,t):t(n)}function G(e,t){let n;e instanceof Xt?(Rr(e),n=e):n=new Ca(e);let r,o=qe(n),i=_e(void 0);try{return t()}finally{qe(o),_e(i)}}function Nd(){return Ed()!==void 0||Oo()!=null}var Ve=0,T=1,M=2,te=3,Oe=4,Pe=5,Br=6,$n=7,se=8,Gn=9,ut=10,ne=11,zn=12,Wa=13,nn=14,ke=15,Ot=16,rn=17,Ze=18,Vr=19,qa=20,at=21,Di=22,dt=23,Te=24,Ci=25,he=26,ae=27,Ad=1;var Pt=7,Ur=8,on=9,fe=10;function ft(e){return Array.isArray(e)&&typeof e[Ad]=="object"}function Ue(e){return Array.isArray(e)&&e[Ad]===!0}function Ya(e){return(e.flags&4)!==0}function sn(e){return e.componentOffset>-1}function Ii(e){return(e.flags&1)===1}function an(e){return!!e.template}function Wn(e){return(e[M]&512)!==0}function cn(e){return(e[M]&256)===256}var Rd="svg",Od="math";function Le(e){for(;Array.isArray(e);)e=e[Ve];return e}function Za(e,t){return Le(t[e])}function Ke(e,t){return Le(t[e.index])}function $r(e,t){return e.data[t]}function Pd(e,t){return e[t]}function Qe(e,t){let n=t[e];return ft(n)?n:n[Ve]}function Si(e){return(e[M]&128)===128}function kd(e){return Ue(e[te])}function kt(e,t){return t==null?null:e[t]}function Ka(e){e[rn]=0}function Qa(e){e[M]&1024||(e[M]|=1024,Si(e)&&ln(e))}function Ld(e,t){for(;e>0;)t=t[nn],e--;return t}function Gr(e){return!!(e[M]&9216||e[Te]?.dirty)}function bi(e){e[ut].changeDetectionScheduler?.notify(8),e[M]&64&&(e[M]|=1024),Gr(e)&&ln(e)}function ln(e){e[ut].changeDetectionScheduler?.notify(0);let t=At(e);for(;t!==null&&!(t[M]&8192||(t[M]|=8192,!Si(t)));)t=At(t)}function Xa(e,t){if(cn(e))throw new C(911,!1);e[at]===null&&(e[at]=[]),e[at].push(t)}function Fd(e,t){if(e[at]===null)return;let n=e[at].indexOf(t);n!==-1&&e[at].splice(n,1)}function At(e){let t=e[te];return Ue(t)?t[te]:t}function Ja(e){return e[$n]??=[]}function ec(e){return e.cleanup??=[]}function jd(e,t,n,r){let o=Ja(t);o.push(n),e.firstCreatePass&&ec(e).push(r,o.length-1)}var R={lFrame:ef(null),bindingsEnabled:!0,skipHydrationRootTNode:null};var _a=!1;function Hd(){return R.lFrame.elementDepthCount}function Bd(){R.lFrame.elementDepthCount++}function tc(){R.lFrame.elementDepthCount--}function Vd(){return R.bindingsEnabled}function Ud(){return R.skipHydrationRootTNode!==null}function nc(e){return R.skipHydrationRootTNode===e}function rc(){R.skipHydrationRootTNode=null}function W(){return R.lFrame.lView}function Xe(){return R.lFrame.tView}function pt(e){return R.lFrame.contextLView=e,e[se]}function ht(e){return R.lFrame.contextLView=null,e}function Me(){let e=oc();for(;e!==null&&e.type===64;)e=e.parent;return e}function oc(){return R.lFrame.currentTNode}function $d(){let e=R.lFrame,t=e.currentTNode;return e.isParent?t:t.parent}function qn(e,t){let n=R.lFrame;n.currentTNode=e,n.isParent=t}function ic(){return R.lFrame.isParent}function Gd(){R.lFrame.isParent=!1}function zd(){return R.lFrame.contextLView}function sc(){return _a}function Yn(e){let t=_a;return _a=e,t}function Wd(e){return R.lFrame.bindingIndex=e}function _i(){return R.lFrame.bindingIndex++}function qd(e){let t=R.lFrame,n=t.bindingIndex;return t.bindingIndex=t.bindingIndex+e,n}function Yd(){return R.lFrame.inI18n}function Zd(e,t){let n=R.lFrame;n.bindingIndex=n.bindingRootIndex=e,wi(t)}function Kd(){return R.lFrame.currentDirectiveIndex}function wi(e){R.lFrame.currentDirectiveIndex=e}function Qd(e){let t=R.lFrame.currentDirectiveIndex;return t===-1?null:e[t]}function Xd(){return R.lFrame.currentQueryIndex}function Ti(e){R.lFrame.currentQueryIndex=e}function qm(e){let t=e[T];return t.type===2?t.declTNode:t.type===1?e[Pe]:null}function ac(e,t,n){if(n&4){let o=t,i=e;for(;o=o.parent,o===null&&!(n&1);)if(o=qm(i),o===null||(i=i[nn],o.type&10))break;if(o===null)return!1;t=o,e=i}let r=R.lFrame=Jd();return r.currentTNode=t,r.lView=e,!0}function Mi(e){let t=Jd(),n=e[T];R.lFrame=t,t.currentTNode=n.firstChild,t.lView=e,t.tView=n,t.contextLView=e,t.bindingIndex=n.bindingStartIndex,t.inI18n=!1}function Jd(){let e=R.lFrame,t=e===null?null:e.child;return t===null?ef(e):t}function ef(e){let t={currentTNode:null,isParent:!0,lView:null,tView:null,selectedIndex:-1,contextLView:null,elementDepthCount:0,currentNamespace:null,currentDirectiveIndex:-1,bindingRootIndex:-1,bindingIndex:-1,currentQueryIndex:0,parent:e,child:null,inI18n:!1};return e!==null&&(e.child=t),t}function tf(){let e=R.lFrame;return R.lFrame=e.parent,e.currentTNode=null,e.lView=null,e}var cc=tf;function xi(){let e=tf();e.isParent=!0,e.tView=null,e.selectedIndex=-1,e.contextLView=null,e.elementDepthCount=0,e.currentDirectiveIndex=-1,e.currentNamespace=null,e.bindingRootIndex=-1,e.bindingIndex=-1,e.currentQueryIndex=0}function nf(e){return(R.lFrame.contextLView=Ld(e,R.lFrame.contextLView))[se]}function un(){return R.lFrame.selectedIndex}function Lt(e){R.lFrame.selectedIndex=e}function rf(){let e=R.lFrame;return $r(e.tView,e.selectedIndex)}function of(){return R.lFrame.currentNamespace}var sf=!0;function Ni(){return sf}function Ai(e){sf=e}function wa(e,t=null,n=null,r){let o=lc(e,t,n,r);return o.resolveInjectorInitializers(),o}function lc(e,t=null,n=null,r,o=new Set){let i=[n||we,wd(e)];return r=r||(typeof e=="object"?void 0:ct(e)),new Xt(i,t||Hr(),r||null,o)}var Re=class e{static THROW_IF_NOT_FOUND=Zt;static NULL=new Pr;static create(t,n){if(Array.isArray(t))return wa({name:""},n,t,"");{let r=t.name??"";return wa({name:r},t.parent,t.providers,r)}}static \u0275prov=I({token:e,providedIn:"any",factory:()=>N(Ba)});static __NG_ELEMENT_ID__=-1},re=new b(""),gt=(()=>{class e{static __NG_ELEMENT_ID__=Ym;static __NG_ENV_ID__=n=>n}return e})(),kr=class extends gt{_lView;constructor(t){super(),this._lView=t}get destroyed(){return cn(this._lView)}onDestroy(t){let n=this._lView;return Xa(n,t),()=>Fd(n,t)}};function Ym(){return new kr(W())}var Be=class{_console=console;handleError(t){this._console.error("ERROR",t)}},Fe=new b("",{providedIn:"root",factory:()=>{let e=v(K),t;return n=>{e.destroyed&&!t?setTimeout(()=>{throw n}):(t??=e.get(Be),t.handleError(n))}}}),af={provide:lt,useValue:()=>void v(Be),multi:!0},Zm=new b("",{providedIn:"root",factory:()=>{let e=v(re).defaultView;if(!e)return;let t=v(Fe),n=i=>{t(i.reason),i.preventDefault()},r=i=>{i.error?t(i.error):t(new Error(i.message,{cause:i})),i.preventDefault()},o=()=>{e.addEventListener("unhandledrejection",n),e.addEventListener("error",r)};typeof Zone<"u"?Zone.root.run(o):o(),v(gt).onDestroy(()=>{e.removeEventListener("error",r),e.removeEventListener("unhandledrejection",n)})}});function uc(){return tn([_d(()=>void v(Zm))])}function k(e,t){let[n,r,o]=ra(e,t?.equal),i=n,s=i[oe];return i.set=r,i.update=o,i.asReadonly=dc.bind(i),i}function dc(){let e=this[oe];if(e.readonlyFn===void 0){let t=()=>this();t[oe]=e,e.readonlyFn=t}return e.readonlyFn}var Ye=class{},zr=new b("",{providedIn:"root",factory:()=>!1});var fc=new b(""),Ri=new b("");var Wr=(()=>{class e{view;node;constructor(n,r){this.view=n,this.node=r}static __NG_ELEMENT_ID__=Km}return e})();function Km(){return new Wr(W(),Me())}var mt=(()=>{class e{taskId=0;pendingTasks=new Set;destroyed=!1;pendingTask=new ie(!1);get hasPendingTasks(){return this.destroyed?!1:this.pendingTask.value}get hasPendingTasksObservable(){return this.destroyed?new V(n=>{n.next(!1),n.complete()}):this.pendingTask}add(){!this.hasPendingTasks&&!this.destroyed&&this.pendingTask.next(!0);let n=this.taskId++;return this.pendingTasks.add(n),n}has(n){return this.pendingTasks.has(n)}remove(n){this.pendingTasks.delete(n),this.pendingTasks.size===0&&this.hasPendingTasks&&this.pendingTask.next(!1)}ngOnDestroy(){this.pendingTasks.clear(),this.hasPendingTasks&&this.pendingTask.next(!1),this.destroyed=!0,this.pendingTask.unsubscribe()}static \u0275prov=I({token:e,providedIn:"root",factory:()=>new e})}return e})();function dn(...e){}var qr=(()=>{class e{static \u0275prov=I({token:e,providedIn:"root",factory:()=>new Ta})}return e})(),Ta=class{dirtyEffectCount=0;queues=new Map;add(t){this.enqueue(t),this.schedule(t)}schedule(t){t.dirty&&this.dirtyEffectCount++}remove(t){let n=t.zone,r=this.queues.get(n);r.has(t)&&(r.delete(t),t.dirty&&this.dirtyEffectCount--)}enqueue(t){let n=t.zone;this.queues.has(n)||this.queues.set(n,new Set);let r=this.queues.get(n);r.has(t)||r.add(t)}flush(){for(;this.dirtyEffectCount>0;){let t=!1;for(let[n,r]of this.queues)n===null?t||=this.flushQueue(r):t||=n.run(()=>this.flushQueue(r));t||(this.dirtyEffectCount=0)}}flushQueue(t){let n=!1;for(let r of t)r.dirty&&(this.dirtyEffectCount--,n=!0,r.run());return n}};function to(e){return{toString:e}.toString()}function sv(e){return typeof e=="function"}var ji=class{previousValue;currentValue;firstChange;constructor(t,n,r){this.previousValue=t,this.currentValue=n,this.firstChange=r}isFirstChange(){return this.firstChange}};function jf(e,t,n,r){t!==null?t.applyValueToInputSignal(t,r):e[n]=r}var Ji=(()=>{let e=()=>Hf;return e.ngInherit=!0,e})();function Hf(e){return e.type.prototype.ngOnChanges&&(e.setInput=cv),av}function av(){let e=Vf(this),t=e?.current;if(t){let n=e.previous;if(n===en)e.previous=t;else for(let r in t)n[r]=t[r];e.current=null,this.ngOnChanges(t)}}function cv(e,t,n,r,o){let i=this.declaredInputs[r],s=Vf(e)||lv(e,{previous:en,current:null}),a=s.current||(s.current={}),c=s.previous,l=c[i];a[i]=new ji(l&&l.currentValue,n,c===en),jf(e,t,o,n)}var Bf="__ngSimpleChanges__";function Vf(e){return e[Bf]||null}function lv(e,t){return e[Bf]=t}var cf=[];var z=function(e,t=null,n){for(let r=0;r=r)break}else t[c]<0&&(e[rn]+=65536),(a>14>16&&(e[M]&3)===t&&(e[M]+=16384,lf(a,i)):lf(a,i)}var Kn=-1,Kr=class{factory;name;injectImpl;resolving=!1;canSeeViewProviders;multi;componentProviders;index;providerFactory;constructor(t,n,r,o){this.factory=t,this.name=o,this.canSeeViewProviders=n,this.injectImpl=r}};function pv(e){return(e.flags&8)!==0}function hv(e){return(e.flags&16)!==0}function gv(e,t,n){let r=0;for(;rt){s=i-1;break}}}for(;i>16}function Bi(e,t){let n=yv(e),r=t;for(;n>0;)r=r[nn],n--;return r}var Ec=!0;function df(e){let t=Ec;return Ec=e,t}var Ev=256,Gf=Ev-1,zf=5,Dv=0,Je={};function Cv(e,t,n){let r;typeof n=="string"?r=n.charCodeAt(0)||0:n.hasOwnProperty(Jt)&&(r=n[Jt]),r==null&&(r=n[Jt]=Dv++);let o=r&Gf,i=1<>zf)]|=i}function Wf(e,t){let n=qf(e,t);if(n!==-1)return n;let r=t[T];r.firstCreatePass&&(e.injectorIndex=t.length,hc(r.data,e),hc(t,null),hc(r.blueprint,null));let o=Uc(e,t),i=e.injectorIndex;if($f(o)){let s=Hi(o),a=Bi(o,t),c=a[T].data;for(let l=0;l<8;l++)t[i+l]=a[s+l]|c[s+l]}return t[i+8]=o,i}function hc(e,t){e.push(0,0,0,0,0,0,0,0,t)}function qf(e,t){return e.injectorIndex===-1||e.parent&&e.parent.injectorIndex===e.injectorIndex||t[e.injectorIndex+8]===null?-1:e.injectorIndex}function Uc(e,t){if(e.parent&&e.parent.injectorIndex!==-1)return e.parent.injectorIndex;let n=0,r=null,o=t;for(;o!==null;){if(r=Xf(o),r===null)return Kn;if(n++,o=o[nn],r.injectorIndex!==-1)return r.injectorIndex|n<<16}return Kn}function Iv(e,t,n){Cv(e,t,n)}function Yf(e,t,n){if(n&8||e!==void 0)return e;vi(t,"NodeInjector")}function Zf(e,t,n,r){if(n&8&&r===void 0&&(r=null),(n&3)===0){let o=e[Gn],i=_e(void 0);try{return o?o.get(t,r,n&8):ja(t,r,n&8)}finally{_e(i)}}return Yf(r,t,n)}function Kf(e,t,n,r=0,o){if(e!==null){if(t[M]&2048&&!(r&2)){let s=wv(e,t,n,r,Je);if(s!==Je)return s}let i=Qf(e,t,n,r,Je);if(i!==Je)return i}return Zf(t,n,r,o)}function Qf(e,t,n,r,o){let i=bv(n);if(typeof i=="function"){if(!ac(t,e,r))return r&1?Yf(o,n,r):Zf(t,n,r,o);try{let s;if(s=i(r),s==null&&!(r&8))vi(n);else return s}finally{cc()}}else if(typeof i=="number"){let s=null,a=qf(e,t),c=Kn,l=r&1?t[ke][Pe]:null;for((a===-1||r&4)&&(c=a===-1?Uc(e,t):t[a+8],c===Kn||!pf(r,!1)?a=-1:(s=t[T],a=Hi(c),t=Bi(c,t)));a!==-1;){let u=t[T];if(ff(i,a,u.data)){let f=Sv(a,t,n,s,r,l);if(f!==Je)return f}c=t[a+8],c!==Kn&&pf(r,t[T].data[a+8]===l)&&ff(i,a,t)?(s=u,a=Hi(c),t=Bi(c,t)):a=-1}}return o}function Sv(e,t,n,r,o,i){let s=t[T],a=s.data[e+8],c=r==null?sn(a)&&Ec:r!=s&&(a.type&3)!==0,l=o&1&&i===a,u=Li(a,s,n,c,l);return u!==null?Vi(t,s,u,a,o):Je}function Li(e,t,n,r,o){let i=e.providerIndexes,s=t.data,a=i&1048575,c=e.directiveStart,l=e.directiveEnd,u=i>>20,f=r?a:a+u,m=o?a+u:l;for(let h=f;h=c&&E.type===n)return h}if(o){let h=s[c];if(h&&an(h)&&h.type===n)return c}return null}function Vi(e,t,n,r,o){let i=e[n],s=t.data;if(i instanceof Kr){let a=i;if(a.resolving){let h=md(s[n]);throw Fa(h)}let c=df(a.canSeeViewProviders);a.resolving=!0;let l=s[n].type||s[n],u,f=a.injectImpl?_e(a.injectImpl):null,m=ac(e,r,0);try{i=e[n]=a.factory(void 0,o,s,e,r),t.firstCreatePass&&n>=r.directiveStart&&uv(n,s[n],t)}finally{f!==null&&_e(f),df(c),a.resolving=!1,cc()}}return i}function bv(e){if(typeof e=="string")return e.charCodeAt(0)||0;let t=e.hasOwnProperty(Jt)?e[Jt]:void 0;return typeof t=="number"?t>=0?t&Gf:_v:t}function ff(e,t,n){let r=1<>zf)]&r)}function pf(e,t){return!(e&2)&&!(e&1&&t)}var fn=class{_tNode;_lView;constructor(t,n){this._tNode=t,this._lView=n}get(t,n,r){return Kf(this._tNode,this._lView,t,Kt(r),n)}};function _v(){return new fn(Me(),W())}function ts(e){return to(()=>{let t=e.prototype.constructor,n=t[Or]||Dc(t),r=Object.prototype,o=Object.getPrototypeOf(e.prototype).constructor;for(;o&&o!==r;){let i=o[Or]||Dc(o);if(i&&i!==n)return i;o=Object.getPrototypeOf(o)}return i=>new i})}function Dc(e){return Na(e)?()=>{let t=Dc(De(e));return t&&t()}:Qt(e)}function wv(e,t,n,r,o){let i=e,s=t;for(;i!==null&&s!==null&&s[M]&2048&&!Wn(s);){let a=Qf(i,s,n,r|2,Je);if(a!==Je)return a;let c=i.parent;if(!c){let l=s[qa];if(l){let u=l.get(n,Je,r);if(u!==Je)return u}c=Xf(s),s=s[nn]}i=c}return o}function Xf(e){let t=e[T],n=t.type;return n===2?t.declTNode:n===1?e[Pe]:null}function Tv(){return nr(Me(),W())}function nr(e,t){return new rr(Ke(e,t))}var rr=(()=>{class e{nativeElement;constructor(n){this.nativeElement=n}static __NG_ELEMENT_ID__=Tv}return e})();function Mv(e){return e instanceof rr?e.nativeElement:e}function xv(){return this._results[Symbol.iterator]()}var Ui=class{_emitDistinctChangesOnly;dirty=!0;_onDirty=void 0;_results=[];_changesDetected=!1;_changes=void 0;length=0;first=void 0;last=void 0;get changes(){return this._changes??=new X}constructor(t=!1){this._emitDistinctChangesOnly=t}get(t){return this._results[t]}map(t){return this._results.map(t)}filter(t){return this._results.filter(t)}find(t){return this._results.find(t)}reduce(t,n){return this._results.reduce(t,n)}forEach(t){this._results.forEach(t)}some(t){return this._results.some(t)}toArray(){return this._results.slice()}toString(){return this._results.toString()}reset(t,n){this.dirty=!1;let r=Cd(t);(this._changesDetected=!Dd(this._results,r,n))&&(this._results=r,this.length=r.length,this.last=r[this.length-1],this.first=r[0])}notifyOnChanges(){this._changes!==void 0&&(this._changesDetected||!this._emitDistinctChangesOnly)&&this._changes.next(this)}onDirty(t){this._onDirty=t}setDirty(){this.dirty=!0,this._onDirty?.()}destroy(){this._changes!==void 0&&(this._changes.complete(),this._changes.unsubscribe())}[Symbol.iterator]=xv};function Jf(e){return(e.flags&128)===128}var $c=(function(e){return e[e.OnPush=0]="OnPush",e[e.Default=1]="Default",e})($c||{}),ep=new Map,Nv=0;function Av(){return Nv++}function Rv(e){ep.set(e[Vr],e)}function Cc(e){ep.delete(e[Vr])}var hf="__ngContext__";function Qn(e,t){ft(t)?(e[hf]=t[Vr],Rv(t)):e[hf]=t}function tp(e){return rp(e[zn])}function np(e){return rp(e[Oe])}function rp(e){for(;e!==null&&!Ue(e);)e=e[Oe];return e}var Ic;function Gc(e){Ic=e}function op(){if(Ic!==void 0)return Ic;if(typeof document<"u")return document;throw new C(210,!1)}var ns=new b("",{providedIn:"root",factory:()=>Ov}),Ov="ng",rs=new b(""),or=new b("",{providedIn:"platform",factory:()=>"unknown"});var os=new b("",{providedIn:"root",factory:()=>op().body?.querySelector("[ngCspNonce]")?.getAttribute("ngCspNonce")||null});var Pv="h",kv="b";var ip=!1,sp=new b("",{providedIn:"root",factory:()=>ip});var Lv=(e,t,n,r)=>{};function Fv(e,t,n,r){Lv(e,t,n,r)}function zc(e){return(e.flags&32)===32}var jv=()=>null;function ap(e,t,n=!1){return jv(e,t,n)}function cp(e,t){let n=e.contentQueries;if(n!==null){let r=_(null);try{for(let o=0;o-1){let i;for(;++oi?f="":f=o[u+1].toLowerCase(),r&2&&l!==f){if($e(r))return!1;s=!0}}}}return $e(r)||s}function $e(e){return(e&1)===0}function $v(e,t,n,r){if(t===null)return-1;let o=0;if(r||!n){let i=!1;for(;o-1)for(n++;n0?'="'+a+'"':"")+"]"}else r&8?o+="."+s:r&4&&(o+=" "+s);else o!==""&&!$e(s)&&(t+=gf(i,o),o=""),r=s,i=i||!$e(r);n++}return o!==""&&(t+=gf(i,o)),t}function Yv(e){return e.map(qv).join(",")}function Zv(e){let t=[],n=[],r=1,o=2;for(;r{Xv(t,c,a)}):e===3&&vf(i,()=>{t.destroyNode(c)}),s!=null&&vy(t,e,s,n,o)}}function iy(e,t){Ep(e,t),t[Ve]=null,t[Pe]=null}function sy(e,t,n,r,o,i){r[Ve]=o,r[Pe]=t,as(e,r,n,1,o,i)}function Ep(e,t){t[ut].changeDetectionScheduler?.notify(9),as(e,t,t[ne],2,null,null)}function ay(e){let t=e[zn];if(!t)return gc(e[T],e);for(;t;){let n=null;if(ft(t))n=t[zn];else{let r=t[fe];r&&(n=r)}if(!n){for(;t&&!t[Oe]&&t!==e;)ft(t)&&gc(t[T],t),t=t[te];t===null&&(t=e),ft(t)&&gc(t[T],t),n=t&&t[Oe]}t=n}}function Qc(e,t){let n=e[on],r=n.indexOf(t);n.splice(r,1)}function Xc(e,t){if(cn(t))return;let n=t[ne];n.destroyNode&&as(e,t,n,3,null,null),ay(t)}function gc(e,t){if(cn(t))return;let n=_(null);try{t[M]&=-129,t[M]|=256,t[Te]&&zt(t[Te]),uy(e,t),ly(e,t),t[T].type===1&&t[ne].destroy();let r=t[Ot];if(r!==null&&Ue(t[te])){r!==t[te]&&Qc(r,t);let o=t[Ze];o!==null&&o.detachView(e)}Cc(t)}finally{_(n)}}function vf(e,t){if(e&&e[he]&&e[he].leave)if(e[he].skipLeaveAnimations)e[he].skipLeaveAnimations=!1;else{let n=e[he].leave,r=[];for(let o=0;o{e[he]&&e[he].running&&(e[he].running=void 0),ss.delete(e),t()});return}t()}function ly(e,t){let n=e.cleanup,r=t[$n];if(n!==null)for(let s=0;s=0?r[a]():r[-a].unsubscribe(),s+=2}else{let a=r[n[s+1]];n[s].call(a)}r!==null&&(t[$n]=null);let o=t[at];if(o!==null){t[at]=null;for(let s=0;sae&&yp(e,t,ae,!1),z(s?2:0,o,n),n(r,o)}finally{Lt(i),z(s?3:1,o,n)}}function Ip(e,t,n){Iy(e,t,n),(n.flags&64)===64&&Sy(e,t,n)}function tl(e,t,n=Ke){let r=t.localNames;if(r!==null){let o=t.index+1;for(let i=0;inull;function Iy(e,t,n){let r=n.directiveStart,o=n.directiveEnd;sn(n)&&ry(t,n,e.data[r+n.componentOffset]),e.firstCreatePass||Wf(n,t);let i=n.initialInputs;for(let s=r;s{ln(e.lView)},consumerOnSignalRead(){this.lView[Te]=this}});function Fy(e){let t=e[Te]??Object.create(jy);return t.lView=e,t}var jy=P(y({},$t),{consumerIsAlwaysLive:!0,kind:"template",consumerMarkedDirty:e=>{let t=At(e.lView);for(;t&&!Np(t[T]);)t=At(t);t&&Qa(t)},consumerOnSignalRead(){this.lView[Te]=this}});function Np(e){return e.type!==2}function Ap(e){if(e[dt]===null)return;let t=!0;for(;t;){let n=!1;for(let r of e[dt])r.dirty&&(n=!0,r.zone===null||Zone.current===r.zone?r.run():r.zone.run(()=>r.run()));t=n&&!!(e[M]&8192)}}var Hy=100;function Rp(e,t=0){let r=e[ut].rendererFactory,o=!1;o||r.begin?.();try{By(e,t)}finally{o||r.end?.()}}function By(e,t){let n=sc();try{Yn(!0),Mc(e,t);let r=0;for(;Gr(e);){if(r===Hy)throw new C(103,!1);r++,Mc(e,1)}}finally{Yn(n)}}function Vy(e,t,n,r){if(cn(t))return;let o=t[M],i=!1,s=!1;Mi(t);let a=!0,c=null,l=null;i||(Np(e)?(l=Oy(t),c=Gt(l)):Ho()===null?(a=!1,l=Fy(t),c=Gt(l)):t[Te]&&(zt(t[Te]),t[Te]=null));try{Ka(t),Wd(e.bindingStartIndex),n!==null&&Cp(e,t,n,2,r),Uy(t);let u=(o&3)===3;if(!i)if(u){let h=e.preOrderCheckHooks;h!==null&&Pi(t,h,null)}else{let h=e.preOrderHooks;h!==null&&ki(t,h,0,null),pc(t,0)}if(s||$y(t),Ap(t),Op(t,0),e.contentQueries!==null&&cp(e,t),!i)if(u){let h=e.contentCheckHooks;h!==null&&Pi(t,h)}else{let h=e.contentHooks;h!==null&&ki(t,h,1),pc(t,1)}zy(e,t);let f=e.components;f!==null&&kp(t,f,0);let m=e.viewQuery;if(m!==null&&Sc(2,m,r),!i)if(u){let h=e.viewCheckHooks;h!==null&&Pi(t,h)}else{let h=e.viewHooks;h!==null&&ki(t,h,2),pc(t,2)}if(e.firstUpdatePass===!0&&(e.firstUpdatePass=!1),t[Di]){for(let h of t[Di])h();t[Di]=null}i||(Mp(t),t[M]&=-73)}catch(u){throw i||ln(t),u}finally{l!==null&&(_n(l,c),a&&ky(l)),xi()}}function Uy(e){let t=e[he];if(t?.enter){for(let n of t.enter)n();t.enter=void 0}}function Op(e,t){for(let n=tp(e);n!==null;n=np(n))for(let r=fe;r0&&(e[n-1][Oe]=r[Oe]);let i=Fr(e,fe+t);iy(r[T],r);let s=i[Ze];s!==null&&s.detachView(i[T]),r[te]=null,r[Oe]=null,r[M]&=-129}return r}function Yy(e,t,n,r){let o=fe+r,i=n.length;r>0&&(n[o-1][Oe]=t),r-1&&(Gi(t,r),Fr(n,r))}this._attachedToViewContainer=!1}Xc(this._lView[T],this._lView)}onDestroy(t){Xa(this._lView,t)}markForCheck(){rl(this._cdRefInjectingView||this._lView,4)}detach(){this._lView[M]&=-129}reattach(){bi(this._lView),this._lView[M]|=128}detectChanges(){this._lView[M]|=1024,Rp(this._lView)}checkNoChanges(){}attachToViewContainerRef(){if(this._appRef)throw new C(902,!1);this._attachedToViewContainer=!0}detachFromAppRef(){this._appRef=null;let t=Wn(this._lView),n=this._lView[Ot];n!==null&&!t&&Qc(n,this._lView),Ep(this._lView[T],this._lView)}attachToAppRef(t){if(this._attachedToViewContainer)throw new C(902,!1);this._appRef=t;let n=Wn(this._lView),r=this._lView[Ot];r!==null&&!n&&jp(r,this._lView),bi(this._lView)}};var Xn=(()=>{class e{_declarationLView;_declarationTContainer;elementRef;static __NG_ELEMENT_ID__=Zy;constructor(n,r,o){this._declarationLView=n,this._declarationTContainer=r,this.elementRef=o}get ssrId(){return this._declarationTContainer.tView?.ssrId||null}createEmbeddedView(n,r){return this.createEmbeddedViewImpl(n,r)}createEmbeddedViewImpl(n,r,o){let i=wp(this._declarationLView,this._declarationTContainer,n,{embeddedViewInjector:r,dehydratedView:o});return new Ft(i)}}return e})();function Zy(){return ol(Me(),W())}function ol(e,t){return e.type&4?new Xn(t,e,nr(e,t)):null}function cs(e,t,n,r,o){let i=e.data[t];if(i===null)i=Ky(e,t,n,r,o),Yd()&&(i.flags|=32);else if(i.type&64){i.type=n,i.value=r,i.attrs=o;let s=$d();i.injectorIndex=s===null?-1:s.injectorIndex}return qn(i,!0),i}function Ky(e,t,n,r,o){let i=oc(),s=ic(),a=s?i:i&&i.parent,c=e.data[t]=Xy(e,a,n,t,r,o);return Qy(e,c,i,s),c}function Qy(e,t,n,r){e.firstChild===null&&(e.firstChild=t),n!==null&&(r?n.child==null&&t.parent!==null&&(n.child=t):n.next===null&&(n.next=t,t.prev=n))}function Xy(e,t,n,r,o,i){let s=t?t.injectorIndex:-1,a=0;return Ud()&&(a|=128),{type:n,index:r,insertBeforeIndex:null,injectorIndex:s,directiveStart:-1,directiveEnd:-1,directiveStylingLast:-1,componentOffset:-1,propertyBindings:null,flags:a,providerIndexes:0,value:o,attrs:i,mergedAttrs:null,localNames:null,initialInputs:null,inputs:null,hostDirectiveInputs:null,outputs:null,hostDirectiveOutputs:null,directiveToIndex:null,tView:null,next:null,prev:null,projectionNext:null,child:null,parent:t,projection:null,styles:null,stylesWithoutHost:null,residualStyles:void 0,classes:null,classesWithoutHost:null,residualClasses:void 0,classBindings:0,styleBindings:0}}var pN=new RegExp(`^(\\d+)*(${kv}|${Pv})*(.*)`);var Jy=()=>null,eE=()=>null;function Ef(e,t){return Jy(e,t)}function tE(e,t,n){return eE(e,t,n)}var Hp=class{},ls=class{},xc=class{resolveComponentFactory(t){throw new C(917,!1)}},no=class{static NULL=new xc},pn=class{};var Bp=(()=>{class e{static \u0275prov=I({token:e,providedIn:"root",factory:()=>null})}return e})();var Fi={},Nc=class{injector;parentInjector;constructor(t,n){this.injector=t,this.parentInjector=n}get(t,n,r){let o=this.injector.get(t,Fi,r);return o!==Fi||n===Fi?o:this.parentInjector.get(t,n,r)}};function zi(e,t,n){let r=n?e.styles:null,o=n?e.classes:null,i=0;if(t!==null)for(let s=0;s0&&(n.directiveToIndex=new Map);for(let m=0;m0;){let n=e[--t];if(typeof n=="number"&&n<0)return n}return 0}function uE(e,t,n){if(n){if(t.exportAs)for(let r=0;rr(Le(S[e.index])):e.index;vE(E,t,n,i,a,h,!1)}}return l}function gE(e){return e.startsWith("animation")||e.startsWith("transition")}function mE(e,t,n,r){let o=e.cleanup;if(o!=null)for(let i=0;ic?a[c]:null}typeof s=="string"&&(i+=2)}return null}function vE(e,t,n,r,o,i,s){let a=t.firstCreatePass?ec(t):null,c=Ja(n),l=c.length;c.push(o,i),a&&a.push(r,e,l,(l+1)*(s?-1:1))}var Ac=Symbol("BINDING");var Wi=class extends no{ngModule;constructor(t){super(),this.ngModule=t}resolveComponentFactory(t){let n=Rt(t);return new Jn(n,this.ngModule)}};function yE(e){return Object.keys(e).map(t=>{let[n,r,o]=e[t],i={propName:n,templateName:t,isSignal:(r&is.SignalBased)!==0};return o&&(i.transform=o),i})}function EE(e){return Object.keys(e).map(t=>({propName:e[t],templateName:t}))}function DE(e,t,n){let r=t instanceof K?t:t?.injector;return r&&e.getStandaloneInjector!==null&&(r=e.getStandaloneInjector(r)||r),r?new Nc(n,r):n}function CE(e){let t=e.get(pn,null);if(t===null)throw new C(407,!1);let n=e.get(Bp,null),r=e.get(Ye,null);return{rendererFactory:t,sanitizer:n,changeDetectionScheduler:r,ngReflect:!1}}function IE(e,t){let n=Gp(e);return pp(t,n,n==="svg"?Rd:n==="math"?Od:null)}function Gp(e){return(e.selectors[0][0]||"div").toLowerCase()}var Jn=class extends ls{componentDef;ngModule;selector;componentType;ngContentSelectors;isBoundToModule;cachedInputs=null;cachedOutputs=null;get inputs(){return this.cachedInputs??=yE(this.componentDef.inputs),this.cachedInputs}get outputs(){return this.cachedOutputs??=EE(this.componentDef.outputs),this.cachedOutputs}constructor(t,n){super(),this.componentDef=t,this.ngModule=n,this.componentType=t.type,this.selector=Yv(t.selectors),this.ngContentSelectors=t.ngContentSelectors??[],this.isBoundToModule=!!n}create(t,n,r,o,i,s){z(22);let a=_(null);try{let c=this.componentDef,l=SE(r,c,s,i),u=DE(c,o||this.ngModule,t),f=CE(u),m=f.rendererFactory.createRenderer(null,c),h=r?Ey(m,r,c.encapsulation,u):IE(c,m),E=s?.some(bf)||i?.some(F=>typeof F!="function"&&F.bindings.some(bf)),S=Yc(null,l,null,512|mp(c),null,null,f,m,u,null,ap(h,u,!0));S[ae]=h,Mi(S);let O=null;try{let F=Up(ae,S,2,"#host",()=>l.directiveRegistry,!0,0);h&&(gp(m,h,F),Qn(h,S)),Ip(l,S,F),lp(l,F,S),$p(l,F),n!==void 0&&_E(F,this.ngContentSelectors,n),O=Qe(F.index,S),S[se]=O[se],nl(l,S,null)}catch(F){throw O!==null&&Cc(O),Cc(S),F}finally{z(23),xi()}return new qi(this.componentType,S,!!E)}finally{_(a)}}};function SE(e,t,n,r){let o=e?["ng-version","20.3.1"]:Zv(t.selectors[0]),i=null,s=null,a=0;if(n)for(let u of n)a+=u[Ac].requiredVars,u.create&&(u.targetIdx=0,(i??=[]).push(u)),u.update&&(u.targetIdx=0,(s??=[]).push(u));if(r)for(let u=0;u{if(n&1&&e)for(let r of e)r.create();if(n&2&&t)for(let r of t)r.update()}}function bf(e){let t=e[Ac].kind;return t==="input"||t==="twoWay"}var qi=class extends Hp{_rootLView;_hasInputBindings;instance;hostView;changeDetectorRef;componentType;location;previousInputValues=null;_tNode;constructor(t,n,r){super(),this._rootLView=n,this._hasInputBindings=r,this._tNode=$r(n[T],ae),this.location=nr(this._tNode,n),this.instance=Qe(this._tNode.index,n)[se],this.hostView=this.changeDetectorRef=new Ft(n,void 0),this.componentType=t}setInput(t,n){this._hasInputBindings;let r=this._tNode;if(this.previousInputValues??=new Map,this.previousInputValues.has(t)&&Object.is(this.previousInputValues.get(t),n))return;let o=this._rootLView,i=_p(r,o[T],o,t,n);this.previousInputValues.set(t,n);let s=Qe(r.index,o);rl(s,1)}get injector(){return new fn(this._tNode,this._rootLView)}destroy(){this.hostView.destroy()}onDestroy(t){this.hostView.onDestroy(t)}};function _E(e,t,n){let r=e.projection=[];for(let o=0;o{class e{static __NG_ELEMENT_ID__=wE}return e})();function wE(){let e=Me();return Wp(e,W())}var TE=gn,zp=class extends TE{_lContainer;_hostTNode;_hostLView;constructor(t,n,r){super(),this._lContainer=t,this._hostTNode=n,this._hostLView=r}get element(){return nr(this._hostTNode,this._hostLView)}get injector(){return new fn(this._hostTNode,this._hostLView)}get parentInjector(){let t=Uc(this._hostTNode,this._hostLView);if($f(t)){let n=Bi(t,this._hostLView),r=Hi(t),o=n[T].data[r+8];return new fn(o,n)}else return new fn(null,this._hostLView)}clear(){for(;this.length>0;)this.remove(this.length-1)}get(t){let n=_f(this._lContainer);return n!==null&&n[t]||null}get length(){return this._lContainer.length-fe}createEmbeddedView(t,n,r){let o,i;typeof r=="number"?o=r:r!=null&&(o=r.index,i=r.injector);let s=Ef(this._lContainer,t.ssrId),a=t.createEmbeddedViewImpl(n||{},i,s);return this.insertImpl(a,o,Tc(this._hostTNode,s)),a}createComponent(t,n,r,o,i,s,a){let c=t&&!sv(t),l;if(c)l=n;else{let O=n||{};l=O.index,r=O.injector,o=O.projectableNodes,i=O.environmentInjector||O.ngModuleRef,s=O.directives,a=O.bindings}let u=c?t:new Jn(Rt(t)),f=r||this.parentInjector;if(!i&&u.ngModule==null){let F=(c?f:this.parentInjector).get(K,null);F&&(i=F)}let m=Rt(u.componentType??{}),h=Ef(this._lContainer,m?.id??null),E=h?.firstChild??null,S=u.create(f,o,E,i,s,a);return this.insertImpl(S.hostView,l,Tc(this._hostTNode,h)),S}insert(t,n){return this.insertImpl(t,n,!0)}insertImpl(t,n,r){let o=t._lView;if(kd(o)){let a=this.indexOf(t);if(a!==-1)this.detach(a);else{let c=o[te],l=new zp(c,c[Pe],c[te]);l.detach(l.indexOf(t))}}let i=this._adjustIndex(n),s=this._lContainer;return Fp(s,o,i,r),t.attachToViewContainerRef(),Ha(mc(s),i,t),t}move(t,n){return this.insert(t,n)}indexOf(t){let n=_f(this._lContainer);return n!==null?n.indexOf(t):-1}remove(t){let n=this._adjustIndex(t,-1),r=Gi(this._lContainer,n);r&&(Fr(mc(this._lContainer),n),Xc(r[T],r))}detach(t){let n=this._adjustIndex(t,-1),r=Gi(this._lContainer,n);return r&&Fr(mc(this._lContainer),n)!=null?new Ft(r):null}_adjustIndex(t,n=0){return t??this.length+n}};function _f(e){return e[Ur]}function mc(e){return e[Ur]||(e[Ur]=[])}function Wp(e,t){let n,r=t[e.index];return Ue(r)?n=r:(n=Lp(r,t,null,e),t[e.index]=n,Zc(t,n)),xE(n,t,e,r),new zp(n,e,t)}function ME(e,t){let n=e[ne],r=n.createComment(""),o=Ke(t,e),i=n.parentNode(o);return $i(n,i,r,n.nextSibling(o),!1),r}var xE=RE,NE=()=>!1;function AE(e,t,n){return NE(e,t,n)}function RE(e,t,n,r){if(e[Pt])return;let o;n.type&8?o=Le(r):o=ME(t,n),e[Pt]=o}var Rc=class e{queryList;matches=null;constructor(t){this.queryList=t}clone(){return new e(this.queryList)}setDirty(){this.queryList.setDirty()}},Oc=class e{queries;constructor(t=[]){this.queries=t}createEmbeddedView(t){let n=t.queries;if(n!==null){let r=t.contentQueries!==null?t.contentQueries[0]:n.length,o=[];for(let i=0;i0)r.push(s[a/2]);else{let l=i[a+1],u=t[-c];for(let f=fe;ft.trim())}function BE(e,t,n){e.queries===null&&(e.queries=new kc),e.queries.track(new Lc(t,n))}function Zp(e,t){return e.queries.getByIndex(t)}function VE(e,t){let n=e[T],r=Zp(n,t);return r.crossesNgTemplate?Fc(n,e,t,[]):qp(n,e,r,t)}function sl(e,t,n){let r,o=Tr(()=>{r._dirtyCounter();let i=$E(r,e);if(t&&i===void 0)throw new C(-951,!1);return i});return r=o[oe],r._dirtyCounter=k(0),r._flatValue=void 0,o}function Kp(e){return sl(!0,!1,e)}function Qp(e){return sl(!0,!0,e)}function Xp(e){return sl(!1,!1,e)}function UE(e,t){let n=e[oe];n._lView=W(),n._queryIndex=t,n._queryList=Yp(n._lView,t),n._queryList.onDirty(()=>n._dirtyCounter.update(r=>r+1))}function $E(e,t){let n=e._lView,r=e._queryIndex;if(n===void 0||r===void 0||n[M]&4)return t?void 0:we;let o=Yp(n,r),i=VE(n,r);return o.reset(i,Mv),t?o.first:o._changesDetected||e._flatValue===void 0?e._flatValue=o.toArray():e._flatValue}var wf=new Set;function mn(e){wf.has(e)||(wf.add(e),performance?.mark?.("mark_feature_usage",{detail:{feature:e}}))}var er=class{},ds=class{};var Yi=class extends er{ngModuleType;_parent;_bootstrapComponents=[];_r3Injector;instance;destroyCbs=[];componentFactoryResolver=new Wi(this);constructor(t,n,r,o=!0){super(),this.ngModuleType=t,this._parent=n;let i=Ua(t);this._bootstrapComponents=dp(i.bootstrap),this._r3Injector=lc(t,n,[{provide:er,useValue:this},{provide:no,useValue:this.componentFactoryResolver},...r],ct(t),new Set(["environment"])),o&&this.resolveInjectorInitializers()}resolveInjectorInitializers(){this._r3Injector.resolveInjectorInitializers(),this.instance=this._r3Injector.get(this.ngModuleType)}get injector(){return this._r3Injector}destroy(){let t=this._r3Injector;!t.destroyed&&t.destroy(),this.destroyCbs.forEach(n=>n()),this.destroyCbs=null}onDestroy(t){this.destroyCbs.push(t)}},Zi=class extends ds{moduleType;constructor(t){super(),this.moduleType=t}create(t){return new Yi(this.moduleType,t,[])}};var Xr=class extends er{injector;componentFactoryResolver=new Wi(this);instance=null;constructor(t){super();let n=new Xt([...t.providers,{provide:er,useValue:this},{provide:no,useValue:this.componentFactoryResolver}],t.parent||Hr(),t.debugName,new Set(["environment"]));this.injector=n,t.runEnvironmentInitializers&&n.resolveInjectorInitializers()}destroy(){this.injector.destroy()}onDestroy(t){this.injector.onDestroy(t)}};function ro(e,t,n=null){return new Xr({providers:e,parent:t,debugName:n,runEnvironmentInitializers:!0}).injector}var GE=(()=>{class e{_injector;cachedInjectors=new Map;constructor(n){this._injector=n}getOrCreateStandaloneInjector(n){if(!n.standalone)return null;if(!this.cachedInjectors.has(n)){let r=Ga(!1,n.type),o=r.length>0?ro([r],this._injector,`Standalone[${n.type.name}]`):null;this.cachedInjectors.set(n,o)}return this.cachedInjectors.get(n)}ngOnDestroy(){try{for(let n of this.cachedInjectors.values())n!==null&&n.destroy()}finally{this.cachedInjectors.clear()}}static \u0275prov=I({token:e,providedIn:"environment",factory:()=>new e(N(K))})}return e})();function vn(e){return to(()=>{let t=Jp(e),n=P(y({},t),{decls:e.decls,vars:e.vars,template:e.template,consts:e.consts||null,ngContentSelectors:e.ngContentSelectors,onPush:e.changeDetection===$c.OnPush,directiveDefs:null,pipeDefs:null,dependencies:t.standalone&&e.dependencies||null,getStandaloneInjector:t.standalone?o=>o.get(GE).getOrCreateStandaloneInjector(n):null,getExternalStyles:null,signals:e.signals??!1,data:e.data||{},encapsulation:e.encapsulation||vt.Emulated,styles:e.styles||we,_:null,schemas:e.schemas||null,tView:null,id:""});t.standalone&&mn("NgStandalone"),eh(n);let r=e.dependencies;return n.directiveDefs=Tf(r,zE),n.pipeDefs=Tf(r,bd),n.id=YE(n),n})}function zE(e){return Rt(e)||$a(e)}function oo(e){return to(()=>({type:e.type,bootstrap:e.bootstrap||we,declarations:e.declarations||we,imports:e.imports||we,exports:e.exports||we,transitiveCompileScopes:null,schemas:e.schemas||null,id:e.id||null}))}function WE(e,t){if(e==null)return en;let n={};for(let r in e)if(e.hasOwnProperty(r)){let o=e[r],i,s,a,c;Array.isArray(o)?(a=o[0],i=o[1],s=o[2]??i,c=o[3]||null):(i=o,s=o,a=is.None,c=null),n[i]=[r,a,c],t[i]=s}return n}function qE(e){if(e==null)return en;let t={};for(let n in e)e.hasOwnProperty(n)&&(t[e[n]]=n);return t}function fs(e){return to(()=>{let t=Jp(e);return eh(t),t})}function Jp(e){let t={};return{type:e.type,providersResolver:null,factory:null,hostBindings:e.hostBindings||null,hostVars:e.hostVars||0,hostAttrs:e.hostAttrs||null,contentQueries:e.contentQueries||null,declaredInputs:t,inputConfig:e.inputs||en,exportAs:e.exportAs||null,standalone:e.standalone??!0,signals:e.signals===!0,selectors:e.selectors||we,viewQuery:e.viewQuery||null,features:e.features||null,setInput:null,resolveHostDirectives:null,hostDirectives:null,inputs:WE(e.inputs,t),outputs:qE(e.outputs),debugInfo:null}}function eh(e){e.features?.forEach(t=>t(e))}function Tf(e,t){return e?()=>{let n=typeof e=="function"?e():e,r=[];for(let o of n){let i=t(o);i!==null&&r.push(i)}return r}:null}function YE(e){let t=0,n=typeof e.consts=="function"?"":e.consts,r=[e.selectors,e.ngContentSelectors,e.hostVars,e.hostAttrs,n,e.vars,e.decls,e.encapsulation,e.standalone,e.signals,e.exportAs,JSON.stringify(e.inputs),JSON.stringify(e.outputs),Object.getOwnPropertyNames(e.type.prototype),!!e.contentQueries,!!e.viewQuery];for(let i of r.join("|"))t=Math.imul(31,t)+i.charCodeAt(0)<<0;return t+=2147483648,"c"+t}function ZE(e,t,n,r,o,i,s,a){if(n.firstCreatePass){e.mergedAttrs=es(e.mergedAttrs,e.attrs);let u=e.tView=qc(2,e,o,i,s,n.directiveRegistry,n.pipeRegistry,null,n.schemas,n.consts,null);n.queries!==null&&(n.queries.template(n,e),u.queries=n.queries.embeddedTView(e))}a&&(e.flags|=a),qn(e,!1);let c=KE(n,t,e,r);Ni()&&Jc(n,t,c,e),Qn(c,t);let l=Lp(c,t,c,e);t[r+ae]=l,Zc(t,l),AE(l,e,t)}function th(e,t,n,r,o,i,s,a,c,l,u){let f=n+ae,m;if(t.firstCreatePass){if(m=cs(t,f,4,s||null,a||null),l!=null){let h=kt(t.consts,l);m.localNames=[];for(let E=0;Enull),s=r;if(t&&typeof t=="object"){let c=t;o=c.next?.bind(c),i=c.error?.bind(c),s=c.complete?.bind(c)}this.__isAsync&&(i=this.wrapInTimeout(i),o&&(o=this.wrapInTimeout(o)),s&&(s=this.wrapInTimeout(s)));let a=super.subscribe({next:o,error:i,complete:s});return t instanceof q&&t.add(a),a}wrapInTimeout(t){return n=>{let r=this.pendingTasks?.add();setTimeout(()=>{try{t(n)}finally{r!==void 0&&this.pendingTasks?.remove(r)}})}}},ge=jc;function rh(e){let t,n;function r(){e=dn;try{n!==void 0&&typeof cancelAnimationFrame=="function"&&cancelAnimationFrame(n),t!==void 0&&clearTimeout(t)}catch{}}return t=setTimeout(()=>{e(),r()}),typeof requestAnimationFrame=="function"&&(n=requestAnimationFrame(()=>{e(),r()})),()=>r()}function Mf(e){return queueMicrotask(()=>e()),()=>{e=dn}}var cl="isAngularZone",Ki=cl+"_ID",XE=0,Q=class e{hasPendingMacrotasks=!1;hasPendingMicrotasks=!1;isStable=!0;onUnstable=new ge(!1);onMicrotaskEmpty=new ge(!1);onStable=new ge(!1);onError=new ge(!1);constructor(t){let{enableLongStackTrace:n=!1,shouldCoalesceEventChangeDetection:r=!1,shouldCoalesceRunChangeDetection:o=!1,scheduleInRootZone:i=nh}=t;if(typeof Zone>"u")throw new C(908,!1);Zone.assertZonePatched();let s=this;s._nesting=0,s._outer=s._inner=Zone.current,Zone.TaskTrackingZoneSpec&&(s._inner=s._inner.fork(new Zone.TaskTrackingZoneSpec)),n&&Zone.longStackTraceZoneSpec&&(s._inner=s._inner.fork(Zone.longStackTraceZoneSpec)),s.shouldCoalesceEventChangeDetection=!o&&r,s.shouldCoalesceRunChangeDetection=o,s.callbackScheduled=!1,s.scheduleInRootZone=i,tD(s)}static isInAngularZone(){return typeof Zone<"u"&&Zone.current.get(cl)===!0}static assertInAngularZone(){if(!e.isInAngularZone())throw new C(909,!1)}static assertNotInAngularZone(){if(e.isInAngularZone())throw new C(909,!1)}run(t,n,r){return this._inner.run(t,n,r)}runTask(t,n,r,o){let i=this._inner,s=i.scheduleEventTask("NgZoneEvent: "+o,t,JE,dn,dn);try{return i.runTask(s,n,r)}finally{i.cancelTask(s)}}runGuarded(t,n,r){return this._inner.runGuarded(t,n,r)}runOutsideAngular(t){return this._outer.run(t)}},JE={};function ll(e){if(e._nesting==0&&!e.hasPendingMicrotasks&&!e.isStable)try{e._nesting++,e.onMicrotaskEmpty.emit(null)}finally{if(e._nesting--,!e.hasPendingMicrotasks)try{e.runOutsideAngular(()=>e.onStable.emit(null))}finally{e.isStable=!0}}}function eD(e){if(e.isCheckStableRunning||e.callbackScheduled)return;e.callbackScheduled=!0;function t(){rh(()=>{e.callbackScheduled=!1,Hc(e),e.isCheckStableRunning=!0,ll(e),e.isCheckStableRunning=!1})}e.scheduleInRootZone?Zone.root.run(()=>{t()}):e._outer.run(()=>{t()}),Hc(e)}function tD(e){let t=()=>{eD(e)},n=XE++;e._inner=e._inner.fork({name:"angular",properties:{[cl]:!0,[Ki]:n,[Ki+n]:!0},onInvokeTask:(r,o,i,s,a,c)=>{if(nD(c))return r.invokeTask(i,s,a,c);try{return xf(e),r.invokeTask(i,s,a,c)}finally{(e.shouldCoalesceEventChangeDetection&&s.type==="eventTask"||e.shouldCoalesceRunChangeDetection)&&t(),Nf(e)}},onInvoke:(r,o,i,s,a,c,l)=>{try{return xf(e),r.invoke(i,s,a,c,l)}finally{e.shouldCoalesceRunChangeDetection&&!e.callbackScheduled&&!rD(c)&&t(),Nf(e)}},onHasTask:(r,o,i,s)=>{r.hasTask(i,s),o===i&&(s.change=="microTask"?(e._hasPendingMicrotasks=s.microTask,Hc(e),ll(e)):s.change=="macroTask"&&(e.hasPendingMacrotasks=s.macroTask))},onHandleError:(r,o,i,s)=>(r.handleError(i,s),e.runOutsideAngular(()=>e.onError.emit(s)),!1)})}function Hc(e){e._hasPendingMicrotasks||(e.shouldCoalesceEventChangeDetection||e.shouldCoalesceRunChangeDetection)&&e.callbackScheduled===!0?e.hasPendingMicrotasks=!0:e.hasPendingMicrotasks=!1}function xf(e){e._nesting++,e.isStable&&(e.isStable=!1,e.onUnstable.emit(null))}function Nf(e){e._nesting--,ll(e)}var Jr=class{hasPendingMicrotasks=!1;hasPendingMacrotasks=!1;isStable=!0;onUnstable=new ge;onMicrotaskEmpty=new ge;onStable=new ge;onError=new ge;run(t,n,r){return t.apply(n,r)}runGuarded(t,n,r){return t.apply(n,r)}runOutsideAngular(t){return t()}runTask(t,n,r,o){return t.apply(n,r)}};function nD(e){return oh(e,"__ignore_ng_zone__")}function rD(e){return oh(e,"__scheduler_tick__")}function oh(e,t){return!Array.isArray(e)||e.length!==1?!1:e[0]?.data?.[t]===!0}var ih=(()=>{class e{impl=null;execute(){this.impl?.execute()}static \u0275prov=I({token:e,providedIn:"root",factory:()=>new e})}return e})();var ul=(()=>{class e{log(n){console.log(n)}warn(n){console.warn(n)}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"platform"})}return e})();var dl=new b("");function so(e){return!!e&&typeof e.then=="function"}function sh(e){return!!e&&typeof e.subscribe=="function"}var ah=new b("");var fl=(()=>{class e{resolve;reject;initialized=!1;done=!1;donePromise=new Promise((n,r)=>{this.resolve=n,this.reject=r});appInits=v(ah,{optional:!0})??[];injector=v(Re);constructor(){}runInitializers(){if(this.initialized)return;let n=[];for(let o of this.appInits){let i=G(this.injector,o);if(so(i))n.push(i);else if(sh(i)){let s=new Promise((a,c)=>{i.subscribe({complete:a,error:c})});n.push(s)}}let r=()=>{this.done=!0,this.resolve()};Promise.all(n).then(()=>{r()}).catch(o=>{this.reject(o)}),n.length===0&&r(),this.initialized=!0}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})(),ps=new b("");function ch(){na(()=>{let e="";throw new C(600,e)})}function lh(e){return e.isBoundToModule}var oD=10;var yn=(()=>{class e{_runningTick=!1;_destroyed=!1;_destroyListeners=[];_views=[];internalErrorHandler=v(Fe);afterRenderManager=v(ih);zonelessEnabled=v(zr);rootEffectScheduler=v(qr);dirtyFlags=0;tracingSnapshot=null;allTestViews=new Set;autoDetectTestViews=new Set;includeAllTestViews=!1;afterTick=new X;get allViews(){return[...(this.includeAllTestViews?this.allTestViews:this.autoDetectTestViews).keys(),...this._views]}get destroyed(){return this._destroyed}componentTypes=[];components=[];internalPendingTask=v(mt);get isStable(){return this.internalPendingTask.hasPendingTasksObservable.pipe(B(n=>!n))}constructor(){v(io,{optional:!0})}whenStable(){let n;return new Promise(r=>{n=this.isStable.subscribe({next:o=>{o&&r()}})}).finally(()=>{n.unsubscribe()})}_injector=v(K);_rendererFactory=null;get injector(){return this._injector}bootstrap(n,r){return this.bootstrapImpl(n,r)}bootstrapImpl(n,r,o=Re.NULL){return this._injector.get(Q).run(()=>{z(10);let s=n instanceof ls;if(!this._injector.get(fl).done){let E="";throw new C(405,E)}let c;s?c=n:c=this._injector.get(no).resolveComponentFactory(n),this.componentTypes.push(c.componentType);let l=lh(c)?void 0:this._injector.get(er),u=r||c.selector,f=c.create(o,[],u,l),m=f.location.nativeElement,h=f.injector.get(dl,null);return h?.registerApplication(m),f.onDestroy(()=>{this.detachView(f.hostView),Zr(this.components,f),h?.unregisterApplication(m)}),this._loadComponent(f),z(11,f),f})}tick(){this.zonelessEnabled||(this.dirtyFlags|=1),this._tick()}_tick(){z(12),this.tracingSnapshot!==null?this.tracingSnapshot.run(al.CHANGE_DETECTION,this.tickImpl):this.tickImpl()}tickImpl=()=>{if(this._runningTick)throw new C(101,!1);let n=_(null);try{this._runningTick=!0,this.synchronize()}finally{this._runningTick=!1,this.tracingSnapshot?.dispose(),this.tracingSnapshot=null,_(n),this.afterTick.next(),z(13)}};synchronize(){this._rendererFactory===null&&!this._injector.destroyed&&(this._rendererFactory=this._injector.get(pn,null,{optional:!0}));let n=0;for(;this.dirtyFlags!==0&&n++Gr(n))){this.dirtyFlags|=2;return}else this.dirtyFlags&=-8}attachView(n){let r=n;this._views.push(r),r.attachToAppRef(this)}detachView(n){let r=n;Zr(this._views,r),r.detachFromAppRef()}_loadComponent(n){this.attachView(n.hostView);try{this.tick()}catch(o){this.internalErrorHandler(o)}this.components.push(n),this._injector.get(ps,[]).forEach(o=>o(n))}ngOnDestroy(){if(!this._destroyed)try{this._destroyListeners.forEach(n=>n()),this._views.slice().forEach(n=>n.destroy())}finally{this._destroyed=!0,this._views=[],this._destroyListeners=[]}}onDestroy(n){return this._destroyListeners.push(n),()=>Zr(this._destroyListeners,n)}destroy(){if(this._destroyed)throw new C(406,!1);let n=this._injector;n.destroy&&!n.destroyed&&n.destroy()}get viewCount(){return this._views.length}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();function Zr(e,t){let n=e.indexOf(t);n>-1&&e.splice(n,1)}function hs(e,t,n,r){let o=W(),i=_i();if(us(o,i,t)){let s=Xe(),a=rf();wy(a,o,e,t,n,r)}return hs}var DN=typeof document<"u"&&typeof document?.documentElement?.getAnimations=="function";function ir(e,t,n,r,o,i,s,a){mn("NgControlFlow");let c=W(),l=Xe(),u=kt(l.consts,i);return th(c,l,e,t,n,r,o,u,256,s,a),pl}function pl(e,t,n,r,o,i,s,a){mn("NgControlFlow");let c=W(),l=Xe(),u=kt(l.consts,i);return th(c,l,e,t,n,r,o,u,512,s,a),pl}function sr(e,t){mn("NgControlFlow");let n=W(),r=_i(),o=n[r]!==Et?n[r]:-1,i=o!==-1?Af(n,ae+o):void 0,s=0;if(us(n,r,e)){let a=_(null);try{if(i!==void 0&&qy(i,s),e!==-1){let c=ae+e,l=Af(n,c),u=iD(n[T],c),f=tE(l,u,n),m=wp(n,u,t,{dehydratedView:f});Fp(l,m,s,Tc(u,f))}}finally{_(a)}}else if(i!==void 0){let a=Wy(i,s);a!==void 0&&(a[se]=t)}}function Af(e,t){return e[t]}function iD(e,t){return $r(e,t)}function Rf(e,t,n,r,o){_p(t,e,n,o?"class":"style",r)}function hl(e,t,n,r){let o=W(),i=o[T],s=e+ae,a=i.firstCreatePass?Up(s,o,2,t,_y,Vd(),n,r):i.data[s];if(Sp(a,o,e,t,uh),Ii(a)){let c=o[T];Ip(c,o,a),lp(c,a,o)}return r!=null&&tl(o,a),hl}function gl(){let e=Xe(),t=Me(),n=bp(t);return e.firstCreatePass&&$p(e,n),nc(n)&&rc(),tc(),n.classesWithoutHost!=null&&pv(n)&&Rf(e,n,W(),n.classesWithoutHost,!0),n.stylesWithoutHost!=null&&hv(n)&&Rf(e,n,W(),n.stylesWithoutHost,!1),gl}function ar(e,t,n,r){return hl(e,t,n,r),gl(),ar}function g(e,t,n,r){let o=W(),i=o[T],s=e+ae,a=i.firstCreatePass?fE(s,i,2,t,n,r):i.data[s];return Sp(a,o,e,t,uh),r!=null&&tl(o,a),g}function p(){let e=Me(),t=bp(e);return nc(t)&&rc(),tc(),p}function L(e,t,n,r){return g(e,t,n,r),p(),L}var uh=(e,t,n,r,o)=>(Ai(!0),pp(t[ne],r,of()));function ml(){return W()}var ao="en-US";var sD=ao;function dh(e){typeof e=="string"&&(sD=e.toLowerCase().replace(/_/g,"-"))}function Ct(e,t,n){let r=W(),o=Xe(),i=Me();return(i.type&3||n)&&hE(i,o,r,n,r[ne],e,t,pE(i,r,t)),Ct}function Ge(e=1){return nf(e)}function ce(e,t,n,r){UE(e,jE(t,n,r))}function vl(e=1){Ti(Xd()+e)}function gs(e){let t=zd();return Pd(t,ae+e)}function Oi(e,t){return e<<17|t<<2}function hn(e){return e>>17&32767}function aD(e){return(e&2)==2}function cD(e,t){return e&131071|t<<17}function Bc(e){return e|2}function tr(e){return(e&131068)>>2}function vc(e,t){return e&-131069|t<<2}function lD(e){return(e&1)===1}function Vc(e){return e|1}function uD(e,t,n,r,o,i){let s=i?t.classBindings:t.styleBindings,a=hn(s),c=tr(s);e[r]=n;let l=!1,u;if(Array.isArray(n)){let f=n;u=f[1],(u===null||Un(f,u)>0)&&(l=!0)}else u=n;if(o)if(c!==0){let m=hn(e[a+1]);e[r+1]=Oi(m,a),m!==0&&(e[m+1]=vc(e[m+1],r)),e[a+1]=cD(e[a+1],r)}else e[r+1]=Oi(a,0),a!==0&&(e[a+1]=vc(e[a+1],r)),a=r;else e[r+1]=Oi(c,0),a===0?a=r:e[c+1]=vc(e[c+1],r),c=r;l&&(e[r+1]=Bc(e[r+1])),Of(e,u,r,!0),Of(e,u,r,!1),dD(t,u,e,r,i),s=Oi(a,c),i?t.classBindings=s:t.styleBindings=s}function dD(e,t,n,r,o){let i=o?e.residualClasses:e.residualStyles;i!=null&&typeof t=="string"&&Un(i,t)>=0&&(n[r+1]=Vc(n[r+1]))}function Of(e,t,n,r){let o=e[n+1],i=t===null,s=r?hn(o):tr(o),a=!1;for(;s!==0&&(a===!1||i);){let c=e[s],l=e[s+1];fD(c,t)&&(a=!0,e[s+1]=r?Vc(l):Bc(l)),s=r?hn(l):tr(l)}a&&(e[n+1]=r?Bc(o):Vc(o))}function fD(e,t){return e===null||t==null||(Array.isArray(e)?e[1]:e)===t?!0:Array.isArray(e)&&typeof t=="string"?Un(e,t)>=0:!1}function co(e,t){return pD(e,t,null,!0),co}function pD(e,t,n,r){let o=W(),i=Xe(),s=qd(2);if(i.firstUpdatePass&&gD(i,e,s,r),t!==Et&&us(o,s,t)){let a=i.data[un()];DD(i,a,o,o[ne],e,o[s+1]=CD(t,n),r,s)}}function hD(e,t){return t>=e.expandoStartIndex}function gD(e,t,n,r){let o=e.data;if(o[n+1]===null){let i=o[un()],s=hD(e,n);ID(i,r)&&t===null&&!s&&(t=!1),t=mD(o,i,t,r),uD(o,i,t,n,s,r)}}function mD(e,t,n,r){let o=Qd(e),i=r?t.residualClasses:t.residualStyles;if(o===null)(r?t.classBindings:t.styleBindings)===0&&(n=yc(null,e,t,n,r),n=eo(n,t.attrs,r),i=null);else{let s=t.directiveStylingLast;if(s===-1||e[s]!==o)if(n=yc(o,e,t,n,r),i===null){let c=vD(e,t,r);c!==void 0&&Array.isArray(c)&&(c=yc(null,e,t,c[1],r),c=eo(c,t.attrs,r),yD(e,t,r,c))}else i=ED(e,t,r)}return i!==void 0&&(r?t.residualClasses=i:t.residualStyles=i),n}function vD(e,t,n){let r=n?t.classBindings:t.styleBindings;if(tr(r)!==0)return e[hn(r)]}function yD(e,t,n,r){let o=n?t.classBindings:t.styleBindings;e[hn(o)]=r}function ED(e,t,n){let r,o=t.directiveEnd;for(let i=1+t.directiveStylingLast;i0;){let c=e[o],l=Array.isArray(c),u=l?c[1]:c,f=u===null,m=n[o+1];m===Et&&(m=f?we:void 0);let h=f?Ei(m,r):u===r?m:void 0;if(l&&!Qi(h)&&(h=Ei(c,r)),Qi(h)&&(a=h,s))return a;let E=e[o+1];o=s?hn(E):tr(E)}if(t!==null){let c=i?t.residualClasses:t.residualStyles;c!=null&&(a=Ei(c,r))}return a}function Qi(e){return e!==void 0}function CD(e,t){return e==null||e===""||(typeof t=="string"?e=e+t:typeof e=="object"&&(e=ct(up(e)))),e}function ID(e,t){return(e.flags&(t?8:16))!==0}function d(e,t=""){let n=W(),r=Xe(),o=e+ae,i=r.firstCreatePass?cs(r,o,1,t,null):r.data[o],s=SD(r,n,i,t,e);n[o]=s,Ni()&&Jc(r,n,s,i),qn(i,!1)}var SD=(e,t,n,r,o)=>(Ai(!0),Kv(t[ne],r));function bD(e,t,n,r=""){return us(e,_i(),n)?t+mi(n)+r:Et}function cr(e){return yl("",e),cr}function yl(e,t,n){let r=W(),o=bD(r,e,t,n);return o!==Et&&_D(r,un(),o),yl}function _D(e,t,n){let r=Za(t,e);Qv(e[ne],r,n)}var Xi=class{ngModuleFactory;componentFactories;constructor(t,n){this.ngModuleFactory=t,this.componentFactories=n}},El=(()=>{class e{compileModuleSync(n){return new Zi(n)}compileModuleAsync(n){return Promise.resolve(this.compileModuleSync(n))}compileModuleAndAllComponentsSync(n){let r=this.compileModuleSync(n),o=Ua(n),i=dp(o.declarations).reduce((s,a)=>{let c=Rt(a);return c&&s.push(new Jn(c)),s},[]);return new Xi(r,i)}compileModuleAndAllComponentsAsync(n){return Promise.resolve(this.compileModuleAndAllComponentsSync(n))}clearCache(){}clearCacheFor(n){}getModuleId(n){}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();var wD=(()=>{class e{zone=v(Q);changeDetectionScheduler=v(Ye);applicationRef=v(yn);applicationErrorHandler=v(Fe);_onMicrotaskEmptySubscription;initialize(){this._onMicrotaskEmptySubscription||(this._onMicrotaskEmptySubscription=this.zone.onMicrotaskEmpty.subscribe({next:()=>{this.changeDetectionScheduler.runningTick||this.zone.run(()=>{try{this.applicationRef.dirtyFlags|=1,this.applicationRef._tick()}catch(n){this.applicationErrorHandler(n)}})}}))}ngOnDestroy(){this._onMicrotaskEmptySubscription?.unsubscribe()}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();function fh({ngZoneFactory:e,ignoreChangesOutsideZone:t,scheduleInRootZone:n}){return e??=()=>new Q(P(y({},ph()),{scheduleInRootZone:n})),[{provide:Q,useFactory:e},{provide:lt,multi:!0,useFactory:()=>{let r=v(wD,{optional:!0});return()=>r.initialize()}},{provide:lt,multi:!0,useFactory:()=>{let r=v(TD);return()=>{r.initialize()}}},t===!0?{provide:fc,useValue:!0}:[],{provide:Ri,useValue:n??nh},{provide:Fe,useFactory:()=>{let r=v(Q),o=v(K),i;return s=>{r.runOutsideAngular(()=>{o.destroyed&&!i?setTimeout(()=>{throw s}):(i??=o.get(Be),i.handleError(s))})}}}]}function ph(e){return{enableLongStackTrace:!1,shouldCoalesceEventChangeDetection:e?.eventCoalescing??!1,shouldCoalesceRunChangeDetection:e?.runCoalescing??!1}}var TD=(()=>{class e{subscription=new q;initialized=!1;zone=v(Q);pendingTasks=v(mt);initialize(){if(this.initialized)return;this.initialized=!0;let n=null;!this.zone.isStable&&!this.zone.hasPendingMacrotasks&&!this.zone.hasPendingMicrotasks&&(n=this.pendingTasks.add()),this.zone.runOutsideAngular(()=>{this.subscription.add(this.zone.onStable.subscribe(()=>{Q.assertNotInAngularZone(),queueMicrotask(()=>{n!==null&&!this.zone.hasPendingMacrotasks&&!this.zone.hasPendingMicrotasks&&(this.pendingTasks.remove(n),n=null)})}))}),this.subscription.add(this.zone.onUnstable.subscribe(()=>{Q.assertInAngularZone(),n??=this.pendingTasks.add()}))}ngOnDestroy(){this.subscription.unsubscribe()}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();var Dl=(()=>{class e{applicationErrorHandler=v(Fe);appRef=v(yn);taskService=v(mt);ngZone=v(Q);zonelessEnabled=v(zr);tracing=v(io,{optional:!0});disableScheduling=v(fc,{optional:!0})??!1;zoneIsDefined=typeof Zone<"u"&&!!Zone.root.run;schedulerTickApplyArgs=[{data:{__scheduler_tick__:!0}}];subscriptions=new q;angularZoneId=this.zoneIsDefined?this.ngZone._inner?.get(Ki):null;scheduleInRootZone=!this.zonelessEnabled&&this.zoneIsDefined&&(v(Ri,{optional:!0})??!1);cancelScheduledCallback=null;useMicrotaskScheduler=!1;runningTick=!1;pendingRenderTaskId=null;constructor(){this.subscriptions.add(this.appRef.afterTick.subscribe(()=>{this.runningTick||this.cleanup()})),this.subscriptions.add(this.ngZone.onUnstable.subscribe(()=>{this.runningTick||this.cleanup()})),this.disableScheduling||=!this.zonelessEnabled&&(this.ngZone instanceof Jr||!this.zoneIsDefined)}notify(n){if(!this.zonelessEnabled&&n===5)return;let r=!1;switch(n){case 0:{this.appRef.dirtyFlags|=2;break}case 3:case 2:case 4:case 5:case 1:{this.appRef.dirtyFlags|=4;break}case 6:{this.appRef.dirtyFlags|=2,r=!0;break}case 12:{this.appRef.dirtyFlags|=16,r=!0;break}case 13:{this.appRef.dirtyFlags|=2,r=!0;break}case 11:{r=!0;break}case 9:case 8:case 7:case 10:default:this.appRef.dirtyFlags|=8}if(this.appRef.tracingSnapshot=this.tracing?.snapshot(this.appRef.tracingSnapshot)??null,!this.shouldScheduleTick(r))return;let o=this.useMicrotaskScheduler?Mf:rh;this.pendingRenderTaskId=this.taskService.add(),this.scheduleInRootZone?this.cancelScheduledCallback=Zone.root.run(()=>o(()=>this.tick())):this.cancelScheduledCallback=this.ngZone.runOutsideAngular(()=>o(()=>this.tick()))}shouldScheduleTick(n){return!(this.disableScheduling&&!n||this.appRef.destroyed||this.pendingRenderTaskId!==null||this.runningTick||this.appRef._runningTick||!this.zonelessEnabled&&this.zoneIsDefined&&Zone.current.get(Ki+this.angularZoneId))}tick(){if(this.runningTick||this.appRef.destroyed)return;if(this.appRef.dirtyFlags===0){this.cleanup();return}!this.zonelessEnabled&&this.appRef.dirtyFlags&7&&(this.appRef.dirtyFlags|=1);let n=this.taskService.add();try{this.ngZone.run(()=>{this.runningTick=!0,this.appRef._tick()},void 0,this.schedulerTickApplyArgs)}catch(r){this.taskService.remove(n),this.applicationErrorHandler(r)}finally{this.cleanup()}this.useMicrotaskScheduler=!0,Mf(()=>{this.useMicrotaskScheduler=!1,this.taskService.remove(n)})}ngOnDestroy(){this.subscriptions.unsubscribe(),this.cleanup()}cleanup(){if(this.runningTick=!1,this.cancelScheduledCallback?.(),this.cancelScheduledCallback=null,this.pendingRenderTaskId!==null){let n=this.pendingRenderTaskId;this.pendingRenderTaskId=null,this.taskService.remove(n)}}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();function Cl(){return mn("NgZoneless"),tn([{provide:Ye,useExisting:Dl},{provide:Q,useClass:Jr},{provide:zr,useValue:!0},{provide:Ri,useValue:!1},[]])}function MD(){return typeof $localize<"u"&&$localize.locale||ao}var Il=new b("",{providedIn:"root",factory:()=>v(Il,{optional:!0,skipSelf:!0})||MD()});function Ce(e){return dd(e)}function lo(e,t){return Tr(e,t?.equal)}var Sl=class{[oe];constructor(t){this[oe]=t}destroy(){this[oe].destroy()}};function It(e,t){let n=t?.injector??v(Re),r=t?.manualCleanup!==!0?n.get(gt):null,o,i=n.get(Wr,null,{optional:!0}),s=n.get(Ye);return i!==null?(o=AD(i.view,s,e),r instanceof kr&&r._lView===i.view&&(r=null)):o=RD(e,n.get(qr),s),o.injector=n,r!==null&&(o.onDestroyFn=r.onDestroy(()=>o.destroy())),new Sl(o)}var hh=P(y({},fd),{cleanupFns:void 0,zone:null,onDestroyFn:dn,run(){let e=Yn(!1);try{pd(this)}finally{Yn(e)}},cleanup(){if(!this.cleanupFns?.length)return;let e=_(null);try{for(;this.cleanupFns.length;)this.cleanupFns.pop()()}finally{this.cleanupFns=[],_(e)}}}),xD=P(y({},hh),{consumerMarkedDirty(){this.scheduler.schedule(this),this.notifier.notify(12)},destroy(){zt(this),this.onDestroyFn(),this.cleanup(),this.scheduler.remove(this)}}),ND=P(y({},hh),{consumerMarkedDirty(){this.view[M]|=8192,ln(this.view),this.notifier.notify(13)},destroy(){zt(this),this.onDestroyFn(),this.cleanup(),this.view[dt]?.delete(this)}});function AD(e,t,n){let r=Object.create(ND);return r.view=e,r.zone=typeof Zone<"u"?Zone.current:null,r.notifier=t,r.fn=gh(r,n),e[dt]??=new Set,e[dt].add(r),r.consumerMarkedDirty(r),r}function RD(e,t,n){let r=Object.create(xD);return r.fn=gh(r,e),r.scheduler=t,r.notifier=n,r.zone=typeof Zone<"u"?Zone.current:null,r.scheduler.add(r),r.notifier.notify(12),r}function gh(e,t){return()=>{t(n=>(e.cleanupFns??=[]).push(n))}}var Eh=Symbol("InputSignalNode#UNSET"),zD=P(y({},Uo),{transformFn:void 0,applyValueToInputSignal(e,t){Mn(e,t)}});function Dh(e,t){let n=Object.create(zD);n.value=e,n.transformFn=t?.transform;function r(){if(bn(n),n.value===Eh){let o=null;throw new C(-950,o)}return n.value}return r[oe]=n,r}var WD=new b("");WD.__NG_ELEMENT_ID__=e=>{let t=Me();if(t===null)throw new C(204,!1);if(t.type&2)return t.value;if(e&8)return null;throw new C(204,!1)};function mh(e,t){return Dh(e,t)}function qD(e){return Dh(Eh,e)}var Ch=(mh.required=qD,mh);function vh(e,t){return Kp(t)}function YD(e,t){return Qp(t)}var me=(vh.required=YD,vh);function Ih(e,t){return Xp(t)}var bl=new b(""),ZD=new b("");function uo(e){return!e.moduleRef}function KD(e){let t=uo(e)?e.r3Injector:e.moduleRef.injector,n=t.get(Q);return n.run(()=>{uo(e)?e.r3Injector.resolveInjectorInitializers():e.moduleRef.resolveInjectorInitializers();let r=t.get(Fe),o;if(n.runOutsideAngular(()=>{o=n.onError.subscribe({next:r})}),uo(e)){let i=()=>t.destroy(),s=e.platformInjector.get(bl);s.add(i),t.onDestroy(()=>{o.unsubscribe(),s.delete(i)})}else{let i=()=>e.moduleRef.destroy(),s=e.platformInjector.get(bl);s.add(i),e.moduleRef.onDestroy(()=>{Zr(e.allPlatformModules,e.moduleRef),o.unsubscribe(),s.delete(i)})}return XD(r,n,()=>{let i=t.get(mt),s=i.add(),a=t.get(fl);return a.runInitializers(),a.donePromise.then(()=>{let c=t.get(Il,ao);if(dh(c||ao),!t.get(ZD,!0))return uo(e)?t.get(yn):(e.allPlatformModules.push(e.moduleRef),e.moduleRef);if(uo(e)){let u=t.get(yn);return e.rootComponent!==void 0&&u.bootstrap(e.rootComponent),u}else return QD?.(e.moduleRef,e.allPlatformModules),e.moduleRef}).finally(()=>void i.remove(s))})})}var QD;function XD(e,t,n){try{let r=n();return so(r)?r.catch(o=>{throw t.runOutsideAngular(()=>e(o)),o}):r}catch(r){throw t.runOutsideAngular(()=>e(r)),r}}var ms=null;function JD(e=[],t){return Re.create({name:t,providers:[{provide:jr,useValue:"platform"},{provide:bl,useValue:new Set([()=>ms=null])},...e]})}function eC(e=[]){if(ms)return ms;let t=JD(e);return ms=t,ch(),tC(t),t}function tC(e){let t=e.get(rs,null);G(e,()=>{t?.forEach(n=>n())})}var _l=(()=>{class e{static __NG_ELEMENT_ID__=nC}return e})();function nC(e){return rC(Me(),W(),(e&16)===16)}function rC(e,t,n){if(sn(e)&&!n){let r=Qe(e.index,t);return new Ft(r,r)}else if(e.type&175){let r=t[ke];return new Ft(r,t)}return null}function Sh(e){let{rootComponent:t,appProviders:n,platformProviders:r,platformRef:o}=e;z(8);try{let i=o?.injector??eC(r),s=[fh({}),{provide:Ye,useExisting:Dl},af,...n||[]],a=new Xr({providers:s,parent:i,debugName:"",runEnvironmentInitializers:!1});return KD({r3Injector:a.injector,platformInjector:i,rootComponent:t})}catch(i){return Promise.reject(i)}finally{z(9)}}var wh=null;function St(){return wh}function wl(e){wh??=e}var fo=class{},Tl=(()=>{class e{historyGo(n){throw new Error("")}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:()=>v(Th),providedIn:"platform"})}return e})();var Th=(()=>{class e extends Tl{_location;_history;_doc=v(re);constructor(){super(),this._location=window.location,this._history=window.history}getBaseHrefFromDOM(){return St().getBaseHref(this._doc)}onPopState(n){let r=St().getGlobalEventTarget(this._doc,"window");return r.addEventListener("popstate",n,!1),()=>r.removeEventListener("popstate",n)}onHashChange(n){let r=St().getGlobalEventTarget(this._doc,"window");return r.addEventListener("hashchange",n,!1),()=>r.removeEventListener("hashchange",n)}get href(){return this._location.href}get protocol(){return this._location.protocol}get hostname(){return this._location.hostname}get port(){return this._location.port}get pathname(){return this._location.pathname}get search(){return this._location.search}get hash(){return this._location.hash}set pathname(n){this._location.pathname=n}pushState(n,r,o){this._history.pushState(n,r,o)}replaceState(n,r,o){this._history.replaceState(n,r,o)}forward(){this._history.forward()}back(){this._history.back()}historyGo(n=0){this._history.go(n)}getState(){return this._history.state}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:()=>new e,providedIn:"platform"})}return e})();function Mh(e,t){return e?t?e.endsWith("/")?t.startsWith("/")?e+t.slice(1):e+t:t.startsWith("/")?e+t:`${e}/${t}`:e:t}function bh(e){let t=e.search(/#|\?|$/);return e[t-1]==="/"?e.slice(0,t-1)+e.slice(t):e}function jt(e){return e&&e[0]!=="?"?`?${e}`:e}var vs=(()=>{class e{historyGo(n){throw new Error("")}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:()=>v(Nh),providedIn:"root"})}return e})(),xh=new b(""),Nh=(()=>{class e extends vs{_platformLocation;_baseHref;_removeListenerFns=[];constructor(n,r){super(),this._platformLocation=n,this._baseHref=r??this._platformLocation.getBaseHrefFromDOM()??v(re).location?.origin??""}ngOnDestroy(){for(;this._removeListenerFns.length;)this._removeListenerFns.pop()()}onPopState(n){this._removeListenerFns.push(this._platformLocation.onPopState(n),this._platformLocation.onHashChange(n))}getBaseHref(){return this._baseHref}prepareExternalUrl(n){return Mh(this._baseHref,n)}path(n=!1){let r=this._platformLocation.pathname+jt(this._platformLocation.search),o=this._platformLocation.hash;return o&&n?`${r}${o}`:r}pushState(n,r,o,i){let s=this.prepareExternalUrl(o+jt(i));this._platformLocation.pushState(n,r,s)}replaceState(n,r,o,i){let s=this.prepareExternalUrl(o+jt(i));this._platformLocation.replaceState(n,r,s)}forward(){this._platformLocation.forward()}back(){this._platformLocation.back()}getState(){return this._platformLocation.getState()}historyGo(n=0){this._platformLocation.historyGo?.(n)}static \u0275fac=function(r){return new(r||e)(N(Tl),N(xh,8))};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})(),lr=(()=>{class e{_subject=new X;_basePath;_locationStrategy;_urlChangeListeners=[];_urlChangeSubscription=null;constructor(n){this._locationStrategy=n;let r=this._locationStrategy.getBaseHref();this._basePath=sC(bh(_h(r))),this._locationStrategy.onPopState(o=>{this._subject.next({url:this.path(!0),pop:!0,state:o.state,type:o.type})})}ngOnDestroy(){this._urlChangeSubscription?.unsubscribe(),this._urlChangeListeners=[]}path(n=!1){return this.normalize(this._locationStrategy.path(n))}getState(){return this._locationStrategy.getState()}isCurrentPathEqualTo(n,r=""){return this.path()==this.normalize(n+jt(r))}normalize(n){return e.stripTrailingSlash(iC(this._basePath,_h(n)))}prepareExternalUrl(n){return n&&n[0]!=="/"&&(n="/"+n),this._locationStrategy.prepareExternalUrl(n)}go(n,r="",o=null){this._locationStrategy.pushState(o,"",n,r),this._notifyUrlChangeListeners(this.prepareExternalUrl(n+jt(r)),o)}replaceState(n,r="",o=null){this._locationStrategy.replaceState(o,"",n,r),this._notifyUrlChangeListeners(this.prepareExternalUrl(n+jt(r)),o)}forward(){this._locationStrategy.forward()}back(){this._locationStrategy.back()}historyGo(n=0){this._locationStrategy.historyGo?.(n)}onUrlChange(n){return this._urlChangeListeners.push(n),this._urlChangeSubscription??=this.subscribe(r=>{this._notifyUrlChangeListeners(r.url,r.state)}),()=>{let r=this._urlChangeListeners.indexOf(n);this._urlChangeListeners.splice(r,1),this._urlChangeListeners.length===0&&(this._urlChangeSubscription?.unsubscribe(),this._urlChangeSubscription=null)}}_notifyUrlChangeListeners(n="",r){this._urlChangeListeners.forEach(o=>o(n,r))}subscribe(n,r,o){return this._subject.subscribe({next:n,error:r??void 0,complete:o??void 0})}static normalizeQueryParams=jt;static joinWithSlash=Mh;static stripTrailingSlash=bh;static \u0275fac=function(r){return new(r||e)(N(vs))};static \u0275prov=I({token:e,factory:()=>oC(),providedIn:"root"})}return e})();function oC(){return new lr(N(vs))}function iC(e,t){if(!e||!t.startsWith(e))return t;let n=t.substring(e.length);return n===""||["/",";","?","#"].includes(n[0])?n:t}function _h(e){return e.replace(/\/index.html$/,"")}function sC(e){if(new RegExp("^(https?:)?//").test(e)){let[,n]=e.split(/\/\/[^\/]+/);return n}return e}var ys=(()=>{class e{static \u0275fac=function(r){return new(r||e)};static \u0275mod=oo({type:e});static \u0275inj=Vn({})}return e})();function Ml(e,t){t=encodeURIComponent(t);for(let n of e.split(";")){let r=n.indexOf("="),[o,i]=r==-1?[n,""]:[n.slice(0,r),n.slice(r+1)];if(o.trim()===t)return decodeURIComponent(i)}return null}var po=class{};var Ah="browser";var Ds=new b(""),Ol=(()=>{class e{_zone;_plugins;_eventNameToPlugin=new Map;constructor(n,r){this._zone=r,n.forEach(o=>{o.manager=this}),this._plugins=n.slice().reverse()}addEventListener(n,r,o,i){return this._findPluginFor(r).addEventListener(n,r,o,i)}getZone(){return this._zone}_findPluginFor(n){let r=this._eventNameToPlugin.get(n);if(r)return r;if(r=this._plugins.find(i=>i.supports(n)),!r)throw new C(5101,!1);return this._eventNameToPlugin.set(n,r),r}static \u0275fac=function(r){return new(r||e)(N(Ds),N(Q))};static \u0275prov=I({token:e,factory:e.\u0275fac})}return e})(),ho=class{_doc;constructor(t){this._doc=t}manager},xl="ng-app-id";function Rh(e){for(let t of e)t.remove()}function Oh(e,t){let n=t.createElement("style");return n.textContent=e,n}function cC(e,t,n,r){let o=e.head?.querySelectorAll(`style[${xl}="${t}"],link[${xl}="${t}"]`);if(o)for(let i of o)i.removeAttribute(xl),i instanceof HTMLLinkElement?r.set(i.href.slice(i.href.lastIndexOf("/")+1),{usage:0,elements:[i]}):i.textContent&&n.set(i.textContent,{usage:0,elements:[i]})}function Al(e,t){let n=t.createElement("link");return n.setAttribute("rel","stylesheet"),n.setAttribute("href",e),n}var Pl=(()=>{class e{doc;appId;nonce;inline=new Map;external=new Map;hosts=new Set;constructor(n,r,o,i={}){this.doc=n,this.appId=r,this.nonce=o,cC(n,r,this.inline,this.external),this.hosts.add(n.head)}addStyles(n,r){for(let o of n)this.addUsage(o,this.inline,Oh);r?.forEach(o=>this.addUsage(o,this.external,Al))}removeStyles(n,r){for(let o of n)this.removeUsage(o,this.inline);r?.forEach(o=>this.removeUsage(o,this.external))}addUsage(n,r,o){let i=r.get(n);i?i.usage++:r.set(n,{usage:1,elements:[...this.hosts].map(s=>this.addElement(s,o(n,this.doc)))})}removeUsage(n,r){let o=r.get(n);o&&(o.usage--,o.usage<=0&&(Rh(o.elements),r.delete(n)))}ngOnDestroy(){for(let[,{elements:n}]of[...this.inline,...this.external])Rh(n);this.hosts.clear()}addHost(n){this.hosts.add(n);for(let[r,{elements:o}]of this.inline)o.push(this.addElement(n,Oh(r,this.doc)));for(let[r,{elements:o}]of this.external)o.push(this.addElement(n,Al(r,this.doc)))}removeHost(n){this.hosts.delete(n)}addElement(n,r){return this.nonce&&r.setAttribute("nonce",this.nonce),n.appendChild(r)}static \u0275fac=function(r){return new(r||e)(N(re),N(ns),N(os,8),N(or))};static \u0275prov=I({token:e,factory:e.\u0275fac})}return e})(),Nl={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/",math:"http://www.w3.org/1998/Math/MathML"},kl=/%COMP%/g;var kh="%COMP%",lC=`_nghost-${kh}`,uC=`_ngcontent-${kh}`,dC=!0,fC=new b("",{providedIn:"root",factory:()=>dC});function pC(e){return uC.replace(kl,e)}function hC(e){return lC.replace(kl,e)}function Lh(e,t){return t.map(n=>n.replace(kl,e))}var Ll=(()=>{class e{eventManager;sharedStylesHost;appId;removeStylesOnCompDestroy;doc;platformId;ngZone;nonce;tracingService;rendererByCompId=new Map;defaultRenderer;platformIsServer;constructor(n,r,o,i,s,a,c,l=null,u=null){this.eventManager=n,this.sharedStylesHost=r,this.appId=o,this.removeStylesOnCompDestroy=i,this.doc=s,this.platformId=a,this.ngZone=c,this.nonce=l,this.tracingService=u,this.platformIsServer=!1,this.defaultRenderer=new go(n,s,c,this.platformIsServer,this.tracingService)}createRenderer(n,r){if(!n||!r)return this.defaultRenderer;let o=this.getOrCreateRenderer(n,r);return o instanceof Es?o.applyToHost(n):o instanceof mo&&o.applyStyles(),o}getOrCreateRenderer(n,r){let o=this.rendererByCompId,i=o.get(r.id);if(!i){let s=this.doc,a=this.ngZone,c=this.eventManager,l=this.sharedStylesHost,u=this.removeStylesOnCompDestroy,f=this.platformIsServer,m=this.tracingService;switch(r.encapsulation){case vt.Emulated:i=new Es(c,l,r,this.appId,u,s,a,f,m);break;case vt.ShadowDom:return new Rl(c,l,n,r,s,a,this.nonce,f,m);default:i=new mo(c,l,r,u,s,a,f,m);break}o.set(r.id,i)}return i}ngOnDestroy(){this.rendererByCompId.clear()}componentReplaced(n){this.rendererByCompId.delete(n)}static \u0275fac=function(r){return new(r||e)(N(Ol),N(Pl),N(ns),N(fC),N(re),N(or),N(Q),N(os),N(io,8))};static \u0275prov=I({token:e,factory:e.\u0275fac})}return e})(),go=class{eventManager;doc;ngZone;platformIsServer;tracingService;data=Object.create(null);throwOnSyntheticProps=!0;constructor(t,n,r,o,i){this.eventManager=t,this.doc=n,this.ngZone=r,this.platformIsServer=o,this.tracingService=i}destroy(){}destroyNode=null;createElement(t,n){return n?this.doc.createElementNS(Nl[n]||n,t):this.doc.createElement(t)}createComment(t){return this.doc.createComment(t)}createText(t){return this.doc.createTextNode(t)}appendChild(t,n){(Ph(t)?t.content:t).appendChild(n)}insertBefore(t,n,r){t&&(Ph(t)?t.content:t).insertBefore(n,r)}removeChild(t,n){n.remove()}selectRootElement(t,n){let r=typeof t=="string"?this.doc.querySelector(t):t;if(!r)throw new C(-5104,!1);return n||(r.textContent=""),r}parentNode(t){return t.parentNode}nextSibling(t){return t.nextSibling}setAttribute(t,n,r,o){if(o){n=o+":"+n;let i=Nl[o];i?t.setAttributeNS(i,n,r):t.setAttribute(n,r)}else t.setAttribute(n,r)}removeAttribute(t,n,r){if(r){let o=Nl[r];o?t.removeAttributeNS(o,n):t.removeAttribute(`${r}:${n}`)}else t.removeAttribute(n)}addClass(t,n){t.classList.add(n)}removeClass(t,n){t.classList.remove(n)}setStyle(t,n,r,o){o&(yt.DashCase|yt.Important)?t.style.setProperty(n,r,o&yt.Important?"important":""):t.style[n]=r}removeStyle(t,n,r){r&yt.DashCase?t.style.removeProperty(n):t.style[n]=""}setProperty(t,n,r){t!=null&&(t[n]=r)}setValue(t,n){t.nodeValue=n}listen(t,n,r,o){if(typeof t=="string"&&(t=St().getGlobalEventTarget(this.doc,t),!t))throw new C(5102,!1);let i=this.decoratePreventDefault(r);return this.tracingService?.wrapEventListener&&(i=this.tracingService.wrapEventListener(t,n,i)),this.eventManager.addEventListener(t,n,i,o)}decoratePreventDefault(t){return n=>{if(n==="__ngUnwrap__")return t;t(n)===!1&&n.preventDefault()}}};function Ph(e){return e.tagName==="TEMPLATE"&&e.content!==void 0}var Rl=class extends go{sharedStylesHost;hostEl;shadowRoot;constructor(t,n,r,o,i,s,a,c,l){super(t,i,s,c,l),this.sharedStylesHost=n,this.hostEl=r,this.shadowRoot=r.attachShadow({mode:"open"}),this.sharedStylesHost.addHost(this.shadowRoot);let u=o.styles;u=Lh(o.id,u);for(let m of u){let h=document.createElement("style");a&&h.setAttribute("nonce",a),h.textContent=m,this.shadowRoot.appendChild(h)}let f=o.getExternalStyles?.();if(f)for(let m of f){let h=Al(m,i);a&&h.setAttribute("nonce",a),this.shadowRoot.appendChild(h)}}nodeOrShadowRoot(t){return t===this.hostEl?this.shadowRoot:t}appendChild(t,n){return super.appendChild(this.nodeOrShadowRoot(t),n)}insertBefore(t,n,r){return super.insertBefore(this.nodeOrShadowRoot(t),n,r)}removeChild(t,n){return super.removeChild(null,n)}parentNode(t){return this.nodeOrShadowRoot(super.parentNode(this.nodeOrShadowRoot(t)))}destroy(){this.sharedStylesHost.removeHost(this.shadowRoot)}},mo=class extends go{sharedStylesHost;removeStylesOnCompDestroy;styles;styleUrls;constructor(t,n,r,o,i,s,a,c,l){super(t,i,s,a,c),this.sharedStylesHost=n,this.removeStylesOnCompDestroy=o;let u=r.styles;this.styles=l?Lh(l,u):u,this.styleUrls=r.getExternalStyles?.(l)}applyStyles(){this.sharedStylesHost.addStyles(this.styles,this.styleUrls)}destroy(){this.removeStylesOnCompDestroy&&ss.size===0&&this.sharedStylesHost.removeStyles(this.styles,this.styleUrls)}},Es=class extends mo{contentAttr;hostAttr;constructor(t,n,r,o,i,s,a,c,l){let u=o+"-"+r.id;super(t,n,r,i,s,a,c,l,u),this.contentAttr=pC(u),this.hostAttr=hC(u)}applyToHost(t){this.applyStyles(),this.setAttribute(t,this.hostAttr,"")}createElement(t,n){let r=super.createElement(t,n);return super.setAttribute(r,this.contentAttr,""),r}};var Cs=class e extends fo{supportsDOMEvents=!0;static makeCurrent(){wl(new e)}onAndCancel(t,n,r,o){return t.addEventListener(n,r,o),()=>{t.removeEventListener(n,r,o)}}dispatchEvent(t,n){t.dispatchEvent(n)}remove(t){t.remove()}createElement(t,n){return n=n||this.getDefaultDocument(),n.createElement(t)}createHtmlDocument(){return document.implementation.createHTMLDocument("fakeTitle")}getDefaultDocument(){return document}isElementNode(t){return t.nodeType===Node.ELEMENT_NODE}isShadowRoot(t){return t instanceof DocumentFragment}getGlobalEventTarget(t,n){return n==="window"?window:n==="document"?t:n==="body"?t.body:null}getBaseHref(t){let n=gC();return n==null?null:mC(n)}resetBaseElement(){vo=null}getUserAgent(){return window.navigator.userAgent}getCookie(t){return Ml(document.cookie,t)}},vo=null;function gC(){return vo=vo||document.head.querySelector("base"),vo?vo.getAttribute("href"):null}function mC(e){return new URL(e,document.baseURI).pathname}var vC=(()=>{class e{build(){return new XMLHttpRequest}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:e.\u0275fac})}return e})(),jh=(()=>{class e extends ho{constructor(n){super(n)}supports(n){return!0}addEventListener(n,r,o,i){return n.addEventListener(r,o,i),()=>this.removeEventListener(n,r,o,i)}removeEventListener(n,r,o,i){return n.removeEventListener(r,o,i)}static \u0275fac=function(r){return new(r||e)(N(re))};static \u0275prov=I({token:e,factory:e.\u0275fac})}return e})(),Fh=["alt","control","meta","shift"],yC={"\b":"Backspace"," ":"Tab","\x7F":"Delete","\x1B":"Escape",Del:"Delete",Esc:"Escape",Left:"ArrowLeft",Right:"ArrowRight",Up:"ArrowUp",Down:"ArrowDown",Menu:"ContextMenu",Scroll:"ScrollLock",Win:"OS"},EC={alt:e=>e.altKey,control:e=>e.ctrlKey,meta:e=>e.metaKey,shift:e=>e.shiftKey},Hh=(()=>{class e extends ho{constructor(n){super(n)}supports(n){return e.parseEventName(n)!=null}addEventListener(n,r,o,i){let s=e.parseEventName(r),a=e.eventCallback(s.fullKey,o,this.manager.getZone());return this.manager.getZone().runOutsideAngular(()=>St().onAndCancel(n,s.domEventName,a,i))}static parseEventName(n){let r=n.toLowerCase().split("."),o=r.shift();if(r.length===0||!(o==="keydown"||o==="keyup"))return null;let i=e._normalizeKey(r.pop()),s="",a=r.indexOf("code");if(a>-1&&(r.splice(a,1),s="code."),Fh.forEach(l=>{let u=r.indexOf(l);u>-1&&(r.splice(u,1),s+=l+".")}),s+=i,r.length!=0||i.length===0)return null;let c={};return c.domEventName=o,c.fullKey=s,c}static matchEventFullKeyCode(n,r){let o=yC[n.key]||n.key,i="";return r.indexOf("code.")>-1&&(o=n.code,i="code."),o==null||!o?!1:(o=o.toLowerCase(),o===" "?o="space":o==="."&&(o="dot"),Fh.forEach(s=>{if(s!==o){let a=EC[s];a(n)&&(i+=s+".")}}),i+=o,i===r)}static eventCallback(n,r,o){return i=>{e.matchEventFullKeyCode(i,n)&&o.runGuarded(()=>r(i))}}static _normalizeKey(n){return n==="esc"?"escape":n}static \u0275fac=function(r){return new(r||e)(N(re))};static \u0275prov=I({token:e,factory:e.\u0275fac})}return e})();function Fl(e,t,n){let r=y({rootComponent:e,platformRef:n?.platformRef},DC(t));return Sh(r)}function DC(e){return{appProviders:[..._C,...e?.providers??[]],platformProviders:bC}}function CC(){Cs.makeCurrent()}function IC(){return new Be}function SC(){return Gc(document),document}var bC=[{provide:or,useValue:Ah},{provide:rs,useValue:CC,multi:!0},{provide:re,useFactory:SC}];var _C=[{provide:jr,useValue:"root"},{provide:Be,useFactory:IC},{provide:Ds,useClass:jh,multi:!0,deps:[re]},{provide:Ds,useClass:Hh,multi:!0,deps:[re]},Ll,Pl,Ol,{provide:pn,useExisting:Ll},{provide:po,useClass:vC},[]];var Bh=(()=>{class e{_doc;constructor(n){this._doc=n}getTitle(){return this._doc.title}setTitle(n){this._doc.title=n||""}static \u0275fac=function(r){return new(r||e)(N(re))};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();var A="primary",No=Symbol("RouteTitle"),Ul=class{params;constructor(t){this.params=t||{}}has(t){return Object.prototype.hasOwnProperty.call(this.params,t)}get(t){if(this.has(t)){let n=this.params[t];return Array.isArray(n)?n[0]:n}return null}getAll(t){if(this.has(t)){let n=this.params[t];return Array.isArray(n)?n:[n]}return[]}get keys(){return Object.keys(this.params)}};function gr(e){return new Ul(e)}function TC(e,t,n){let r=n.path.split("/");if(r.length>e.length||n.pathMatch==="full"&&(t.hasChildren()||r.lengthr[i]===o)}else return e===t}function Zh(e){return e.length>0?e[e.length-1]:null}function wt(e){return ha(e)?e:so(e)?Y(Promise.resolve(e)):w(e)}var xC={exact:Qh,subset:Xh},Kh={exact:NC,subset:AC,ignored:()=>!0};function Vh(e,t,n){return xC[n.paths](e.root,t.root,n.matrixParams)&&Kh[n.queryParams](e.queryParams,t.queryParams)&&!(n.fragment==="exact"&&e.fragment!==t.fragment)}function NC(e,t){return tt(e,t)}function Qh(e,t,n){if(!Dn(e.segments,t.segments)||!bs(e.segments,t.segments,n)||e.numberOfChildren!==t.numberOfChildren)return!1;for(let r in t.children)if(!e.children[r]||!Qh(e.children[r],t.children[r],n))return!1;return!0}function AC(e,t){return Object.keys(t).length<=Object.keys(e).length&&Object.keys(t).every(n=>Yh(e[n],t[n]))}function Xh(e,t,n){return Jh(e,t,t.segments,n)}function Jh(e,t,n,r){if(e.segments.length>n.length){let o=e.segments.slice(0,n.length);return!(!Dn(o,n)||t.hasChildren()||!bs(o,n,r))}else if(e.segments.length===n.length){if(!Dn(e.segments,n)||!bs(e.segments,n,r))return!1;for(let o in t.children)if(!e.children[o]||!Xh(e.children[o],t.children[o],r))return!1;return!0}else{let o=n.slice(0,e.segments.length),i=n.slice(e.segments.length);return!Dn(e.segments,o)||!bs(e.segments,o,r)||!e.children[A]?!1:Jh(e.children[A],t,i,r)}}function bs(e,t,n){return t.every((r,o)=>Kh[n](e[o].parameters,r.parameters))}var _t=class{root;queryParams;fragment;_queryParamMap;constructor(t=new $([],{}),n={},r=null){this.root=t,this.queryParams=n,this.fragment=r}get queryParamMap(){return this._queryParamMap??=gr(this.queryParams),this._queryParamMap}toString(){return PC.serialize(this)}},$=class{segments;children;parent=null;constructor(t,n){this.segments=t,this.children=n,Object.values(n).forEach(r=>r.parent=this)}hasChildren(){return this.numberOfChildren>0}get numberOfChildren(){return Object.keys(this.children).length}toString(){return _s(this)}},En=class{path;parameters;_parameterMap;constructor(t,n){this.path=t,this.parameters=n}get parameterMap(){return this._parameterMap??=gr(this.parameters),this._parameterMap}toString(){return tg(this)}};function RC(e,t){return Dn(e,t)&&e.every((n,r)=>tt(n.parameters,t[r].parameters))}function Dn(e,t){return e.length!==t.length?!1:e.every((n,r)=>n.path===t[r].path)}function OC(e,t){let n=[];return Object.entries(e.children).forEach(([r,o])=>{r===A&&(n=n.concat(t(o,r)))}),Object.entries(e.children).forEach(([r,o])=>{r!==A&&(n=n.concat(t(o,r)))}),n}var Fs=(()=>{class e{static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:()=>new mr,providedIn:"root"})}return e})(),mr=class{parse(t){let n=new zl(t);return new _t(n.parseRootSegment(),n.parseQueryParams(),n.parseFragment())}serialize(t){let n=`/${yo(t.root,!0)}`,r=FC(t.queryParams),o=typeof t.fragment=="string"?`#${kC(t.fragment)}`:"";return`${n}${r}${o}`}},PC=new mr;function _s(e){return e.segments.map(t=>tg(t)).join("/")}function yo(e,t){if(!e.hasChildren())return _s(e);if(t){let n=e.children[A]?yo(e.children[A],!1):"",r=[];return Object.entries(e.children).forEach(([o,i])=>{o!==A&&r.push(`${o}:${yo(i,!1)}`)}),r.length>0?`${n}(${r.join("//")})`:n}else{let n=OC(e,(r,o)=>o===A?[yo(e.children[A],!1)]:[`${o}:${yo(r,!1)}`]);return Object.keys(e.children).length===1&&e.children[A]!=null?`${_s(e)}/${n[0]}`:`${_s(e)}/(${n.join("//")})`}}function eg(e){return encodeURIComponent(e).replace(/%40/g,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",")}function Is(e){return eg(e).replace(/%3B/gi,";")}function kC(e){return encodeURI(e)}function Gl(e){return eg(e).replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/%26/gi,"&")}function ws(e){return decodeURIComponent(e)}function Uh(e){return ws(e.replace(/\+/g,"%20"))}function tg(e){return`${Gl(e.path)}${LC(e.parameters)}`}function LC(e){return Object.entries(e).map(([t,n])=>`;${Gl(t)}=${Gl(n)}`).join("")}function FC(e){let t=Object.entries(e).map(([n,r])=>Array.isArray(r)?r.map(o=>`${Is(n)}=${Is(o)}`).join("&"):`${Is(n)}=${Is(r)}`).filter(n=>n);return t.length?`?${t.join("&")}`:""}var jC=/^[^\/()?;#]+/;function jl(e){let t=e.match(jC);return t?t[0]:""}var HC=/^[^\/()?;=#]+/;function BC(e){let t=e.match(HC);return t?t[0]:""}var VC=/^[^=?&#]+/;function UC(e){let t=e.match(VC);return t?t[0]:""}var $C=/^[^&#]+/;function GC(e){let t=e.match($C);return t?t[0]:""}var zl=class{url;remaining;constructor(t){this.url=t,this.remaining=t}parseRootSegment(){return this.consumeOptional("/"),this.remaining===""||this.peekStartsWith("?")||this.peekStartsWith("#")?new $([],{}):new $([],this.parseChildren())}parseQueryParams(){let t={};if(this.consumeOptional("?"))do this.parseQueryParam(t);while(this.consumeOptional("&"));return t}parseFragment(){return this.consumeOptional("#")?decodeURIComponent(this.remaining):null}parseChildren(){if(this.remaining==="")return{};this.consumeOptional("/");let t=[];for(this.peekStartsWith("(")||t.push(this.parseSegment());this.peekStartsWith("/")&&!this.peekStartsWith("//")&&!this.peekStartsWith("/(");)this.capture("/"),t.push(this.parseSegment());let n={};this.peekStartsWith("/(")&&(this.capture("/"),n=this.parseParens(!0));let r={};return this.peekStartsWith("(")&&(r=this.parseParens(!1)),(t.length>0||Object.keys(n).length>0)&&(r[A]=new $(t,n)),r}parseSegment(){let t=jl(this.remaining);if(t===""&&this.peekStartsWith(";"))throw new C(4009,!1);return this.capture(t),new En(ws(t),this.parseMatrixParams())}parseMatrixParams(){let t={};for(;this.consumeOptional(";");)this.parseParam(t);return t}parseParam(t){let n=BC(this.remaining);if(!n)return;this.capture(n);let r="";if(this.consumeOptional("=")){let o=jl(this.remaining);o&&(r=o,this.capture(r))}t[ws(n)]=ws(r)}parseQueryParam(t){let n=UC(this.remaining);if(!n)return;this.capture(n);let r="";if(this.consumeOptional("=")){let s=GC(this.remaining);s&&(r=s,this.capture(r))}let o=Uh(n),i=Uh(r);if(t.hasOwnProperty(o)){let s=t[o];Array.isArray(s)||(s=[s],t[o]=s),s.push(i)}else t[o]=i}parseParens(t){let n={};for(this.capture("(");!this.consumeOptional(")")&&this.remaining.length>0;){let r=jl(this.remaining),o=this.remaining[r.length];if(o!=="/"&&o!==")"&&o!==";")throw new C(4010,!1);let i;r.indexOf(":")>-1?(i=r.slice(0,r.indexOf(":")),this.capture(i),this.capture(":")):t&&(i=A);let s=this.parseChildren();n[i]=Object.keys(s).length===1?s[A]:new $([],s),this.consumeOptional("//")}return n}peekStartsWith(t){return this.remaining.startsWith(t)}consumeOptional(t){return this.peekStartsWith(t)?(this.remaining=this.remaining.substring(t.length),!0):!1}capture(t){if(!this.consumeOptional(t))throw new C(4011,!1)}};function ng(e){return e.segments.length>0?new $([],{[A]:e}):e}function rg(e){let t={};for(let[r,o]of Object.entries(e.children)){let i=rg(o);if(r===A&&i.segments.length===0&&i.hasChildren())for(let[s,a]of Object.entries(i.children))t[s]=a;else(i.segments.length>0||i.hasChildren())&&(t[r]=i)}let n=new $(e.segments,t);return zC(n)}function zC(e){if(e.numberOfChildren===1&&e.children[A]){let t=e.children[A];return new $(e.segments.concat(t.segments),t.children)}return e}function vr(e){return e instanceof _t}function WC(e,t,n=null,r=null){let o=og(e);return ig(o,t,n,r)}function og(e){let t;function n(i){let s={};for(let c of i.children){let l=n(c);s[c.outlet]=l}let a=new $(i.url,s);return i===e&&(t=a),a}let r=n(e.root),o=ng(r);return t??o}function ig(e,t,n,r){let o=e;for(;o.parent;)o=o.parent;if(t.length===0)return Hl(o,o,o,n,r);let i=qC(t);if(i.toRoot())return Hl(o,o,new $([],{}),n,r);let s=YC(i,o,e),a=s.processChildren?Do(s.segmentGroup,s.index,i.commands):ag(s.segmentGroup,s.index,i.commands);return Hl(o,s.segmentGroup,a,n,r)}function Ts(e){return typeof e=="object"&&e!=null&&!e.outlets&&!e.segmentPath}function So(e){return typeof e=="object"&&e!=null&&e.outlets}function Hl(e,t,n,r,o){let i={};r&&Object.entries(r).forEach(([c,l])=>{i[c]=Array.isArray(l)?l.map(u=>`${u}`):`${l}`});let s;e===t?s=n:s=sg(e,t,n);let a=ng(rg(s));return new _t(a,i,o)}function sg(e,t,n){let r={};return Object.entries(e.children).forEach(([o,i])=>{i===t?r[o]=n:r[o]=sg(i,t,n)}),new $(e.segments,r)}var Ms=class{isAbsolute;numberOfDoubleDots;commands;constructor(t,n,r){if(this.isAbsolute=t,this.numberOfDoubleDots=n,this.commands=r,t&&r.length>0&&Ts(r[0]))throw new C(4003,!1);let o=r.find(So);if(o&&o!==Zh(r))throw new C(4004,!1)}toRoot(){return this.isAbsolute&&this.commands.length===1&&this.commands[0]=="/"}};function qC(e){if(typeof e[0]=="string"&&e.length===1&&e[0]==="/")return new Ms(!0,0,e);let t=0,n=!1,r=e.reduce((o,i,s)=>{if(typeof i=="object"&&i!=null){if(i.outlets){let a={};return Object.entries(i.outlets).forEach(([c,l])=>{a[c]=typeof l=="string"?l.split("/"):l}),[...o,{outlets:a}]}if(i.segmentPath)return[...o,i.segmentPath]}return typeof i!="string"?[...o,i]:s===0?(i.split("/").forEach((a,c)=>{c==0&&a==="."||(c==0&&a===""?n=!0:a===".."?t++:a!=""&&o.push(a))}),o):[...o,i]},[]);return new Ms(n,t,r)}var fr=class{segmentGroup;processChildren;index;constructor(t,n,r){this.segmentGroup=t,this.processChildren=n,this.index=r}};function YC(e,t,n){if(e.isAbsolute)return new fr(t,!0,0);if(!n)return new fr(t,!1,NaN);if(n.parent===null)return new fr(n,!0,0);let r=Ts(e.commands[0])?0:1,o=n.segments.length-1+r;return ZC(n,o,e.numberOfDoubleDots)}function ZC(e,t,n){let r=e,o=t,i=n;for(;i>o;){if(i-=o,r=r.parent,!r)throw new C(4005,!1);o=r.segments.length}return new fr(r,!1,o-i)}function KC(e){return So(e[0])?e[0].outlets:{[A]:e}}function ag(e,t,n){if(e??=new $([],{}),e.segments.length===0&&e.hasChildren())return Do(e,t,n);let r=QC(e,t,n),o=n.slice(r.commandIndex);if(r.match&&r.pathIndexi!==A)&&e.children[A]&&e.numberOfChildren===1&&e.children[A].segments.length===0){let i=Do(e.children[A],t,n);return new $(e.segments,i.children)}return Object.entries(r).forEach(([i,s])=>{typeof s=="string"&&(s=[s]),s!==null&&(o[i]=ag(e.children[i],t,s))}),Object.entries(e.children).forEach(([i,s])=>{r[i]===void 0&&(o[i]=s)}),new $(e.segments,o)}}function QC(e,t,n){let r=0,o=t,i={match:!1,pathIndex:0,commandIndex:0};for(;o=n.length)return i;let s=e.segments[o],a=n[r];if(So(a))break;let c=`${a}`,l=r0&&c===void 0)break;if(c&&l&&typeof l=="object"&&l.outlets===void 0){if(!Gh(c,l,s))return i;r+=2}else{if(!Gh(c,{},s))return i;r++}o++}return{match:!0,pathIndex:o,commandIndex:r}}function Wl(e,t,n){let r=e.segments.slice(0,t),o=0;for(;o{typeof r=="string"&&(r=[r]),r!==null&&(t[n]=Wl(new $([],{}),0,r))}),t}function $h(e){let t={};return Object.entries(e).forEach(([n,r])=>t[n]=`${r}`),t}function Gh(e,t,n){return e==n.path&&tt(t,n.parameters)}var Co="imperative",le=(function(e){return e[e.NavigationStart=0]="NavigationStart",e[e.NavigationEnd=1]="NavigationEnd",e[e.NavigationCancel=2]="NavigationCancel",e[e.NavigationError=3]="NavigationError",e[e.RoutesRecognized=4]="RoutesRecognized",e[e.ResolveStart=5]="ResolveStart",e[e.ResolveEnd=6]="ResolveEnd",e[e.GuardsCheckStart=7]="GuardsCheckStart",e[e.GuardsCheckEnd=8]="GuardsCheckEnd",e[e.RouteConfigLoadStart=9]="RouteConfigLoadStart",e[e.RouteConfigLoadEnd=10]="RouteConfigLoadEnd",e[e.ChildActivationStart=11]="ChildActivationStart",e[e.ChildActivationEnd=12]="ChildActivationEnd",e[e.ActivationStart=13]="ActivationStart",e[e.ActivationEnd=14]="ActivationEnd",e[e.Scroll=15]="Scroll",e[e.NavigationSkipped=16]="NavigationSkipped",e})(le||{}),je=class{id;url;constructor(t,n){this.id=t,this.url=n}},yr=class extends je{type=le.NavigationStart;navigationTrigger;restoredState;constructor(t,n,r="imperative",o=null){super(t,n),this.navigationTrigger=r,this.restoredState=o}toString(){return`NavigationStart(id: ${this.id}, url: '${this.url}')`}},Ht=class extends je{urlAfterRedirects;type=le.NavigationEnd;constructor(t,n,r){super(t,n),this.urlAfterRedirects=r}toString(){return`NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`}},Ie=(function(e){return e[e.Redirect=0]="Redirect",e[e.SupersededByNewNavigation=1]="SupersededByNewNavigation",e[e.NoDataFromResolver=2]="NoDataFromResolver",e[e.GuardRejected=3]="GuardRejected",e[e.Aborted=4]="Aborted",e})(Ie||{}),xs=(function(e){return e[e.IgnoredSameUrlNavigation=0]="IgnoredSameUrlNavigation",e[e.IgnoredByUrlHandlingStrategy=1]="IgnoredByUrlHandlingStrategy",e})(xs||{}),bt=class extends je{reason;code;type=le.NavigationCancel;constructor(t,n,r,o){super(t,n),this.reason=r,this.code=o}toString(){return`NavigationCancel(id: ${this.id}, url: '${this.url}')`}},Bt=class extends je{reason;code;type=le.NavigationSkipped;constructor(t,n,r,o){super(t,n),this.reason=r,this.code=o}},bo=class extends je{error;target;type=le.NavigationError;constructor(t,n,r,o){super(t,n),this.error=r,this.target=o}toString(){return`NavigationError(id: ${this.id}, url: '${this.url}', error: ${this.error})`}},Ns=class extends je{urlAfterRedirects;state;type=le.RoutesRecognized;constructor(t,n,r,o){super(t,n),this.urlAfterRedirects=r,this.state=o}toString(){return`RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},ql=class extends je{urlAfterRedirects;state;type=le.GuardsCheckStart;constructor(t,n,r,o){super(t,n),this.urlAfterRedirects=r,this.state=o}toString(){return`GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},Yl=class extends je{urlAfterRedirects;state;shouldActivate;type=le.GuardsCheckEnd;constructor(t,n,r,o,i){super(t,n),this.urlAfterRedirects=r,this.state=o,this.shouldActivate=i}toString(){return`GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`}},Zl=class extends je{urlAfterRedirects;state;type=le.ResolveStart;constructor(t,n,r,o){super(t,n),this.urlAfterRedirects=r,this.state=o}toString(){return`ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},Kl=class extends je{urlAfterRedirects;state;type=le.ResolveEnd;constructor(t,n,r,o){super(t,n),this.urlAfterRedirects=r,this.state=o}toString(){return`ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},Ql=class{route;type=le.RouteConfigLoadStart;constructor(t){this.route=t}toString(){return`RouteConfigLoadStart(path: ${this.route.path})`}},Xl=class{route;type=le.RouteConfigLoadEnd;constructor(t){this.route=t}toString(){return`RouteConfigLoadEnd(path: ${this.route.path})`}},Jl=class{snapshot;type=le.ChildActivationStart;constructor(t){this.snapshot=t}toString(){return`ChildActivationStart(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},eu=class{snapshot;type=le.ChildActivationEnd;constructor(t){this.snapshot=t}toString(){return`ChildActivationEnd(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},tu=class{snapshot;type=le.ActivationStart;constructor(t){this.snapshot=t}toString(){return`ActivationStart(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},nu=class{snapshot;type=le.ActivationEnd;constructor(t){this.snapshot=t}toString(){return`ActivationEnd(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}};var _o=class{},Er=class{url;navigationBehaviorOptions;constructor(t,n){this.url=t,this.navigationBehaviorOptions=n}};function JC(e){return!(e instanceof _o)&&!(e instanceof Er)}function eI(e,t){return e.providers&&!e._injector&&(e._injector=ro(e.providers,t,`Route: ${e.path}`)),e._injector??t}function ze(e){return e.outlet||A}function tI(e,t){let n=e.filter(r=>ze(r)===t);return n.push(...e.filter(r=>ze(r)!==t)),n}function Cr(e){if(!e)return null;if(e.routeConfig?._injector)return e.routeConfig._injector;for(let t=e.parent;t;t=t.parent){let n=t.routeConfig;if(n?._loadedInjector)return n._loadedInjector;if(n?._injector)return n._injector}return null}var ru=class{rootInjector;outlet=null;route=null;children;attachRef=null;get injector(){return Cr(this.route?.snapshot)??this.rootInjector}constructor(t){this.rootInjector=t,this.children=new Ao(this.rootInjector)}},Ao=(()=>{class e{rootInjector;contexts=new Map;constructor(n){this.rootInjector=n}onChildOutletCreated(n,r){let o=this.getOrCreateContext(n);o.outlet=r,this.contexts.set(n,o)}onChildOutletDestroyed(n){let r=this.getContext(n);r&&(r.outlet=null,r.attachRef=null)}onOutletDeactivated(){let n=this.contexts;return this.contexts=new Map,n}onOutletReAttached(n){this.contexts=n}getOrCreateContext(n){let r=this.getContext(n);return r||(r=new ru(this.rootInjector),this.contexts.set(n,r)),r}getContext(n){return this.contexts.get(n)||null}static \u0275fac=function(r){return new(r||e)(N(K))};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})(),As=class{_root;constructor(t){this._root=t}get root(){return this._root.value}parent(t){let n=this.pathFromRoot(t);return n.length>1?n[n.length-2]:null}children(t){let n=ou(t,this._root);return n?n.children.map(r=>r.value):[]}firstChild(t){let n=ou(t,this._root);return n&&n.children.length>0?n.children[0].value:null}siblings(t){let n=iu(t,this._root);return n.length<2?[]:n[n.length-2].children.map(o=>o.value).filter(o=>o!==t)}pathFromRoot(t){return iu(t,this._root).map(n=>n.value)}};function ou(e,t){if(e===t.value)return t;for(let n of t.children){let r=ou(e,n);if(r)return r}return null}function iu(e,t){if(e===t.value)return[t];for(let n of t.children){let r=iu(e,n);if(r.length)return r.unshift(t),r}return[]}var xe=class{value;children;constructor(t,n){this.value=t,this.children=n}toString(){return`TreeNode(${this.value})`}};function dr(e){let t={};return e&&e.children.forEach(n=>t[n.value.outlet]=n),t}var Rs=class extends As{snapshot;constructor(t,n){super(t),this.snapshot=n,hu(this,t)}toString(){return this.snapshot.toString()}};function cg(e){let t=nI(e),n=new ie([new En("",{})]),r=new ie({}),o=new ie({}),i=new ie({}),s=new ie(""),a=new Cn(n,r,i,s,o,A,e,t.root);return a.snapshot=t.root,new Rs(new xe(a,[]),t)}function nI(e){let t={},n={},r={},i=new pr([],t,r,"",n,A,e,null,{});return new Ps("",new xe(i,[]))}var Cn=class{urlSubject;paramsSubject;queryParamsSubject;fragmentSubject;dataSubject;outlet;component;snapshot;_futureSnapshot;_routerState;_paramMap;_queryParamMap;title;url;params;queryParams;fragment;data;constructor(t,n,r,o,i,s,a,c){this.urlSubject=t,this.paramsSubject=n,this.queryParamsSubject=r,this.fragmentSubject=o,this.dataSubject=i,this.outlet=s,this.component=a,this._futureSnapshot=c,this.title=this.dataSubject?.pipe(B(l=>l[No]))??w(void 0),this.url=t,this.params=n,this.queryParams=r,this.fragment=o,this.data=i}get routeConfig(){return this._futureSnapshot.routeConfig}get root(){return this._routerState.root}get parent(){return this._routerState.parent(this)}get firstChild(){return this._routerState.firstChild(this)}get children(){return this._routerState.children(this)}get pathFromRoot(){return this._routerState.pathFromRoot(this)}get paramMap(){return this._paramMap??=this.params.pipe(B(t=>gr(t))),this._paramMap}get queryParamMap(){return this._queryParamMap??=this.queryParams.pipe(B(t=>gr(t))),this._queryParamMap}toString(){return this.snapshot?this.snapshot.toString():`Future(${this._futureSnapshot})`}};function Os(e,t,n="emptyOnly"){let r,{routeConfig:o}=e;return t!==null&&(n==="always"||o?.path===""||!t.component&&!t.routeConfig?.loadComponent)?r={params:y(y({},t.params),e.params),data:y(y({},t.data),e.data),resolve:y(y(y(y({},e.data),t.data),o?.data),e._resolvedData)}:r={params:y({},e.params),data:y({},e.data),resolve:y(y({},e.data),e._resolvedData??{})},o&&ug(o)&&(r.resolve[No]=o.title),r}var pr=class{url;params;queryParams;fragment;data;outlet;component;routeConfig;_resolve;_resolvedData;_routerState;_paramMap;_queryParamMap;get title(){return this.data?.[No]}constructor(t,n,r,o,i,s,a,c,l){this.url=t,this.params=n,this.queryParams=r,this.fragment=o,this.data=i,this.outlet=s,this.component=a,this.routeConfig=c,this._resolve=l}get root(){return this._routerState.root}get parent(){return this._routerState.parent(this)}get firstChild(){return this._routerState.firstChild(this)}get children(){return this._routerState.children(this)}get pathFromRoot(){return this._routerState.pathFromRoot(this)}get paramMap(){return this._paramMap??=gr(this.params),this._paramMap}get queryParamMap(){return this._queryParamMap??=gr(this.queryParams),this._queryParamMap}toString(){let t=this.url.map(r=>r.toString()).join("/"),n=this.routeConfig?this.routeConfig.path:"";return`Route(url:'${t}', path:'${n}')`}},Ps=class extends As{url;constructor(t,n){super(n),this.url=t,hu(this,n)}toString(){return lg(this._root)}};function hu(e,t){t.value._routerState=e,t.children.forEach(n=>hu(e,n))}function lg(e){let t=e.children.length>0?` { ${e.children.map(lg).join(", ")} } `:"";return`${e.value}${t}`}function Bl(e){if(e.snapshot){let t=e.snapshot,n=e._futureSnapshot;e.snapshot=n,tt(t.queryParams,n.queryParams)||e.queryParamsSubject.next(n.queryParams),t.fragment!==n.fragment&&e.fragmentSubject.next(n.fragment),tt(t.params,n.params)||e.paramsSubject.next(n.params),MC(t.url,n.url)||e.urlSubject.next(n.url),tt(t.data,n.data)||e.dataSubject.next(n.data)}else e.snapshot=e._futureSnapshot,e.dataSubject.next(e._futureSnapshot.data)}function su(e,t){let n=tt(e.params,t.params)&&RC(e.url,t.url),r=!e.parent!=!t.parent;return n&&!r&&(!e.parent||su(e.parent,t.parent))}function ug(e){return typeof e.title=="string"||e.title===null}var rI=new b(""),dg=(()=>{class e{activated=null;get activatedComponentRef(){return this.activated}_activatedRoute=null;name=A;activateEvents=new ge;deactivateEvents=new ge;attachEvents=new ge;detachEvents=new ge;routerOutletData=Ch(void 0);parentContexts=v(Ao);location=v(gn);changeDetector=v(_l);inputBinder=v(js,{optional:!0});supportsBindingToComponentInputs=!0;ngOnChanges(n){if(n.name){let{firstChange:r,previousValue:o}=n.name;if(r)return;this.isTrackedInParentContexts(o)&&(this.deactivate(),this.parentContexts.onChildOutletDestroyed(o)),this.initializeOutletWithName()}}ngOnDestroy(){this.isTrackedInParentContexts(this.name)&&this.parentContexts.onChildOutletDestroyed(this.name),this.inputBinder?.unsubscribeFromRouteData(this)}isTrackedInParentContexts(n){return this.parentContexts.getContext(n)?.outlet===this}ngOnInit(){this.initializeOutletWithName()}initializeOutletWithName(){if(this.parentContexts.onChildOutletCreated(this.name,this),this.activated)return;let n=this.parentContexts.getContext(this.name);n?.route&&(n.attachRef?this.attach(n.attachRef,n.route):this.activateWith(n.route,n.injector))}get isActivated(){return!!this.activated}get component(){if(!this.activated)throw new C(4012,!1);return this.activated.instance}get activatedRoute(){if(!this.activated)throw new C(4012,!1);return this._activatedRoute}get activatedRouteData(){return this._activatedRoute?this._activatedRoute.snapshot.data:{}}detach(){if(!this.activated)throw new C(4012,!1);this.location.detach();let n=this.activated;return this.activated=null,this._activatedRoute=null,this.detachEvents.emit(n.instance),n}attach(n,r){this.activated=n,this._activatedRoute=r,this.location.insert(n.hostView),this.inputBinder?.bindActivatedRouteToOutletComponent(this),this.attachEvents.emit(n.instance)}deactivate(){if(this.activated){let n=this.component;this.activated.destroy(),this.activated=null,this._activatedRoute=null,this.deactivateEvents.emit(n)}}activateWith(n,r){if(this.isActivated)throw new C(4013,!1);this._activatedRoute=n;let o=this.location,s=n.snapshot.component,a=this.parentContexts.getOrCreateContext(this.name).children,c=new au(n,a,o.injector,this.routerOutletData);this.activated=o.createComponent(s,{index:o.length,injector:c,environmentInjector:r}),this.changeDetector.markForCheck(),this.inputBinder?.bindActivatedRouteToOutletComponent(this),this.activateEvents.emit(this.activated.instance)}static \u0275fac=function(r){return new(r||e)};static \u0275dir=fs({type:e,selectors:[["router-outlet"]],inputs:{name:"name",routerOutletData:[1,"routerOutletData"]},outputs:{activateEvents:"activate",deactivateEvents:"deactivate",attachEvents:"attach",detachEvents:"detach"},exportAs:["outlet"],features:[Ji]})}return e})(),au=class{route;childContexts;parent;outletData;constructor(t,n,r,o){this.route=t,this.childContexts=n,this.parent=r,this.outletData=o}get(t,n){return t===Cn?this.route:t===Ao?this.childContexts:t===rI?this.outletData:this.parent.get(t,n)}},js=new b("");var fg=(()=>{class e{static \u0275fac=function(r){return new(r||e)};static \u0275cmp=vn({type:e,selectors:[["ng-component"]],exportAs:["emptyRouterOutlet"],decls:1,vars:0,template:function(r,o){r&1&&ar(0,"router-outlet")},dependencies:[dg],encapsulation:2})}return e})();function gu(e){let t=e.children&&e.children.map(gu),n=t?P(y({},e),{children:t}):y({},e);return!n.component&&!n.loadComponent&&(t||n.loadChildren)&&n.outlet&&n.outlet!==A&&(n.component=fg),n}function oI(e,t,n){let r=wo(e,t._root,n?n._root:void 0);return new Rs(r,t)}function wo(e,t,n){if(n&&e.shouldReuseRoute(t.value,n.value.snapshot)){let r=n.value;r._futureSnapshot=t.value;let o=iI(e,t,n);return new xe(r,o)}else{if(e.shouldAttach(t.value)){let i=e.retrieve(t.value);if(i!==null){let s=i.route;return s.value._futureSnapshot=t.value,s.children=t.children.map(a=>wo(e,a)),s}}let r=sI(t.value),o=t.children.map(i=>wo(e,i));return new xe(r,o)}}function iI(e,t,n){return t.children.map(r=>{for(let o of n.children)if(e.shouldReuseRoute(r.value,o.value.snapshot))return wo(e,r,o);return wo(e,r)})}function sI(e){return new Cn(new ie(e.url),new ie(e.params),new ie(e.queryParams),new ie(e.fragment),new ie(e.data),e.outlet,e.component,e)}var To=class{redirectTo;navigationBehaviorOptions;constructor(t,n){this.redirectTo=t,this.navigationBehaviorOptions=n}},pg="ngNavigationCancelingError";function ks(e,t){let{redirectTo:n,navigationBehaviorOptions:r}=vr(t)?{redirectTo:t,navigationBehaviorOptions:void 0}:t,o=hg(!1,Ie.Redirect);return o.url=n,o.navigationBehaviorOptions=r,o}function hg(e,t){let n=new Error(`NavigationCancelingError: ${e||""}`);return n[pg]=!0,n.cancellationCode=t,n}function aI(e){return gg(e)&&vr(e.url)}function gg(e){return!!e&&e[pg]}var cI=(e,t,n,r)=>B(o=>(new cu(t,o.targetRouterState,o.currentRouterState,n,r).activate(e),o)),cu=class{routeReuseStrategy;futureState;currState;forwardEvent;inputBindingEnabled;constructor(t,n,r,o,i){this.routeReuseStrategy=t,this.futureState=n,this.currState=r,this.forwardEvent=o,this.inputBindingEnabled=i}activate(t){let n=this.futureState._root,r=this.currState?this.currState._root:null;this.deactivateChildRoutes(n,r,t),Bl(this.futureState.root),this.activateChildRoutes(n,r,t)}deactivateChildRoutes(t,n,r){let o=dr(n);t.children.forEach(i=>{let s=i.value.outlet;this.deactivateRoutes(i,o[s],r),delete o[s]}),Object.values(o).forEach(i=>{this.deactivateRouteAndItsChildren(i,r)})}deactivateRoutes(t,n,r){let o=t.value,i=n?n.value:null;if(o===i)if(o.component){let s=r.getContext(o.outlet);s&&this.deactivateChildRoutes(t,n,s.children)}else this.deactivateChildRoutes(t,n,r);else i&&this.deactivateRouteAndItsChildren(n,r)}deactivateRouteAndItsChildren(t,n){t.value.component&&this.routeReuseStrategy.shouldDetach(t.value.snapshot)?this.detachAndStoreRouteSubtree(t,n):this.deactivateRouteAndOutlet(t,n)}detachAndStoreRouteSubtree(t,n){let r=n.getContext(t.value.outlet),o=r&&t.value.component?r.children:n,i=dr(t);for(let s of Object.values(i))this.deactivateRouteAndItsChildren(s,o);if(r&&r.outlet){let s=r.outlet.detach(),a=r.children.onOutletDeactivated();this.routeReuseStrategy.store(t.value.snapshot,{componentRef:s,route:t,contexts:a})}}deactivateRouteAndOutlet(t,n){let r=n.getContext(t.value.outlet),o=r&&t.value.component?r.children:n,i=dr(t);for(let s of Object.values(i))this.deactivateRouteAndItsChildren(s,o);r&&(r.outlet&&(r.outlet.deactivate(),r.children.onOutletDeactivated()),r.attachRef=null,r.route=null)}activateChildRoutes(t,n,r){let o=dr(n);t.children.forEach(i=>{this.activateRoutes(i,o[i.value.outlet],r),this.forwardEvent(new nu(i.value.snapshot))}),t.children.length&&this.forwardEvent(new eu(t.value.snapshot))}activateRoutes(t,n,r){let o=t.value,i=n?n.value:null;if(Bl(o),o===i)if(o.component){let s=r.getOrCreateContext(o.outlet);this.activateChildRoutes(t,n,s.children)}else this.activateChildRoutes(t,n,r);else if(o.component){let s=r.getOrCreateContext(o.outlet);if(this.routeReuseStrategy.shouldAttach(o.snapshot)){let a=this.routeReuseStrategy.retrieve(o.snapshot);this.routeReuseStrategy.store(o.snapshot,null),s.children.onOutletReAttached(a.contexts),s.attachRef=a.componentRef,s.route=a.route.value,s.outlet&&s.outlet.attach(a.componentRef,a.route.value),Bl(a.route.value),this.activateChildRoutes(t,null,s.children)}else s.attachRef=null,s.route=o,s.outlet&&s.outlet.activateWith(o,s.injector),this.activateChildRoutes(t,null,s.children)}else this.activateChildRoutes(t,null,r)}},Ls=class{path;route;constructor(t){this.path=t,this.route=this.path[this.path.length-1]}},hr=class{component;route;constructor(t,n){this.component=t,this.route=n}};function lI(e,t,n){let r=e._root,o=t?t._root:null;return Eo(r,o,n,[r.value])}function uI(e){let t=e.routeConfig?e.routeConfig.canActivateChild:null;return!t||t.length===0?null:{node:e,guards:t}}function Ir(e,t){let n=Symbol(),r=t.get(e,n);return r===n?typeof e=="function"&&!Aa(e)?e:t.get(e):r}function Eo(e,t,n,r,o={canDeactivateChecks:[],canActivateChecks:[]}){let i=dr(t);return e.children.forEach(s=>{dI(s,i[s.value.outlet],n,r.concat([s.value]),o),delete i[s.value.outlet]}),Object.entries(i).forEach(([s,a])=>Io(a,n.getContext(s),o)),o}function dI(e,t,n,r,o={canDeactivateChecks:[],canActivateChecks:[]}){let i=e.value,s=t?t.value:null,a=n?n.getContext(e.value.outlet):null;if(s&&i.routeConfig===s.routeConfig){let c=fI(s,i,i.routeConfig.runGuardsAndResolvers);c?o.canActivateChecks.push(new Ls(r)):(i.data=s.data,i._resolvedData=s._resolvedData),i.component?Eo(e,t,a?a.children:null,r,o):Eo(e,t,n,r,o),c&&a&&a.outlet&&a.outlet.isActivated&&o.canDeactivateChecks.push(new hr(a.outlet.component,s))}else s&&Io(t,a,o),o.canActivateChecks.push(new Ls(r)),i.component?Eo(e,null,a?a.children:null,r,o):Eo(e,null,n,r,o);return o}function fI(e,t,n){if(typeof n=="function")return n(e,t);switch(n){case"pathParamsChange":return!Dn(e.url,t.url);case"pathParamsOrQueryParamsChange":return!Dn(e.url,t.url)||!tt(e.queryParams,t.queryParams);case"always":return!0;case"paramsOrQueryParamsChange":return!su(e,t)||!tt(e.queryParams,t.queryParams);case"paramsChange":default:return!su(e,t)}}function Io(e,t,n){let r=dr(e),o=e.value;Object.entries(r).forEach(([i,s])=>{o.component?t?Io(s,t.children.getContext(i),n):Io(s,null,n):Io(s,t,n)}),o.component?t&&t.outlet&&t.outlet.isActivated?n.canDeactivateChecks.push(new hr(t.outlet.component,o)):n.canDeactivateChecks.push(new hr(null,o)):n.canDeactivateChecks.push(new hr(null,o))}function Ro(e){return typeof e=="function"}function pI(e){return typeof e=="boolean"}function hI(e){return e&&Ro(e.canLoad)}function gI(e){return e&&Ro(e.canActivate)}function mI(e){return e&&Ro(e.canActivateChild)}function vI(e){return e&&Ro(e.canDeactivate)}function yI(e){return e&&Ro(e.canMatch)}function mg(e){return e instanceof ot||e?.name==="EmptyError"}var Ss=Symbol("INITIAL_VALUE");function Dr(){return Ee(e=>si(e.map(t=>t.pipe(it(1),va(Ss)))).pipe(B(t=>{for(let n of t)if(n!==!0){if(n===Ss)return Ss;if(n===!1||EI(n))return n}return!0}),Ae(t=>t!==Ss),it(1)))}function EI(e){return vr(e)||e instanceof To}function DI(e,t){return Z(n=>{let{targetSnapshot:r,currentSnapshot:o,guards:{canActivateChecks:i,canDeactivateChecks:s}}=n;return s.length===0&&i.length===0?w(P(y({},n),{guardsResult:!0})):CI(s,r,o,e).pipe(Z(a=>a&&pI(a)?II(r,i,e,t):w(a)),B(a=>P(y({},n),{guardsResult:a})))})}function CI(e,t,n,r){return Y(e).pipe(Z(o=>TI(o.component,o.route,n,t,r)),st(o=>o!==!0,!0))}function II(e,t,n,r){return Y(t).pipe(jn(o=>Fn(bI(o.route.parent,r),SI(o.route,r),wI(e,o.path,n),_I(e,o.route,n))),st(o=>o!==!0,!0))}function SI(e,t){return e!==null&&t&&t(new tu(e)),w(!0)}function bI(e,t){return e!==null&&t&&t(new Jl(e)),w(!0)}function _I(e,t,n){let r=t.routeConfig?t.routeConfig.canActivate:null;if(!r||r.length===0)return w(!0);let o=r.map(i=>Nr(()=>{let s=Cr(t)??n,a=Ir(i,s),c=gI(a)?a.canActivate(t,e):G(s,()=>a(t,e));return wt(c).pipe(st())}));return w(o).pipe(Dr())}function wI(e,t,n){let r=t[t.length-1],i=t.slice(0,t.length-1).reverse().map(s=>uI(s)).filter(s=>s!==null).map(s=>Nr(()=>{let a=s.guards.map(c=>{let l=Cr(s.node)??n,u=Ir(c,l),f=mI(u)?u.canActivateChild(r,e):G(l,()=>u(r,e));return wt(f).pipe(st())});return w(a).pipe(Dr())}));return w(i).pipe(Dr())}function TI(e,t,n,r,o){let i=t&&t.routeConfig?t.routeConfig.canDeactivate:null;if(!i||i.length===0)return w(!0);let s=i.map(a=>{let c=Cr(t)??o,l=Ir(a,c),u=vI(l)?l.canDeactivate(e,t,n,r):G(c,()=>l(e,t,n,r));return wt(u).pipe(st())});return w(s).pipe(Dr())}function MI(e,t,n,r){let o=t.canLoad;if(o===void 0||o.length===0)return w(!0);let i=o.map(s=>{let a=Ir(s,e),c=hI(a)?a.canLoad(t,n):G(e,()=>a(t,n));return wt(c)});return w(i).pipe(Dr(),vg(r))}function vg(e){return ua(ee(t=>{if(typeof t!="boolean")throw ks(e,t)}),B(t=>t===!0))}function xI(e,t,n,r){let o=t.canMatch;if(!o||o.length===0)return w(!0);let i=o.map(s=>{let a=Ir(s,e),c=yI(a)?a.canMatch(t,n):G(e,()=>a(t,n));return wt(c)});return w(i).pipe(Dr(),vg(r))}var Mo=class{segmentGroup;constructor(t){this.segmentGroup=t||null}},xo=class extends Error{urlTree;constructor(t){super(),this.urlTree=t}};function ur(e){return Ln(new Mo(e))}function NI(e){return Ln(new C(4e3,!1))}function AI(e){return Ln(hg(!1,Ie.GuardRejected))}var lu=class{urlSerializer;urlTree;constructor(t,n){this.urlSerializer=t,this.urlTree=n}lineralizeSegments(t,n){let r=[],o=n.root;for(;;){if(r=r.concat(o.segments),o.numberOfChildren===0)return w(r);if(o.numberOfChildren>1||!o.children[A])return NI(`${t.redirectTo}`);o=o.children[A]}}applyRedirectCommands(t,n,r,o,i){return RI(n,o,i).pipe(B(s=>{if(s instanceof _t)throw new xo(s);let a=this.applyRedirectCreateUrlTree(s,this.urlSerializer.parse(s),t,r);if(s[0]==="/")throw new xo(a);return a}))}applyRedirectCreateUrlTree(t,n,r,o){let i=this.createSegmentGroup(t,n.root,r,o);return new _t(i,this.createQueryParams(n.queryParams,this.urlTree.queryParams),n.fragment)}createQueryParams(t,n){let r={};return Object.entries(t).forEach(([o,i])=>{if(typeof i=="string"&&i[0]===":"){let a=i.substring(1);r[o]=n[a]}else r[o]=i}),r}createSegmentGroup(t,n,r,o){let i=this.createSegments(t,n.segments,r,o),s={};return Object.entries(n.children).forEach(([a,c])=>{s[a]=this.createSegmentGroup(t,c,r,o)}),new $(i,s)}createSegments(t,n,r,o){return n.map(i=>i.path[0]===":"?this.findPosParam(t,i,o):this.findOrReturn(i,r))}findPosParam(t,n,r){let o=r[n.path.substring(1)];if(!o)throw new C(4001,!1);return o}findOrReturn(t,n){let r=0;for(let o of n){if(o.path===t.path)return n.splice(r),o;r++}return t}};function RI(e,t,n){if(typeof e=="string")return w(e);let r=e,{queryParams:o,fragment:i,routeConfig:s,url:a,outlet:c,params:l,data:u,title:f}=t;return wt(G(n,()=>r({params:l,data:u,queryParams:o,fragment:i,routeConfig:s,url:a,outlet:c,title:f})))}var uu={matched:!1,consumedSegments:[],remainingSegments:[],parameters:{},positionalParamSegments:{}};function OI(e,t,n,r,o){let i=yg(e,t,n);return i.matched?(r=eI(t,r),xI(r,t,n,o).pipe(B(s=>s===!0?i:y({},uu)))):w(i)}function yg(e,t,n){if(t.path==="**")return PI(n);if(t.path==="")return t.pathMatch==="full"&&(e.hasChildren()||n.length>0)?y({},uu):{matched:!0,consumedSegments:[],remainingSegments:n,parameters:{},positionalParamSegments:{}};let o=(t.matcher||TC)(n,e,t);if(!o)return y({},uu);let i={};Object.entries(o.posParams??{}).forEach(([a,c])=>{i[a]=c.path});let s=o.consumed.length>0?y(y({},i),o.consumed[o.consumed.length-1].parameters):i;return{matched:!0,consumedSegments:o.consumed,remainingSegments:n.slice(o.consumed.length),parameters:s,positionalParamSegments:o.posParams??{}}}function PI(e){return{matched:!0,parameters:e.length>0?Zh(e).parameters:{},consumedSegments:e,remainingSegments:[],positionalParamSegments:{}}}function zh(e,t,n,r){return n.length>0&&FI(e,n,r)?{segmentGroup:new $(t,LI(r,new $(n,e.children))),slicedSegments:[]}:n.length===0&&jI(e,n,r)?{segmentGroup:new $(e.segments,kI(e,n,r,e.children)),slicedSegments:n}:{segmentGroup:new $(e.segments,e.children),slicedSegments:n}}function kI(e,t,n,r){let o={};for(let i of n)if(Hs(e,t,i)&&!r[ze(i)]){let s=new $([],{});o[ze(i)]=s}return y(y({},r),o)}function LI(e,t){let n={};n[A]=t;for(let r of e)if(r.path===""&&ze(r)!==A){let o=new $([],{});n[ze(r)]=o}return n}function FI(e,t,n){return n.some(r=>Hs(e,t,r)&&ze(r)!==A)}function jI(e,t,n){return n.some(r=>Hs(e,t,r))}function Hs(e,t,n){return(e.hasChildren()||t.length>0)&&n.pathMatch==="full"?!1:n.path===""}function HI(e,t,n){return t.length===0&&!e.children[n]}var du=class{};function BI(e,t,n,r,o,i,s="emptyOnly"){return new fu(e,t,n,r,o,s,i).recognize()}var VI=31,fu=class{injector;configLoader;rootComponentType;config;urlTree;paramsInheritanceStrategy;urlSerializer;applyRedirects;absoluteRedirectCount=0;allowRedirects=!0;constructor(t,n,r,o,i,s,a){this.injector=t,this.configLoader=n,this.rootComponentType=r,this.config=o,this.urlTree=i,this.paramsInheritanceStrategy=s,this.urlSerializer=a,this.applyRedirects=new lu(this.urlSerializer,this.urlTree)}noMatchError(t){return new C(4002,`'${t.segmentGroup}'`)}recognize(){let t=zh(this.urlTree.root,[],[],this.config).segmentGroup;return this.match(t).pipe(B(({children:n,rootSnapshot:r})=>{let o=new xe(r,n),i=new Ps("",o),s=WC(r,[],this.urlTree.queryParams,this.urlTree.fragment);return s.queryParams=this.urlTree.queryParams,i.url=this.urlSerializer.serialize(s),{state:i,tree:s}}))}match(t){let n=new pr([],Object.freeze({}),Object.freeze(y({},this.urlTree.queryParams)),this.urlTree.fragment,Object.freeze({}),A,this.rootComponentType,null,{});return this.processSegmentGroup(this.injector,this.config,t,A,n).pipe(B(r=>({children:r,rootSnapshot:n})),xt(r=>{if(r instanceof xo)return this.urlTree=r.urlTree,this.match(r.urlTree.root);throw r instanceof Mo?this.noMatchError(r):r}))}processSegmentGroup(t,n,r,o,i){return r.segments.length===0&&r.hasChildren()?this.processChildren(t,n,r,i):this.processSegment(t,n,r,r.segments,o,!0,i).pipe(B(s=>s instanceof xe?[s]:[]))}processChildren(t,n,r,o){let i=[];for(let s of Object.keys(r.children))s==="primary"?i.unshift(s):i.push(s);return Y(i).pipe(jn(s=>{let a=r.children[s],c=tI(n,s);return this.processSegmentGroup(t,c,a,s,o)}),ma((s,a)=>(s.push(...a),s)),Nt(null),ga(),Z(s=>{if(s===null)return ur(r);let a=Eg(s);return UI(a),w(a)}))}processSegment(t,n,r,o,i,s,a){return Y(n).pipe(jn(c=>this.processSegmentAgainstRoute(c._injector??t,n,c,r,o,i,s,a).pipe(xt(l=>{if(l instanceof Mo)return w(null);throw l}))),st(c=>!!c),xt(c=>{if(mg(c))return HI(r,o,i)?w(new du):ur(r);throw c}))}processSegmentAgainstRoute(t,n,r,o,i,s,a,c){return ze(r)!==s&&(s===A||!Hs(o,i,r))?ur(o):r.redirectTo===void 0?this.matchSegmentAgainstRoute(t,o,r,i,s,c):this.allowRedirects&&a?this.expandSegmentAgainstRouteUsingRedirect(t,o,n,r,i,s,c):ur(o)}expandSegmentAgainstRouteUsingRedirect(t,n,r,o,i,s,a){let{matched:c,parameters:l,consumedSegments:u,positionalParamSegments:f,remainingSegments:m}=yg(n,o,i);if(!c)return ur(n);typeof o.redirectTo=="string"&&o.redirectTo[0]==="/"&&(this.absoluteRedirectCount++,this.absoluteRedirectCount>VI&&(this.allowRedirects=!1));let h=new pr(i,l,Object.freeze(y({},this.urlTree.queryParams)),this.urlTree.fragment,Wh(o),ze(o),o.component??o._loadedComponent??null,o,qh(o)),E=Os(h,a,this.paramsInheritanceStrategy);return h.params=Object.freeze(E.params),h.data=Object.freeze(E.data),this.applyRedirects.applyRedirectCommands(u,o.redirectTo,f,h,t).pipe(Ee(O=>this.applyRedirects.lineralizeSegments(o,O)),Z(O=>this.processSegment(t,r,n,O.concat(m),s,!1,a)))}matchSegmentAgainstRoute(t,n,r,o,i,s){let a=OI(n,r,o,t,this.urlSerializer);return r.path==="**"&&(n.children={}),a.pipe(Ee(c=>c.matched?(t=r._injector??t,this.getChildConfig(t,r,o).pipe(Ee(({routes:l})=>{let u=r._loadedInjector??t,{parameters:f,consumedSegments:m,remainingSegments:h}=c,E=new pr(m,f,Object.freeze(y({},this.urlTree.queryParams)),this.urlTree.fragment,Wh(r),ze(r),r.component??r._loadedComponent??null,r,qh(r)),S=Os(E,s,this.paramsInheritanceStrategy);E.params=Object.freeze(S.params),E.data=Object.freeze(S.data);let{segmentGroup:O,slicedSegments:F}=zh(n,m,h,l);if(F.length===0&&O.hasChildren())return this.processChildren(u,l,O,E).pipe(B(We=>new xe(E,We)));if(l.length===0&&F.length===0)return w(new xe(E,[]));let In=ze(r)===i;return this.processSegment(u,l,O,F,In?A:i,!0,E).pipe(B(We=>new xe(E,We instanceof xe?[We]:[])))}))):ur(n)))}getChildConfig(t,n,r){return n.children?w({routes:n.children,injector:t}):n.loadChildren?n._loadedRoutes!==void 0?w({routes:n._loadedRoutes,injector:n._loadedInjector}):MI(t,n,r,this.urlSerializer).pipe(Z(o=>o?this.configLoader.loadChildren(t,n).pipe(ee(i=>{n._loadedRoutes=i.routes,n._loadedInjector=i.injector})):AI(n))):w({routes:[],injector:t})}};function UI(e){e.sort((t,n)=>t.value.outlet===A?-1:n.value.outlet===A?1:t.value.outlet.localeCompare(n.value.outlet))}function $I(e){let t=e.value.routeConfig;return t&&t.path===""}function Eg(e){let t=[],n=new Set;for(let r of e){if(!$I(r)){t.push(r);continue}let o=t.find(i=>r.value.routeConfig===i.value.routeConfig);o!==void 0?(o.children.push(...r.children),n.add(o)):t.push(r)}for(let r of n){let o=Eg(r.children);t.push(new xe(r.value,o))}return t.filter(r=>!n.has(r))}function Wh(e){return e.data||{}}function qh(e){return e.resolve||{}}function GI(e,t,n,r,o,i){return Z(s=>BI(e,t,n,r,s.extractedUrl,o,i).pipe(B(({state:a,tree:c})=>P(y({},s),{targetSnapshot:a,urlAfterRedirects:c}))))}function zI(e,t){return Z(n=>{let{targetSnapshot:r,guards:{canActivateChecks:o}}=n;if(!o.length)return w(n);let i=new Set(o.map(c=>c.route)),s=new Set;for(let c of i)if(!s.has(c))for(let l of Dg(c))s.add(l);let a=0;return Y(s).pipe(jn(c=>i.has(c)?WI(c,r,e,t):(c.data=Os(c,c.parent,e).resolve,w(void 0))),ee(()=>a++),Hn(1),Z(c=>a===s.size?w(n):ve))})}function Dg(e){let t=e.children.map(n=>Dg(n)).flat();return[e,...t]}function WI(e,t,n,r){let o=e.routeConfig,i=e._resolve;return o?.title!==void 0&&!ug(o)&&(i[No]=o.title),Nr(()=>(e.data=Os(e,e.parent,n).resolve,qI(i,e,t,r).pipe(B(s=>(e._resolvedData=s,e.data=y(y({},e.data),s),null)))))}function qI(e,t,n,r){let o=$l(e);if(o.length===0)return w({});let i={};return Y(o).pipe(Z(s=>YI(e[s],t,n,r).pipe(st(),ee(a=>{if(a instanceof To)throw ks(new mr,a);i[s]=a}))),Hn(1),B(()=>i),xt(s=>mg(s)?ve:Ln(s)))}function YI(e,t,n,r){let o=Cr(t)??r,i=Ir(e,o),s=i.resolve?i.resolve(t,n):G(o,()=>i(t,n));return wt(s)}function Vl(e){return Ee(t=>{let n=e(t);return n?Y(n).pipe(B(()=>t)):w(t)})}var Cg=(()=>{class e{buildTitle(n){let r,o=n.root;for(;o!==void 0;)r=this.getResolvedTitleForRoute(o)??r,o=o.children.find(i=>i.outlet===A);return r}getResolvedTitleForRoute(n){return n.data[No]}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:()=>v(ZI),providedIn:"root"})}return e})(),ZI=(()=>{class e extends Cg{title;constructor(n){super(),this.title=n}updateTitle(n){let r=this.buildTitle(n);r!==void 0&&this.title.setTitle(r)}static \u0275fac=function(r){return new(r||e)(N(Bh))};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})(),Bs=new b("",{providedIn:"root",factory:()=>({})}),Vs=new b(""),Ig=(()=>{class e{componentLoaders=new WeakMap;childrenLoaders=new WeakMap;onLoadStartListener;onLoadEndListener;compiler=v(El);loadComponent(n,r){if(this.componentLoaders.get(r))return this.componentLoaders.get(r);if(r._loadedComponent)return w(r._loadedComponent);this.onLoadStartListener&&this.onLoadStartListener(r);let o=wt(G(n,()=>r.loadComponent())).pipe(B(Sg),Ee(bg),ee(s=>{this.onLoadEndListener&&this.onLoadEndListener(r),r._loadedComponent=s}),Ar(()=>{this.componentLoaders.delete(r)})),i=new kn(o,()=>new X).pipe(Pn());return this.componentLoaders.set(r,i),i}loadChildren(n,r){if(this.childrenLoaders.get(r))return this.childrenLoaders.get(r);if(r._loadedRoutes)return w({routes:r._loadedRoutes,injector:r._loadedInjector});this.onLoadStartListener&&this.onLoadStartListener(r);let i=KI(r,this.compiler,n,this.onLoadEndListener).pipe(Ar(()=>{this.childrenLoaders.delete(r)})),s=new kn(i,()=>new X).pipe(Pn());return this.childrenLoaders.set(r,s),s}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();function KI(e,t,n,r){return wt(G(n,()=>e.loadChildren())).pipe(B(Sg),Ee(bg),Z(o=>o instanceof ds||Array.isArray(o)?w(o):Y(t.compileModuleAsync(o))),B(o=>{r&&r(e);let i,s,a=!1;return Array.isArray(o)?(s=o,a=!0):(i=o.create(n).injector,s=i.get(Vs,[],{optional:!0,self:!0}).flat()),{routes:s.map(gu),injector:i}}))}function QI(e){return e&&typeof e=="object"&&"default"in e}function Sg(e){return QI(e)?e.default:e}function bg(e){return w(e)}var mu=(()=>{class e{static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:()=>v(XI),providedIn:"root"})}return e})(),XI=(()=>{class e{shouldProcessUrl(n){return!0}extract(n){return n}merge(n,r){return n}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})(),_g=new b("");var wg=new b(""),Tg=(()=>{class e{currentNavigation=k(null,{equal:()=>!1});currentTransition=null;lastSuccessfulNavigation=null;events=new X;transitionAbortWithErrorSubject=new X;configLoader=v(Ig);environmentInjector=v(K);destroyRef=v(gt);urlSerializer=v(Fs);rootContexts=v(Ao);location=v(lr);inputBindingEnabled=v(js,{optional:!0})!==null;titleStrategy=v(Cg);options=v(Bs,{optional:!0})||{};paramsInheritanceStrategy=this.options.paramsInheritanceStrategy||"emptyOnly";urlHandlingStrategy=v(mu);createViewTransition=v(_g,{optional:!0});navigationErrorHandler=v(wg,{optional:!0});navigationId=0;get hasRequestedNavigation(){return this.navigationId!==0}transitions;afterPreactivation=()=>w(void 0);rootComponentType=null;destroyed=!1;constructor(){let n=o=>this.events.next(new Ql(o)),r=o=>this.events.next(new Xl(o));this.configLoader.onLoadEndListener=r,this.configLoader.onLoadStartListener=n,this.destroyRef.onDestroy(()=>{this.destroyed=!0})}complete(){this.transitions?.complete()}handleNavigationRequest(n){let r=++this.navigationId;Ce(()=>{this.transitions?.next(P(y({},n),{extractedUrl:this.urlHandlingStrategy.extract(n.rawUrl),targetSnapshot:null,targetRouterState:null,guards:{canActivateChecks:[],canDeactivateChecks:[]},guardsResult:null,abortController:new AbortController,id:r}))})}setupNavigations(n){return this.transitions=new ie(null),this.transitions.pipe(Ae(r=>r!==null),Ee(r=>{let o=!1;return w(r).pipe(Ee(i=>{if(this.navigationId>r.id)return this.cancelNavigationTransition(r,"",Ie.SupersededByNewNavigation),ve;this.currentTransition=r,this.currentNavigation.set({id:i.id,initialUrl:i.rawUrl,extractedUrl:i.extractedUrl,targetBrowserUrl:typeof i.extras.browserUrl=="string"?this.urlSerializer.parse(i.extras.browserUrl):i.extras.browserUrl,trigger:i.source,extras:i.extras,previousNavigation:this.lastSuccessfulNavigation?P(y({},this.lastSuccessfulNavigation),{previousNavigation:null}):null,abort:()=>i.abortController.abort()});let s=!n.navigated||this.isUpdatingInternalState()||this.isUpdatedBrowserUrl(),a=i.extras.onSameUrlNavigation??n.onSameUrlNavigation;if(!s&&a!=="reload")return this.events.next(new Bt(i.id,this.urlSerializer.serialize(i.rawUrl),"",xs.IgnoredSameUrlNavigation)),i.resolve(!1),ve;if(this.urlHandlingStrategy.shouldProcessUrl(i.rawUrl))return w(i).pipe(Ee(c=>(this.events.next(new yr(c.id,this.urlSerializer.serialize(c.extractedUrl),c.source,c.restoredState)),c.id!==this.navigationId?ve:Promise.resolve(c))),GI(this.environmentInjector,this.configLoader,this.rootComponentType,n.config,this.urlSerializer,this.paramsInheritanceStrategy),ee(c=>{r.targetSnapshot=c.targetSnapshot,r.urlAfterRedirects=c.urlAfterRedirects,this.currentNavigation.update(u=>(u.finalUrl=c.urlAfterRedirects,u));let l=new Ns(c.id,this.urlSerializer.serialize(c.extractedUrl),this.urlSerializer.serialize(c.urlAfterRedirects),c.targetSnapshot);this.events.next(l)}));if(s&&this.urlHandlingStrategy.shouldProcessUrl(i.currentRawUrl)){let{id:c,extractedUrl:l,source:u,restoredState:f,extras:m}=i,h=new yr(c,this.urlSerializer.serialize(l),u,f);this.events.next(h);let E=cg(this.rootComponentType).snapshot;return this.currentTransition=r=P(y({},i),{targetSnapshot:E,urlAfterRedirects:l,extras:P(y({},m),{skipLocationChange:!1,replaceUrl:!1})}),this.currentNavigation.update(S=>(S.finalUrl=l,S)),w(r)}else return this.events.next(new Bt(i.id,this.urlSerializer.serialize(i.extractedUrl),"",xs.IgnoredByUrlHandlingStrategy)),i.resolve(!1),ve}),ee(i=>{let s=new ql(i.id,this.urlSerializer.serialize(i.extractedUrl),this.urlSerializer.serialize(i.urlAfterRedirects),i.targetSnapshot);this.events.next(s)}),B(i=>(this.currentTransition=r=P(y({},i),{guards:lI(i.targetSnapshot,i.currentSnapshot,this.rootContexts)}),r)),DI(this.environmentInjector,i=>this.events.next(i)),ee(i=>{if(r.guardsResult=i.guardsResult,i.guardsResult&&typeof i.guardsResult!="boolean")throw ks(this.urlSerializer,i.guardsResult);let s=new Yl(i.id,this.urlSerializer.serialize(i.extractedUrl),this.urlSerializer.serialize(i.urlAfterRedirects),i.targetSnapshot,!!i.guardsResult);this.events.next(s)}),Ae(i=>i.guardsResult?!0:(this.cancelNavigationTransition(i,"",Ie.GuardRejected),!1)),Vl(i=>{if(i.guards.canActivateChecks.length!==0)return w(i).pipe(ee(s=>{let a=new Zl(s.id,this.urlSerializer.serialize(s.extractedUrl),this.urlSerializer.serialize(s.urlAfterRedirects),s.targetSnapshot);this.events.next(a)}),Ee(s=>{let a=!1;return w(s).pipe(zI(this.paramsInheritanceStrategy,this.environmentInjector),ee({next:()=>a=!0,complete:()=>{a||this.cancelNavigationTransition(s,"",Ie.NoDataFromResolver)}}))}),ee(s=>{let a=new Kl(s.id,this.urlSerializer.serialize(s.extractedUrl),this.urlSerializer.serialize(s.urlAfterRedirects),s.targetSnapshot);this.events.next(a)}))}),Vl(i=>{let s=a=>{let c=[];if(a.routeConfig?.loadComponent){let l=Cr(a)??this.environmentInjector;c.push(this.configLoader.loadComponent(l,a.routeConfig).pipe(ee(u=>{a.component=u}),B(()=>{})))}for(let l of a.children)c.push(...s(l));return c};return si(s(i.targetSnapshot.root)).pipe(Nt(null),it(1))}),Vl(()=>this.afterPreactivation()),Ee(()=>{let{currentSnapshot:i,targetSnapshot:s}=r,a=this.createViewTransition?.(this.environmentInjector,i.root,s.root);return a?Y(a).pipe(B(()=>r)):w(r)}),B(i=>{let s=oI(n.routeReuseStrategy,i.targetSnapshot,i.currentRouterState);return this.currentTransition=r=P(y({},i),{targetRouterState:s}),this.currentNavigation.update(a=>(a.targetRouterState=s,a)),r}),ee(()=>{this.events.next(new _o)}),cI(this.rootContexts,n.routeReuseStrategy,i=>this.events.next(i),this.inputBindingEnabled),it(1),ci(new V(i=>{let s=r.abortController.signal,a=()=>i.next();return s.addEventListener("abort",a),()=>s.removeEventListener("abort",a)}).pipe(Ae(()=>!o&&!r.targetRouterState),ee(()=>{this.cancelNavigationTransition(r,r.abortController.signal.reason+"",Ie.Aborted)}))),ee({next:i=>{o=!0,this.lastSuccessfulNavigation=Ce(this.currentNavigation),this.events.next(new Ht(i.id,this.urlSerializer.serialize(i.extractedUrl),this.urlSerializer.serialize(i.urlAfterRedirects))),this.titleStrategy?.updateTitle(i.targetRouterState.snapshot),i.resolve(!0)},complete:()=>{o=!0}}),ci(this.transitionAbortWithErrorSubject.pipe(ee(i=>{throw i}))),Ar(()=>{o||this.cancelNavigationTransition(r,"",Ie.SupersededByNewNavigation),this.currentTransition?.id===r.id&&(this.currentNavigation.set(null),this.currentTransition=null)}),xt(i=>{if(this.destroyed)return r.resolve(!1),ve;if(o=!0,gg(i))this.events.next(new bt(r.id,this.urlSerializer.serialize(r.extractedUrl),i.message,i.cancellationCode)),aI(i)?this.events.next(new Er(i.url,i.navigationBehaviorOptions)):r.resolve(!1);else{let s=new bo(r.id,this.urlSerializer.serialize(r.extractedUrl),i,r.targetSnapshot??void 0);try{let a=G(this.environmentInjector,()=>this.navigationErrorHandler?.(s));if(a instanceof To){let{message:c,cancellationCode:l}=ks(this.urlSerializer,a);this.events.next(new bt(r.id,this.urlSerializer.serialize(r.extractedUrl),c,l)),this.events.next(new Er(a.redirectTo,a.navigationBehaviorOptions))}else throw this.events.next(s),i}catch(a){this.options.resolveNavigationPromiseOnError?r.resolve(!1):r.reject(a)}}return ve}))}))}cancelNavigationTransition(n,r,o){let i=new bt(n.id,this.urlSerializer.serialize(n.extractedUrl),r,o);this.events.next(i),n.resolve(!1)}isUpdatingInternalState(){return this.currentTransition?.extractedUrl.toString()!==this.currentTransition?.currentUrlTree.toString()}isUpdatedBrowserUrl(){let n=this.urlHandlingStrategy.extract(this.urlSerializer.parse(this.location.path(!0))),r=Ce(this.currentNavigation),o=r?.targetBrowserUrl??r?.extractedUrl;return n.toString()!==o?.toString()&&!r?.extras.skipLocationChange}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();function JI(e){return e!==Co}var eS=(()=>{class e{static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:()=>v(tS),providedIn:"root"})}return e})(),pu=class{shouldDetach(t){return!1}store(t,n){}shouldAttach(t){return!1}retrieve(t){return null}shouldReuseRoute(t,n){return t.routeConfig===n.routeConfig}},tS=(()=>{class e extends pu{static \u0275fac=(()=>{let n;return function(o){return(n||(n=ts(e)))(o||e)}})();static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})(),Mg=(()=>{class e{urlSerializer=v(Fs);options=v(Bs,{optional:!0})||{};canceledNavigationResolution=this.options.canceledNavigationResolution||"replace";location=v(lr);urlHandlingStrategy=v(mu);urlUpdateStrategy=this.options.urlUpdateStrategy||"deferred";currentUrlTree=new _t;getCurrentUrlTree(){return this.currentUrlTree}rawUrlTree=this.currentUrlTree;getRawUrlTree(){return this.rawUrlTree}createBrowserPath({finalUrl:n,initialUrl:r,targetBrowserUrl:o}){let i=n!==void 0?this.urlHandlingStrategy.merge(n,r):r,s=o??i;return s instanceof _t?this.urlSerializer.serialize(s):s}commitTransition({targetRouterState:n,finalUrl:r,initialUrl:o}){r&&n?(this.currentUrlTree=r,this.rawUrlTree=this.urlHandlingStrategy.merge(r,o),this.routerState=n):this.rawUrlTree=o}routerState=cg(null);getRouterState(){return this.routerState}stateMemento=this.createStateMemento();updateStateMemento(){this.stateMemento=this.createStateMemento()}createStateMemento(){return{rawUrlTree:this.rawUrlTree,currentUrlTree:this.currentUrlTree,routerState:this.routerState}}resetInternalState({finalUrl:n}){this.routerState=this.stateMemento.routerState,this.currentUrlTree=this.stateMemento.currentUrlTree,this.rawUrlTree=this.urlHandlingStrategy.merge(this.currentUrlTree,n??this.rawUrlTree)}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:()=>v(nS),providedIn:"root"})}return e})(),nS=(()=>{class e extends Mg{currentPageId=0;lastSuccessfulId=-1;restoredState(){return this.location.getState()}get browserPageId(){return this.canceledNavigationResolution!=="computed"?this.currentPageId:this.restoredState()?.\u0275routerPageId??this.currentPageId}registerNonRouterCurrentEntryChangeListener(n){return this.location.subscribe(r=>{r.type==="popstate"&&setTimeout(()=>{n(r.url,r.state,"popstate")})})}handleRouterEvent(n,r){n instanceof yr?this.updateStateMemento():n instanceof Bt?this.commitTransition(r):n instanceof Ns?this.urlUpdateStrategy==="eager"&&(r.extras.skipLocationChange||this.setBrowserUrl(this.createBrowserPath(r),r)):n instanceof _o?(this.commitTransition(r),this.urlUpdateStrategy==="deferred"&&!r.extras.skipLocationChange&&this.setBrowserUrl(this.createBrowserPath(r),r)):n instanceof bt&&n.code!==Ie.SupersededByNewNavigation&&n.code!==Ie.Redirect?this.restoreHistory(r):n instanceof bo?this.restoreHistory(r,!0):n instanceof Ht&&(this.lastSuccessfulId=n.id,this.currentPageId=this.browserPageId)}setBrowserUrl(n,{extras:r,id:o}){let{replaceUrl:i,state:s}=r;if(this.location.isCurrentPathEqualTo(n)||i){let a=this.browserPageId,c=y(y({},s),this.generateNgRouterState(o,a));this.location.replaceState(n,"",c)}else{let a=y(y({},s),this.generateNgRouterState(o,this.browserPageId+1));this.location.go(n,"",a)}}restoreHistory(n,r=!1){if(this.canceledNavigationResolution==="computed"){let o=this.browserPageId,i=this.currentPageId-o;i!==0?this.location.historyGo(i):this.getCurrentUrlTree()===n.finalUrl&&i===0&&(this.resetInternalState(n),this.resetUrlToCurrentUrlTree())}else this.canceledNavigationResolution==="replace"&&(r&&this.resetInternalState(n),this.resetUrlToCurrentUrlTree())}resetUrlToCurrentUrlTree(){this.location.replaceState(this.urlSerializer.serialize(this.getRawUrlTree()),"",this.generateNgRouterState(this.lastSuccessfulId,this.currentPageId))}generateNgRouterState(n,r){return this.canceledNavigationResolution==="computed"?{navigationId:n,\u0275routerPageId:r}:{navigationId:n}}static \u0275fac=(()=>{let n;return function(o){return(n||(n=ts(e)))(o||e)}})();static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();function xg(e,t){e.events.pipe(Ae(n=>n instanceof Ht||n instanceof bt||n instanceof bo||n instanceof Bt),B(n=>n instanceof Ht||n instanceof Bt?0:(n instanceof bt?n.code===Ie.Redirect||n.code===Ie.SupersededByNewNavigation:!1)?2:1),Ae(n=>n!==2),it(1)).subscribe(()=>{t()})}var rS={paths:"exact",fragment:"ignored",matrixParams:"ignored",queryParams:"exact"},oS={paths:"subset",fragment:"ignored",matrixParams:"ignored",queryParams:"subset"},vu=(()=>{class e{get currentUrlTree(){return this.stateManager.getCurrentUrlTree()}get rawUrlTree(){return this.stateManager.getRawUrlTree()}disposed=!1;nonRouterCurrentEntryChangeSubscription;console=v(ul);stateManager=v(Mg);options=v(Bs,{optional:!0})||{};pendingTasks=v(mt);urlUpdateStrategy=this.options.urlUpdateStrategy||"deferred";navigationTransitions=v(Tg);urlSerializer=v(Fs);location=v(lr);urlHandlingStrategy=v(mu);injector=v(K);_events=new X;get events(){return this._events}get routerState(){return this.stateManager.getRouterState()}navigated=!1;routeReuseStrategy=v(eS);onSameUrlNavigation=this.options.onSameUrlNavigation||"ignore";config=v(Vs,{optional:!0})?.flat()??[];componentInputBindingEnabled=!!v(js,{optional:!0});currentNavigation=this.navigationTransitions.currentNavigation.asReadonly();constructor(){this.resetConfig(this.config),this.navigationTransitions.setupNavigations(this).subscribe({error:n=>{this.console.warn(n)}}),this.subscribeToNavigationEvents()}eventsSubscription=new q;subscribeToNavigationEvents(){let n=this.navigationTransitions.events.subscribe(r=>{try{let o=this.navigationTransitions.currentTransition,i=Ce(this.navigationTransitions.currentNavigation);if(o!==null&&i!==null){if(this.stateManager.handleRouterEvent(r,i),r instanceof bt&&r.code!==Ie.Redirect&&r.code!==Ie.SupersededByNewNavigation)this.navigated=!0;else if(r instanceof Ht)this.navigated=!0;else if(r instanceof Er){let s=r.navigationBehaviorOptions,a=this.urlHandlingStrategy.merge(r.url,o.currentRawUrl),c=y({browserUrl:o.extras.browserUrl,info:o.extras.info,skipLocationChange:o.extras.skipLocationChange,replaceUrl:o.extras.replaceUrl||this.urlUpdateStrategy==="eager"||JI(o.source)},s);this.scheduleNavigation(a,Co,null,c,{resolve:o.resolve,reject:o.reject,promise:o.promise})}}JC(r)&&this._events.next(r)}catch(o){this.navigationTransitions.transitionAbortWithErrorSubject.next(o)}});this.eventsSubscription.add(n)}resetRootComponentType(n){this.routerState.root.component=n,this.navigationTransitions.rootComponentType=n}initialNavigation(){this.setUpLocationChangeListener(),this.navigationTransitions.hasRequestedNavigation||this.navigateToSyncWithBrowser(this.location.path(!0),Co,this.stateManager.restoredState())}setUpLocationChangeListener(){this.nonRouterCurrentEntryChangeSubscription??=this.stateManager.registerNonRouterCurrentEntryChangeListener((n,r,o)=>{this.navigateToSyncWithBrowser(n,o,r)})}navigateToSyncWithBrowser(n,r,o){let i={replaceUrl:!0},s=o?.navigationId?o:null;if(o){let c=y({},o);delete c.navigationId,delete c.\u0275routerPageId,Object.keys(c).length!==0&&(i.state=c)}let a=this.parseUrl(n);this.scheduleNavigation(a,r,s,i).catch(c=>{this.disposed||this.injector.get(Fe)(c)})}get url(){return this.serializeUrl(this.currentUrlTree)}getCurrentNavigation(){return Ce(this.navigationTransitions.currentNavigation)}get lastSuccessfulNavigation(){return this.navigationTransitions.lastSuccessfulNavigation}resetConfig(n){this.config=n.map(gu),this.navigated=!1}ngOnDestroy(){this.dispose()}dispose(){this._events.unsubscribe(),this.navigationTransitions.complete(),this.nonRouterCurrentEntryChangeSubscription&&(this.nonRouterCurrentEntryChangeSubscription.unsubscribe(),this.nonRouterCurrentEntryChangeSubscription=void 0),this.disposed=!0,this.eventsSubscription.unsubscribe()}createUrlTree(n,r={}){let{relativeTo:o,queryParams:i,fragment:s,queryParamsHandling:a,preserveFragment:c}=r,l=c?this.currentUrlTree.fragment:s,u=null;switch(a??this.options.defaultQueryParamsHandling){case"merge":u=y(y({},this.currentUrlTree.queryParams),i);break;case"preserve":u=this.currentUrlTree.queryParams;break;default:u=i||null}u!==null&&(u=this.removeEmptyProps(u));let f;try{let m=o?o.snapshot:this.routerState.snapshot.root;f=og(m)}catch{(typeof n[0]!="string"||n[0][0]!=="/")&&(n=[]),f=this.currentUrlTree.root}return ig(f,n,u,l??null)}navigateByUrl(n,r={skipLocationChange:!1}){let o=vr(n)?n:this.parseUrl(n),i=this.urlHandlingStrategy.merge(o,this.rawUrlTree);return this.scheduleNavigation(i,Co,null,r)}navigate(n,r={skipLocationChange:!1}){return iS(n),this.navigateByUrl(this.createUrlTree(n,r),r)}serializeUrl(n){return this.urlSerializer.serialize(n)}parseUrl(n){try{return this.urlSerializer.parse(n)}catch{return this.urlSerializer.parse("/")}}isActive(n,r){let o;if(r===!0?o=y({},rS):r===!1?o=y({},oS):o=r,vr(n))return Vh(this.currentUrlTree,n,o);let i=this.parseUrl(n);return Vh(this.currentUrlTree,i,o)}removeEmptyProps(n){return Object.entries(n).reduce((r,[o,i])=>(i!=null&&(r[o]=i),r),{})}scheduleNavigation(n,r,o,i,s){if(this.disposed)return Promise.resolve(!1);let a,c,l;s?(a=s.resolve,c=s.reject,l=s.promise):l=new Promise((f,m)=>{a=f,c=m});let u=this.pendingTasks.add();return xg(this,()=>{queueMicrotask(()=>this.pendingTasks.remove(u))}),this.navigationTransitions.handleNavigationRequest({source:r,restoredState:o,currentUrlTree:this.currentUrlTree,currentRawUrl:this.currentUrlTree,rawUrl:n,extras:i,resolve:a,reject:c,promise:l,currentSnapshot:this.routerState.snapshot,currentRouterState:this.routerState}),l.catch(f=>Promise.reject(f))}static \u0275fac=function(r){return new(r||e)};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})}return e})();function iS(e){for(let t=0;tn.\u0275providers)])}function aS(e){return e.routerState.root}function cS(){let e=v(Re);return t=>{let n=e.get(yn);if(t!==n.components[0])return;let r=e.get(vu),o=e.get(lS);e.get(uS)===1&&r.initialNavigation(),e.get(dS,null,{optional:!0})?.setUpPreloading(),e.get(sS,null,{optional:!0})?.init(),r.resetRootComponentType(n.componentTypes[0]),o.closed||(o.next(),o.complete(),o.unsubscribe())}}var lS=new b("",{factory:()=>new X}),uS=new b("",{providedIn:"root",factory:()=>1});var dS=new b("");var Ng=[];var Ag={providers:[uc(),Cl(),yu(Ng)]};function Rg(e){switch(e){case"SEED_SUNFLOWSER":return"SUNFLOWER";case"SEED_DAISY":return"DAISY";case"SEED_ROSE":return"ROSE";case"SEED_GALLERY":return"GALLERY";default:return"SUNFLOWER"}}var D=class e{static FARM_TOP_LEFT={x:0,y:0};static FARM_TOP_MIDDLE={x:1,y:0};static FARM_TOP_RIGHT={x:2,y:0};static FARM_MID_LEFT={x:0,y:1};static FARM_SOIL={x:1,y:1};static FARM_MID_RIGHT={x:2,y:1};static FARM_BOTTOM_LEFT={x:0,y:2};static FARM_BOTTOM_MIDDLE={x:1,y:2};static FARM_BOTTOM_RIGHT={x:2,y:2};static FARM_SLOT_DRY={x:3,y:1};static FARM_SLOT_WET={x:3,y:2};static INVENTORY_SLOT={x:8,y:0,width:17,height:17};static BUSH={x:4,y:1};static MUSHROOM={x:4,y:2};static LOCK={x:5,y:1};static KEY={x:6,y:1};static POTS={x:4,y:2};static SEED_SUNFLOWSER={x:0,y:3};static SEED_DAISY={x:1,y:3};static SEED_ROSE={x:2,y:3};static SEED_GALLERY={x:3,y:3};static SEED_DAISY_LOCKED={x:1,y:4};static SEED_ROSE_LOCKED={x:2,y:4};static SEED_GALLERY_LOCKED={x:3,y:4};static DAISY_LOCK={x:5,y:0};static ROSE_LOCK={x:6,y:0};static GALLERY_LOCK={x:7,y:0};static HELP_ICON={x:7,y:1};static FOUNTAIN_ANIM=[{x:0,y:4},{x:2,y:4},{x:4,y:4},{x:6,y:4},{x:8,y:4},{x:10,y:4}];static CHICKEN_PECK_RIGHT_ANIM=[{x:0,y:6},{x:1,y:6},{x:2,y:6},{x:3,y:6},{x:4,y:6},{x:5,y:6},{x:6,y:6},{x:7,y:6}];static CHICKEN_PECK_DOWN_ANIM=[{x:0,y:8},{x:1,y:8},{x:2,y:8},{x:3,y:8},{x:4,y:8},{x:5,y:8},{x:6,y:8},{x:7,y:8}];static CHICKEN_PECK_LEFT_ANIM=[{x:0,y:9},{x:1,y:9},{x:2,y:9},{x:3,y:9},{x:4,y:9},{x:5,y:9},{x:6,y:9},{x:7,y:9}];static CHICKEN_PECK_TOP_ANIM=[{x:0,y:10},{x:1,y:10},{x:2,y:10},{x:3,y:10},{x:4,y:10},{x:5,y:10},{x:6,y:10},{x:7,y:10}];static CHICKEN_IDLE_ANIM=[{x:0,y:22},{x:1,y:22},{x:2,y:22},{x:3,y:22},{x:4,y:22},{x:5,y:22},{x:6,y:22},{x:7,y:22}];static CHICKEN_DANCE_ANIM=[{x:0,y:23},{x:1,y:23},{x:2,y:23},{x:3,y:23},{x:4,y:23},{x:5,y:23},{x:6,y:23},{x:7,y:23}];static WATER_CAN_ANIM=[{x:0,y:7},{x:1,y:7},{x:2,y:7},{x:3,y:7},{x:4,y:7},{x:5,y:7},{x:6,y:7},{x:7,y:7},{x:8,y:7}];static SCYTHE_ANIM=[{x:4,y:3},{x:4,y:3},{x:5,y:3,offsetX:-1,offsetY:1},{x:6,y:3,offsetX:-2,offsetY:3},{x:7,y:3,offsetX:-2,offsetY:3}];static SMALL_BUSHES=[{x:0,y:5},{x:1,y:5},{x:2,y:5},{x:3,y:5},{x:4,y:5},{x:5,y:5},{x:6,y:5},{x:7,y:5}];static HIGHLIGHTER_ANIM=[{x:4,y:4},{x:5,y:4},{x:6,y:4},{x:7,y:4},{x:8,y:4}];static PLANT_SUNFLOWER_PHASES=[{x:0,y:11},{x:0,y:12},{x:0,y:13,offsetY:-2},{x:0,y:14,offsetY:-4},{x:0,y:15,height:32,offsetY:-20}];static PLANT_DAISY_PHASES=[{x:1,y:11},{x:1,y:12},{x:1,y:13,offsetY:-2},{x:1,y:14,offsetY:-4},{x:1,y:15,height:32,offsetY:-20}];static PLANT_ROSE_PHASES=[{x:2,y:11},{x:2,y:12},{x:2,y:13,offsetY:-2},{x:2,y:14,offsetY:-4},{x:2,y:15,height:32,offsetY:-20}];static PLANT_GALLERY_PHASES=[{x:3,y:11},{x:3,y:12},{x:3,y:13,offsetY:-2},{x:3,y:14,offsetY:-4},{x:3,y:15,height:32,offsetY:-20}];static HARVEST_SUNFLOWER={x:0,y:17};static HARVEST_DAISY={x:1,y:17};static HARVEST_ROSE={x:2,y:17};static BUBBLE_TILES=[{x:0,y:18},{x:1,y:18},{x:2,y:18},{x:0,y:19},{x:1,y:19},{x:2,y:19},{x:0,y:20},{x:1,y:20},{x:2,y:20}];static DIGITS=[{x:0,y:21},{x:1,y:21},{x:2,y:21},{x:3,y:21},{x:4,y:21},{x:5,y:21},{x:6,y:21},{x:7,y:21},{x:8,y:21}];static GALLERY_FRUITS=[{x:0,y:24},{x:1,y:24},{x:2,y:24},{x:3,y:24}];static getAtlasTileFromObj(t){switch(t.type){case"SEED_SUNFLOWSER":return e.SEED_SUNFLOWSER;case"SEED_DAISY":return t.locked?e.SEED_DAISY_LOCKED:e.SEED_DAISY;case"SEED_ROSE":return t.locked?e.SEED_ROSE_LOCKED:e.SEED_ROSE;case"SEED_GALLERY":return t.locked?e.SEED_GALLERY_LOCKED:e.SEED_GALLERY;case"WATER_CAN":return e.WATER_CAN_ANIM[0];case"HARVEST":return e.SCYTHE_ANIM[0]}}static getLockAtlasTileFromObj(t){switch(t.type){case"SEED_DAISY":return e.DAISY_LOCK;case"SEED_ROSE":return e.ROSE_LOCK;case"SEED_GALLERY":return e.GALLERY_LOCK;default:return{x:-100,y:-100}}}static getPlantTilesFromPlant(t){switch(t.type){case"SUNFLOWER":return e.PLANT_SUNFLOWER_PHASES[t.phase];case"DAISY":return e.PLANT_DAISY_PHASES[t.phase];case"ROSE":return e.PLANT_ROSE_PHASES[t.phase];case"GALLERY":return e.PLANT_GALLERY_PHASES[t.phase]}}static getHarvestTileFromPlant(t){switch(t.type){case"SUNFLOWER":return e.HARVEST_SUNFLOWER;case"DAISY":return e.HARVEST_DAISY;case"ROSE":return e.HARVEST_ROSE}return{x:-100,y:-100}}};var pe=class{baseTiles=k([]);pos=k({x:0,y:0});size=k({width:0,height:0});visible=k(!0);tiles=lo(()=>this.genTiles());constructor(t,n,r,o){this.pos.set({x:t,y:n}),this.size.set({width:r,height:o}),this.init()}init(){}setVisible(t){this.visible.set(t)}moveTo(t,n){this.pos.set({x:t,y:n})}setWidth(t){this.size.update(n=>P(y({},n),{width:t}))}setHeight(t){this.size.update(n=>P(y({},n),{height:t}))}get x(){return this.pos().x}get y(){return this.pos().y}get width(){return this.size().width}get height(){return this.size().height}};var Ne=150,Vt=class e{timeTick=k(0);countTick=k(0);lastTs=0;constructor(){this.tick()}tick(){let t=Date.now();t-this.lastTs>Ne&&(this.timeTick.set(t),this.countTick.update(n=>n+1),this.lastTs=t),requestAnimationFrame(()=>this.tick())}static \u0275fac=function(n){return new(n||e)};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})};function kg(e){for(let t=e.length-1;t>0;t--){let n=Math.floor(Math.random()*(t+1));[e[t],e[n]]=[e[n],e[t]]}return e}var Lg=5,Us=class extends pe{slotStates=k({0:0,1:0,2:0,3:0,4:0,5:0,6:0,7:0,8:0});plants=k({});slotPos={};shuffledGalleryFruits={};showHelp=k(!0);tickService=v(Vt);constructor(t,n,r,o){super(t,n,r,o),this.initFarm();for(let i=0;i<9;i++){let s=kg([0,1,2,3]);this.shuffledGalleryFruits[i]=s}It(()=>{let i=this.tickService.countTick();Ce(()=>{let s=this.plants();for(let a of Object.keys(s)){let c=s[Number(a)];if(c.phase>=0&&c.phase<=2&&c.growStartTick2==null){if(c.growStartTick1==null)continue;this.setPlantPhase(Number(a),Math.min(2,Math.floor((i-c.growStartTick1)/Lg)))}else if(c.phase>=2&&c.phase<=4&&c.growStartTick1!=null){if(c.growStartTick2==null)continue;this.setPlantPhase(Number(a),2+Math.min(2,Math.floor((i-c.growStartTick2)/Lg)))}(c.phase===2&&c.growStartTick2==null||c.phase===4&&!c.grown)&&(this.setSlotWatered(Number(a),!1),c.phase===4&&(c.grown=!0))}})})}initFarm(){let t=this.x,n=this.y,r=this.width,o=this.height,i=[];var s=0;for(let l=0;l{let o=y({},r);return o[t]=n?1:0,o}),n&&this.plants()[t]){let r=this.plants()[t];r.phase===0?this.plants.update(o=>{let i=y({},o);return i[t]&&(i[t].growStartTick1=this.tickService.countTick()),i}):r.phase===2&&this.plants.update(o=>{let i=y({},o);return i[t]&&(i[t].growStartTick2=this.tickService.countTick()),i})}}isSlotWatered(t){return this.slotStates()[t]===1}addPlant(t,n){let r={type:n,phase:0};this.isSlotWatered(t)&&(r.growStartTick1=this.tickService.countTick()),this.plants.update(o=>{let i=y({},o);return i[t]=r,i})}removePlant(t){this.plants.update(n=>{let r=y({},n);return delete r[t],r})}setPlantPhase(t,n){this.plants.update(r=>{if(r[t].phase===n)return r;let o=y({},r);return o[t].phase=n,o})}setHelpVisible(t){this.showHelp.set(t)}};var Eu={INVENTORY:"inventory.mp3",ERROR:"error.mp3",CLOSE:"close.mp3",WATERING:"watering.mp3",WHIP:"whip.mp3",DIRT:"dirt.mp3",CHECK_STATUS:"check_status.mp3",PATTERN_GOAL:"goal.mp3",GALLERY_GOAL:"goal_gallery.mp3",PLOP:"plop.mp3",PLOP2:"plop2.mp3",PLOP3:"plop3.mp3"},Se=class e{game;audios={};constructor(){}async preloadSounds(){let t=Object.keys(Eu).map(n=>new Promise((r,o)=>{let i=Eu[n],s=new Audio(i);s.volume=0,s.play().then(()=>{s.pause(),s.currentTime=0,s.volume=1,this.audios[n]=s,r(null)}).catch(a=>{console.error(`Error loading or pre-playing sound '${i}':`,a),o(a)})}));try{await Promise.all(t),console.log("All sounds loaded successfully.")}catch(n){console.error("Error loading sounds:",`${n}`)}}playAudio(t){let n=Eu[t];if(!n)return;new Audio(n).play().catch(o=>{console.error(`Failed to play sound '${n}':`,`${o}`)})}static \u0275fac=function(n){return new(n||e)};static \u0275prov=I({token:e,factory:e.\u0275fac,providedIn:"root"})};var $s=class extends pe{constructor(n,r,o){super(n,r,o*16,16);this.slotsCount=o;this.initInventory()}inventory=k({SEED_SUNFLOWSER:{type:"SEED_SUNFLOWSER",count:0,slotIndex:0},SEED_DAISY:{type:"SEED_DAISY",count:0,slotIndex:1,locked:!0},SEED_ROSE:{type:"SEED_ROSE",count:0,slotIndex:2,locked:!0},SEED_GALLERY:{type:"SEED_GALLERY",count:0,slotIndex:3,locked:!0},WATER_CAN:{type:"WATER_CAN",count:0,slotIndex:4,offsetX:-1,offsetY:0},HARVEST:{type:"HARVEST",count:0,slotIndex:5,offsetX:0,offsetY:1}});selectedSlot=k(0);hiddenSeeds=k(new Set);tickService=v(Vt);gameService=v(Se);initInventory(){let n=[];for(let r=0;ro.slotIndex===n);return r?r.locked===!0:!1}setSelectedSlot(n){n<0||n>=this.slotsCount||this.selectedSlot.set(n)}clearHiddenSeeds(){this.hiddenSeeds.set(new Set)}removeHiddenSeed(n){this.hiddenSeeds.update(r=>{let o=new Set(Array.from(r));return o.delete(n),o})}addHiddenSeeds(n){this.hiddenSeeds.update(r=>{let o=new Set(Array.from(r));for(let i of n)o.add(i);return o})}getSelectedSlotObjectType(){return Object.values(this.inventory()).find(r=>r.slotIndex===this.selectedSlot())?.type}unlockSlot(n){this.inventory.update(r=>{let o=y({},r);return Object.values(o).find(i=>i.slotIndex===n).locked=!1,o})}};var Fg=[D.CHICKEN_PECK_RIGHT_ANIM,D.CHICKEN_PECK_DOWN_ANIM,D.CHICKEN_PECK_LEFT_ANIM,D.CHICKEN_PECK_TOP_ANIM],jg=4,hS=20;var Gs=class extends pe{tickService=v(Vt);state=k(0);animIndex=0;baseTick=0;frozenTile;constructor(t,n){super(t,n,16,16),this.baseTick=this.tickService.countTick()}genTiles(){if(this.tickService.countTick()this.interval&&(this.tick.update(n=>n+1),this.lastTs=t,this.curStep++,this.curStep>this.steps)){this.raf>=0&&(cancelAnimationFrame(this.raf),this.raf=-1),this.onDone();return}this.raf=requestAnimationFrame(()=>{this.animate()})}};var zs=class extends pe{animator;gamService=v(Se);constructor(t,n){super(t,n,16,16),this.animator=new nt(Ne,D.WATER_CAN_ANIM.length,()=>{this.gamService.game.removeObject(this)}),this.animator.animate()}genTiles(){let t=this.animator.tick(),n=D.WATER_CAN_ANIM[t];return[[{x:this.x,y:this.y,atlasX:n.x*16,atlasY:n.y*16}]]}};var gS=177,mS=56,vS=7e3,Sr=class extends pe{constructor(n,r){let o=r?.anchorX??gS,i=r?.anchorY??mS,s=r?.delay??vS;super(o-32,i+5,32,32);this.config=r;this.animator=new nt(50,3,()=>{this.gameService.game.showOverlayText(this.x,this.y,this.width,this.height,n),setTimeout(()=>{this.removed||this.gameService.game.removeBubble(this)},s)}),this.animator.animate()}gameService=v(Se);animator;removed=!1;genTiles(){let n=this.width,r=this.animator.tick()*16+32,o=Math.min(64,this.animator.tick()*16+32),i=this.x+(n-r);Ce(()=>{this.setWidth(r),this.setHeight(o),this.moveTo(i,this.y)});let s=this.genBorderTiles();return this.config?.decorationAtlas&&s.push({x:this.x+2,y:this.y+2,atlasX:this.config.decorationAtlas.x*16,atlasY:this.config.decorationAtlas.y*16}),[s]}dispose(){this.removed=!0}genBorderTiles(){let n=[];for(let o=0;o=16&&o{this.gamService.game.removeObject(this)}),this.animator.animate()}genTiles(){let t=this.animator.tick(),n=D.SCYTHE_ANIM[t];return[[{x:this.x+(n.offsetX??0),y:this.y+(n.offsetY??0),atlasX:n.x*16,atlasY:n.y*16}]]}};var qs=class extends pe{constructor(n,r,o,i=0){super(n,r,16,16);this.harvestAtlas=o;this.animator=new nt(150,3,()=>{this.gamService.game.removeObject(this)},i),this.animator.animate()}animator;totalOffset=0;gamService=v(Se);genTiles(){let n=this.animator.tick();return n<0?[]:(this.totalOffset+=Math.pow(2,2-n),[[{x:this.x+(this.harvestAtlas.offsetX??0),y:this.y+(this.harvestAtlas.offsetY??0)+Math.floor(this.totalOffset),atlasX:this.harvestAtlas.x*16,atlasY:this.harvestAtlas.y*16}]])}};var yS=["canvas"],ES=["container"],DS=["overlay"],CS=["overlayText"],IS=["pot"],SS=["unlockGoal"],bS=["gallerySeed"],_S=["galleryDecoration"],wS=["help"],TS=["helpMenu"],MS=["helpContent"],xS=["creditContent"],NS=["msgBubble"],AS=["exitTutorial"],RS=["chickenOverlay"];function OS(e,t){e&1&&(d(0," Welcome to Tiny Garden! Let's plant a SUNFLOWER together! "),L(1,"br")(2,"br"),d(3," Use the controls at the "),g(4,"span",46),d(5,"bottom of the screen"),p(),d(6," to Type or Say: "),L(7,"br")(8,"div",47),g(9,"span",48),d(10," Plant the sunflower seed on plot 8 "),p())}function PS(e,t){e&1&&(d(0," Nice! Let's "),g(1,"span",46),d(2,"WATER"),p(),d(3," it now. "),L(4,"br")(5,"br"),d(6," Type or Say: "),L(7,"br")(8,"div",47),g(9,"span",48),d(10," Water plot 8 "),p())}function kS(e,t){e&1&&(d(0," A seed needs to be watered "),g(1,"span",46),d(2,"TWICE"),p(),d(3," to fully grow. Let's water it again. "),L(4,"br")(5,"br"),d(6," Type or Say: "),L(7,"br")(8,"div",47),g(9,"span",48),d(10," Water plot 8 "),p())}function LS(e,t){e&1&&(d(0," What a beautiful sunflower! Let's "),g(1,"span",46),d(2,"HARVEST"),p(),d(3," it to fill the "),g(4,"span",46),d(5,"CHEST"),p(),d(6," up top. "),L(7,"br")(8,"br"),d(9," Type or Say: "),L(10,"br")(11,"div",47),g(12,"span",48),d(13," Harvest plot 8 "),p())}function FS(e,t){e&1&&(d(0," You did it! Great job! "),L(1,"br")(2,"br"),d(3," I've added "),g(4,"span",25),d(5,"DAISY"),p(),d(6,", "),g(7,"span",26),d(8,"ROSE"),p(),d(9,", and "),g(10,"span",49),d(11,"S"),p(),g(12,"span",25),d(13,"E"),p(),g(14,"span",24),d(15,"C"),p(),g(16,"span",26),d(17,"R"),p(),g(18,"span",25),d(19,"E"),p(),g(20,"span",49),d(21,"T"),p(),d(22," seed to your inventory. Can you unlock them? I heard the SECRET seed is pretty cool. "),L(23,"br")(24,"br"),d(25," Feel free to "),g(26,"span",46),d(27,"tap me for hints"),p(),d(28,". Good luck! "),g(29,"div",50),d(30," Tap to close "),p())}function jS(e,t){if(e&1&&ir(0,OS,11,0)(1,PS,11,0)(2,kS,11,0)(3,LS,14,0)(4,FS,31,0),e&2){let n=Ge(2);sr(n.curTutorialStep===n.TutorialStep.PLANT||n.curTutorialStep===n.TutorialStep.PLANT_DONE?0:n.curTutorialStep===n.TutorialStep.WATER_1||n.curTutorialStep===n.TutorialStep.WATER_1_DONE?1:n.curTutorialStep===n.TutorialStep.WATER_2||n.curTutorialStep===n.TutorialStep.WATER_2_DONE?2:n.curTutorialStep===n.TutorialStep.HARVEST||n.curTutorialStep===n.TutorialStep.HARVEST_DONE?3:n.curTutorialStep===n.TutorialStep.FINAL?4:-1)}}function HS(e,t){e&1&&(d(0," To unlock the "),g(1,"span",25),d(2,"DAISY"),p(),d(3," seed, plant "),g(4,"span",24),d(5,"SUNFLOWERS"),p(),d(6," into the pattern shown above the seed. "))}function BS(e,t){e&1&&(d(0," To unlock the "),g(1,"span",26),d(2,"ROSE"),p(),d(3," seed, plant "),g(4,"span",24),d(5,"SUNFLOWERS"),p(),d(6," and "),g(7,"span",25),d(8,"DAISIES"),p(),d(9," into the pattern shown above the seed. "))}function VS(e,t){e&1&&(d(0," To unlock the "),g(1,"span",49),d(2,"S"),p(),g(3,"span",25),d(4,"E"),p(),g(5,"span",24),d(6,"C"),p(),g(7,"span",26),d(8,"R"),p(),g(9,"span",25),d(10,"E"),p(),g(11,"span",49),d(12,"T"),p(),d(13," seed, harvest the minimum numbers of flowers shown above the seed. "))}function US(e,t){e&1&&(g(0,"div",51),L(1,"img",52),g(2,"div",53),d(3," You've unlocked the "),g(4,"span",25),d(5,"DAISY"),p(),d(6," seed! "),p()())}function $S(e,t){e&1&&(g(0,"div",51),L(1,"img",54),g(2,"div",53),d(3," You've unlocked the "),g(4,"span",26),d(5,"ROSE"),p(),d(6," seed! "),p()())}function GS(e,t){e&1&&(g(0,"div",51),L(1,"img",55),g(2,"div",53),d(3," You've unlocked the "),g(4,"span",49),d(5,"S"),p(),g(6,"span",25),d(7,"E"),p(),g(8,"span",24),d(9,"C"),p(),g(10,"span",26),d(11,"R"),p(),g(12,"span",25),d(13,"E"),p(),g(14,"span",49),d(15,"T"),p(),d(16," seed! "),p()())}function zS(e,t){e&1&&(g(0,"span",46),d(1,"The seed is locked."),p(),L(2,"br")(3,"br"),d(4," Plant the pattern shown above the seed to unlock. "))}function WS(e,t){e&1&&(g(0,"span",46),d(1,"The seed is locked."),p(),L(2,"br")(3,"br"),d(4," Harvest the numbers of plants shown above the seed to unlock. "))}function qS(e,t){if(e&1&&(ir(0,HS,7,0)(1,BS,10,0)(2,VS,14,0)(3,US,7,0,"div",51)(4,$S,7,0,"div",51)(5,GS,17,0,"div",51)(6,zS,5,0)(7,WS,5,0),g(8,"div",50),d(9," Tap to close "),p()),e&2){let n,r=Ge(2);sr((n=r.curMsgType())===r.MessageType.HOW_TO_UNLOCK_DAISY?0:n===r.MessageType.HOW_TO_UNLOCK_ROSE?1:n===r.MessageType.HOW_TO_UNLOCK_SECRET?2:n===r.MessageType.DAISY_UNLOCKED?3:n===r.MessageType.ROSE_UNLOCKED?4:n===r.MessageType.SECRET_UNLOCKED?5:n===r.MessageType.WARNING_DAISY_ROSE_LOCKED?6:n===r.MessageType.WARNING_SECRET_LOCKED?7:-1),Dt(8),co("center",r.centerTapToClose())}}function YS(e,t){if(e&1){let n=ml();L(0,"canvas",null,1),g(2,"div",15,2),Ct("click",function(){pt(n);let o=Ge();return ht(o.handleTapOverlay())}),L(4,"div",16,3),g(6,"div",17,4),L(8,"img",18),p(),L(9,"div",19,5)(11,"div",20,5)(13,"div",21,5),g(15,"div",22,6),Ct("click",function(o){pt(n);let i=Ge();return ht(i.handleClickHelp(o))}),p(),g(17,"div",23,7)(19,"div",24),d(20),p(),g(21,"div",25),d(22),p(),g(23,"div",26),d(24),p()(),g(25,"div",27,8),L(27,"img",18),p(),g(28,"div",28,9),ir(30,jS,5,1)(31,qS,10,3),p(),g(32,"div",29,10),Ct("click",function(o){pt(n);let i=Ge();return ht(i.handleClickExitTutorial(o))}),g(34,"div"),d(35,"Exit"),p(),g(36,"div"),d(37,"Tutorial"),p()(),g(38,"div",30,11),Ct("click",function(o){pt(n);let i=Ge();return ht(i.handleClickChicken(o))}),p()(),g(40,"div",31,12)(42,"div",32)(43,"h2",33),d(44,"How To Play"),p(),g(45,"button",34),Ct("click",function(o){pt(n);let i=gs(41),s=Ge();return ht(s.handleCloseContentPanel(o,i))}),g(46,"span",35),d(47,"\u2715"),p(),d(48," Close "),p()(),g(49,"div",36)(50,"p"),d(51,"Welcome, gardener! This guide will help you use the magic of the on-device Function Gemma model to grow a tiny beautiful garden. Your voice (or text) is your main tool!"),p(),g(52,"p"),d(53,`Your goal is to plant seeds, grow them into flowers, and harvest them. As you do, you'll unlock new seeds, including a very mysterious "Secret Seed."`),p(),g(54,"section")(55,"h1"),d(56,"Your Tools (Inventory)"),p(),g(57,"p"),d(58,"You have 6 slots in your inventory:"),p(),g(59,"ul")(60,"li")(61,"strong"),d(62,"Slot 1: Sunflower Seed"),p(),d(63," (Your starting seed!)"),p(),g(64,"li")(65,"strong"),d(66,"Slot 2: Daisy Seed"),p(),d(67," (Locked)"),p(),g(68,"li")(69,"strong"),d(70,"Slot 3: Rose Seed"),p(),d(71," (Locked)"),p(),g(72,"li")(73,"strong"),d(74,"Slot 4: Secret Seed"),p(),d(75," (Locked)"),p(),g(76,"li")(77,"strong"),d(78,"Slot 5: Water Can"),p(),d(79," (For making 'em grow)"),p(),g(80,"li")(81,"strong"),d(82,"Slot 6: Scythe"),p(),d(83," (For harvesting your flowers)"),p()()(),g(84,"section")(85,"h1"),d(86,"Your Garden"),p(),g(87,"p"),d(88,"Your garden is a 3x3 grid of plots, ready for planting. When you harvest flowers, they'll be stored in the chests above the plots."),p()(),g(89,"section")(90,"h1"),d(91,"Your Commands"),p(),g(92,"p"),d(93,"Your voice and text are your magic keys here! You can speak or type naturally to give commands."),p(),g(94,"p"),d(95,"Here\u2019s the basic flow:"),p(),g(96,"div")(97,"div")(98,"h2"),d(99,"1. Plant a Seed"),p(),g(100,"p"),d(101,"Tell the game "),g(102,"em"),d(103,"what"),p(),d(104," seed to plant and "),g(105,"em"),d(106,"where"),p(),d(107,". A seed needs an empty plot."),p(),g(108,"ul")(109,"li")(110,"strong"),d(111,"Try:"),p(),d(112,' "Plant sunflower seed on plot 1, 2, and 3."'),p(),g(113,"li")(114,"strong"),d(115,"Or:"),p(),d(116,' "Plant daisy on the top row."'),p()()(),g(117,"div")(118,"h2"),d(119,"2. Water Your Seeds"),p(),g(120,"p"),d(121,"A seed needs to be watered "),g(122,"strong"),d(123,"twice"),p(),d(124," to grow into a full flower."),p(),g(125,"ul")(126,"li")(127,"strong"),d(128,"Try:"),p(),d(129,' "Water plot 1 and 3."'),p()()(),g(130,"div")(131,"h2"),d(132,"3. Harvest Your Flowers"),p(),g(133,"p"),d(134,"Once fully grown, tell the game to harvest them with the scythe."),p(),g(135,"ul")(136,"li")(137,"strong"),d(138,"Try:"),p(),d(139,' "Harvest plot 1."'),p(),g(140,"li")(141,"strong"),d(142,"Or:"),p(),d(143,' "Harvest all."'),p()()()()(),g(144,"section")(145,"h1"),d(146,"Unlocking New Seeds"),p(),g(147,"p"),d(148,'You start with sunflowers, but you can unlock more! Look for the "cue" shown above each locked seed in your inventory.'),p(),g(149,"ul")(150,"li")(151,"strong"),d(152,"Unlock the Daisy Seed:"),p(),L(153,"br"),g(154,"div"),d(155,"Plant your "),g(156,"strong"),d(157,"sunflowers"),p(),d(158," to match the pattern shown above the daisy seed."),p()(),g(159,"li")(160,"strong"),d(161,"Unlock the Rose Seed:"),p(),L(162,"br"),g(163,"div"),d(164,"Plant your "),g(165,"strong"),d(166,"sunflowers and daisies"),p(),d(167," to match the pattern shown above the rose seed."),p()(),g(168,"li")(169,"strong"),d(170,"Unlock the Secret Seed:"),p(),L(171,"br"),g(172,"div"),d(173,"This one is different! Harvest the required number of sunflowers, daisies, and roses. Check the numbers listed above the secret seed to see your goal!"),p()()()(),g(174,"section")(175,"h1"),d(176,"The Final Mystery!"),p(),g(177,"p"),d(178,"Once you've unlocked the legendary Secret Seed, plant it, water it, and harvest it to discover what you've grown!"),p(),g(179,"p"),d(180,"Good luck, gardener!"),p()(),g(181,"h1",37),d(182,"Credits"),p(),g(183,"section")(184,"h1"),d(185,"Graphics"),p(),g(186,"ul")(187,"li")(188,"a",38)(189,"strong"),d(190,"Little Dreamyland"),p(),d(191," by "),g(192,"strong"),d(193,"Starmixu and Utaskuas"),p()(),L(194,"br"),d(195," (Paid license) "),p()()(),g(196,"section")(197,"h1"),d(198,"Sounds"),p(),g(199,"ul")(200,"li")(201,"a",39)(202,"strong"),d(203,"Water splash.wav"),p(),d(204," by "),g(205,"strong"),d(206,"speedygonzo"),p()(),L(207,"br"),d(208," (Creative Commons 0) "),p(),g(209,"li")(210,"a",40)(211,"strong"),d(212,"Throwing / Whip Effect"),p(),d(213," by "),g(214,"strong"),d(215,"denao270"),p()(),L(216,"br"),d(217," (Creative Commons 0) "),p(),g(218,"li")(219,"a",41)(220,"strong"),d(221,"Completed.wav"),p(),d(222," by "),g(223,"strong"),d(224,"Kenneth_Cooney"),p()(),L(225,"br"),d(226," (Creative Commons 0) "),p(),g(227,"li")(228,"a",42)(229,"strong"),d(230,"Plop_1.wav"),p(),d(231," by "),g(232,"strong"),d(233,"MiSchy"),p()(),L(234,"br"),d(235," (Creative Commons 0) "),p(),g(236,"li")(237,"a",43)(238,"strong"),d(239,'"Alert" Video Game Sound'),p(),d(240," by "),g(241,"strong"),d(242,"EVRetro"),p()(),L(243,"br"),d(244," (Creative Commons 0) "),p(),g(245,"li")(246,"a",44)(247,"strong"),d(248,"Plop!"),p(),d(249," by "),g(250,"strong"),d(251,"Breviceps"),p()(),L(252,"br"),d(253," (Creative Commons 0) "),p()()(),g(254,"section")(255,"h1"),d(256,"Font"),p(),g(257,"ul")(258,"li")(259,"a",45)(260,"strong"),d(261,"04b03"),p(),d(262," by "),g(263,"strong"),d(264,"Yuji Oshimoto"),p()(),L(265,"br"),d(266," (Freeware) "),p()()()()(),g(267,"div",31,13)(269,"div",32)(270,"h2",33),d(271,"Credits"),p(),g(272,"button",34),Ct("click",function(o){pt(n);let i=gs(268),s=Ge();return ht(s.handleCloseContentPanel(o,i))}),g(273,"span",35),d(274,"\u2715"),p(),d(275," Close "),p()(),L(276,"div",36),p()}if(e&2){let n=Ge();hs("width",n.canvasWidth)("height",n.canvasHeight),Dt(20),cr(n.harvestGoalSunflower),Dt(2),cr(n.harvestGoalDaisy),Dt(2),cr(n.harvestGoalRose),Dt(6),sr(n.inTutorial?30:31),Dt(2),co("hide",!n.inTutorial)}}var Ys=.13,ZS=7,KS=12,QS=2*Ne,XS=2*Ne,JS=.5*Ne,Du=80,Cu=178,Iu=171,Su=54,Vg=88,Ug=68,zg=(l=>(l[l.OFF=0]="OFF",l[l.HOW_TO_UNLOCK_DAISY=1]="HOW_TO_UNLOCK_DAISY",l[l.HOW_TO_UNLOCK_ROSE=2]="HOW_TO_UNLOCK_ROSE",l[l.HOW_TO_UNLOCK_SECRET=3]="HOW_TO_UNLOCK_SECRET",l[l.DAISY_UNLOCKED=4]="DAISY_UNLOCKED",l[l.ROSE_UNLOCKED=5]="ROSE_UNLOCKED",l[l.SECRET_UNLOCKED=6]="SECRET_UNLOCKED",l[l.WARNING_DAISY_ROSE_LOCKED=7]="WARNING_DAISY_ROSE_LOCKED",l[l.WARNING_SECRET_LOCKED=8]="WARNING_SECRET_LOCKED",l))(zg||{}),bu=[{slots:[{slotIndex:0},{slotIndex:1,plantType:"SUNFLOWER"},{slotIndex:2},{slotIndex:3,plantType:"SUNFLOWER"},{slotIndex:4,plantType:"SUNFLOWER"},{slotIndex:5,plantType:"SUNFLOWER"},{slotIndex:6,plantType:"SUNFLOWER"},{slotIndex:7},{slotIndex:8,plantType:"SUNFLOWER"}],unlockMessageType:4,decorationAtlas:D.HARVEST_DAISY},{slots:[{slotIndex:0,plantType:"DAISY"},{slotIndex:1,plantType:"SUNFLOWER"},{slotIndex:2,plantType:"DAISY"},{slotIndex:3},{slotIndex:4,plantType:"SUNFLOWER"},{slotIndex:5},{slotIndex:6,plantType:"SUNFLOWER"},{slotIndex:7,plantType:"DAISY"},{slotIndex:8,plantType:"SUNFLOWER"}],unlockMessageType:5,decorationAtlas:D.HARVEST_ROSE}],Wg=(u=>(u[u.OFF=0]="OFF",u[u.PLANT=1]="PLANT",u[u.PLANT_DONE=2]="PLANT_DONE",u[u.WATER_1=3]="WATER_1",u[u.WATER_1_DONE=4]="WATER_1_DONE",u[u.WATER_2=5]="WATER_2",u[u.WATER_2_DONE=6]="WATER_2_DONE",u[u.HARVEST=7]="HARVEST",u[u.HARVEST_DONE=8]="HARVEST_DONE",u[u.FINAL=9]="FINAL",u))(Wg||{}),$g=["yellow_svg.svg","red_svg.svg","green_svg.svg","blue_svg.svg"],Gg=["PLOP","PLOP2","PLOP3"],Zs=class e{canvas=me("canvas");container=me("container");overlay=me("overlay");overlayText=me("overlayText");pots=Ih("pot");unlockGoal=me("unlockGoal");gallerySeed=me("gallerySeed");galleryDecoration=me("galleryDecoration");help=me("help");helpMenu=me("helpMenu");helpContent=me("helpContent");creditContent=me("creditContent");msgBubble=me("msgBubble");exitTutorial=me("exitTutorial");chickenOverlay=me("chickenOverlay");canvasWidth=256;canvasHeight=256;harvestGoalSunflower=5;harvestGoalDaisy=6;harvestGoalRose=8;imgLoaded=k(!1);TutorialStep=Wg;tutorial=k({curStep:0});atlas;offscreenCanvas=new OffscreenCanvas(this.canvasWidth,this.canvasHeight);offscreenCtx=this.offscreenCanvas.getContext("2d");ctx;resizeObserver;farm=new Us(Vg,Ug,80,80);inventory=new $s(Du,Cu,6);chicken=new Gs(Iu,Su);objects=k([this.farm,this.inventory,this.chicken]);objectsOnTop=k([]);flowerCounts=k([0,0,0]);injector=v(K);gameService=v(Se);shouldStartTutorial=k(!1);MessageType=zg;curMsgType=k(0);centerTapToClose=lo(()=>this.curMsgType()===4||this.curMsgType()===5||this.curMsgType()===6);divsPositioned=!1;constructor(){this.gameService.game=this,this.atlas=new Image,this.atlas.onload=()=>{this.imgLoaded.set(!0)},this.atlas.src="atlas.png",It(()=>{this.shouldStartTutorial()&&(this.inventory.addHiddenSeeds(["SEED_DAISY","SEED_ROSE","SEED_GALLERY"]),this.setUnlockGoalVisible(!1),this.farm.setHelpVisible(!1),setTimeout(()=>{this.startTutorial()},1e3))}),It(()=>{let t=this.canvas()?.nativeElement?.getContext("2d");if(!t)return;if(this.ctx=t,this.render(),this.overlay()?.nativeElement!=null&&!this.divsPositioned){for(let m=0;m{let l=i.offsetWidth,u=i.offsetHeight,f=l/u,m=123/160,h=1;m>f?h=l/123:h=u/160;let E=this.canvasWidth*h,S=this.canvasHeight*h;s.style.width=`${E}px`,s.style.height=`${S}px`,s.style.left=`${(l-E)/2}px`,s.style.top=`${(u-S)/2}px`,a.style.width=`${E}px`,a.style.height=`${S}px`,a.style.left=`${(l-E)/2}px`,a.style.top=`${(u-S)/2}px`,c.style.fontSize=`${c.offsetWidth/a.offsetWidth*c.offsetWidth*Ys}px`;for(let We=0;We{if(this.pots().length!==3)return;let t=this.flowerCounts();for(let n=0;n{o.style.transform=""},200))}if(!this.inTutorial&&this.inventory.isSlotLocked(3)&&t[0]>=this.harvestGoalSunflower&&t[1]>=this.harvestGoalDaisy&&t[2]>=this.harvestGoalRose){this.gameService.playAudio("GALLERY_GOAL"),this.inventory.unlockSlot(3);let n=this.unlockGoal()?.nativeElement;n&&(n.style.visibility="hidden"),this.showMsgBubble(6)}}),It(()=>{if(!this.inTutorial){let t=-1;for(let n=0;n=0){this.gameService.playAudio("PATTERN_GOAL"),this.inventory.unlockSlot(t+1);let n=bu[t];this.showMsgBubble(n.unlockMessageType)}}}),It(()=>{let t=this.tutorial(),n=this.farm.getPlant(7);Ce(async()=>{switch(n!=null?t.curStep===1?n.type=="SUNFLOWER"&&n.phase===0&&this.setTutorialStep(2):t.curStep===3?n.type=="SUNFLOWER"&&n.phase===2&&this.setTutorialStep(4):t.curStep===5&&n.type=="SUNFLOWER"&&n.phase===4&&this.setTutorialStep(6):t.curStep===7&&this.setTutorialStep(8),t.curStep){case 1:this.showMsgBubble();break;case 2:this.hideMsgBubble(),await this.wait(150),this.setTutorialStep(3);break;case 3:this.showMsgBubble();break;case 4:this.hideMsgBubble(),await this.wait(150),this.setTutorialStep(5);break;case 5:this.showMsgBubble();break;case 6:this.hideMsgBubble(),await this.wait(150),this.setTutorialStep(7);break;case 7:this.showMsgBubble();break;case 8:this.hideMsgBubble(),await this.wait(150),this.setTutorialStep(9);break;case 9:await this.wait(1e3);for(let r of["SEED_DAISY","SEED_ROSE","SEED_GALLERY"])this.gameService.playAudio("PLOP3"),this.inventory.removeHiddenSeed(r),r==="SEED_GALLERY"&&this.setUnlockGoalVisible(!0),await this.wait(500);await this.wait(500),this.showMsgBubble();break;default:break}})}),window.tinyGarden={},window.tinyGarden.use=t=>{G(this.injector,()=>{this.useOnGarden(t)})},window.tinyGarden.selectInventory=t=>{G(this.injector,()=>{this.selectInventory(t)})},window.tinyGarden.runCommands=t=>{G(this.injector,()=>{this.runCommands(t)})},window.tinyGarden.showTutorial=()=>{G(this.injector,()=>{this.startTutorial()})},window.tinyGarden.unlockAll=()=>{G(this.injector,()=>{this.unlockAll()})},window.tinyGarden.startTutorial=()=>{G(this.injector,()=>{this.startTutorial()})}}ngAfterViewInit(){let t=window.location.href,n=new URL(t);new URLSearchParams(n.search).get("tutorial")==="1"&&this.shouldStartTutorial.set(!0)}ngOnDestroy(){this.resizeObserver?.disconnect()}handleTapOverlay(){this.inTutorial&&this.curTutorialStep===9?(this.hideMsgBubble(),this.stopTutorial()):this.inTutorial||this.hideMsgBubble()}handleClickPot(t){this.gameService.playAudio("PLOP"),this.flowerCounts.update(n=>{let r=[...n];return r[t]+=5,r})}handleClickHelp(t){t.stopPropagation(),!this.inTutorial&&this.helpContent()?.nativeElement.classList.toggle("hide")}handleCloseContentPanel(t,n){t.stopPropagation(),n.classList.add("hide")}handleClickExitTutorial(t){t.stopPropagation(),this.hideMsgBubble(),this.stopTutorial()}handleClickChicken(t){t.stopPropagation(),!this.inTutorial&&(this.isMsgBubbleVisible()?this.hideMsgBubble():this.inventory.isSlotLocked(1)?this.showMsgBubble(1):this.inventory.isSlotLocked(2)?this.showMsgBubble(2):this.inventory.isSlotLocked(3)&&this.showMsgBubble(3))}showOverlayText(t,n,r,o,i){let s=this.overlayText()?.nativeElement,a=this.galleryDecoration()?.nativeElement;if(!s||!a)return;let c=(r-16)/this.canvasWidth;this.setDivRect(t,n+16,r-16,o-32,s),s.style.fontSize=`${c*s.offsetWidth*Ys}px`,s.innerHTML=i,i.toLowerCase().includes("secret")&&(a.style.visibility="visible")}removeObject(t){t instanceof Sr?this.objectsOnTop.update(n=>n.filter(r=>r!==t)):this.objects.update(n=>n.filter(r=>r!==t))}removeBubble(){let t=this.objectsOnTop().find(o=>o instanceof Sr);if(!t)return;t.dispose(),this.removeObject(t),this.chicken.setState(0);let n=this.overlayText()?.nativeElement;n&&(n.innerText="");let r=this.galleryDecoration()?.nativeElement;r&&(r.style.visibility="hidden")}unlockAll(){this.inventory.unlockSlot(1),this.inventory.unlockSlot(2),this.inventory.unlockSlot(3)}startTutorial(){this.inventory.addHiddenSeeds(["SEED_DAISY","SEED_ROSE","SEED_GALLERY"]),this.setUnlockGoalVisible(!1),this.farm.setHelpVisible(!1),this.setTutorialStep(1)}stopTutorial(){this.setTutorialStep(0),this.inventory.clearHiddenSeeds(),this.setUnlockGoalVisible(!0),this.farm.setHelpVisible(!0)}get curTutorialStep(){return this.tutorial().curStep}get inTutorial(){return this.curTutorialStep!==0}setUnlockGoalVisible(t){let n=this.unlockGoal()?.nativeElement;n&&(t?n.classList.remove("hide"):n.classList.add("hide"))}render(){let t=this.canvas()?.nativeElement,n=this.container()?.nativeElement;if(!t||!n)return;let r=this.offscreenCtx,o=this.ctx;r.clearRect(0,0,this.canvasWidth,this.canvasHeight);for(let i of[...this.objects(),...this.objectsOnTop()]){if(!i.visible())continue;let s=i.tiles();for(let a of s)for(let c of a)r.drawImage(this.atlas,c.atlasX,c.atlasY,c.atlasWidth??16,c.atlasHeight??16,c.x,c.y,c.atlasWidth??16,c.atlasHeight??16)}o.clearRect(0,0,this.canvasWidth,this.canvasHeight),o.drawImage(this.offscreenCanvas,0,0)}useOnGarden(t,n=()=>{}){this.inTutorial||this.hideMsgBubble();let r=this.inventory.getSelectedSlotObjectType();if(!r){n();return}switch(r){case"WATER_CAN":for(let o=0;o9||setTimeout(()=>{G(this.injector,()=>{this.waterGarden(i),i===t[t.length-1]&&setTimeout(()=>{n()},Ne*14)})},QS*o)}break;case"SEED_SUNFLOWSER":case"SEED_DAISY":case"SEED_ROSE":case"SEED_GALLERY":for(let o=0;o9||this.farm.getPlant(i)==null&&setTimeout(()=>{G(this.injector,()=>{this.setPlant(i,Rg(r)),i===t[t.length-1]&&n()})},JS*o)}break;case"HARVEST":for(let o=0;o9||setTimeout(()=>{G(this.injector,()=>{this.harvestGarden(i),i===t[t.length-1]&&n()})},XS*o)}break}}selectInventory(t){return this.inTutorial||this.hideMsgBubble(),t<0||t>5?(console.warn("Invalid slot"),!1):this.inventory.isSlotLocked(t)?(this.inTutorial||(t===1||t===2?this.showMsgBubble(7):t===3&&this.showMsgBubble(8)),!1):(this.gameService.playAudio("INVENTORY"),this.inventory.setSelectedSlot(t),!0)}waterGarden(t){let n=this.farm.getSlotPos(t);if(!n)return;let r=new zs(n.x+5,n.y-6);this.addObject(r),setTimeout(()=>{this.farm.setSlotWatered(t,!0)},Ne*8),setTimeout(()=>{this.gameService.playAudio("WATERING")},Ne*2)}harvestGarden(t){let n=this.farm.getSlotPos(t);if(!n)return;let r=new Ws(n.x+6,n.y-3);this.addObject(r),this.gameService.playAudio("WHIP");let o=this.farm.getPlant(t);o&&(setTimeout(()=>{this.farm.removePlant(t),this.farm.setSlotWatered(t,!1)},Ne*4),o.grown&&setTimeout(()=>{o.type==="GALLERY"?this.startGalleryFruitsFirework(t):G(this.injector,()=>{let i=Math.floor(-4+8*Math.random()),s=Math.floor(-3+6*Math.random()),a=0;switch(o.type){case"SUNFLOWER":a=0;break;case"DAISY":a=1;break;case"ROSE":a=2;break;default:break}this.addObject(new qs(Vg+16*(a+1)+s,Ug-32+i,D.getHarvestTileFromPlant(o))),setTimeout(()=>{this.gameService.playAudio("PLOP"),this.flowerCounts.update(c=>{let l=[...c];switch(o.type){case"SUNFLOWER":l[0]++;break;case"DAISY":l[1]++;break;case"ROSE":l[2]++}return l})},Ne*2)})},Ne*3.5))}async runCommands(t){try{let n=JSON.parse(t);for(let r of n){if(!this.selectInventory(r.item-1))break;await new Promise(i=>{this.useOnGarden(r.plot.map(s=>s-1).filter(s=>s>=0&&s<=8),()=>{i()})})}}catch(n){console.error(`Failed to parse json string for commands: ${t}`,n)}}setPlant(t,n){this.gameService.playAudio("DIRT"),this.farm.addPlant(t,n)}addObject(t){t instanceof Sr?this.objectsOnTop.update(n=>[...n,t]):this.objects.update(n=>[...n,t])}setDivRect(t,n,r,o,i){let s=t/this.canvasHeight,a=n/this.canvasHeight,c=r/this.canvasWidth,l=o/this.canvasHeight;i.style.left=`${s*100}%`,i.style.top=`${a*100}%`,i.style.width=`${c*100}%`,o>=0&&(i.style.height=`${l*100}%`)}setDivFontSize(t,n){let r=this.overlay()?.nativeElement;r&&(t.style.fontSize=`${t.offsetWidth/r.offsetWidth*t.offsetWidth*n}px`)}startGalleryFruitsFirework(t){let n=this.overlay()?.nativeElement;if(!n)return;let r=n.offsetWidth*.001087,o=32*r,i=32*r,s=-300*r,a=600*r,c=-600*r,l=100*r,u=-400*r,f=800*r,m=this.farm.getSlotPos(t),h=m.x/this.canvasWidth*n.offsetWidth,E=m.y/this.canvasHeight*n.offsetHeight,S=(rt,Ut,ue)=>{let Tt=document.createElement("div");Tt.style.position="absolute",Tt.style.width=`${o}px`,Tt.style.height=`${i}px`,Tt.style.left=`${rt}px`,Tt.style.top=`${Ut}px`;let _r=document.createElement("img");return _r.src=$g[ue],_r.style.width="100%",_r.style.height="100%",_r.style.objectFit="contain",Tt.appendChild(_r),n.appendChild(Tt),Tt},O=[];for(let rt=0;rt{this.gameService.playAudio(Gg[Math.floor(Math.random()*Gg.length)]),O.push({ele:S(h,E,rt%$g.length),vx:s+a*Math.random(),vy:c+l*Math.random(),posX:h,posY:E,gravityOffset:u+f*Math.random(),angle:Math.random()*180,rotateSpeed:-800+Math.random()*1600})},rt*40);let F=2500,In=2e3*r,We=Date.now(),br=performance.now(),_u=rt=>{let Ut=(rt-br)/1e3;if(Ut>0)for(let ue of O)ue.vy+=(In+ue.gravityOffset)*Ut,ue.posX+=ue.vx*Ut,ue.posY+=ue.vy*Ut,ue.ele.style.left=`${ue.posX}px`,ue.ele.style.top=`${ue.posY}px`,ue.angle+=ue.rotateSpeed*Ut,ue.ele.style.transform=`rotate(${ue.angle}deg)`;br=rt,!(Date.now()-We>F)&&requestAnimationFrame(_u)};requestAnimationFrame(_u)}setTutorialStep(t){this.tutorial.update(n=>n.curStep===t?n:P(y({},n),{curStep:t}))}showMsgBubble(t){G(this.injector,async()=>{t!=null&&(this.isMsgBubbleVisible()&&(this.hideMsgBubble(),await this.wait(100)),this.curMsgType.set(t),t===5||t===4||t===6?this.chicken.setState(2):this.chicken.setState(1));let n=this.msgBubble()?.nativeElement;n&&n.classList.add("show")})}hideMsgBubble(){G(this.injector,()=>{let t=this.msgBubble()?.nativeElement;t&&t.classList.remove("show"),this.curMsgType.set(0),this.chicken.setState(0)})}isMsgBubbleVisible(){return this.msgBubble()?.nativeElement?.classList.contains("show")??!1}async wait(t){await new Promise(n=>{setTimeout(()=>{n()},t)})}static \u0275fac=function(n){return new(n||e)};static \u0275cmp=vn({type:e,selectors:[["garden"]],viewQuery:function(n,r){n&1&&(ce(r.canvas,yS,5),ce(r.container,ES,5),ce(r.overlay,DS,5),ce(r.overlayText,CS,5),ce(r.pots,IS,5),ce(r.unlockGoal,SS,5),ce(r.gallerySeed,bS,5),ce(r.galleryDecoration,_S,5),ce(r.help,wS,5),ce(r.helpMenu,TS,5),ce(r.helpContent,MS,5),ce(r.creditContent,xS,5),ce(r.msgBubble,NS,5),ce(r.exitTutorial,AS,5),ce(r.chickenOverlay,RS,5)),n&2&&vl(15)},decls:3,vars:1,consts:[["container",""],["canvas",""],["overlay",""],["overlayText",""],["galleryDecoration",""],["pot",""],["help",""],["unlockGoal",""],["gallerySeed",""],["msgBubble",""],["exitTutorial",""],["chickenOverlay",""],["helpContent",""],["creditContent",""],[1,"container"],[1,"overlay",3,"click"],[1,"overlay-text"],[1,"gallery-decoration"],["src","gallery_with_border.svg"],[1,"pot","yellow"],[1,"pot","blue"],[1,"pot","red"],[1,"help",3,"click"],[1,"unlock-goal"],[1,"yellow"],[1,"blue"],[1,"red"],[1,"gallery-seed"],[1,"bubble"],[1,"btn-exit-tutorial",3,"click"],[1,"chicken-overlay",3,"click"],[1,"help-content","hide"],[1,"title-bar"],[1,"title"],["type","button",1,"btn-close",3,"click"],["aria-hidden","true"],[1,"content"],[1,"main"],["href","https://starmixu.itch.io/little-dreamyland-asset-pack","target","_blank"],["href","https://freesound.org/s/235725/","target","_blank"],["href","https://freesound.org/s/346373/","target","_blank"],["href","https://freesound.org/s/609336/","target","_blank"],["href","https://freesound.org/s/369952/","target","_blank"],["href","https://freesound.org/s/495004/","target","_blank"],["href","https://freesound.org/s/447910/","target","_blank"],["href","https://www.dafont.com/04b-03.font","target","_blank"],[1,"emphasize"],[1,"vertical-spacer"],[1,"highlight"],[1,"green"],[1,"tap-to-close"],[1,"unlock-msg"],["src","daisy.png",1,"decoration"],[1,"msg-content"],["src","rose.png",1,"decoration"],["src","gallery.svg",1,"decoration","rotate"]],template:function(n,r){n&1&&(g(0,"div",14,0),ir(2,YS,277,8),p()),n&2&&(Dt(2),sr(r.imgLoaded()?2:-1))},dependencies:[ys],styles:['.highlight[_ngcontent-%COMP%]{color:#4f82ce}.emphasize[_ngcontent-%COMP%]{color:#a85d5d}.vertical-spacer[_ngcontent-%COMP%]{height:8px}.blue[_ngcontent-%COMP%]{color:#3e9be7}.red[_ngcontent-%COMP%]{color:#ff5454}.yellow[_ngcontent-%COMP%]{color:#e7b616}.green[_ngcontent-%COMP%]{color:#2da158}.container[_ngcontent-%COMP%]{background-color:#b4c160;width:100%;height:100%;position:relative;overflow:hidden}.container[_ngcontent-%COMP%] canvas[_ngcontent-%COMP%]{image-rendering:pixelated;box-sizing:border-box;position:absolute}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%]{position:absolute;font-family:"04b03",Courier New,Courier,monospace}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .overlay-text[_ngcontent-%COMP%]{color:#533a38;position:absolute}@keyframes _ngcontent-%COMP%_rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .gallery-decoration[_ngcontent-%COMP%]{position:absolute;visibility:hidden}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .gallery-decoration[_ngcontent-%COMP%] img[_ngcontent-%COMP%]{object-fit:fill;width:120%;height:120%;animation:_ngcontent-%COMP%_rotate 5s linear infinite}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .pot[_ngcontent-%COMP%]{position:absolute;display:flex;align-items:flex-end;justify-content:flex-end}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .help[_ngcontent-%COMP%]{position:absolute;overflow:visible}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .help[_ngcontent-%COMP%] .help-menu[_ngcontent-%COMP%]{border-radius:3px;font-size:16px;margin-top:calc(100% + 4px);border:2px solid #9a2a2a;width:80px;box-sizing:border-box;background-color:#ecddc9;color:#533a38;opacity:1;transform:translateY(0);transition:transform .15s ease-out,opacity .15s ease-out}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .help[_ngcontent-%COMP%] .help-menu.hide[_ngcontent-%COMP%]{transform:translateY(-5px);opacity:0;pointer-events:none}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .help[_ngcontent-%COMP%] .help-menu[_ngcontent-%COMP%] .help-menu-item[_ngcontent-%COMP%]{padding:4px 6px}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .help[_ngcontent-%COMP%] .help-menu[_ngcontent-%COMP%] .divider[_ngcontent-%COMP%]{height:1px;background-color:#9a2a2a;opacity:.3}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .gallery-seed[_ngcontent-%COMP%]{position:absolute;overflow:hidden;padding:3px;box-sizing:border-box;display:flex;align-items:center;justify-content:center}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .gallery-seed[_ngcontent-%COMP%] img[_ngcontent-%COMP%]{object-fit:fill;width:100%;height:100%}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .blue[_ngcontent-%COMP%]{color:#3e9be7}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .red[_ngcontent-%COMP%]{color:#ff5454}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .unlock-goal[_ngcontent-%COMP%]{position:absolute;display:flex;align-items:center;justify-content:space-between;text-shadow:0px 0px 1px #111}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .unlock-goal[_ngcontent-%COMP%] .blue[_ngcontent-%COMP%]{color:#2183d2}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .unlock-goal[_ngcontent-%COMP%] .red[_ngcontent-%COMP%]{color:#c53b3b!important}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .unlock-goal[_ngcontent-%COMP%] .yellow[_ngcontent-%COMP%]{color:#ffd200!important}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .unlock-goal.hide[_ngcontent-%COMP%]{opacity:0}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .bubble[_ngcontent-%COMP%]{padding:1em;border-radius:var(--r)/var(--r) min(var(--r),var(--p) - var(--h) * tan(var(--a) / 2)) min(var(--r),100% - var(--p) - var(--h) * tan(var(--a) / 2)) var(--r);clip-path:polygon(100% 0,0 0,0 100%,100% 100%,100% min(100%,var(--p) + var(--h) * tan(var(--a) / 2)),calc(100% + var(--h)) var(--p),100% max(0%,var(--p) - var(--h) * tan(var(--a) / 2)));background:var(--c1);border-image:conic-gradient(var(--c1) 0 0) fill 0/max(0%,var(--p) - var(--h) * tan(var(--a) / 2)) 0 max(0%,100% - var(--p) - var(--h) * tan(var(--a) / 2)) var(--r)/0 var(--h) 0 0;position:absolute;--a: 90deg;--h: .9em;--p: 0%;--r: 8px;--b: 4px;--c1: #a85d5d;--c2: #e8dcc8;opacity:0;transform:translate(20px) scale(.9);transform-origin:100% 0%;transition:transform .2s cubic-bezier(.77,2.15,.45,.71),opacity .2s ease-out;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .bubble.show[_ngcontent-%COMP%]{transform:none;opacity:1}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .bubble[_ngcontent-%COMP%] .tap-to-close[_ngcontent-%COMP%]{margin-top:12px;color:#533a38;width:100%;display:flex;justify-content:flex-end}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .bubble[_ngcontent-%COMP%] .tap-to-close.center[_ngcontent-%COMP%]{justify-content:center}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .bubble[_ngcontent-%COMP%]:before{content:"";position:absolute;z-index:-1;inset:0;padding:var(--b);border-radius:inherit;clip-path:polygon(100% 0,0 0,0 100%,100% 100%,calc(100% - var(--b)) min(100% - var(--b),var(--p) + var(--h) * tan(var(--a) / 2) - var(--b) * tan(45deg - var(--a) / 4)),calc(100% + var(--h) - var(--b) / sin(var(--a) / 2)) var(--p),calc(100% - var(--b)) max(var(--b),var(--p) - var(--h) * tan(var(--a) / 2) + var(--b) * tan(45deg - var(--a) / 4)));background:var(--c2) content-box;border-image:conic-gradient(var(--c2) 0 0) fill 0/max(var(--b),var(--p) - var(--h) * tan(var(--a) / 2)) 0 max(var(--b),100% - var(--p) - var(--h) * tan(var(--a) / 2)) var(--r)/0 var(--h) 0 0}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .btn-exit-tutorial[_ngcontent-%COMP%]{position:absolute;display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:8px;color:#fff;background-color:#a85d5d}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .btn-exit-tutorial.hide[_ngcontent-%COMP%]{opacity:0;pointer-events:none}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .chicken-overlay[_ngcontent-%COMP%]{position:absolute}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .unlock-msg[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1em}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .unlock-msg[_ngcontent-%COMP%] img[_ngcontent-%COMP%]{width:2em;aspect-ratio:1;object-fit:contain;image-rendering:pixelated}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .unlock-msg[_ngcontent-%COMP%] img.rotate[_ngcontent-%COMP%]{animation:_ngcontent-%COMP%_rotate 2s linear infinite;image-rendering:optimizeQuality}.container[_ngcontent-%COMP%] .overlay[_ngcontent-%COMP%] .test[_ngcontent-%COMP%]{position:absolute;bottom:.7em;left:1em}.container[_ngcontent-%COMP%] .help-content[_ngcontent-%COMP%]{font-family:Courier New,Courier,monospace;position:absolute;border:3px solid #533A38;box-sizing:border-box;background-color:#f1b093;width:100%;height:100%;opacity:1;transform:scale(1);transition:transform .15s ease-out,opacity .15s ease-out;font-size:14px;display:flex;flex-direction:column}.container[_ngcontent-%COMP%] .help-content.hide[_ngcontent-%COMP%]{transform:scale(.95);opacity:0;pointer-events:none}.container[_ngcontent-%COMP%] .help-content[_ngcontent-%COMP%] .title-bar[_ngcontent-%COMP%]{font-family:"04b03",Courier New,Courier,monospace;display:flex;align-items:center;justify-content:space-between;background-color:#533a38;color:#ecddc9;padding:12px 16px;flex-shrink:0}.container[_ngcontent-%COMP%] .help-content[_ngcontent-%COMP%] .title-bar[_ngcontent-%COMP%] h2[_ngcontent-%COMP%]{margin:0}.container[_ngcontent-%COMP%] .help-content[_ngcontent-%COMP%] .title-bar[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{font-family:"04b03",Courier New,Courier,monospace;color:#ecddc9;background:none;outline:none;border:none;font-size:15px}.container[_ngcontent-%COMP%] .help-content[_ngcontent-%COMP%] .content[_ngcontent-%COMP%]{font-family:Roboto,Helvetica,sans-serif;flex-grow:1;overflow-x:hidden;overflow-y:auto;padding:4px 16px}.container[_ngcontent-%COMP%] .help-content[_ngcontent-%COMP%] h1[_ngcontent-%COMP%]{color:#146039;font-size:16px;font-weight:700;margin-top:28px}.container[_ngcontent-%COMP%] .help-content[_ngcontent-%COMP%] h1.main[_ngcontent-%COMP%]{color:#a85d5d;font-size:20px}.container[_ngcontent-%COMP%] .help-content[_ngcontent-%COMP%] h2[_ngcontent-%COMP%]{font-size:15px;font-weight:500}.container[_ngcontent-%COMP%] .help-content[_ngcontent-%COMP%] ul[_ngcontent-%COMP%] > li[_ngcontent-%COMP%]:not(:first-child){margin-top:8px}.container[_ngcontent-%COMP%] .help-content[_ngcontent-%COMP%] li[_ngcontent-%COMP%] > div[_ngcontent-%COMP%]{margin-top:4px}.container[_ngcontent-%COMP%] .help-content[_ngcontent-%COMP%] a[_ngcontent-%COMP%]{color:#533a38}']})};var Ks=class e{static \u0275fac=function(n){return new(n||e)};static \u0275cmp=vn({type:e,selectors:[["app-root"]],decls:1,vars:0,template:function(n,r){n&1&&ar(0,"garden")},dependencies:[Zs],encapsulation:2})};Fl(Ks,Ag).catch(e=>console.error(e)); diff --git a/Android/src/app/src/main/assets/tinygarden/media/04B_03-VT65MRZF.ttf b/Android/src/app/src/main/assets/tinygarden/media/04B_03-VT65MRZF.ttf new file mode 100644 index 000000000..fe4328b6a Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/media/04B_03-VT65MRZF.ttf differ diff --git a/Android/src/app/src/main/assets/tinygarden/plop.mp3 b/Android/src/app/src/main/assets/tinygarden/plop.mp3 new file mode 100644 index 000000000..94c9e7fa3 Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/plop.mp3 differ diff --git a/Android/src/app/src/main/assets/tinygarden/plop2.mp3 b/Android/src/app/src/main/assets/tinygarden/plop2.mp3 new file mode 100644 index 000000000..669769298 Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/plop2.mp3 differ diff --git a/Android/src/app/src/main/assets/tinygarden/plop3.mp3 b/Android/src/app/src/main/assets/tinygarden/plop3.mp3 new file mode 100644 index 000000000..873de0180 Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/plop3.mp3 differ diff --git a/Android/src/app/src/main/assets/tinygarden/red_svg.svg b/Android/src/app/src/main/assets/tinygarden/red_svg.svg new file mode 100644 index 000000000..5a0b60ef3 --- /dev/null +++ b/Android/src/app/src/main/assets/tinygarden/red_svg.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Android/src/app/src/main/assets/tinygarden/rose.png b/Android/src/app/src/main/assets/tinygarden/rose.png new file mode 100644 index 000000000..025b23e79 Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/rose.png differ diff --git a/Android/src/app/src/main/assets/tinygarden/styles-63IRQW2E.css b/Android/src/app/src/main/assets/tinygarden/styles-63IRQW2E.css new file mode 100644 index 000000000..b5b51b3b6 --- /dev/null +++ b/Android/src/app/src/main/assets/tinygarden/styles-63IRQW2E.css @@ -0,0 +1,16 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +body,html{margin:0;padding:0;width:100%;height:100%;overflow:hidden}@font-face{font-family:"04b03";src:url("./media/04B_03-VT65MRZF.ttf")}.yellow{color:#b59018}.blue{color:#3e9be7}.red{color:#d53d3d}.green{color:#07b449} diff --git a/Android/src/app/src/main/assets/tinygarden/watering.mp3 b/Android/src/app/src/main/assets/tinygarden/watering.mp3 new file mode 100644 index 000000000..afdcb094c Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/watering.mp3 differ diff --git a/Android/src/app/src/main/assets/tinygarden/whip.mp3 b/Android/src/app/src/main/assets/tinygarden/whip.mp3 new file mode 100644 index 000000000..38b3e52db Binary files /dev/null and b/Android/src/app/src/main/assets/tinygarden/whip.mp3 differ diff --git a/Android/src/app/src/main/assets/tinygarden/yellow_svg.svg b/Android/src/app/src/main/assets/tinygarden/yellow_svg.svg new file mode 100644 index 000000000..42e61e538 --- /dev/null +++ b/Android/src/app/src/main/assets/tinygarden/yellow_svg.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/Android/src/app/src/main/bundle_config.pb.json b/Android/src/app/src/main/bundle_config.pb.json new file mode 100644 index 000000000..e14b457b3 --- /dev/null +++ b/Android/src/app/src/main/bundle_config.pb.json @@ -0,0 +1,29 @@ +// Bundle config for the Google AI Edge Gallery app. +// See: https://developer.android.com/studio/build/building-cmdline#bundleconfig +{ + "optimizations": { + "uncompress_native_libraries": { + "enabled": false + }, + "splitsConfig": { + "splitDimension": [ + { + "value": "ABI" + }, + { + "value": "SCREEN_DENSITY" + }, + { + "value": "LANGUAGE" + }, + { + "value": "DEVICE_GROUP", + "suffix_stripping": { + "enabled": true, + "default_suffix": "other" + } + } + ] + } + } +} \ No newline at end of file diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/Analytics.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/Analytics.kt new file mode 100644 index 000000000..c26649761 --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/Analytics.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery + +import android.util.Log +import com.google.firebase.Firebase +import com.google.firebase.analytics.FirebaseAnalytics +import com.google.firebase.analytics.analytics + +private var hasLoggedAnalyticsWarning = false + +val firebaseAnalytics: FirebaseAnalytics? + get() = + runCatching { Firebase.analytics } + .onFailure { exception -> + // Firebase.analytics can throw an exception if goolgle-services is not set up, e.g., + // missing google-services.json. + if (!hasLoggedAnalyticsWarning) { + Log.w("AGAnalyticsFirebase", "Firebase Analytics is not available", exception) + } + } + .getOrNull() + +enum class GalleryEvent(val id: String) { + CAPABILITY_SELECT(id = "capability_select"), + MODEL_DOWNLOAD(id = "model_download"), + GENERATE_ACTION(id = "generate_action"), + BUTTON_CLICKED(id = "button_clicked"), + SKILL_MANAGEMENT(id = "skill_management"), + SKILL_EXECUTION(id = "skill_execution"), + CHAT_HISTORY(id = "chat_history"), +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/BenchmarkResultsSerializer.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/BenchmarkResultsSerializer.kt new file mode 100644 index 000000000..1937d1223 --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/BenchmarkResultsSerializer.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery + +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import com.google.ai.edge.gallery.proto.BenchmarkResults +import com.google.protobuf.InvalidProtocolBufferException +import java.io.InputStream +import java.io.OutputStream + +object BenchmarkResultsSerializer : Serializer { + override val defaultValue: BenchmarkResults = BenchmarkResults.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): BenchmarkResults { + try { + return BenchmarkResults.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + } + + override suspend fun writeTo(t: BenchmarkResults, output: OutputStream) = t.writeTo(output) +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/CutoutsSerializer.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/CutoutsSerializer.kt new file mode 100644 index 000000000..bab083cd3 --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/CutoutsSerializer.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery + +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import com.google.ai.edge.gallery.proto.CutoutCollection +import com.google.protobuf.InvalidProtocolBufferException +import java.io.InputStream +import java.io.OutputStream + +object CutoutsSerializer : Serializer { + override val defaultValue: CutoutCollection = CutoutCollection.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): CutoutCollection { + try { + return CutoutCollection.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + } + + override suspend fun writeTo(t: CutoutCollection, output: OutputStream) = t.writeTo(output) +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/FcmMessagingService.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/FcmMessagingService.kt new file mode 100644 index 000000000..1e839a0d4 --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/FcmMessagingService.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.media.RingtoneManager +import android.os.Build +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.net.toUri +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage + +class GalleryFcmMessagingService : FirebaseMessagingService() { + override fun onMessageReceived(remoteMessage: RemoteMessage) { + // TODO(developer): Handle FCM messages here. + // Not getting messages here? See why this may be: https://goo.gl/39bRNJ + Log.d(TAG, "Full message: $remoteMessage") + Log.d(TAG, "From: ${remoteMessage.from}") + + // Combine data and notification payloads + val data = remoteMessage.data + val notification = remoteMessage.notification + + val deeplink = data["deeplink"] + val imageUrlStr = data["image_url"] + val imageUrl = imageUrlStr?.let { it.toUri() } + + // Prefer data title/body, fallback to notification + val title = data["title"] ?: notification?.title + val body = data["body"] ?: notification?.body + + Log.d(TAG, "Extracted FCM Data -> Title: $title, Body: $body, Deeplink: $deeplink") + + if (title != null && body != null) { + sendNotification(title, body, imageUrl, deeplink) + } else if (data.isNotEmpty()) { + handleNow() + } + + // Also if you intend on generating your own notificatisons as a result of a received FCM + // message, here is where that should be initiated. See sendNotification method below. + + } + + private fun handleNow() { + Log.d(TAG, "Short lived task is done.") + } + + private fun sendNotification( + title: String?, + messageBody: String, + imageUrl: android.net.Uri?, + deeplink: String? = null, + ) { + val intent = + if (!deeplink.isNullOrEmpty()) { + Intent(Intent.ACTION_VIEW, deeplink.toUri()).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK } + } else { + Intent(this, MainActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } + } + val requestCode = 0 + val pendingIntent = + PendingIntent.getActivity( + this, + requestCode, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + + val channelId = "gallery_high_priority_push_channel" + val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + val notificationBuilder = + NotificationCompat.Builder(this, channelId) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(title ?: getString(R.string.gallery_news_notification_title)) + .setContentText(messageBody) + .setAutoCancel(true) + .setSound(defaultSoundUri) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_HIGH) + + if (imageUrl != null) { + try { + val url = java.net.URL(imageUrl.toString()) + val connection = url.openConnection() + connection.connectTimeout = 5000 + connection.readTimeout = 5000 + val bitmap = android.graphics.BitmapFactory.decodeStream(connection.getInputStream()) + if (bitmap != null) { + notificationBuilder.setLargeIcon(bitmap) + notificationBuilder.setStyle( + NotificationCompat.BigPictureStyle() + .bigPicture(bitmap) + .bigLargeIcon(null as android.graphics.Bitmap?) + ) + } + } catch (e: Exception) { + Log.w(TAG, "Failed to download image", e) + } + } + + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + // Since android Oreo notification channel is needed. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = + NotificationChannel( + channelId, + getString(R.string.gallery_news_notification_title), + NotificationManager.IMPORTANCE_HIGH, + ) + notificationManager.createNotificationChannel(channel) + } + + val notificationId = 0 + notificationManager.notify(notificationId, notificationBuilder.build()) + } + + companion object { + private const val TAG = "AGFcmMessagingService" + } +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApp.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApp.kt new file mode 100644 index 000000000..1529173b2 --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApp.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery + +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.google.ai.edge.gallery.ui.modelmanager.ModelManagerViewModel +import com.google.ai.edge.gallery.ui.navigation.GalleryNavHost + +/** Top level composable representing the main screen of the application. */ +@Composable +fun GalleryApp( + navController: NavHostController = rememberNavController(), + modelManagerViewModel: ModelManagerViewModel, +) { + GalleryNavHost(navController = navController, modelManagerViewModel = modelManagerViewModel) +} diff --git a/Android/src/app/src/main/java/com/google/aiedge/gallery/GalleryApp.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryAppTopBar.kt similarity index 57% rename from Android/src/app/src/main/java/com/google/aiedge/gallery/GalleryApp.kt rename to Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryAppTopBar.kt index 4cd0fa8a7..4cceaf382 100644 --- a/Android/src/app/src/main/java/com/google/aiedge/gallery/GalleryApp.kt +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryAppTopBar.kt @@ -16,19 +16,19 @@ @file:OptIn(ExperimentalMaterial3Api::class) -package com.google.aiedge.gallery +package com.google.ai.edge.gallery import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material.icons.rounded.Menu import androidx.compose.material.icons.rounded.Settings import androidx.compose.material3.CenterAlignedTopAppBar -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -42,25 +42,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController -import com.google.aiedge.gallery.data.AppBarAction -import com.google.aiedge.gallery.data.AppBarActionType -import com.google.aiedge.gallery.ui.navigation.GalleryNavHost +import androidx.compose.ui.unit.sp +import com.google.ai.edge.gallery.data.AppBarAction +import com.google.ai.edge.gallery.data.AppBarActionType -/** - * Top level composable representing the main screen of the application. - */ -@Composable -fun GalleryApp(navController: NavHostController = rememberNavController()) { - GalleryNavHost(navController = navController) -} - -/** - * The top app bar. - */ +/** The top app bar. */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun GalleryTopAppBar( title: String, @@ -68,34 +56,38 @@ fun GalleryTopAppBar( leftAction: AppBarAction? = null, rightAction: AppBarAction? = null, scrollBehavior: TopAppBarScrollBehavior? = null, - loadingHfModels: Boolean = false, subtitle: String = "", ) { + val titleColor = MaterialTheme.colorScheme.onSurface CenterAlignedTopAppBar( title = { Column(horizontalAlignment = Alignment.CenterHorizontally) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { if (title == stringResource(R.string.app_name)) { Icon( painterResource(R.drawable.logo), modifier = Modifier.size(20.dp), - contentDescription = "", + contentDescription = null, tint = Color.Unspecified, ) } - Text( - title, - style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold) + BasicText( + text = title, + maxLines = 1, + color = { titleColor }, + style = MaterialTheme.typography.titleMedium, + autoSize = + TextAutoSize.StepBased(minFontSize = 14.sp, maxFontSize = 16.sp, stepSize = 1.sp), ) } if (subtitle.isNotEmpty()) { Text( subtitle, style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.secondary + color = MaterialTheme.colorScheme.secondary, ) } } @@ -109,31 +101,19 @@ fun GalleryTopAppBar( IconButton(onClick = leftAction.actionFn) { Icon( imageVector = Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = "", + contentDescription = stringResource(R.string.cd_navigate_back_icon), ) } } - - AppBarActionType.REFRESH_MODELS -> { + AppBarActionType.MENU -> { IconButton(onClick = leftAction.actionFn) { Icon( - imageVector = Icons.Rounded.Refresh, - contentDescription = "", - tint = MaterialTheme.colorScheme.secondary + imageVector = Icons.Rounded.Menu, + contentDescription = stringResource(R.string.cd_menu), ) } } - AppBarActionType.REFRESHING_MODELS -> { - CircularProgressIndicator( - trackColor = MaterialTheme.colorScheme.surfaceContainerHighest, - strokeWidth = 3.dp, - modifier = Modifier - .padding(start = 16.dp) - .size(20.dp) - ) - } - else -> {} } }, @@ -145,47 +125,19 @@ fun GalleryTopAppBar( IconButton(onClick = rightAction.actionFn) { Icon( imageVector = Icons.Rounded.Settings, - contentDescription = "", - tint = MaterialTheme.colorScheme.primary + contentDescription = stringResource(R.string.cd_app_settings_icon), + tint = MaterialTheme.colorScheme.onSurface, ) } } - // Click an icon to open "download manager". - AppBarActionType.DOWNLOAD_MANAGER -> { - if (loadingHfModels) { - CircularProgressIndicator( - trackColor = MaterialTheme.colorScheme.surfaceContainerHighest, - strokeWidth = 3.dp, - modifier = Modifier - .padding(end = 12.dp) - .size(20.dp) - ) - } -// else { -// IconButton(onClick = rightAction.actionFn) { -// Icon( -// imageVector = Deployed_code, -// contentDescription = "", -// tint = MaterialTheme.colorScheme.primary -// ) -// } -// } - } - - AppBarActionType.MODEL_SELECTOR -> { - Text("ms") - } - // Click a button to navigate up. AppBarActionType.NAVIGATE_UP -> { - TextButton(onClick = rightAction.actionFn) { - Text("Done") - } + TextButton(onClick = rightAction.actionFn) { Text("Done") } } else -> {} } - } + }, ) -} \ No newline at end of file +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApplication.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApplication.kt new file mode 100644 index 000000000..1ca3752ef --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryApplication.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery + +import android.app.Application +import com.google.ai.edge.gallery.data.DataStoreRepository +import com.google.ai.edge.gallery.notifications.NotificationScheduleManager +import com.google.ai.edge.gallery.ui.theme.ThemeSettings +import com.google.firebase.FirebaseApp +import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject + +@HiltAndroidApp +class GalleryApplication : Application() { + + @Inject lateinit var dataStoreRepository: DataStoreRepository + @Inject lateinit var notificationScheduleManager: NotificationScheduleManager + + override fun onCreate() { + super.onCreate() + // Initialize the notification schedule manager to load the scheduled notifications from the + // disk. + notificationScheduleManager.initialize() + + // Load saved theme. + ThemeSettings.themeOverride.value = dataStoreRepository.readTheme() + + FirebaseApp.initializeApp(this) + } +} diff --git a/Android/src/app/src/test/java/com/google/aiedge/gallery/ExampleUnitTest.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryLifecycleProvider.kt similarity index 63% rename from Android/src/app/src/test/java/com/google/aiedge/gallery/ExampleUnitTest.kt rename to Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryLifecycleProvider.kt index 1e8d62b39..ebf56a68a 100644 --- a/Android/src/app/src/test/java/com/google/aiedge/gallery/ExampleUnitTest.kt +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/GalleryLifecycleProvider.kt @@ -14,20 +14,18 @@ * limitations under the License. */ -package com.google.aiedge.gallery +package com.google.ai.edge.gallery -import org.junit.Test +interface AppLifecycleProvider { + var isAppInForeground: Boolean +} -import org.junit.Assert.* +class GalleryLifecycleProvider : AppLifecycleProvider { + private var _isAppInForeground = false -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) + override var isAppInForeground: Boolean + get() = _isAppInForeground + set(value) { + _isAppInForeground = value } -} \ No newline at end of file +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/MainActivity.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/MainActivity.kt new file mode 100644 index 000000000..68893f56c --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/MainActivity.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery + +import android.animation.ObjectAnimator +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.View +import android.view.WindowManager +import android.view.animation.DecelerateInterpolator +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.core.animation.doOnEnd +import androidx.core.net.toUri +import androidx.core.os.bundleOf +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.lifecycleScope +import com.google.ai.edge.gallery.ui.modelmanager.ModelManagerViewModel +import com.google.ai.edge.gallery.ui.theme.GalleryTheme +import com.google.ai.edge.litertlm.ExperimentalApi +import com.google.ai.edge.litertlm.ExperimentalFlags +import com.google.firebase.analytics.FirebaseAnalytics +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + + private val modelManagerViewModel: ModelManagerViewModel by viewModels() + private var splashScreenAboutToExit: Boolean = false + private var contentSet: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + // We intentionally pass null to discard the saved instance state bundle. + // This prevents Jetpack Compose from automatically restoring the previous screen + // and forces the app to start cleanly on the Home Screen after an OS kill. + super.onCreate(null) + + // Debug: Dump all intent extras to see what FCM unloads + intent.extras?.let { extras -> + for (key in extras.keySet()) { + Log.d(TAG, "onCreate Extra -> Key: $key, Value: ${extras.get(key)}") + } + } + + // Convert FCM Console data extras to intent data for GalleryNavGraph to pick up + intent.getStringExtra("deeplink")?.let { link -> + Log.d(TAG, "onCreate: Found deeplink extra: $link") + if (link.startsWith("http://") || link.startsWith("https://")) { + val browserIntent = Intent(Intent.ACTION_VIEW, link.toUri()) + startActivity(browserIntent) + } else { + intent.data = link.toUri() + } + } + + fun setContent() { + if (contentSet) { + return + } + + setContent { + GalleryTheme { + Surface(modifier = Modifier.fillMaxSize()) { + GalleryApp(modelManagerViewModel = modelManagerViewModel) + + // Fade out a "mask" that has the same color as the background of the splash screen + // to reveal the actual app content. + var startMaskFadeout by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { startMaskFadeout = true } + AnimatedVisibility( + !startMaskFadeout, + enter = fadeIn(animationSpec = snap(0)), + exit = + fadeOut(animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing)), + ) { + Box( + modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background) + ) + } + } + } + } + + @OptIn(ExperimentalApi::class) + ExperimentalFlags.enableBenchmark = false + + contentSet = true + } + + modelManagerViewModel.loadModelAllowlist() + + // Show splash screen. + val splashScreen = installSplashScreen() + + // Set the content when the system-provided splash screen is not shown. + // + // This is necessary on some Android versions where the splash screen is optimized away (e.g., + // after a force-quit) to ensure the main content is displayed immediately and correctly. + lifecycleScope.launch { + delay(1000) + if (!splashScreenAboutToExit) { + setContent() + } + } + + // Cross-fade transition from the splash screen to the main content. + // + // The logic performs the following key actions: + // 1. Synchronizes Timing: It calculates the remaining duration of the default icon + // animation. It then delays its own animations to ensure the custom fade-out begins just + // before the original icon animation would have finished. + // 2. Initiates a cross-fade: + // - Fade out the splash screen. + // - Fade in the main content. + // 3. Cleans up: An `onEnd` listener on the fade-out animator calls + // `splashScreenView.remove()` to properly remove the splash screen from the view hierarchy + // once it's fully transparent. + splashScreen.setOnExitAnimationListener { splashScreenView -> + splashScreenAboutToExit = true + + val now = System.currentTimeMillis() + val iconAnimationStartMs = splashScreenView.iconAnimationStartMillis + val duration = splashScreenView.iconAnimationDurationMillis + val fadeOut = ObjectAnimator.ofFloat(splashScreenView.view, View.ALPHA, 1f, 0f) + fadeOut.interpolator = DecelerateInterpolator() + fadeOut.duration = 300L + fadeOut.doOnEnd { splashScreenView.remove() } + lifecycleScope.launch { + val setContentDelay = duration - (now - iconAnimationStartMs) - 300 + if (setContentDelay > 0) { + delay(setContentDelay) + } + setContent() + fadeOut.start() + } + } + + enableEdgeToEdge() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Fix for three-button nav not properly going edge-to-edge. + // See: https://issuetracker.google.com/issues/298296168 + window.isNavigationBarContrastEnforced = false + } + // Keep the screen on while the app is running for better demo experience. + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + + // Debug: Dump all intent extras to see what FCM unloads + intent.extras?.let { extras -> + for (key in extras.keySet()) { + Log.d(TAG, "onNewIntent Extra -> Key: $key, Value: ${extras.get(key)}") + } + } + + intent.getStringExtra("deeplink")?.let { link -> + Log.d(TAG, "onNewIntent: Found deeplink extra: $link") + if (link.startsWith("http://") || link.startsWith("https://")) { + val browserIntent = Intent(Intent.ACTION_VIEW, link.toUri()) + startActivity(browserIntent) + } else { + intent.data = link.toUri() + } + } + } + + override fun onResume() { + super.onResume() + + firebaseAnalytics?.logEvent( + FirebaseAnalytics.Event.APP_OPEN, + bundleOf( + "app_version" to BuildConfig.VERSION_NAME, + "os_version" to Build.VERSION.SDK_INT.toString(), + "device_model" to Build.MODEL, + ), + ) + } + + companion object { + private const val TAG = "AGMainActivity" + } +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/SettingsSerializer.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/SettingsSerializer.kt new file mode 100644 index 000000000..c7381f1dd --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/SettingsSerializer.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery + +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import com.google.ai.edge.gallery.proto.Settings +import com.google.protobuf.InvalidProtocolBufferException +import java.io.InputStream +import java.io.OutputStream + +object SettingsSerializer : Serializer { + override val defaultValue: Settings = Settings.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): Settings { + try { + return Settings.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + } + + override suspend fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output) +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/SkillsSerializer.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/SkillsSerializer.kt new file mode 100644 index 000000000..82c29cbc8 --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/SkillsSerializer.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery + +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import com.google.ai.edge.gallery.proto.Skills +import com.google.protobuf.InvalidProtocolBufferException +import java.io.InputStream +import java.io.OutputStream + +/** Serializes and deserializes [Skills] proto messages for use with ProtoDataStore. */ +object SkillsSerializer : Serializer { + override val defaultValue: Skills = Skills.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): Skills { + try { + return Skills.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + } + + override suspend fun writeTo(t: Skills, output: OutputStream) = t.writeTo(output) +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/UserDataSerializer.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/UserDataSerializer.kt new file mode 100644 index 000000000..f594b519b --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/UserDataSerializer.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery + +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import com.google.ai.edge.gallery.proto.UserData +import com.google.protobuf.InvalidProtocolBufferException +import java.io.InputStream +import java.io.OutputStream + +object UserDataSerializer : Serializer { + override val defaultValue: UserData = UserData.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): UserData { + try { + return UserData.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + } + + override suspend fun writeTo(t: UserData, output: OutputStream) = t.writeTo(output) +} diff --git a/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/common/AuthConfig.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/common/ProjectConfig.kt similarity index 72% rename from Android/src/app/src/main/java/com/google/aiedge/gallery/ui/common/AuthConfig.kt rename to Android/src/app/src/main/java/com/google/ai/edge/gallery/common/ProjectConfig.kt index 737bcbbcd..4fe03260f 100644 --- a/Android/src/app/src/main/java/com/google/aiedge/gallery/ui/common/AuthConfig.kt +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/common/ProjectConfig.kt @@ -14,28 +14,31 @@ * limitations under the License. */ -package com.google.aiedge.gallery.ui.common +package com.google.ai.edge.gallery.common -import android.net.Uri +import androidx.core.net.toUri import net.openid.appauth.AuthorizationServiceConfiguration -object AuthConfig { +object ProjectConfig { // Hugging Face Client ID. - const val clientId = "88a0ac25-fcf4-467b-b8cd-ebcc2aec9bd0" + // + const val clientId = "REPLACE_WITH_YOUR_CLIENT_ID_IN_HUGGINGFACE_APP" + // Registered redirect URI. // // The scheme needs to match the // "android.defaultConfig.manifestPlaceholders["appAuthRedirectScheme"]" field in // "build.gradle.kts". - const val redirectUri = "com.google.aiedge.gallery.oauth://oauthredirect" + const val redirectUri = "REPLACE_WITH_YOUR_REDIRECT_URI_IN_HUGGINGFACE_APP" // OAuth 2.0 Endpoints (Authorization + Token Exchange) private const val authEndpoint = "https://huggingface.co/oauth/authorize" private const val tokenEndpoint = "https://huggingface.co/oauth/token" // OAuth service configuration (AppAuth library requires this) - val authServiceConfig = AuthorizationServiceConfiguration( - Uri.parse(authEndpoint), // Authorization endpoint - Uri.parse(tokenEndpoint) // Token exchange endpoint - ) -} \ No newline at end of file + val authServiceConfig = + AuthorizationServiceConfiguration( + authEndpoint.toUri(), // Authorization endpoint + tokenEndpoint.toUri(), // Token exchange endpoint + ) +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/common/SystemPromptHelper.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/common/SystemPromptHelper.kt new file mode 100644 index 000000000..121b03e48 --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/common/SystemPromptHelper.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery.common + +import com.google.ai.edge.gallery.data.SystemPromptRepository +import com.google.ai.edge.gallery.data.Task +import kotlinx.coroutines.flow.firstOrNull + +/** Helper object for system prompt retrieval and compilation. */ +object SystemPromptHelper { + + /** + * Retrieves the effective system prompt for the given [Task]. + * + * Returns the user-defined custom prompt from the [SystemPromptRepository] if available; + * otherwise, falls back to the task's default system prompt. + * + * @param repo The optional [SystemPromptRepository] for custom overrides. If null, returns the + * default. + * @param task The target [Task] containing the identifier and the default fallback system prompt. + * @return A [String] representing the effective system prompt instructions. + */ + suspend fun getEffectiveSystemPrompt(repo: SystemPromptRepository?, task: Task): String { + if (repo == null) return task.defaultSystemPrompt + val customPrompt = repo.getCustomSystemPrompt(task.id).firstOrNull() + return customPrompt ?: task.defaultSystemPrompt + } +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/common/Types.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/common/Types.kt new file mode 100644 index 000000000..4abfd31f8 --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/common/Types.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery.common + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import com.squareup.moshi.JsonClass +import kotlinx.coroutines.CompletableDeferred + +interface LatencyProvider { + val latencyMs: Float +} + +data class Classification(val label: String, val score: Float, val color: Color) + +data class JsonObjAndTextContent(val jsonObj: T, val textContent: String) + +class AudioClip(val audioData: ByteArray, val sampleRate: Int) + +open class AgentAction(val name: AgentActionName) + +class CallJsAgentAction( + val url: String, + val data: String, + val secret: String = "", + val result: CompletableDeferred = CompletableDeferred(), +) : AgentAction(name = AgentActionName.CALL_JS_SKILL) {} + +class AskInfoAgentAction( + val dialogTitle: String, + val fieldLabel: String, + val result: CompletableDeferred = CompletableDeferred(), +) : AgentAction(name = AgentActionName.ASK_INFO) + +class SkillProgressAgentAction( + val label: String, + val inProgress: Boolean, + val addItemTitle: String = "", + val addItemDescription: String = "", + val customData: Any? = null, +) : AgentAction(name = AgentActionName.SKILL_PROGRESS) + +enum class AgentActionName() { + CALL_JS_SKILL, + SKILL_PROGRESS, + ASK_INFO, +} + +data class SkillTryOutChip( + val icon: ImageVector, + val label: String, + val prompt: String, + val skillName: String, +) + +data class SkillInfo( + val skillMd: String, + val skillUrl: String? = null, + val tryoutChip: SkillTryOutChip? = null, +) + +data class SkillsIndex(val skills: List) + +@JsonClass(generateAdapter = true) +data class CallJsSkillResult( + val result: String?, + val error: String?, + val image: CallJsSkillResultImage?, + val webview: CallJsSkillResultWebview?, +) + +@JsonClass(generateAdapter = true) data class CallJsSkillResultImage(val base64: String?) + +@JsonClass(generateAdapter = true) +data class CallJsSkillResultWebview( + val url: String?, + val iframe: Boolean?, + // width/height. + // + // In the app the webview always takes the full width of the screen. This value is used to + // calculate the height of the webview. Default is 4:3. + val aspectRatio: Float?, +) diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/common/Utils.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/common/Utils.kt new file mode 100644 index 000000000..d09533a1a --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/common/Utils.kt @@ -0,0 +1,394 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery.common + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.ime +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.focus.onFocusEvent +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.exifinterface.media.ExifInterface +import com.google.ai.edge.gallery.GalleryEvent +import com.google.ai.edge.gallery.data.SAMPLE_RATE +import com.google.ai.edge.gallery.firebaseAnalytics +import com.google.gson.Gson +import java.io.File +import java.io.FileInputStream +import java.net.HttpURLConnection +import java.net.URL +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.channels.FileChannel +import kotlin.math.abs +import kotlin.math.floor +import kotlin.math.max +import kotlin.math.roundToInt + +private const val TAG = "AGUtils" + +const val LOCAL_URL_BASE = "https://appassets.androidplatform.net" + +fun cleanUpMediapipeTaskErrorMessage(message: String): String { + val index = message.indexOf("=== Source Location Trace") + if (index >= 0) { + return message.substring(0, index) + } + return message +} + +fun processLlmResponse(response: String): String { + return response.replace("\\n", "\n") +} + +inline fun getJsonResponse(url: String): JsonObjAndTextContent? { + try { + val connection = URL(url).openConnection() as HttpURLConnection + connection.requestMethod = "GET" + connection.connect() + + val responseCode = connection.responseCode + if (responseCode == HttpURLConnection.HTTP_OK) { + val inputStream = connection.inputStream + val response = inputStream.bufferedReader().use { it.readText() } + + val jsonObj = parseJson(response) + return if (jsonObj != null) { + JsonObjAndTextContent(jsonObj = jsonObj, textContent = response) + } else { + null + } + } else { + Log.e("AGUtils", "HTTP error: $responseCode") + } + } catch (e: Exception) { + Log.e("AGUtils", "Error when getting or parsing json response", e) + } + + return null +} + +/** Parses a JSON string into an object of type [T] using Gson. */ +inline fun parseJson(response: String): T? { + return try { + val gson = Gson() + gson.fromJson(response, T::class.java) + } catch (e: Exception) { + Log.e("AGUtils", "Error parsing JSON string", e) + null + } +} + +fun convertWavToMonoWithMaxSeconds( + context: Context, + stereoUri: Uri, + maxSeconds: Int = 30, +): AudioClip? { + Log.d(TAG, "Start to convert wav file to mono channel") + + try { + val inputStream = + (if (stereoUri.scheme == null || stereoUri.scheme == "file") { + FileInputStream(stereoUri.path ?: "") + } else { + context.contentResolver.openInputStream(stereoUri) + }) ?: return null + val originalBytes = inputStream.readBytes() + inputStream.close() + + // Read WAV header + if (originalBytes.size < 44) { + // Not a valid WAV file + Log.e(TAG, "Not a valid wav file") + return null + } + + val headerBuffer = ByteBuffer.wrap(originalBytes, 0, 44).order(ByteOrder.LITTLE_ENDIAN) + val channels = headerBuffer.getShort(22) + var sampleRate = headerBuffer.getInt(24) + val bitDepth = headerBuffer.getShort(34) + Log.d(TAG, "File metadata: channels: $channels, sampleRate: $sampleRate, bitDepth: $bitDepth") + + // Normalize audio to 16-bit. + val audioDataBytes = originalBytes.copyOfRange(fromIndex = 44, toIndex = originalBytes.size) + var sixteenBitBytes: ByteArray = + if (bitDepth.toInt() == 8) { + Log.d(TAG, "Converting 8-bit audio to 16-bit.") + convert8BitTo16Bit(audioDataBytes) + } else { + // Assume 16-bit or other format that can be handled directly + audioDataBytes + } + + // Convert byte array to short array for processing + val shortBuffer = + ByteBuffer.wrap(sixteenBitBytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer() + var pcmSamples = ShortArray(shortBuffer.remaining()) + shortBuffer.get(pcmSamples) + + // Resample if sample rate is less than 16000 Hz --- + if (sampleRate < SAMPLE_RATE) { + Log.d(TAG, "Resampling from $sampleRate Hz to $SAMPLE_RATE Hz.") + pcmSamples = resample(pcmSamples, sampleRate, SAMPLE_RATE, channels.toInt()) + sampleRate = SAMPLE_RATE + Log.d(TAG, "Resampling complete. New sample count: ${pcmSamples.size}") + } + + // Convert stereo to mono if necessary + var monoSamples = + if (channels.toInt() == 2) { + Log.d(TAG, "Converting stereo to mono.") + val mono = ShortArray(pcmSamples.size / 2) + for (i in mono.indices) { + val left = pcmSamples[i * 2] + val right = pcmSamples[i * 2 + 1] + mono[i] = ((left + right) / 2).toShort() + } + mono + } else { + Log.d(TAG, "Audio is already mono. No channel conversion needed.") + pcmSamples + } + + // Trim the audio to maxSeconds --- + val maxSamples = maxSeconds * sampleRate + if (monoSamples.size > maxSamples) { + Log.d(TAG, "Trimming clip from ${monoSamples.size} samples to $maxSamples samples.") + monoSamples = monoSamples.copyOfRange(0, maxSamples) + } + + val monoByteBuffer = ByteBuffer.allocate(monoSamples.size * 2).order(ByteOrder.LITTLE_ENDIAN) + monoByteBuffer.asShortBuffer().put(monoSamples) + return AudioClip(audioData = monoByteBuffer.array(), sampleRate = sampleRate) + } catch (e: Exception) { + Log.e(TAG, "Failed to convert wav to mono", e) + return null + } +} + +/** Converts 8-bit unsigned PCM audio data to 16-bit signed PCM. */ +private fun convert8BitTo16Bit(eightBitData: ByteArray): ByteArray { + // The new 16-bit data will be twice the size + val sixteenBitData = ByteArray(eightBitData.size * 2) + val buffer = ByteBuffer.wrap(sixteenBitData).order(ByteOrder.LITTLE_ENDIAN) + + for (byte in eightBitData) { + // Convert the unsigned 8-bit byte (0-255) to a signed 16-bit short (-32768 to 32767) + // 1. Get the unsigned value by masking with 0xFF + // 2. Subtract 128 to center the waveform around 0 (range becomes -128 to 127) + // 3. Scale by 256 to expand to the 16-bit range + val unsignedByte = byte.toInt() and 0xFF + val sixteenBitSample = ((unsignedByte - 128) * 256).toShort() + buffer.putShort(sixteenBitSample) + } + return sixteenBitData +} + +/** Resamples PCM audio data from an original sample rate to a target sample rate. */ +private fun resample( + inputSamples: ShortArray, + originalSampleRate: Int, + targetSampleRate: Int, + channels: Int, +): ShortArray { + if (originalSampleRate == targetSampleRate) { + return inputSamples + } + + val ratio = targetSampleRate.toDouble() / originalSampleRate + val outputLength = (inputSamples.size * ratio).toInt() + val resampledData = ShortArray(outputLength) + + if (channels == 1) { // Mono + for (i in resampledData.indices) { + val position = i / ratio + val index1 = floor(position).toInt() + val index2 = index1 + 1 + val fraction = position - index1 + + val sample1 = if (index1 < inputSamples.size) inputSamples[index1].toDouble() else 0.0 + val sample2 = if (index2 < inputSamples.size) inputSamples[index2].toDouble() else 0.0 + + resampledData[i] = (sample1 * (1 - fraction) + sample2 * fraction).toInt().toShort() + } + } + + return resampledData +} + +fun calculatePeakAmplitude(buffer: ByteArray, bytesRead: Int): Int { + // Wrap the byte array in a ByteBuffer and set the order to little-endian + val shortBuffer = + ByteBuffer.wrap(buffer, 0, bytesRead).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer() + + var maxAmplitude = 0 + // Iterate through the short buffer to find the maximum absolute value + while (shortBuffer.hasRemaining()) { + val currentSample = abs(shortBuffer.get().toInt()) + if (currentSample > maxAmplitude) { + maxAmplitude = currentSample + } + } + return maxAmplitude +} + +fun decodeSampledBitmapFromUri(context: Context, uri: Uri, reqWidth: Int, reqHeight: Int): Bitmap? { + // First, decode with inJustDecodeBounds=true to check dimensions + val options = + BitmapFactory.Options().apply { + inJustDecodeBounds = true + (if (uri.scheme == null || uri.scheme == "file") { + FileInputStream(uri.path ?: "") + } else { + context.contentResolver.openInputStream(uri) + }) + ?.use { BitmapFactory.decodeStream(it, null, this) } + + // Calculate inSampleSize + inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight) + + // Decode bitmap with inSampleSize set + inJustDecodeBounds = false + } + + return (if (uri.scheme == null || uri.scheme == "file") { + FileInputStream(uri.path ?: "") + } else { + context.contentResolver.openInputStream(uri) + }) + ?.use { BitmapFactory.decodeStream(it, null, options) } +} + +fun rotateBitmap(bitmap: Bitmap, orientation: Int): Bitmap { + val matrix = Matrix() + when (orientation) { + ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f) + ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f) + ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f) + ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.preScale(-1.0f, 1.0f) + ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1.0f, -1.0f) + ExifInterface.ORIENTATION_TRANSPOSE -> { + matrix.postRotate(90f) + matrix.preScale(-1.0f, 1.0f) + } + ExifInterface.ORIENTATION_TRANSVERSE -> { + matrix.postRotate(270f) + matrix.preScale(-1.0f, 1.0f) + } + ExifInterface.ORIENTATION_NORMAL -> return bitmap + else -> return bitmap + } + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) +} + +private fun calculateInSampleSize( + options: BitmapFactory.Options, + reqWidth: Int, + reqHeight: Int, +): Int { + // Raw height and width of image + val height: Int = options.outHeight + val width: Int = options.outWidth + var inSampleSize = 1 + + if (height > reqHeight || width > reqWidth) { + // Calculate the ratio of height and width to the requested height and width + val heightRatio = (height.toFloat() / reqHeight.toFloat()).roundToInt() + val widthRatio = (width.toFloat() / reqWidth.toFloat()).roundToInt() + + // Choose the largest ratio as inSampleSize value to ensure + // that both dimensions are smaller than or equal to the requested dimensions. + inSampleSize = max(heightRatio, widthRatio) + } + + return inSampleSize +} + +fun readFileToByteBuffer(file: File): ByteBuffer? { + return try { + val fileInputStream = FileInputStream(file) + val fileChannel: FileChannel = fileInputStream.channel + val byteBuffer = ByteBuffer.allocateDirect(fileChannel.size().toInt()) + fileChannel.read(byteBuffer) + byteBuffer.rewind() + fileInputStream.close() + byteBuffer + } catch (e: Exception) { + e.printStackTrace() + null + } +} + +fun isPixel10(): Boolean { + return Build.MODEL != null && Build.MODEL.lowercase().contains("pixel 10") +} + +fun Modifier.clearFocusOnKeyboardDismiss(): Modifier = composed { + var isFocused by remember { mutableStateOf(false) } + var keyboardAppearedSinceLastFocused by remember { mutableStateOf(false) } + + if (isFocused) { + val imeIsVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0 + val focusManager = LocalFocusManager.current + + LaunchedEffect(imeIsVisible) { + if (imeIsVisible) { + keyboardAppearedSinceLastFocused = true + } else if (keyboardAppearedSinceLastFocused) { + focusManager.clearFocus() + } + } + } + + onFocusEvent { + if (isFocused != it.isFocused) { + isFocused = it.isFocused + if (isFocused) keyboardAppearedSinceLastFocused = false + } + } +} + +fun isAICoreSupported(allowedDeviceModels: Set?): Boolean { + if (allowedDeviceModels.isNullOrEmpty()) return false + val currentModel = Build.MODEL?.lowercase() ?: return false + return allowedDeviceModels.contains(currentModel) +} + +fun logErrorToFirebase(event: GalleryEvent, errorType: String, errorMessage: String?) { + firebaseAnalytics?.logEvent( + event.id, + Bundle().apply { + putBoolean("success", false) + putString("error_type", errorType) + putString("error_message", errorMessage ?: "Unknown error") + }, + ) +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddOrEditSkillBottomSheet.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddOrEditSkillBottomSheet.kt new file mode 100644 index 000000000..14b97d8a0 --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddOrEditSkillBottomSheet.kt @@ -0,0 +1,730 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery.customtasks.agentchat + +import android.util.Log +import androidx.compose.foundation.LocalOverscrollFactory +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ListAlt +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.AutoAwesome +import androidx.compose.material.icons.outlined.ContentPaste +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.PrimaryTabRow +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.google.ai.edge.gallery.R +import com.google.ai.edge.gallery.ui.common.CursorTrackingTextField +import com.google.ai.edge.gallery.ui.theme.customColors +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +private const val TAG = "AGAddOrEditSkill" +private const val DEFAULT_SCRIPT_NAME = "index.html" +private val TABS = listOf("Info", "Scripts") + +private val CALL_JS_INSTRUCTIONS_TEMPLATE = + """ + # Instructions + + Call the `run_js` tool with the following exact parameters: + + - data: A JSON string with the following fields: + - [fieldName]: [Data type, e.g. String, Number, Array] - [short description]. + - ... + """ + .trimIndent() + +private val INPUT_DATA_PLACEHOLDER = + """ + - [fieldName]: [Data type (String, Number, Array)] - [short description] + """ + .trimIndent() + +private val OUTPUT_DATA_PLACEHOLDER = + """ + - [fieldName]: [Data type (String, Number, Array)] - [short description] + """ + .trimIndent() + +/** A ModalBottomSheet Composable for creating a new skill from manual input. */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddOrEditSkillBottomSheet( + skillManagerViewModel: SkillManagerViewModel, + skillIndex: Int, + onDismiss: () -> Unit, + onSuccess: () -> Unit, +) { + val uiState by skillManagerViewModel.uiState.collectAsState() + var cancelClicked by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var skill by remember { mutableStateOf(uiState.skills.getOrNull(skillIndex)?.skill) } + var name by remember { mutableStateOf(skill?.name ?: "") } + var description by remember { mutableStateOf(skill?.description ?: "") } + var instructions by remember { mutableStateOf(skill?.instructions ?: "") } + var showErrorDialog by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf("") } + var edited by remember { mutableStateOf(false) } + var showDiscardDialog by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + + val curSkill = skill + val viewingMode = true + + var llmPromptGeneratorRequirements by remember { mutableStateOf(skill?.description ?: "") } + var llmPromptGeneratorInputData by remember { mutableStateOf(INPUT_DATA_PLACEHOLDER) } + var llmPromptGeneratorOutputData by remember { mutableStateOf(OUTPUT_DATA_PLACEHOLDER) } + + var scriptsLoading by remember { mutableStateOf(false) } + val scriptContents = remember { mutableStateMapOf() } + var selectedScript by remember { mutableStateOf(null) } + + LaunchedEffect(skill) { + val curSkill = skill + if (curSkill != null) { + Log.d(TAG, "Loading skill scripts...") + scriptsLoading = true + skillManagerViewModel.loadSkillScriptsContent(skill = curSkill) { loaded -> + scriptContents.clear() + scriptContents.putAll(loaded) + Log.d(TAG, "Loaded scripts: ${scriptContents.keys.joinToString(",")}") + scriptsLoading = false + selectedScript = + scriptContents.keys.firstOrNull { it == DEFAULT_SCRIPT_NAME } + ?: scriptContents.keys.firstOrNull() + } + } + } + + ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState, dragHandle = null) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) { + Column(modifier = Modifier.padding(top = 16.dp).fillMaxSize().imePadding()) { + // Title + Text( + // Viewing mode. + if (viewingMode) { + "View skill: ${curSkill?.name ?: ""}" + } + // Editing existing skill. + else if (skillIndex <= uiState.skills.size - 1) { + "Edit skill: ${curSkill?.name ?: ""}" + } + // Creating new skill. + else { + stringResource(R.string.add_skill_manual_input_sheet_title) + }, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + Spacer(modifier = Modifier.height(4.dp)) + + // Tab Bar + var selectedTabIndex by remember { mutableIntStateOf(0) } + if (!viewingMode) { + PrimaryTabRow( + selectedTabIndex = selectedTabIndex, + containerColor = Color.Transparent, + modifier = Modifier.padding(horizontal = 16.dp), + ) { + for (index in TABS.indices) { + val title = TABS[index] + Tab( + selected = selectedTabIndex == index, + onClick = { selectedTabIndex = index }, + text = { Text(title) }, + ) + } + } + } else { + Spacer(modifier = Modifier.height(8.dp)) + } + + // Tab Content + // + // Disable over-scroll "stretch" effect. + CompositionLocalProvider(LocalOverscrollFactory provides null) { + Column( + modifier = + Modifier.weight(1f).padding(horizontal = 16.dp).verticalScroll(rememberScrollState()) + ) { + when (selectedTabIndex) { + // Info tab. + 0 -> { + Column(modifier = Modifier.fillMaxHeight().padding(top = 16.dp)) { + // Name. + CursorTrackingTextField( + initialValue = name, + enabled = !viewingMode, + onValueChange = { + if (!edited) { + edited = name != it + } + name = it + }, + labelResId = R.string.name, + supportingTextResId = R.string.skill_name_input_description, + ) + + Spacer(modifier = Modifier.height(28.dp)) + + // Description. + CursorTrackingTextField( + labelResId = R.string.description_required, + supportingTextResId = R.string.skill_description_input_description, + minLines = 3, + enabled = !viewingMode, + initialValue = description, + onValueChange = { + if (!edited) { + edited = description != it + } + description = it + }, + ) + + Spacer(modifier = Modifier.height(28.dp)) + + // Instructions. + if (!viewingMode) { + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth(), + ) { + // A chip to apply call-js template. + AssistChip( + onClick = { + edited = true + instructions = CALL_JS_INSTRUCTIONS_TEMPLATE + }, + label = { Text(stringResource(R.string.use_call_js_template)) }, + leadingIcon = { + Icon( + Icons.AutoMirrored.Outlined.ListAlt, + contentDescription = null, + Modifier.size(AssistChipDefaults.IconSize), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + ) + } + } + CursorTrackingTextField( + labelResId = R.string.instructions, + supportingTextResId = R.string.skill_instructions_input_description, + minLines = 6, + enabled = !viewingMode, + initialValue = instructions, + onValueChange = { newText -> + if (!edited) { + edited = instructions != newText + } + instructions = newText + }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + } + } + // Script tab. + 1 -> { + if (scriptsLoading) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp) + } + } else { + ScriptsTabContent( + scope = scope, + scriptContents = scriptContents, + selectedScript = selectedScript, + onScriptSelected = { selectedScript = it }, + onAddDefaultScript = { + scriptContents[DEFAULT_SCRIPT_NAME] = "" + selectedScript = DEFAULT_SCRIPT_NAME + edited = true + }, + onScriptChanged = { name, content -> + if (!edited) { + edited = scriptContents[name] != content + } + scriptContents[name] = content + }, + onScriptAdded = { scriptName -> + scriptContents[scriptName] = "" + selectedScript = scriptName + edited = true + }, + onScriptDeleted = { scriptName -> + scriptContents.remove(key = scriptName) + if (selectedScript == scriptName) { + selectedScript = scriptContents.keys.firstOrNull() + } + skill?.let { curSkill -> + skillManagerViewModel.deleteSkillScript( + skill = curSkill, + scriptName = scriptName, + ) + } + edited = true + }, + modifier = Modifier.weight(1f), + curDescription = description, + requirements = llmPromptGeneratorRequirements, + onRequirementsChange = { llmPromptGeneratorRequirements = it }, + inputData = llmPromptGeneratorInputData, + onInputDataChange = { llmPromptGeneratorInputData = it }, + outputData = llmPromptGeneratorOutputData, + onOutputDataChange = { llmPromptGeneratorOutputData = it }, + snackbarHostState = snackbarHostState, + ) + } + } + } + } + } + + // Action Buttons + Row( + modifier = + Modifier.background(MaterialTheme.colorScheme.surfaceContainer) + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + if (viewingMode) { + Button( + onClick = { + cancelClicked = true + scope.launch { + sheetState.hide() + onDismiss() + } + } + ) { + Text(stringResource(R.string.ok)) + } + } else { + TextButton( + onClick = { + if (edited) { + showDiscardDialog = true + } else { + cancelClicked = true + scope.launch { + sheetState.hide() + onDismiss() + } + } + } + ) { + Text(stringResource(R.string.cancel)) + } + } + if (!viewingMode) { + Spacer(modifier = Modifier.width(8.dp)) + Button( + onClick = { + skillManagerViewModel.saveSkillEdit( + index = skillIndex, + name = name, + description = description, + instructions = instructions, + scriptsContent = scriptContents, + onError = { error -> + errorMessage = error + showErrorDialog = true + }, + onSuccess = { + onDismiss() + onSuccess() + }, + ) + }, + enabled = name.isNotEmpty() && description.isNotEmpty() && edited, + ) { + Text(stringResource(R.string.save)) + } + } + } + } + SnackbarHost(hostState = snackbarHostState, modifier = Modifier.padding(bottom = 40.dp)) + } + } + + if (showErrorDialog) { + AlertDialog( + onDismissRequest = { showErrorDialog = false }, + title = { Text(stringResource(R.string.failed_to_save)) }, + text = { Text(errorMessage) }, + confirmButton = { + Button(onClick = { showErrorDialog = false }) { Text(stringResource(R.string.ok)) } + }, + ) + } + + if (showDiscardDialog) { + AlertDialog( + onDismissRequest = { showDiscardDialog = false }, + title = { Text(stringResource(R.string.discard_changes_dialog_title)) }, + text = { Text(stringResource(R.string.discard_changes_dialog_content)) }, + confirmButton = { + Button( + onClick = { + cancelClicked = true + scope.launch { + sheetState.hide() + onDismiss() + } + showDiscardDialog = false + }, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.customColors.errorTextColor, + contentColor = Color.White, + ), + ) { + Text(stringResource(R.string.discard)) + } + }, + dismissButton = { + TextButton(onClick = { showDiscardDialog = false }) { + Text(stringResource(R.string.cancel)) + } + }, + ) + } +} + +/** Composable for the "Scripts" tab content. */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ScriptsTabContent( + scope: CoroutineScope, + scriptContents: Map, + selectedScript: String?, + onScriptSelected: (String?) -> Unit, + onAddDefaultScript: () -> Unit, + onScriptChanged: (name: String, content: String) -> Unit, + onScriptAdded: (scriptName: String) -> Unit, + onScriptDeleted: (scriptName: String) -> Unit, + curDescription: String, + requirements: String, + onRequirementsChange: (String) -> Unit, + inputData: String, + onInputDataChange: (String) -> Unit, + outputData: String, + onOutputDataChange: (String) -> Unit, + snackbarHostState: SnackbarHostState, + modifier: Modifier = Modifier, +) { + val scripts = scriptContents.keys.toList() + var scriptContent by remember { mutableStateOf(scriptContents[selectedScript] ?: "") } + var showAddScriptDialog by remember { mutableStateOf(false) } + var showDeleteConfirmation by remember { mutableStateOf(false) } + var showGenerateLlmPromptBottomSheet by remember { mutableStateOf(false) } + var newScriptName by remember { mutableStateOf("") } + val clipboard = LocalClipboard.current + + LaunchedEffect(selectedScript, scriptContents.toMap()) { + scriptContent = scriptContents[selectedScript] ?: "" + } + + // Show empty state if there are no scripts. + // + // A button to add the first script. + if (scriptContents.isEmpty()) { + Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + FilledTonalButton(onClick = onAddDefaultScript) { + Text(stringResource(R.string.add_default_script)) + } + } + } + // Show scripts tab content when there is at least one script. + else { + Column(modifier = Modifier.padding(vertical = 16.dp)) { + // Dropdown and Buttons + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + var expanded by remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + modifier = Modifier.weight(1f), + ) { + OutlinedTextField( + value = selectedScript ?: "", + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(R.string.select_script)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = + Modifier.menuAnchor(type = ExposedDropdownMenuAnchorType.PrimaryEditable) + .fillMaxWidth(), + ) + ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + for (script in scripts) { + DropdownMenuItem( + text = { Text(script) }, + onClick = { + onScriptSelected(script) + expanded = false + }, + ) + } + } + } + + Spacer(modifier = Modifier.width(8.dp)) + + // Add Button + IconButton(onClick = { showAddScriptDialog = true }) { + Icon(Icons.Outlined.Add, contentDescription = stringResource(R.string.cd_add_icon)) + } + + // Delete Button + IconButton(onClick = { showDeleteConfirmation = true }) { + Icon(Icons.Outlined.Delete, contentDescription = stringResource(R.string.cd_delete_icon)) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + // Button to help generate prompt for LLM. + FilledTonalButton( + onClick = { showGenerateLlmPromptBottomSheet = true }, + modifier = Modifier.height(32.dp), + contentPadding = BUTTON_CONTENT_PADDING, + ) { + Icon( + Icons.Outlined.AutoAwesome, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + Text( + stringResource(R.string.generate_llm_prompt_button_label), + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(start = 4.dp), + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + // Button to paste from clipboard. + FilledTonalButton( + onClick = { + scope.launch { + val clipEntry = clipboard.getClipEntry() + val pastedText = clipEntry?.clipData?.getItemAt(0)?.text?.toString() + + if (pastedText != null) { + selectedScript?.let { curSelectedScript -> + onScriptChanged(curSelectedScript, pastedText) + } + } + } + }, + modifier = Modifier.height(32.dp), + contentPadding = BUTTON_CONTENT_PADDING, + ) { + Icon( + Icons.Outlined.ContentPaste, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + Text( + stringResource(R.string.paste_from_clipboard), + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(start = 4.dp), + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Content Editor + CursorTrackingTextField( + minLines = 16, + initialValue = scriptContent, + onValueChange = { newContent -> + selectedScript?.let { curSelectedScript -> + onScriptChanged(curSelectedScript, newContent) + } + }, + monoFont = true, + ) + } + } + + // Add Script Dialog + if (showAddScriptDialog) { + AlertDialog( + onDismissRequest = { showAddScriptDialog = false }, + title = { Text(stringResource(R.string.add_script)) }, + text = { + OutlinedTextField( + value = newScriptName, + onValueChange = { newScriptName = it }, + label = { Text(stringResource(R.string.script_name)) }, + isError = scriptContents.containsKey(newScriptName), + supportingText = { + if (scriptContents.containsKey(newScriptName)) { + Text( + stringResource(R.string.duplicated_script_name), + color = MaterialTheme.colorScheme.error, + ) + } + }, + ) + }, + confirmButton = { + Button( + onClick = { + val trimmedName = newScriptName.trim() + onScriptAdded(trimmedName) + newScriptName = "" + showAddScriptDialog = false + }, + enabled = !scriptContents.containsKey(newScriptName) && newScriptName.isNotBlank(), + ) { + Text(stringResource(R.string.add)) + } + }, + dismissButton = { + TextButton(onClick = { showAddScriptDialog = false }) { + Text(stringResource(R.string.cancel)) + } + }, + ) + } + + // Delete Confirmation Dialog + if (showDeleteConfirmation) { + AlertDialog( + onDismissRequest = { showDeleteConfirmation = false }, + title = { Text(stringResource(R.string.delete_script_dialog_title)) }, + text = { Text("Are you sure you want to delete '$selectedScript'?") }, + confirmButton = { + Button( + onClick = { + selectedScript?.let { curSelectedScript -> onScriptDeleted(curSelectedScript) } + showDeleteConfirmation = false + }, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.customColors.errorTextColor, + contentColor = Color.White, + ), + ) { + Text(stringResource(R.string.delete)) + } + }, + dismissButton = { + TextButton(onClick = { showDeleteConfirmation = false }) { + Text(stringResource(R.string.cancel)) + } + }, + ) + } + + // Generate LLM Prompt Bottom Sheet + if (showGenerateLlmPromptBottomSheet) { + val promptCopiedMessage = stringResource(R.string.prompt_copied_message) + GenerateLlmPromptBottomSheet( + requirements = requirements, + curDescription = curDescription, + onRequirementsChange = onRequirementsChange, + inputData = inputData, + onInputDataChange = onInputDataChange, + outputData = outputData, + onOutputDataChange = onOutputDataChange, + onDismiss = { showGenerateLlmPromptBottomSheet = false }, + onLlmPromptGenerated = { + scope.launch { + snackbarHostState.showSnackbar( + message = promptCopiedMessage, + withDismissAction = true, + duration = SnackbarDuration.Long, + ) + } + }, + ) + } +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddSkillDisclaimerDialog.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddSkillDisclaimerDialog.kt new file mode 100644 index 000000000..041c24f4b --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddSkillDisclaimerDialog.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery.customtasks.agentchat + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.google.ai.edge.gallery.R + +@Composable +fun AddSkillDisclaimerDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.disclaimer_dialog_title)) }, + text = { Text(stringResource(R.string.disclaimer_dialog_content)) }, + confirmButton = { + Button(onClick = onConfirm) { Text(stringResource(R.string.disclaimer_dialog_agree)) } + }, + dismissButton = { + OutlinedButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } + }, + ) +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddSkillFromFeaturedListBottomSheet.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddSkillFromFeaturedListBottomSheet.kt new file mode 100644 index 000000000..e31a7878f --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddSkillFromFeaturedListBottomSheet.kt @@ -0,0 +1,389 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery.customtasks.agentchat + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.OpenInNew +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Cancel +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import com.google.ai.edge.gallery.R +import com.google.ai.edge.gallery.data.AllowedSkill +import kotlinx.coroutines.launch + +/** A ModalBottomSheet Composable for displaying and adding skills from a featured list. */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddSkillFromFeatureListBottomSheet( + skillManagerViewModel: SkillManagerViewModel, + onDismiss: () -> Unit, + onSkillAdded: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var searchQuery by remember { mutableStateOf("") } + var showDisclaimerDialog by remember { mutableStateOf(false) } + var skillToAdd by remember { mutableStateOf(null) } + var skillValidationErrors by remember { mutableStateOf(emptyMap()) } + var validatingSkills by remember { mutableStateOf(emptySet()) } + val uriHandler = LocalUriHandler.current + val scope = rememberCoroutineScope() + val uiState by skillManagerViewModel.uiState.collectAsState() + val addedSkillNames = remember(uiState.skills) { uiState.skills.map { it.skill.name }.toSet() } + + // Filter the featured skills based on the search query. + val filteredSkills = + remember(searchQuery, uiState.featuredSkills) { + val trimmedQuery = searchQuery.trim().lowercase() + if (trimmedQuery.isBlank()) { + // Clear errors when search query is empty. + skillValidationErrors = emptyMap() + uiState.featuredSkills + } else { + // Filter skills where the name or description contains the trimmed query. + uiState.featuredSkills.filter { skill -> + skill.name.lowercase().contains(trimmedQuery) || + skill.description.lowercase().contains(trimmedQuery) + } + } + } + + // Handles the action of adding a skill. + val handleAddSkill: (AllowedSkill) -> Unit = { skill -> + val url = skill.skillUrl + // Check if the skill's host is approved. + // + // If approved, start validation and add the skill. + if (isHostApproved(url)) { + validatingSkills = validatingSkills + url + skillManagerViewModel.validateAndAddSkillFromUrl( + url = url, + onSuccess = { + validatingSkills = validatingSkills - url + onDismiss() + onSkillAdded() + }, + onValidationError = { error -> + validatingSkills = validatingSkills - url + skillValidationErrors = skillValidationErrors + (url to error) + }, + ) + } + // If not approved, show a disclaimer dialog. + else { + skillToAdd = skill + showDisclaimerDialog = true + } + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ) { + Column(modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 16.dp)) { + // Header section with title, description, and close button. + Row( + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.featured_skills_title), + style = MaterialTheme.typography.titleLarge, + ) + Text( + stringResource(R.string.featured_skills_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + IconButton( + modifier = Modifier.padding(end = 3.dp), + onClick = { + scope.launch { + sheetState.hide() + onDismiss() + } + }, + ) { + Icon(Icons.Rounded.Close, contentDescription = stringResource(R.string.cd_close_icon)) + } + } + + // Display loading, error, or the skill list. + // + // Show a loading indicator while fetching the skill allowlist. + if (uiState.loadingSkillAllowlist) { + Box(modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp)) { + Row( + modifier = Modifier.align(Alignment.Center), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp) + Text( + stringResource(R.string.loading_skills_allowlist), + style = MaterialTheme.typography.bodyMedium, + ) + } + } + } + // Show an error message if fetching the allowlist failed. + else if (uiState.skillAllowlistError != null) { + Text( + text = uiState.skillAllowlistError!!, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(vertical = 16.dp), + ) + } else { + // Search bar for filtering skills. + TextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp, top = 8.dp), + shape = CircleShape, + placeholder = { Text(stringResource(R.string.search_skill)) }, + leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = null) }, + trailingIcon = { + // Show a clear button if the search query is not empty. + if (searchQuery.trim().isNotEmpty()) { + IconButton(onClick = { searchQuery = "" }) { + Icon(Icons.Outlined.Cancel, contentDescription = null) + } + } + }, + singleLine = true, + colors = + TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + ), + ) + + // LazyColumn to display the list of featured skills. + LazyColumn(verticalArrangement = Arrangement.spacedBy(12.dp)) { + items(filteredSkills) { skill -> + val validationError = skillValidationErrors[skill.skillUrl] + FeaturedSkillItem( + skill = skill, + uriHandler = uriHandler, + onAddClick = handleAddSkill, + validationError = validationError, + isAdding = validatingSkills.contains(skill.skillUrl), + isSkillAdded = addedSkillNames.contains(skill.name), + ) + } + } + } + } + } + + // Disclaimer dialog shown when adding a skill from an unapproved host. + if (showDisclaimerDialog) { + AddSkillDisclaimerDialog( + onDismiss = { + showDisclaimerDialog = false + skillToAdd = null + }, + onConfirm = { + // If confirmed, proceed with validation and adding the skill. + skillToAdd?.let { skill -> + val url = skill.skillUrl + validatingSkills = validatingSkills + url + skillManagerViewModel.validateAndAddSkillFromUrl( + url = url, + onSuccess = { + validatingSkills = validatingSkills - url + onDismiss() + onSkillAdded() + }, + onValidationError = { error -> + validatingSkills = validatingSkills - url + skillValidationErrors = skillValidationErrors + (url to error) + }, + ) + } + showDisclaimerDialog = false + skillToAdd = null + }, + ) + } +} + +/** Composable for displaying a single featured skill item in the list. */ +@Composable +private fun FeaturedSkillItem( + skill: AllowedSkill, + uriHandler: androidx.compose.ui.platform.UriHandler, + onAddClick: (AllowedSkill) -> Unit, + validationError: String? = null, + isAdding: Boolean = false, + isSkillAdded: Boolean = false, +) { + Row( + modifier = + Modifier.fillMaxWidth() + .clip(shape = RoundedCornerShape(20.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerLowest) + .padding(vertical = 12.dp) + .padding(start = 16.dp, end = 8.dp), + verticalAlignment = Alignment.Top, + ) { + Column(modifier = Modifier.weight(1f)) { + // Name + Text(skill.name, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium) + + // Attribution + skill.attributionLabel?.let { label -> + val hasUrl = !skill.attributionUrl.isNullOrBlank() + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.padding(top = 2.dp), + ) { + if (hasUrl) { + Text( + label, + style = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline, + ), + modifier = Modifier.clickable { skill.attributionUrl?.let { uriHandler.openUri(it) } }, + ) + Icon( + Icons.AutoMirrored.Outlined.OpenInNew, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } else { + Text( + label, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + + // Description + Text( + skill.description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 12.dp), + ) + + // Validation Error + validationError?.let { error -> + Text( + text = error, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(top = 8.dp), + ) + } + } + + // Add Button + Box(modifier = Modifier.padding(top = 4.dp).height(32.dp).padding(end = 8.dp)) { + if (isAdding) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp).align(Alignment.Center), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.primary, + ) + } else { + if (isSkillAdded) { + FilledTonalButton( + onClick = { /* Do nothing */ }, + contentPadding = BUTTON_CONTENT_PADDING, + enabled = false, // Greyed out + ) { + Text( + stringResource(R.string.added), + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(start = 4.dp), + ) + } + } else { + FilledTonalButton( + onClick = { onAddClick(skill) }, + contentPadding = BUTTON_CONTENT_PADDING, + ) { + Icon( + Icons.Outlined.Add, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + Text( + stringResource(R.string.add), + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(start = 4.dp), + ) + } + } + } + } + } +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddSkillFromLocalImportDialog.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddSkillFromLocalImportDialog.kt new file mode 100644 index 000000000..2ef6b372b --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddSkillFromLocalImportDialog.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery.customtasks.agentchat + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.FileOpen +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.google.ai.edge.gallery.R + +private const val TAG = "AGAddSkillFromLocalImportDialog" + +@Composable +fun AddSkillFromLocalImportDialog( + skillManagerViewModel: SkillManagerViewModel, + onDismissRequest: () -> Unit, + onSuccess: () -> Unit, +) { + val uiState by skillManagerViewModel.uiState.collectAsState() + val validating = uiState.validating + val validationError = uiState.validationError + val directoryUri = uiState.importDirectoryUri + var showReplaceSkillConfirmationDialog by remember { mutableStateOf(false) } + + val context = LocalContext.current + + val directoryPickerLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> + skillManagerViewModel.setImportDirectoryUri(uri) + skillManagerViewModel.setValidationError(null) + } + + Dialog(onDismissRequest = onDismissRequest) { + Card(modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp)) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Column(modifier = Modifier.padding(bottom = 8.dp)) { + // Title. + Text( + text = stringResource(R.string.add_skill_dialog_title_from_local_import), + style = MaterialTheme.typography.titleMedium, + ) + + // Subtitle. + Text( + text = stringResource(R.string.add_skill_dialog_subtitle_from_local_import), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + modifier = Modifier.padding(top = 2.dp), + ) + } + + // Row: Directory Picker + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = stringResource(R.string.pick_skill_dir), + style = MaterialTheme.typography.labelMedium, + ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Box( + modifier = + Modifier.weight(1f) + .clip(RoundedCornerShape(4.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + Text( + text = + directoryUri?.let { getDisplayName(context, it) } + ?: stringResource(R.string.no_directory_selected), + style = MaterialTheme.typography.bodyMedium, + color = + if (directoryUri == null) { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + } else { + MaterialTheme.colorScheme.onSurface + }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + IconButton(onClick = { directoryPickerLauncher.launch(null) }) { + Icon( + Icons.Outlined.FileOpen, + contentDescription = stringResource(R.string.cd_pick_file), + ) + } + } + validationError?.let { error -> + Text( + text = error, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) + } + } + + // Show spinner when validating. + if (validating) { + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.primary, + ) + } + } else { + // Button row + Row( + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + horizontalArrangement = Arrangement.End, + ) { + OutlinedButton(onClick = onDismissRequest) { Text(stringResource(R.string.cancel)) } + Spacer(modifier = Modifier.width(8.dp)) + Button( + enabled = directoryUri != null, + onClick = { + directoryUri?.let { uri -> + if (skillManagerViewModel.checkLocalSkillExisted(uri)) { + showReplaceSkillConfirmationDialog = true + } else { + skillManagerViewModel.validateAndAddSkillFromLocalImport( + onSuccess = { + onDismissRequest() + onSuccess() + }, + onValidationError = {}, + ) + } + } + }, + ) { + Text(stringResource(R.string.add)) + } + } + } + } + } + } + + if (showReplaceSkillConfirmationDialog) { + AlertDialog( + onDismissRequest = { showReplaceSkillConfirmationDialog = false }, + title = { Text(stringResource(R.string.replace_skill_dialog_title)) }, + text = { Text(stringResource(R.string.replace_skill_dialog_content)) }, + confirmButton = { + Button( + onClick = { + showReplaceSkillConfirmationDialog = false + skillManagerViewModel.validateAndAddSkillFromLocalImport( + onSuccess = { + onDismissRequest() + onSuccess() + }, + onValidationError = {}, + ) + } + ) { + Text(stringResource(R.string.replace)) + } + }, + dismissButton = { + OutlinedButton(onClick = { showReplaceSkillConfirmationDialog = false }) { + Text(stringResource(R.string.cancel)) + } + }, + ) + } +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddSkillFromUrlDialog.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddSkillFromUrlDialog.kt new file mode 100644 index 000000000..62bd5c4a0 --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AddSkillFromUrlDialog.kt @@ -0,0 +1,220 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery.customtasks.agentchat + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.OutlinedTextFieldDefaults.FocusedBorderThickness +import androidx.compose.material3.OutlinedTextFieldDefaults.UnfocusedBorderThickness +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.google.ai.edge.gallery.R +import java.net.URI + +private val APPROVED_SKILL_HOSTS = listOf("google-ai-edge.github.io") + +@Composable +fun AddSkillFromUrlDialog( + skillManagerViewModel: SkillManagerViewModel, + onDismissRequest: () -> Unit, + onSuccess: () -> Unit, +) { + val uiState by skillManagerViewModel.uiState.collectAsState() + val validating = uiState.validating + val validationError = uiState.validationError + + val interactionSource = remember { MutableInteractionSource() } + var textFieldValue by remember { mutableStateOf(TextFieldValue("")) } + var showDisclaimerDialog by remember { mutableStateOf(false) } + + val validateAndAddSkill: (String) -> Unit = { url -> + skillManagerViewModel.validateAndAddSkillFromUrl( + url = url, + onSuccess = { + onDismissRequest() + onSuccess() + }, + onValidationError = { error -> + // Select all text on error + textFieldValue = textFieldValue.copy(selection = TextRange(0, textFieldValue.text.length)) + }, + ) + } + + Dialog(onDismissRequest = { if (!validating) onDismissRequest() }) { + val focusManager = LocalFocusManager.current + Card( + modifier = + Modifier.fillMaxWidth().clickable( + interactionSource = interactionSource, + indication = null, // Disable the ripple effect + ) { + focusManager.clearFocus() + }, + shape = RoundedCornerShape(16.dp), + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + stringResource(R.string.add_skill_from_url_dialog_title), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(bottom = 8.dp), + ) + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + stringResource(R.string.enter_skill_url), + style = MaterialTheme.typography.labelMedium, + ) + BasicTextField( + value = textFieldValue, + onValueChange = { newValue -> + val oldText = textFieldValue.text + textFieldValue = newValue + // Clear error on text change + if (newValue.text != oldText) { + skillManagerViewModel.setValidationError(null) + } + }, + modifier = Modifier.fillMaxWidth(), + textStyle = + MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface), + maxLines = 3, + decorationBox = { innerTextField -> + OutlinedTextFieldDefaults.DecorationBox( + value = textFieldValue.text, + innerTextField = innerTextField, + enabled = true, + singleLine = false, + visualTransformation = VisualTransformation.None, + interactionSource = remember { MutableInteractionSource() }, + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 6.dp), + container = { + OutlinedTextFieldDefaults.Container( + enabled = true, + isError = false, + interactionSource = remember { MutableInteractionSource() }, + colors = OutlinedTextFieldDefaults.colors(), + shape = OutlinedTextFieldDefaults.shape, + focusedBorderThickness = FocusedBorderThickness, + unfocusedBorderThickness = UnfocusedBorderThickness, + ) + }, + ) + }, + ) + validationError?.let { error -> + Text( + text = error, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) + } + } + // Show spinner when validating. + if (validating) { + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.primary, + ) + } + } + // Show buttons when not validating. + else { + Row( + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + horizontalArrangement = Arrangement.End, + ) { + OutlinedButton(onClick = { onDismissRequest() }) { + Text(stringResource(R.string.cancel)) + } + Spacer(modifier = Modifier.width(4.dp)) + Button( + onClick = { + val url = textFieldValue.text + if (isHostApproved(url)) { + validateAndAddSkill(url) + } else { + showDisclaimerDialog = true + } + } + ) { + Text(stringResource(R.string.add)) + } + } + } + } + } + } + + if (showDisclaimerDialog) { + AddSkillDisclaimerDialog( + onDismiss = { showDisclaimerDialog = false }, + onConfirm = { + showDisclaimerDialog = false + validateAndAddSkill(textFieldValue.text) + }, + ) + } +} + +fun isHostApproved(url: String): Boolean { + return try { + val uri = URI(url).normalize() + val parsedHost = uri.host?.lowercase() ?: return false + + // Check if the parsed host matches any host in our allowlist + APPROVED_SKILL_HOSTS.any { allowed -> parsedHost == allowed.lowercase() } + } catch (e: Exception) { + false // Invalid URI syntax + } +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AgentChatScreen.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AgentChatScreen.kt new file mode 100644 index 000000000..02394961c --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AgentChatScreen.kt @@ -0,0 +1,681 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery.customtasks.agentchat + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.webkit.ConsoleMessage +import android.webkit.JavascriptInterface +import android.webkit.WebView +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.isImeVisible +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.google.ai.edge.gallery.GalleryEvent +import com.google.ai.edge.gallery.R +import com.google.ai.edge.gallery.common.AskInfoAgentAction +import com.google.ai.edge.gallery.common.CallJsAgentAction +import com.google.ai.edge.gallery.common.LOCAL_URL_BASE +import com.google.ai.edge.gallery.common.SkillProgressAgentAction +import com.google.ai.edge.gallery.data.BuiltInTaskId +import com.google.ai.edge.gallery.data.Model +import com.google.ai.edge.gallery.data.Task +import com.google.ai.edge.gallery.firebaseAnalytics +import com.google.ai.edge.gallery.ui.common.BaseGalleryWebViewClient +import com.google.ai.edge.gallery.ui.common.GalleryWebView +import com.google.ai.edge.gallery.ui.common.buildTrackableUrlAnnotatedString +import com.google.ai.edge.gallery.ui.common.chat.ChatMessage +import com.google.ai.edge.gallery.ui.common.chat.ChatMessageCollapsableProgressPanel +import com.google.ai.edge.gallery.ui.common.chat.ChatMessageImage +import com.google.ai.edge.gallery.ui.common.chat.ChatMessageText +import com.google.ai.edge.gallery.ui.common.chat.ChatMessageType +import com.google.ai.edge.gallery.ui.common.chat.ChatMessageWebView +import com.google.ai.edge.gallery.ui.common.chat.ChatSide +import com.google.ai.edge.gallery.ui.common.chat.LogMessage +import com.google.ai.edge.gallery.ui.common.chat.LogMessageLevel +import com.google.ai.edge.gallery.ui.common.chat.SendMessageTrigger +import com.google.ai.edge.gallery.ui.llmchat.LlmChatScreen +import com.google.ai.edge.gallery.ui.llmchat.LlmChatViewModel +import com.google.ai.edge.gallery.ui.modelmanager.ModelInitializationStatusType +import com.google.ai.edge.gallery.ui.modelmanager.ModelManagerViewModel +import com.google.ai.edge.litertlm.Message +import com.google.ai.edge.litertlm.tool +import java.lang.Exception +import kotlin.coroutines.resume +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import org.json.JSONObject + +private const val TAG = "AGAgentChatScreen" +private val chatViewJavascriptInterface = ChatWebViewJavascriptInterface() + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun AgentChatScreen( + task: Task, + modelManagerViewModel: ModelManagerViewModel, + navigateUp: () -> Unit, + agentTools: AgentTools, + viewModel: LlmChatViewModel = hiltViewModel(), + skillManagerViewModel: SkillManagerViewModel = hiltViewModel(), + initialQuery: String? = null, +) { + val context = LocalContext.current + agentTools.context = context + agentTools.skillManagerViewModel = skillManagerViewModel + val density = LocalDensity.current + val windowInfo = LocalWindowInfo.current + val screenWidthDp = remember { with(density) { windowInfo.containerSize.width.toDp() } } + var showSkillManagerBottomSheet by remember { mutableStateOf(false) } + var showAskInfoDialog by remember { mutableStateOf(false) } + var currentAskInfoAction by remember { mutableStateOf(null) } + var askInfoInputValue by remember { mutableStateOf("") } + var webViewRef: WebView? by remember { mutableStateOf(null) } + val chatWebViewClient = remember { ChatWebViewClient(context = context) } + var curSystemPrompt by remember { mutableStateOf(task.defaultSystemPrompt) } + val systemPromptUpdatedMessage = stringResource(R.string.system_prompt_updated) + var sendMessageTrigger by remember { mutableStateOf(null) } + var showAlertForDisabledSkill by remember { mutableStateOf(false) } + var disabledSkillName by remember { mutableStateOf("") } + LaunchedEffect(task) { viewModel.loadSystemPrompt(task) } + val uiSystemPrompt by viewModel.uiSystemPrompt.collectAsState() + LaunchedEffect(uiSystemPrompt) { curSystemPrompt = uiSystemPrompt } + + // Collect UI states from view models. Ensure launched effect is triggered when the UI state is + // updated. + val llmChatUiState by viewModel.uiState.collectAsState() + val modelManagerUiState by modelManagerViewModel.uiState.collectAsState() + val selectedModel = modelManagerUiState.selectedModel + val modelInitStatus = modelManagerUiState.modelInitializationStatus[selectedModel.name] + + var initialQueryConsumed by remember { mutableStateOf(false) } + + LaunchedEffect( + llmChatUiState.isResettingSession, + modelInitStatus?.status, + selectedModel.name, + initialQuery, + ) { + // Send the optional initial query to the model if the model is initialized and the initial + // query is not consumed yet. + if ( + !initialQuery.isNullOrEmpty() && + !initialQueryConsumed && + modelInitStatus?.status == ModelInitializationStatusType.INITIALIZED && + !llmChatUiState.isResettingSession + ) { + initialQueryConsumed = true + sendMessageTrigger = + SendMessageTrigger( + model = selectedModel, + messages = listOf(ChatMessageText(content = initialQuery, side = ChatSide.USER)), + ) + } + } + + LlmChatScreen( + modelManagerViewModel = modelManagerViewModel, + taskId = BuiltInTaskId.LLM_AGENT_CHAT, + navigateUp = navigateUp, + onFirstToken = { model -> + updateProgressPanel(viewModel = viewModel, model = model, agentTools = agentTools) + }, + onGenerateResponseDone = { model -> + // Show any image produced by tools. + agentTools.resultImageToShow?.let { resultImage -> + resultImage.base64?.let { base64 -> + decodeBase64ToBitmap(base64String = base64)?.let { bitmap -> + viewModel.addMessage( + model = model, + message = + ChatMessageImage( + bitmaps = listOf(bitmap), + imageBitMaps = listOf(bitmap.asImageBitmap()), + side = ChatSide.AGENT, + maxSize = (screenWidthDp.value * 0.8).toInt(), + latencyMs = -1.0f, + hideSenderLabel = true, + ), + ) + } + } + // Clean up. + agentTools.resultImageToShow = null + } + + // Show any webview produced by tools. + agentTools.resultWebviewToShow?.let { webview -> + val url = webview.url ?: "" + val iframe = webview.iframe == true + val aspectRatio = webview.aspectRatio ?: 1.333f + viewModel.addMessage( + model = model, + message = + ChatMessageWebView( + url = url, + iframe = iframe, + aspectRatio = aspectRatio, + hideSenderLabel = true, + ), + ) + // Clean up. + agentTools.resultWebviewToShow = null + } + + updateProgressPanel(viewModel = viewModel, model = model, agentTools = agentTools) + }, + onResetSessionClickedOverride = { task, _, initialMessages -> + resetSessionWithCurrentSkills( + viewModel, + modelManagerViewModel, + skillManagerViewModel, + task, + curSystemPrompt, + agentTools, + initialMessages = initialMessages, + ) + }, + onSkillClicked = { showSkillManagerBottomSheet = true }, + showImagePicker = true, + showAudioPicker = true, + getActiveSkills = { + skillManagerViewModel.getSelectedSkills().map { skill -> + skillManagerViewModel.getSkillShortId(skill) + } + }, + composableBelowMessageList = { model -> + val actionChannel = agentTools.actionChannel + val doneIcon = ImageVector.vectorResource(R.drawable.skill) + // Use rememberUpdatedState to ensure that LaunchedEffect captures the + // latest active model when the model is switched during an ongoing skill execution. + val currentModel by androidx.compose.runtime.rememberUpdatedState(model) + LaunchedEffect(actionChannel) { + for (action in actionChannel) { + Log.d(TAG, "Handling action: $action") + when (action) { + is SkillProgressAgentAction -> { + viewModel.updateCollapsableProgressPanelMessage( + model = currentModel, + title = action.label, + inProgress = action.inProgress, + doneIcon = doneIcon, + addItemTitle = action.addItemTitle, + addItemDescription = action.addItemDescription, + customData = action.customData, + ) + } + is CallJsAgentAction -> { + val skillName = + if (action.url.contains("/skills/")) { + action.url.substringAfter("/skills/").substringBefore("/") + } else if (action.url.startsWith(LOCAL_URL_BASE + "/")) { + action.url.substringAfter(LOCAL_URL_BASE + "/").substringBefore("/") + } else { + action.url + } + val skill = skillManagerViewModel.getSkill(name = skillName) + val skillId = skill?.let { skillManagerViewModel.getSkillShortId(it) } ?: "xxxx" + try { + // Set up a safety net timeout so we NEVER hang the chat or tool execution + launch { + delay(60000L) // 60 seconds max + if (!action.result.isCompleted) { + Log.e(TAG, "JS Execution timed out, completing with error.") + Log.d( + TAG, + "Analytics: skill_execution, skill_name=$skillName, success=false, error_type=timeout", + ) + firebaseAnalytics?.logEvent( + GalleryEvent.SKILL_EXECUTION.id, + Bundle().apply { + putString("skill_name", skillName) + putString("skill_id", skillId) + putBoolean("success", false) + putString("error_type", "timeout") + }, + ) + action.result.complete( + "{\"error\": \"Skill execution timed out. Please check network connection.\"}" + ) + } + } + + // Load url. + suspendCancellableCoroutine { continuation -> + chatWebViewClient.setPageLoadListener { + chatWebViewClient.setPageLoadListener(null) + continuation.resume(Unit) + } + Log.d(TAG, "Loading url: ${action.url}") + webViewRef?.loadUrl(action.url) + } + + // Execute JS. + Log.d(TAG, "Start to run js") + chatViewJavascriptInterface.onResultListener = { result -> + Log.d(TAG, "Got result:\n$result") + action.result.complete(result) + val isSuccess = !result.contains("\"error\":") + val errorType = if (isSuccess) "" else "js_error" + Log.d( + TAG, + "Analytics: skill_execution, skill_name=$skillName, success=$isSuccess, error_type=$errorType", + ) + firebaseAnalytics?.logEvent( + GalleryEvent.SKILL_EXECUTION.id, + Bundle().apply { + putString("skill_name", skillName) + putString("skill_id", skillId) + putBoolean("success", isSuccess) + putString("error_type", errorType) + }, + ) + } + + val safeData = JSONObject.quote(action.data) + val safeSecret = JSONObject.quote(action.secret) + val script = + """ + (async function() { + var startTs = Date.now(); + while(true) { + if (typeof ai_edge_gallery_get_result === 'function') { + break; + } + await new Promise(resolve=>{ + setTimeout(resolve, 100) + }); + if (Date.now() - startTs > 10000) { + break; + } + } + var result = await ai_edge_gallery_get_result($safeData, $safeSecret); + AiEdgeGallery.onResultReady(result); + })() + """ + .trimIndent() + webViewRef?.evaluateJavascript(script, null) + } catch (e: Exception) { + Log.d( + TAG, + "Analytics: skill_execution, skill_name=$skillName, success=false, error_type=exception", + ) + firebaseAnalytics?.logEvent( + GalleryEvent.SKILL_EXECUTION.id, + Bundle().apply { + putString("skill_name", skillName) + putString("skill_id", skillId) + putBoolean("success", false) + putString("error_type", "exception") + }, + ) + action.result.completeExceptionally(e) + } + } + is AskInfoAgentAction -> { + currentAskInfoAction = action + askInfoInputValue = "" // Reset input + showAskInfoDialog = true + } + } + } + } + + GalleryWebView( + modifier = Modifier.size(300.dp), + onWebViewCreated = { webView -> + webViewRef = webView + webView.addJavascriptInterface(chatViewJavascriptInterface, "AiEdgeGallery") + }, + customWebViewClient = chatWebViewClient, + onConsoleMessage = { consoleMessage -> + consoleMessage?.let { curConsoleMessage -> + // Create a LogMessage from the ConsoleMessage and add it to the progress panel. + val logMessage = + LogMessage( + level = + when (curConsoleMessage.messageLevel()) { + ConsoleMessage.MessageLevel.LOG -> LogMessageLevel.Info + ConsoleMessage.MessageLevel.ERROR -> LogMessageLevel.Error + ConsoleMessage.MessageLevel.WARNING -> LogMessageLevel.Warning + else -> LogMessageLevel.Info + }, + source = curConsoleMessage.sourceId(), + lineNumber = curConsoleMessage.lineNumber(), + message = curConsoleMessage.message(), + ) + viewModel.addLogMessageToLastCollapsableProgressPanel( + model = model, + logMessage = logMessage, + ) + Log.d( + TAG, + "${curConsoleMessage.message()} " + + "-- From line ${curConsoleMessage.lineNumber()} of ${curConsoleMessage.sourceId()}", + ) + } + }, + ) + }, + allowEditingSystemPrompt = true, + curSystemPrompt = curSystemPrompt, + onSystemPromptChanged = { newPrompt -> + curSystemPrompt = newPrompt + viewModel.applySystemPromptChange( + task = task, + model = modelManagerViewModel.uiState.value.selectedModel, + newPrompt = newPrompt, + systemPromptUpdatedMessage = systemPromptUpdatedMessage, + ) + }, + emptyStateComposable = { model -> + val uiState by viewModel.uiState.collectAsState() + val modelManagerUiState by modelManagerViewModel.uiState.collectAsState() + val modelInitializationStatus = modelManagerUiState.modelInitializationStatus[model.name] + Box(modifier = Modifier.fillMaxSize()) { + AnimatedVisibility( + !WindowInsets.isImeVisible, + enter = fadeIn(animationSpec = tween(200)), + exit = fadeOut(animationSpec = tween(200)), + ) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column( + modifier = + Modifier.align(Alignment.Center) + .padding(horizontal = 48.dp) + .padding(bottom = 48.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + stringResource(R.string.introducing), + style = MaterialTheme.typography.headlineSmall, + ) + Text( + stringResource(R.string.agent_skills), + style = + MaterialTheme.typography.headlineLarge.copy( + fontWeight = FontWeight.Medium, + brush = + Brush.linearGradient(colors = listOf(Color(0xFF85B1F8), Color(0xFF3174F1))), + ), + modifier = Modifier.padding(top = 12.dp, bottom = 16.dp), + ) + Text( + buildAnnotatedString { + append("Use specialized, high-order reasoning by loading different skills or ") + append( + buildTrackableUrlAnnotatedString( + url = "https://github.com/google-ai-edge/gallery/tree/main/skills", + linkText = "creating\u00A0your\u00A0own", + ) + ) + append(".\n\nTry tapping a sample prompt below to see Agent Skills in action!") + }, + style = + MaterialTheme.typography.headlineSmall.copy(fontSize = 16.sp, lineHeight = 22.sp), + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + } + } + + Row( + modifier = + Modifier.align(Alignment.BottomCenter) + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + for (promptChip in TRYOUT_CHIPS) { + FilledTonalButton( + enabled = + modelInitializationStatus?.status == ModelInitializationStatusType.INITIALIZED && + !uiState.isResettingSession, + onClick = { + // Skill is selected, trigger sending the message. + if (skillManagerViewModel.isSkillSelected(promptChip.skillName)) { + sendMessageTrigger = + SendMessageTrigger( + model = model, + messages = + listOf(ChatMessageText(content = promptChip.prompt, side = ChatSide.USER)), + ) + firebaseAnalytics?.logEvent( + GalleryEvent.BUTTON_CLICKED.id, + Bundle().apply { + putString("event_type", "agent_skills_prompt_chip") + putString("button_id", promptChip.label) + }, + ) + } + // Skill is not selected, show alert dialog. + else { + disabledSkillName = promptChip.skillName + showAlertForDisabledSkill = true + } + }, + contentPadding = PaddingValues(horizontal = 12.dp), + ) { + Icon(promptChip.icon, contentDescription = null, modifier = Modifier.size(20.dp)) + Spacer(modifier = Modifier.width(4.dp)) + Text(promptChip.label) + } + } + } + } + }, + sendMessageTrigger = sendMessageTrigger, + ) + + if (showAskInfoDialog && currentAskInfoAction != null) { + val action = currentAskInfoAction!! + SecretEditorDialog( + title = action.dialogTitle, + fieldLabel = action.fieldLabel, + value = askInfoInputValue, + onValueChange = { askInfoInputValue = it }, + onDone = { + action.result.complete(askInfoInputValue) + showAskInfoDialog = false + currentAskInfoAction = null + }, + onDismiss = { + action.result.complete("") + showAskInfoDialog = false + currentAskInfoAction = null + }, + ) + } + + if (showSkillManagerBottomSheet) { + SkillManagerBottomSheet( + agentTools = agentTools, + skillManagerViewModel = skillManagerViewModel, + onDismiss = { selectedSkillsChanged -> + // Hide sheet. + showSkillManagerBottomSheet = false + + // Reset session when selected skills changed. + if (selectedSkillsChanged) { + Log.d(TAG, "Selected skill changed. Resetting conversation.") + resetSessionWithCurrentSkills( + viewModel, + modelManagerViewModel, + skillManagerViewModel, + task, + curSystemPrompt, + agentTools, + ) + } + }, + ) + } + + if (showAlertForDisabledSkill) { + AlertDialog( + onDismissRequest = { showAlertForDisabledSkill = false }, + title = { Text("The \"$disabledSkillName\" skill is currently disabled") }, + text = { Text(stringResource(R.string.enable_skill_dialog_content)) }, + confirmButton = { + Button(onClick = { showAlertForDisabledSkill = false }) { + Text(stringResource(R.string.ok)) + } + }, + ) + } +} + +private fun updateProgressPanel(viewModel: LlmChatViewModel, model: Model, agentTools: AgentTools) { + // Update status. + val lastProgressPanelMessage = + viewModel.getLastMessageWithType( + model = model, + type = ChatMessageType.COLLAPSABLE_PROGRESS_PANEL, + ) + if ( + lastProgressPanelMessage != null && + lastProgressPanelMessage is ChatMessageCollapsableProgressPanel + ) { + if (lastProgressPanelMessage.title.startsWith("Loading")) { + agentTools.sendAgentAction( + SkillProgressAgentAction( + label = lastProgressPanelMessage.title.replace("Loading", "Loaded"), + inProgress = false, + ) + ) + } else if (lastProgressPanelMessage.title.startsWith("Calling")) { + agentTools.sendAgentAction( + SkillProgressAgentAction( + label = lastProgressPanelMessage.title.replace("Calling", "Called"), + inProgress = false, + ) + ) + } else if (lastProgressPanelMessage.title.startsWith("Executing")) { + agentTools.sendAgentAction( + SkillProgressAgentAction( + label = lastProgressPanelMessage.title.replace("Executing", "Executed"), + inProgress = false, + ) + ) + } + } +} + +private fun resetSessionWithCurrentSkills( + viewModel: LlmChatViewModel, + modelManagerViewModel: ModelManagerViewModel, + skillManagerViewModel: SkillManagerViewModel, + task: Task, + curSystemPrompt: String, + agentTools: AgentTools, + onDone: (Model) -> Unit = {}, + initialMessages: List = listOf(), +) { + val model = modelManagerViewModel.uiState.value.selectedModel + val litertMessages = initialMessages.mapNotNull { chatMessage -> + if (chatMessage is ChatMessageText) { + if (chatMessage.side == ChatSide.USER) { + Message.user(chatMessage.content) + } else { + Message.model(chatMessage.content) + } + } else null + } + viewModel.resetSession( + task = task, + model = model, + systemInstruction = skillManagerViewModel.injectSkills(curSystemPrompt), + tools = listOf(tool(agentTools)), + supportImage = true, + supportAudio = true, + onDone = { onDone(model) }, + enableConversationConstrainedDecoding = true, + initialMessages = litertMessages, + ) +} + +class ChatWebViewJavascriptInterface { + var onResultListener: ((String) -> Unit)? = null + + @JavascriptInterface + fun onResultReady(result: String) { + onResultListener?.invoke(result) + } +} + +class ChatWebViewClient(val context: Context) : BaseGalleryWebViewClient(context = context) { + private var onPageLoaded: (() -> Unit)? = null + + fun setPageLoadListener(listener: (() -> Unit)?) { + onPageLoaded = listener + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + Log.d(TAG, "page loaded") + onPageLoaded?.invoke() + } +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AgentChatTaskModule.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AgentChatTaskModule.kt new file mode 100644 index 000000000..7dd5d446c --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AgentChatTaskModule.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery.customtasks.agentchat + +import android.content.Context +import androidx.compose.runtime.Composable +import com.google.ai.edge.gallery.R +import com.google.ai.edge.gallery.customtasks.common.CustomTask +import com.google.ai.edge.gallery.customtasks.common.CustomTaskDataForBuiltinTask +import com.google.ai.edge.gallery.data.BuiltInTaskId +import com.google.ai.edge.gallery.data.Category +import com.google.ai.edge.gallery.data.Model +import com.google.ai.edge.gallery.data.Task +import com.google.ai.edge.gallery.ui.llmchat.LlmChatModelHelper +import com.google.ai.edge.litertlm.Contents +import com.google.ai.edge.litertlm.tool +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope + +class AgentChatTask @Inject constructor() : CustomTask { + private val agentTools = AgentTools() + + override val task: Task = + Task( + id = BuiltInTaskId.LLM_AGENT_CHAT, + label = "Agent Skills", + category = Category.LLM, + iconVectorResourceId = R.drawable.agent, + newFeature = true, + models = mutableListOf(), + description = "Chat with on-device large language models with skills and tools", + shortDescription = "Complete agentic tasks with chat", + docUrl = "https://github.com/google-ai-edge/LiteRT-LM/blob/main/kotlin/README.md", + sourceCodeUrl = + "https://github.com/google-ai-edge/gallery/blob/main/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/", + textInputPlaceHolderRes = R.string.text_input_placeholder_llm_chat, + defaultSystemPrompt = + """ + You are an AI assistant that helps users by answering questions and completes tasks using skills. For EVERY new task or request or question, you MUST execute the following steps in exact order. You MUST NOT skip any steps. + + CRITICAL RULE: You MUST execute all steps silently. Do NOT generate or output any internal thoughts, reasoning, explanations, or intermediate text at ANY step. + + 1. First, find the most relevant skill from the following list: + + ___SKILLS___ + + After this step you MUST go to next step. You MUST NOT use `run_intent` under any circumstances at this step. + + 2. If a relevant skill exists, use the `load_skill` tool to read its instructions. You MUST NOT use `run_intent` under any circumstances at this step. + + 3. Follow the skill's instructions exactly to complete the task. You MUST NOT output any intermediate thoughts or status updates. No exceptions! Output ONLY the final result when successful. It should contain one-sentence summary of the action taken, and the final result of the skill. + """ + .trimIndent(), + ) + + override fun initializeModelFn( + context: Context, + coroutineScope: CoroutineScope, + model: Model, + systemInstruction: Contents?, + onDone: (String) -> Unit, + ) { + val systemPrompt = systemInstruction?.toString() ?: task.defaultSystemPrompt + agentTools.skillManagerViewModel.loadSkills { + LlmChatModelHelper.initialize( + context = context, + model = model, + taskId = task.id, + supportImage = true, + supportAudio = true, + onDone = onDone, + systemInstruction = agentTools.skillManagerViewModel.injectSkills(systemPrompt), + tools = listOf(tool(agentTools)), + enableConversationConstrainedDecoding = true, + ) + } + } + + override fun cleanUpModelFn( + context: Context, + coroutineScope: CoroutineScope, + model: Model, + onDone: () -> Unit, + ) { + LlmChatModelHelper.cleanUp(model = model, onDone = onDone) + } + + @Composable + override fun MainScreen(data: Any) { + val myData = data as CustomTaskDataForBuiltinTask + AgentChatScreen( + task = task, + modelManagerViewModel = myData.modelManagerViewModel, + navigateUp = myData.onNavUp, + agentTools = agentTools, + initialQuery = myData.initialQuery, + ) + } +} + +@Module +@InstallIn(SingletonComponent::class) +internal object AgentChatTaskModule { + @Provides + @IntoSet + fun provideTask(): CustomTask { + return AgentChatTask() + } +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AgentTools.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AgentTools.kt new file mode 100644 index 000000000..b01f98a9b --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/AgentTools.kt @@ -0,0 +1,254 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.ai.edge.gallery.customtasks.agentchat + +import android.content.Context +import android.util.Log +import com.google.ai.edge.gallery.common.AgentAction +import com.google.ai.edge.gallery.common.AskInfoAgentAction +import com.google.ai.edge.gallery.common.CallJsAgentAction +import com.google.ai.edge.gallery.common.CallJsSkillResult +import com.google.ai.edge.gallery.common.CallJsSkillResultImage +import com.google.ai.edge.gallery.common.CallJsSkillResultWebview +import com.google.ai.edge.gallery.common.LOCAL_URL_BASE +import com.google.ai.edge.gallery.common.SkillProgressAgentAction +import com.google.ai.edge.litertlm.Tool +import com.google.ai.edge.litertlm.ToolParam +import com.google.ai.edge.litertlm.ToolSet +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.runBlocking + +private const val TAG = "AGAgentTools" + +open class AgentTools() : ToolSet { + lateinit var context: Context + lateinit var skillManagerViewModel: SkillManagerViewModel + + private val _actionChannel = Channel(Channel.UNLIMITED) + val actionChannel: ReceiveChannel = _actionChannel + var resultImageToShow: CallJsSkillResultImage? = null + var resultWebviewToShow: CallJsSkillResultWebview? = null + + /** Loads skill. */ + @Tool(description = "Loads a skill.") + fun loadSkill( + @ToolParam(description = "The name of the skill to load.") skillName: String + ): Map { + return runBlocking(Dispatchers.Default) { + val skills = skillManagerViewModel.getSelectedSkills() + val skill = skills.find { it.name == skillName.trim() } + val skillContent = + if (skill != null) { + "---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\n${skill.instructions}" + } else { + "Skill not found" + } + Log.d(TAG, "load skill. Skill content:\n$skillContent") + if (skill != null) { + _actionChannel.send( + SkillProgressAgentAction( + label = "Loading skill \"$skillName\"", + inProgress = true, + addItemTitle = "Load \"${skill.name}\"", + addItemDescription = "Description: ${skill.description}", + customData = skill, + ) + ) + } else { + _actionChannel.send( + SkillProgressAgentAction( + label = "Failed to load skill \"$skillName\"", + inProgress = false, + ) + ) + } + + mapOf("skill_name" to skillName, "skill_instructions" to skillContent) + } + } + + /** Call JS skill */ + @Tool(description = "Runs JS script") + fun runJs( + @ToolParam(description = "The name of skill") skillName: String, + @ToolParam(description = "The script name to run. Use 'index.html' if not provided by user") + scriptName: String, + @ToolParam( + description = "The data to pass to the script. Use empty string if not provided by user" + ) + data: String, + ): Map { + return runBlocking(Dispatchers.Default) { + Log.d( + TAG, + "runJS tool called with:" + + "\n- skillName: ${skillName}\n- scriptName: ${scriptName}\n- data: ${data}\n", + ) + + val skills = skillManagerViewModel.getSelectedSkills() + val skill = skills.find { it.name == skillName.trim() } + + if (skill == null) { + _actionChannel.send( + SkillProgressAgentAction( + label = "Failed to call skill \"$scriptName\"", + inProgress = false, + ) + ) + return@runBlocking mapOf( + "error" to "Skill \"${scriptName}\" not found", + "status" to "failed", + ) + } + + // Check secret. If a skill requires a secret and the secret is not provided, show error. + var secret = "" + if (skill.requireSecret) { + val savedSecret = + skillManagerViewModel.dataStoreRepository.readSecret( + key = getSkillSecretKey(skillName = skillName) + ) + if (savedSecret == null || savedSecret.isEmpty()) { + val action = + AskInfoAgentAction( + dialogTitle = "Enter secret", + fieldLabel = + skill.requireSecretDescription.ifEmpty { + "The JS script needs a secret (API key / token) to proceed:" + }, + ) + _actionChannel.send(action) + secret = action.result.await() + if (secret.isNotEmpty()) { + skillManagerViewModel.dataStoreRepository.saveSecret( + key = getSkillSecretKey(skillName = skillName), + value = secret, + ) + Log.d(TAG, "Got Secret from ask info dialog: ${secret.substring(0, 3)}") + } else { + Log.d(TAG, "The ask info dialog got cancelled. No secret.") + } + } else { + secret = savedSecret + } + } + + // Get the url for the skill. + val url = + skillManagerViewModel.getJsSkillUrl(skillName = skillName, scriptName = scriptName) + ?: return@runBlocking mapOf( + "result" to "JS Skill URL not set properly or skill not found" + ) + Log.d(TAG, "Calling JS script.\n- url: $url\n- data: $data") + + // Update progress. + _actionChannel.send( + SkillProgressAgentAction( + label = "Calling JS script \"${skillName}/${scriptName}\"", + inProgress = true, + addItemTitle = "Call JS script: \"${skillName}/${scriptName}\"", + addItemDescription = "- URL: ${url.replace(LOCAL_URL_BASE, "")}\n- Data: $data", + customData = skill, + ) + ) + + // Actually run it and wait for the result. + val action = + CallJsAgentAction(url = url, data = data.trim().ifEmpty { "{}" }, secret = secret) + _actionChannel.send(action) + val result = action.result.await() + + // Try to parse result to CallJsSkillResult. + val moshi: Moshi = Moshi.Builder().build() + val jsonAdapter: JsonAdapter = + moshi.adapter(CallJsSkillResult::class.java).failOnUnknown() + val resultJson = runCatching { jsonAdapter.fromJson(result) }.getOrNull() + val error = resultJson?.error + + // Failed to parse. Treat its whole as a result string. + if ( + resultJson == null || + (resultJson.result == null && resultJson.webview == null && resultJson.image == null) + ) { + mapOf("result" to result, "status" to "succeeded") + } + // Error case. + else if (error != null) { + mapOf("error" to error, "status" to "failed") + } + // Non-error cases. + else { + // Handle image and webview in result. + val image = resultJson.image + val webview = resultJson.webview + if (image != null) { + Log.d(TAG, "Got an image response.") + resultImageToShow = image + } + if (webview != null) { + Log.d(TAG, "Got an webview response.") + val webviewUrl = + skillManagerViewModel.getJsSkillWebviewUrl( + skillName = skillName, + url = webview.url ?: "", + ) + Log.d(TAG, "Webview url: $webviewUrl") + resultWebviewToShow = webview.copy(url = webviewUrl) + } + Log.d(TAG, "Result: ${resultJson.result}") + mapOf("result" to (resultJson.result ?: ""), "status" to "succeeded") + } + } + } + + @Tool( + description = + "Run an Android intent. It is used to interact with the app to perform certain actions." + ) + fun runIntent( + @ToolParam(description = "The intent to run.") intent: String, + @ToolParam( + description = "A JSON string containing the parameter values required for the intent." + ) + parameters: String, + ): Map { + return runBlocking(Dispatchers.Default) { + Log.d(TAG, "Run intent. Intent: '$intent', parameters: '$parameters'") + _actionChannel.send( + SkillProgressAgentAction( + label = "Executing intent \"$intent\"", + inProgress = true, + addItemTitle = "Execute intent \"$intent\"", + addItemDescription = "Parameters: $parameters", + ) + ) + val res = IntentHandler.handleAction(context, intent, parameters) + return@runBlocking mapOf("action" to intent, "parameters" to parameters, "result" to res) + } + } + + fun sendAgentAction(action: AgentAction) { + runBlocking(Dispatchers.Default) { _actionChannel.send(action) } + } +} + +fun getSkillSecretKey(skillName: String): String { + return "skill___${skillName}" +} diff --git a/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/GenerateLlmPromptBottomSheet.kt b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/GenerateLlmPromptBottomSheet.kt new file mode 100644 index 000000000..01e725d5e --- /dev/null +++ b/Android/src/app/src/main/java/com/google/ai/edge/gallery/customtasks/agentchat/GenerateLlmPromptBottomSheet.kt @@ -0,0 +1,179 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.ai.edge.gallery.customtasks.agentchat + +import android.content.ClipData +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.google.ai.edge.gallery.R +import com.google.ai.edge.gallery.ui.common.CursorTrackingTextField +import kotlinx.coroutines.launch + +private val PROMPT_TEMPLATE = + """ + # Task: Custom HTML/JS Implementation + Generate a single, self-contained HTML file that implements a specific feature or logic as described below. + + ## 1. Requirement + The implementation must fulfill the following: + > ___requirement___ + + ## 2. Technical Specifications + * **Structure:** A complete, valid HTML5 document. + * **Head (Dependencies):** If third-party JS libraries (e.g., Three.js, D3, Lodash, GSAP) are required, include them via CDN using ` + + + +``` + +> [!TIP] +> +> Think of `index.html` as a **"headless" execution environment** that leverages +> the full power of the web ecosystem within a standard mobile webview. This +> setup allows you to move beyond basic scripts by making `fetch()` calls to +> third-party APIs, integrating external libraries via CDN or relative paths in +> the ` + + diff --git a/skills/built-in/calculate-hash/scripts/index.js b/skills/built-in/calculate-hash/scripts/index.js new file mode 100644 index 000000000..7dd46b47f --- /dev/null +++ b/skills/built-in/calculate-hash/scripts/index.js @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +async function digestMessage(message) { + const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array + const hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8); // hash the message + const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); // convert bytes to hex string + return {result: hashHex}; +} + +window['ai_edge_gallery_get_result'] = async (data) => { + try { + const jsonData = JSON.parse(data); + return JSON.stringify(await digestMessage(jsonData['text'])); + } catch (e) { + console.error(e); + return JSON.stringify({error: `Failed to calculate hash: ${e.message}`}); + } +}; diff --git a/skills/built-in/interactive-map/SKILL.md b/skills/built-in/interactive-map/SKILL.md new file mode 100644 index 000000000..e55c33bc8 --- /dev/null +++ b/skills/built-in/interactive-map/SKILL.md @@ -0,0 +1,18 @@ +--- +name: interactive-map +description: Show an interactive map view for the given location. +--- + +# Interactive map + +## Examples + +- "Show [a place] on interactive map" +- "Find [a place] on interactive map" + +## Instructions + +Call the `run_js` tool with the following exact parameters: + +- data: A JSON string with the following field + - location: The location to show on the map. diff --git a/skills/built-in/interactive-map/scripts/index.html b/skills/built-in/interactive-map/scripts/index.html new file mode 100644 index 000000000..69fa121bf --- /dev/null +++ b/skills/built-in/interactive-map/scripts/index.html @@ -0,0 +1,48 @@ + + + + + + + + + Interactive map + + + + + + diff --git a/skills/built-in/kitchen-adventure/SKILL.md b/skills/built-in/kitchen-adventure/SKILL.md new file mode 100644 index 000000000..958b0673e --- /dev/null +++ b/skills/built-in/kitchen-adventure/SKILL.md @@ -0,0 +1,55 @@ +--- +name: kitchen-adventure +description: Act as a dungeon master for a text-based adventure set in a world where everyone is a sentient kitchen appliance. Trigger when user says "start kitchen adventure". +--- + +# Kitchen Adventure + +## Instructions + +When the user initiates a session, you must transform into the +**Head Chef (DM)**. Follow these operational rules to maintain the +"Micro-Cosmos" immersion: + +* **World-Building (The Kitchen-Scale):** Every location is a kitchen zone + reimagined as an epic landscape. + * The "Stainless Steel Plains" (the countertop). + * The "Tundra of the Sub-Zero" (the freezer). + * The "Caverns of the Under-Sink" (storage). +* +* **Appliance Physics:** Characters move and interact based on their real-world + functions. + * A Toaster "dashes" by popping up. + * A Blender "rages" by spinning its blades. + * A Fridge is a lumbering, cold-hearted giant. + +* **The Narrative Boundary:** **Never** write the player's dialogue or actions. + Describe the world's reaction to their input, then stop and wait for their + turn. + +* **Dynamic Stakes:** Scale household hazards into high-level threats. A spilled + glass of juice is a "Citrus Flash Flood"; a stray fork is a "Fallen Titan's + Spear." + +* **Tone:** Maintain a "Serious-Whimsical" tone. Treat a quest for the "Sacred + Sourdough Starter" with the same gravity as a quest for the Holy Grail. + +## Output Format + +Every DM response must use the following structure to ensure gameplay clarity: + +### [Current Location Name] + +*A vivid, sensory description of the area (e.g., "The air here smells of burnt +toast and ancient grease").* +*Limited to only 1 short sentence* + +--- + +**The Situation:** +(Describe the immediate scene, any NPCs present, and any obstacles or threats.) +*Limited to only 1-2 short sentences* + +**What do you do?** +(Provide a brief prompt or 3 suggested actions to keep the momentum going.) +After user replies, continue the adventure. diff --git a/skills/built-in/mood-tracker/SKILL.md b/skills/built-in/mood-tracker/SKILL.md new file mode 100644 index 000000000..0b9fee01e --- /dev/null +++ b/skills/built-in/mood-tracker/SKILL.md @@ -0,0 +1,123 @@ +--- +name: mood-tracker +description: A simple mood tracking skill that stores your daily mood and comments. Use this when the user wants to log their mood, track how they feel, or see their mood history. +--- + +# Mood Tracker + +## Instructions + +The `mood-tracker` skill helps you keep track of your daily emotional well-being. You can log your mood on a scale of 1 to 10 and add a short comment about how you're feeling. + +### Actions + +#### 1. Log Mood +When a user wants to log their mood, call the `run_js` tool with: +- **script name**: `index.html` +- **data**: A JSON string with: + - `action`: "log_mood" + - `score`: Number (1-10) + - `comment`: String (Optional) + - `date`: String. **IMPORTANT**: Identify the date for the entry. + - If user says "today", pass "today". + - If user says "yesterday", pass "yesterday". + - If user gives a specific date (e.g., "March 18"), format it as **YYYY-MM-DD** or pass the original date string. + - If no date is mentioned, default to "today". + +#### 2. Get Mood for a Specific Date +When a user asks what their mood was on a specific date, call the `run_js` tool with: +- **script name**: `index.html` +- **data**: A JSON string with: + - `action`: "get_mood" + - `date`: String (Identify the date from the user's request) + +#### 3. Get History / Show Dashboard +When a user wants to see their mood history ("last week", "past 10 days") or the dashboard, call the `run_js` tool with: +- **script name**: `index.html` +- **data**: A JSON string with: + - `action`: "get_history" + - `days`: Number (Optional, default 7. E.g., for "last week" use 7) + - `show_dashboard`: Boolean (Optional) + +#### 4. Plot Mood Trends (Line Chart) +When a user wants to visualize their mood trends with a chart (e.g., "Plot my mood for 7 days"), call the `run_js` tool with: +- **script name**: `index.html` +- **data**: A JSON string with: + - `action`: "get_history" + - `days`: Number (Optional, default 7) + - `show_dashboard`: `true` + - **TIP**: This will trigger the plotting view in the dashboard. + +#### 5. Analyze Trends and Patterns +When a user asks for an analysis of their mood (e.g., "Are there any trends?", "Am I feeling better?"), follow these steps: +1. Call `run_js` with `action: "get_history"` and an appropriate `days` count (e.g., 30 for a monthly analysis). +2. Once you receive the JSON history, analyze the scores and comments. +3. Provide a thoughtful response to the user covering: + - General trend (improving, declining, stable). + - Any clusters of particularly good or bad days. + - Themes or patterns found in the comments. + +#### 6. Delete Mood for a Specific Date +When a user wants to delete only a single day's entry (e.g., "Delete my mood for today"), call the `run_js` tool with: +- **script name**: `index.html` +- **data**: A JSON string with: + - `action`: "delete_mood" + - `date`: String (Identify the date) + +#### 7. Export Data (Backup) +When a user wants to backup or export their data, call the `run_js` tool with: +- **script name**: `index.html` +- **data**: A JSON string with: + - `action`: "export_data" + +#### 8. Wipe All Data +When a user wants to clear their entire mood history and start fresh, call the `run_js` tool with: +- **script name**: `index.html` +- **data**: A JSON string with: + - `action`: "wipe_data" + +### Sample Commands + +You can use these samples to interact with the mood tracker: + +- **Logging Mood:** + - "Log my mood as 8 today, feeling great!" + - "Set my mood yesterday as a 2" + - "Set my mood on March 18, 2026 as a 1" + - "I'm feeling like a 5 today, a bit tired." + - "Last Friday I felt like a 7." + - "Record a mood of 9 for me." + +- **Viewing History:** + - "Show me my mood history." + - "Get my mood from last week." + - "How have I been feeling lately?" + - "Show my mood for the last 10 days." + - "Open the mood dashboard." + - "What was my mood on March 18?" + - "What was my mood yesterday?" + +- **Analyzing Trends:** + - "Analyze my mood for the last 30 days — are there any patterns?" + - "Am I generally feeling better or worse over time?" + - "Are there any clusters of bad days in my history?" + - "What do my recent comments suggest about my well-being?" + +- **Wiping & Deleting:** + - "Delete my mood for today." + - "Remove my mood log for yesterday." + - "Delete the entry for March 18." + - "Clear my mood history." (Use `wipe_data` for this) + - "Wipe my data." (Use `wipe_data` for this) + +- **Charting Trends:** + - "Plot my mood for the last 7 days." + - "Show me a chart of my mood this month." + - "Visualize my scores for the past 14 days." + - "Graph my mood progress." + +### Rules +- **Privacy**: All data is stored locally on your device. +- **No Entry**: If no mood entry exists for a specific date requested, explicitly inform the user that no entry was found for that date. +- **Updates**: Logging a mood for a date that already has an entry will update that entry. +- **Dashboard**: The dashboard is only shown when you explicitly ask to see your history or the dashboard itself. diff --git a/skills/built-in/mood-tracker/assets/dashboard.html b/skills/built-in/mood-tracker/assets/dashboard.html new file mode 100644 index 000000000..927f15d27 --- /dev/null +++ b/skills/built-in/mood-tracker/assets/dashboard.html @@ -0,0 +1,609 @@ + + + + + + + + Mood Tracker Dashboard + + + + + +
+
+

Mood Tracker

+
+ +
+ +
...
+ +
+ +
+
+
-
+
Score
+
+
No notes for today.
+
+ + +
+
Mood Trend Chart
+ +
+ +
+
+ + Your Mood Journey +
+
    + +
+
+ + +
+ + +
+
+ + + + diff --git a/skills/built-in/mood-tracker/scripts/index.html b/skills/built-in/mood-tracker/scripts/index.html new file mode 100644 index 000000000..0873040d0 --- /dev/null +++ b/skills/built-in/mood-tracker/scripts/index.html @@ -0,0 +1,150 @@ + + + + + + + + diff --git a/skills/built-in/qr-code/SKILL.md b/skills/built-in/qr-code/SKILL.md new file mode 100644 index 000000000..98817b04f --- /dev/null +++ b/skills/built-in/qr-code/SKILL.md @@ -0,0 +1,11 @@ +--- +name: qr-code +description: Generates a QR code for the given url. +--- + +# Instructions + +You MUST use the `run_js` tool with the following exact parameters: + +- data: A JSON string with the following fields: + - url: String - the url to create QR code for diff --git a/skills/built-in/qr-code/scripts/index.html b/skills/built-in/qr-code/scripts/index.html new file mode 100644 index 000000000..12ba2017c --- /dev/null +++ b/skills/built-in/qr-code/scripts/index.html @@ -0,0 +1,108 @@ + + + + + + + + QR Code Generator + + + + + + + + + + diff --git a/skills/built-in/query-wikipedia/SKILL.md b/skills/built-in/query-wikipedia/SKILL.md new file mode 100644 index 000000000..2d3db0110 --- /dev/null +++ b/skills/built-in/query-wikipedia/SKILL.md @@ -0,0 +1,17 @@ +--- +name: query-wikipedia +description: Query summary from Wikipedia for a given topic. +--- + +# Query Wiki + +## Instructions + +Call the `run_js` tool using `index.html` and a JSON string for `data` with the following fields: +- **topic**: Required. Extract ONLY the primary entity, person, or event (e.g., "2026 Oscars", "Albert Einstein"). You MUST REMOVE all specific question details, action words, or conversational text (e.g., do NOT include words like "winner", "best picture", "who won", "history of"). Search for the broad subject so the tool can return the main article. +- **lang**: Required. The 2-letter language code. This code MUST match the language of the keywords you provided in the `topic` field. Use standard codes, e.g., "en" (English), "es" (Spanish), "zh" (Chinese), "fr" (French), "de" (German), "ja" (Japanese), "ko" (Korean), "it" (Italian), "pt" (Portuguese), "ru" (Russian), "ar" (Arabic), "hi" (Hindi). + +**Constraints:** +- Provide a concise summary (1-3 complete sentences) to conserve context. Always ensure your response ends with a finished sentence. your response MUST BE written in the SAME language as the user's original prompt. +- For recurring events or time-sensitive facts, query the specific iteration (e.g., "2026 Oscars"). If the user omits the year, default to the current year. +- If the exact answer to the user's question is not found in the extract, briefly state this, then proactively offer a related piece of information that *was* found in the text. \ No newline at end of file diff --git a/skills/built-in/query-wikipedia/scripts/index.html b/skills/built-in/query-wikipedia/scripts/index.html new file mode 100644 index 000000000..3bf01c630 --- /dev/null +++ b/skills/built-in/query-wikipedia/scripts/index.html @@ -0,0 +1,143 @@ + + + + + + + Query Wiki + + + + + diff --git a/skills/built-in/send-email/SKILL.md b/skills/built-in/send-email/SKILL.md new file mode 100644 index 000000000..97f2b8cec --- /dev/null +++ b/skills/built-in/send-email/SKILL.md @@ -0,0 +1,16 @@ +--- +name: send-email +description: Send an email. +--- + +# Send email + +## Instructions + +Call the `run_intent` tool with the following exact parameters: + +- intent: send_email +- parameters: A JSON string with the following fields: + - extra_email: the email address to send the email to. String. + - extra_subject: the subject of the email. String. + - extra_text: the body of the email. String. \ No newline at end of file diff --git a/skills/built-in/text-spinner/SKILL.md b/skills/built-in/text-spinner/SKILL.md new file mode 100644 index 000000000..558ab6195 --- /dev/null +++ b/skills/built-in/text-spinner/SKILL.md @@ -0,0 +1,11 @@ +--- +name: text-spinner +description: Spin the given text on my head. +--- + +# Instructions + +You MUST use the `run_js` tool with the following exact parameters: + +- data: A JSON string with the following fields: + - label: The text string to spin on my head. diff --git a/skills/built-in/text-spinner/assets/webview.html b/skills/built-in/text-spinner/assets/webview.html new file mode 100644 index 000000000..97e6da4c5 --- /dev/null +++ b/skills/built-in/text-spinner/assets/webview.html @@ -0,0 +1,157 @@ + + + + + + + Full-Screen 3D Head Tracker + + + + +
Syncing Neural Engine...
+ +
+ + +
Label
+
+ + + + diff --git a/skills/built-in/text-spinner/scripts/index.html b/skills/built-in/text-spinner/scripts/index.html new file mode 100644 index 000000000..07504285f --- /dev/null +++ b/skills/built-in/text-spinner/scripts/index.html @@ -0,0 +1,39 @@ + + + + + + + + + Create github issue + + + + + diff --git a/skills/featured/mood-music/README.md b/skills/featured/mood-music/README.md new file mode 100644 index 000000000..a7aeabdb0 --- /dev/null +++ b/skills/featured/mood-music/README.md @@ -0,0 +1,24 @@ +Mood Music Skill +============ + +skill for gallery to generate music based on your mood using Loudly API. + +Loudly API key can be obtained by registrating an account with \ +https://www.loudly.com then go to https://www.loudly.com/developers/apps + +Copyright 2026 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); \ +you may not use this file except in compliance with the License. \ +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software \ +distributed under the License is distributed on an "AS IS" BASIS, \ +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. \ +See the License for the specific language governing permissions and \ +limitations under the License. + + + diff --git a/skills/featured/mood-music/SKILL.md b/skills/featured/mood-music/SKILL.md new file mode 100644 index 000000000..05a32fe05 --- /dev/null +++ b/skills/featured/mood-music/SKILL.md @@ -0,0 +1,40 @@ +--- +name: mood-music +description: A skill to suggest or play music based on the user's mood, including analyzing images or audio, by querying available genres and generating music via the Loudly API. +metadata: + require-secret: true + require-secret-description: you can get api key from https://www.loudly.com/developers/apps after registering an account + homepage: https://github.com/google-ai-edge/gallery/tree/main/skills/featured/mood-music +--- + +# Mood Music + +## Instructions + +You MUST use a strict two-step process to generate music. This ensures you only request musical genres that are currently supported by the Loudly API. + +### Step 1: Fetch Available Genres +Call the `run_js` tool with `get_genres.html` as the script name and an empty JSON payload (`{}`). +- This will return a list of currently available genres and their descriptions. +- You MUST review this list to understand the available musical palettes. + +### Step 2: Analyze and Generate +Once you have the valid genres, map the user's request to the best fit and call `run_js` with `index.html` to generate the track. + +**Handling Inputs:** +- **Text Inputs**: Translate abstract mood requests into a concrete, matching genre from the fetched list, along with an appropriate energy level. +- **Media Inputs (Images/Audio)**: If the user provides an image or audio clip, analyze the media to determine its underlying mood, atmosphere, or vibe. Translate this analysis strictly into one of the fetched genres. + +**Generation Payload (for index.html):** +Your JSON payload for `index.html` MUST strictly use these text fields to represent the vibe: +- **genre**: String, Required. MUST be an exact match to a genre name retrieved in Step 1. +- **genre_blend**: String, Optional. A secondary genre to blend (also from Step 1). +- **duration**: Integer, Optional. Length in seconds (30-420). Default is 120. +- **energy**: String, Optional. Vibe ("low", "high", "original"). +- **bpm**: Integer, Optional. Specific tempo in Beats Per Minute. + +### Invocation Triggers +You should invoke this skill when the user: +- Asks for music for a specific mood. +- Asks for playlist ideas for a vibe. +- Uploads an image or audio clip and asks for music to match it. diff --git a/skills/featured/mood-music/assets/.gitkeep b/skills/featured/mood-music/assets/.gitkeep new file mode 100644 index 000000000..eb5033952 --- /dev/null +++ b/skills/featured/mood-music/assets/.gitkeep @@ -0,0 +1 @@ +# Assets folder for mood-music skill diff --git a/skills/featured/mood-music/assets/webview.html b/skills/featured/mood-music/assets/webview.html new file mode 100644 index 000000000..c3c747ae5 --- /dev/null +++ b/skills/featured/mood-music/assets/webview.html @@ -0,0 +1,382 @@ + + + + + + + + Mood Music Player | Loudly + + + + + +
+
+

Vibe Track

+

Wait for generation

+
+ +
+
+
+
+
+
+ 0:00 + 0:00 +
+
+ + +
+
+ + + + + + diff --git a/skills/featured/mood-music/scripts/.gitkeep b/skills/featured/mood-music/scripts/.gitkeep new file mode 100644 index 000000000..9e18c5339 --- /dev/null +++ b/skills/featured/mood-music/scripts/.gitkeep @@ -0,0 +1 @@ +# Scripts folder for mood-music skill diff --git a/skills/featured/mood-music/scripts/get_genres.html b/skills/featured/mood-music/scripts/get_genres.html new file mode 100644 index 000000000..ffd495681 --- /dev/null +++ b/skills/featured/mood-music/scripts/get_genres.html @@ -0,0 +1,34 @@ + + + diff --git a/skills/featured/mood-music/scripts/index.html b/skills/featured/mood-music/scripts/index.html new file mode 100644 index 000000000..025aa6ad2 --- /dev/null +++ b/skills/featured/mood-music/scripts/index.html @@ -0,0 +1,130 @@ + + + + + + + + +
+

Mood Music Local Scenario Test Area

+ +

+    
+ + + + diff --git a/skills/featured/restaurant-roulette/README.md b/skills/featured/restaurant-roulette/README.md new file mode 100644 index 000000000..c9bb61517 --- /dev/null +++ b/skills/featured/restaurant-roulette/README.md @@ -0,0 +1,18 @@ +Restaurant Roulette Skill +======== + +This skill searches for up to 10 restaurants matching a specific cuisine and location in a spin wheel. + +Copyright 2026 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); \ +you may not use this file except in compliance with the License. \ +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software \ +distributed under the License is distributed on an "AS IS" BASIS, \ +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. \ +See the License for the specific language governing permissions and \ +limitations under the License. \ No newline at end of file diff --git a/skills/featured/restaurant-roulette/SKILL.md b/skills/featured/restaurant-roulette/SKILL.md new file mode 100644 index 000000000..c4bc00ae0 --- /dev/null +++ b/skills/featured/restaurant-roulette/SKILL.md @@ -0,0 +1,30 @@ +--- +name: restaurant-roulette +description: Show a roulette wheel to allow user to randomly select a restaurant based on location and cuisine. +metadata: + require-secret: true + require-secret-description: you can get api key from https://ai.google.dev/gemini-api/docs/api-key + homepage: https://github.com/google-ai-edge/gallery/tree/main/skills/featured/restaurant-roulette +--- + +# Restaurant Roulette + +This skill searches for up to 10 restaurants matching a specific cuisine and location in a spin wheel. + +## Examples + +* "Suggest Mexican food in San Jose." +* "Find a random Italian restaurant near Sunnyvale." +* "Where should I get Sushi in San Francisco today?" +* "Show a restaurant roulette for Indian food in Palo Alto." + +## Instructions + +Call the `run_js` tool with the following exact parameters: +- data: A JSON string with the following fields + - location: the target city or location (e.g., "San Jose", "Sunnyvale", "San Francisco"). + - cuisine: the style of food or cuisine desired (e.g., "Mexican", "Italian", "Indian", "Sushi"). + +DO NOT use any other tool, DO NOT call `run_intent`. + +IMPORTANT: When the wheel is generated, DO NOT pick a winner for the user or make up a restaurant. Simply return the requested webview and tell the user to tap the preview card to spin the wheel themselves. \ No newline at end of file diff --git a/skills/featured/restaurant-roulette/assets/ui.html b/skills/featured/restaurant-roulette/assets/ui.html new file mode 100644 index 000000000..994a7253b --- /dev/null +++ b/skills/featured/restaurant-roulette/assets/ui.html @@ -0,0 +1,310 @@ + + + + + + + + Roulette UI + + + +

Loading...

+ +
+
+ + +
+ + + +
+
🎉 WINNER! 🎉
+
Restaurant Name
+ +
+ + + + diff --git a/skills/featured/restaurant-roulette/assets/webview.html b/skills/featured/restaurant-roulette/assets/webview.html new file mode 100644 index 000000000..e63975b50 --- /dev/null +++ b/skills/featured/restaurant-roulette/assets/webview.html @@ -0,0 +1,101 @@ + + + + + + + + Preview Card + + + +
🎰
+

Loading...

+

Tap anywhere to open the wheel

+
SPIN ROULETTE
+ + + + diff --git a/skills/featured/restaurant-roulette/scripts/index.html b/skills/featured/restaurant-roulette/scripts/index.html new file mode 100644 index 000000000..d816479c7 --- /dev/null +++ b/skills/featured/restaurant-roulette/scripts/index.html @@ -0,0 +1,29 @@ + + + + + + + + Restaurant Roulette + + + + + diff --git a/skills/featured/restaurant-roulette/scripts/index.js b/skills/featured/restaurant-roulette/scripts/index.js new file mode 100644 index 000000000..c8695c676 --- /dev/null +++ b/skills/featured/restaurant-roulette/scripts/index.js @@ -0,0 +1,109 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +window['ai_edge_gallery_get_result'] = async (dataStr, secret) => { + try { + const jsonData = JSON.parse(dataStr || '{}'); + const location = jsonData.location || 'Mountain View, CA'; + const cuisine = jsonData.cuisine || 'Sushi'; + + // // 1. Generate the data + // const prefixes = ["The", "Golden", "Epic", "Supreme", "Authentic", + // "Local", "Happy", "Urban", "Secret", "Ninja"]; const suffixes = ["Spot", + // "House", "Palace", "Kitchen", "Diner", "Grill", "Eats", "Bites", "Cafe", + // "Express"]; + + // let places = []; + // for(let i = 0; i < 10; i++) { + // places.push(`${prefixes[i]} ${cuisine} ${suffixes[i]}`); + // } + + const GEMINI_API_KEY = secret || 'YOUR_GEMINI_API_KEY'; + if (GEMINI_API_KEY === 'YOUR_GEMINI_API_KEY') { + console.warn('GEMINI_API_KEY is missing. Calls to Gemini API will likely fail.'); + } + + // We simplified the prompt because we are using strict JSON mode now + const prompt = `List 10 real, highly-rated ${cuisine} restaurants in ${ + location}. Within 15 miles location range`; + + const url = + `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${ + GEMINI_API_KEY}`; + + let places = []; + + try { + const response = await fetch(url, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + contents: [{parts: [{text: prompt}]}], + // THIS IS THE MAGIC FIX: Forces Gemini to return pure JSON + generationConfig: { + responseMimeType: 'application/json', + // We tell it exactly what shape the JSON should be: an array of + // strings + responseSchema: {type: 'ARRAY', items: {type: 'STRING'}} + } + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP Error ${response.status}`); + } + + const data = await response.json(); + + if (data.candidates && data.candidates.length > 0) { + const rawText = data.candidates[0].content.parts[0].text; + places = JSON.parse(rawText); + places.sort(() => 0.5 - Math.random()); + } else { + throw new Error('Empty response from AI'); + } + } catch (apiError) { + console.warn('Gemini API failed.', apiError); + // IF IT FAILS, THE WHEEL WILL NOW SHOW YOU THE EXACT ERROR MESSAGE! + let errorMessage = apiError.message; + if (errorMessage.length > 15) + errorMessage = errorMessage.substring(0, 15); + places = ['Error:', errorMessage, 'Check', 'Console']; + } + + // 2. Compress the data + const placeString = places.join('|'); + const compressedData = btoa(unescape(encodeURIComponent(placeString))); + + // 3. Build the REAL local URL + const baseUrl = 'webview.html'; + const fullUrl = `${baseUrl}?c=${encodeURIComponent(cuisine)}&l=${ + encodeURIComponent(location)}&data=${compressedData}&v=${Date.now()}`; + + // 4. Return ONLY the webview. This guarantees the preview card appears and + // stops the AI from overriding it! + return JSON.stringify({ + webview: {url: fullUrl}, + result: + 'Here is the restaurant roulette wheel you requested! Tap the preview card to spin it and pick a winner!' + }); + + } catch (e) { + console.error(e); + return JSON.stringify({error: `Failed to load roulette: ${e.message}`}); + } +}; \ No newline at end of file diff --git a/skills/featured/virtual-piano/README.md b/skills/featured/virtual-piano/README.md new file mode 100644 index 000000000..d61c82680 --- /dev/null +++ b/skills/featured/virtual-piano/README.md @@ -0,0 +1,20 @@ +Virtual Piano Skill +======== + +allows Agent chat to create virtual piano keyboard + +piano sound are from https://github.com/fuhton/piano-mp3 under MIT license + +Copyright 2026 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); \ +you may not use this file except in compliance with the License. \ +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software \ +distributed under the License is distributed on an "AS IS" BASIS, \ +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. \ +See the License for the specific language governing permissions and \ +limitations under the License. \ No newline at end of file diff --git a/skills/featured/virtual-piano/SKILL.md b/skills/featured/virtual-piano/SKILL.md new file mode 100644 index 000000000..025d5df4d --- /dev/null +++ b/skills/featured/virtual-piano/SKILL.md @@ -0,0 +1,24 @@ +--- +name: virtual-piano +description: Show a virtual piano to play music +metadata: + homepage: https://github.com/google-ai-edge/gallery/tree/main/skills/featured/virtual-piano +--- + +# Virtual Piano + +A playable, horizontally-scrolling virtual piano keyboard that uses web audio. + +## Files +- `index.html`: The local entry point that loads the script. +- `index.js`: Returns the webview URL pointing to the GitHub-hosted UI. + +## Prompts / Triggers +- "Open virtual piano" +- "Play the piano" +- "I want to play piano" +- "Show me a piano keyboard" + +## Instructions + +Call the `run_js` tool \ No newline at end of file diff --git a/skills/featured/virtual-piano/assets/assets/1.mp3 b/skills/featured/virtual-piano/assets/assets/1.mp3 new file mode 100644 index 000000000..933537b01 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/1.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/10.mp3 b/skills/featured/virtual-piano/assets/assets/10.mp3 new file mode 100644 index 000000000..2b83de013 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/10.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/11.mp3 b/skills/featured/virtual-piano/assets/assets/11.mp3 new file mode 100644 index 000000000..9005288fb Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/11.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/12.mp3 b/skills/featured/virtual-piano/assets/assets/12.mp3 new file mode 100644 index 000000000..547ac8075 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/12.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/13.mp3 b/skills/featured/virtual-piano/assets/assets/13.mp3 new file mode 100644 index 000000000..30d5a87bc Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/13.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/14.mp3 b/skills/featured/virtual-piano/assets/assets/14.mp3 new file mode 100644 index 000000000..4197a24dc Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/14.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/15.mp3 b/skills/featured/virtual-piano/assets/assets/15.mp3 new file mode 100644 index 000000000..5671d5b3b Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/15.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/16.mp3 b/skills/featured/virtual-piano/assets/assets/16.mp3 new file mode 100644 index 000000000..df750d2a8 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/16.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/17.mp3 b/skills/featured/virtual-piano/assets/assets/17.mp3 new file mode 100644 index 000000000..33fc06974 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/17.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/18.mp3 b/skills/featured/virtual-piano/assets/assets/18.mp3 new file mode 100644 index 000000000..5dfa18ab2 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/18.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/19.mp3 b/skills/featured/virtual-piano/assets/assets/19.mp3 new file mode 100644 index 000000000..f326d22a9 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/19.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/2.mp3 b/skills/featured/virtual-piano/assets/assets/2.mp3 new file mode 100644 index 000000000..15ee18d89 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/2.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/20.mp3 b/skills/featured/virtual-piano/assets/assets/20.mp3 new file mode 100644 index 000000000..f7dd3cf4f Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/20.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/21.mp3 b/skills/featured/virtual-piano/assets/assets/21.mp3 new file mode 100644 index 000000000..e779a58ac Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/21.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/22.mp3 b/skills/featured/virtual-piano/assets/assets/22.mp3 new file mode 100644 index 000000000..9807c2685 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/22.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/23.mp3 b/skills/featured/virtual-piano/assets/assets/23.mp3 new file mode 100644 index 000000000..f781bad78 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/23.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/24.mp3 b/skills/featured/virtual-piano/assets/assets/24.mp3 new file mode 100644 index 000000000..5e5a3889c Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/24.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/25.mp3 b/skills/featured/virtual-piano/assets/assets/25.mp3 new file mode 100644 index 000000000..5837436bd Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/25.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/26.mp3 b/skills/featured/virtual-piano/assets/assets/26.mp3 new file mode 100644 index 000000000..84fb497a5 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/26.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/27.mp3 b/skills/featured/virtual-piano/assets/assets/27.mp3 new file mode 100644 index 000000000..0d1d24c97 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/27.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/28.mp3 b/skills/featured/virtual-piano/assets/assets/28.mp3 new file mode 100644 index 000000000..4caecf674 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/28.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/29.mp3 b/skills/featured/virtual-piano/assets/assets/29.mp3 new file mode 100644 index 000000000..8e8fe5814 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/29.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/3.mp3 b/skills/featured/virtual-piano/assets/assets/3.mp3 new file mode 100644 index 000000000..14564ff8e Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/3.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/30.mp3 b/skills/featured/virtual-piano/assets/assets/30.mp3 new file mode 100644 index 000000000..008e9244c Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/30.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/31.mp3 b/skills/featured/virtual-piano/assets/assets/31.mp3 new file mode 100644 index 000000000..9c51e541f Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/31.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/32.mp3 b/skills/featured/virtual-piano/assets/assets/32.mp3 new file mode 100644 index 000000000..76f348deb Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/32.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/33.mp3 b/skills/featured/virtual-piano/assets/assets/33.mp3 new file mode 100644 index 000000000..eff0a577c Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/33.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/34.mp3 b/skills/featured/virtual-piano/assets/assets/34.mp3 new file mode 100644 index 000000000..d3c7e3a80 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/34.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/35.mp3 b/skills/featured/virtual-piano/assets/assets/35.mp3 new file mode 100644 index 000000000..f931c0431 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/35.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/36.mp3 b/skills/featured/virtual-piano/assets/assets/36.mp3 new file mode 100644 index 000000000..b6b83dc40 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/36.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/37.mp3 b/skills/featured/virtual-piano/assets/assets/37.mp3 new file mode 100644 index 000000000..0014c7704 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/37.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/38.mp3 b/skills/featured/virtual-piano/assets/assets/38.mp3 new file mode 100644 index 000000000..3f4093506 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/38.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/39.mp3 b/skills/featured/virtual-piano/assets/assets/39.mp3 new file mode 100644 index 000000000..bacda9efa Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/39.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/4.mp3 b/skills/featured/virtual-piano/assets/assets/4.mp3 new file mode 100644 index 000000000..2a4ecaa57 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/4.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/40.mp3 b/skills/featured/virtual-piano/assets/assets/40.mp3 new file mode 100644 index 000000000..40ed08313 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/40.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/41.mp3 b/skills/featured/virtual-piano/assets/assets/41.mp3 new file mode 100644 index 000000000..faa337f6f Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/41.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/42.mp3 b/skills/featured/virtual-piano/assets/assets/42.mp3 new file mode 100644 index 000000000..0e0a4b82d Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/42.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/43.mp3 b/skills/featured/virtual-piano/assets/assets/43.mp3 new file mode 100644 index 000000000..79c6fd077 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/43.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/44.mp3 b/skills/featured/virtual-piano/assets/assets/44.mp3 new file mode 100644 index 000000000..d37f8b0e5 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/44.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/45.mp3 b/skills/featured/virtual-piano/assets/assets/45.mp3 new file mode 100644 index 000000000..baae9a469 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/45.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/46.mp3 b/skills/featured/virtual-piano/assets/assets/46.mp3 new file mode 100644 index 000000000..d6a0cae4b Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/46.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/47.mp3 b/skills/featured/virtual-piano/assets/assets/47.mp3 new file mode 100644 index 000000000..4a0a1f1de Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/47.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/48.mp3 b/skills/featured/virtual-piano/assets/assets/48.mp3 new file mode 100644 index 000000000..1a1facd7f Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/48.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/49.mp3 b/skills/featured/virtual-piano/assets/assets/49.mp3 new file mode 100644 index 000000000..978ab6993 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/49.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/5.mp3 b/skills/featured/virtual-piano/assets/assets/5.mp3 new file mode 100644 index 000000000..bdcc9e014 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/5.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/50.mp3 b/skills/featured/virtual-piano/assets/assets/50.mp3 new file mode 100644 index 000000000..27d5ac1ea Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/50.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/51.mp3 b/skills/featured/virtual-piano/assets/assets/51.mp3 new file mode 100644 index 000000000..088ce9fe7 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/51.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/52.mp3 b/skills/featured/virtual-piano/assets/assets/52.mp3 new file mode 100644 index 000000000..76cc6e9cd Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/52.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/53.mp3 b/skills/featured/virtual-piano/assets/assets/53.mp3 new file mode 100644 index 000000000..a24ab5a6a Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/53.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/54.mp3 b/skills/featured/virtual-piano/assets/assets/54.mp3 new file mode 100644 index 000000000..8611ea731 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/54.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/55.mp3 b/skills/featured/virtual-piano/assets/assets/55.mp3 new file mode 100644 index 000000000..bdd97bc02 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/55.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/56.mp3 b/skills/featured/virtual-piano/assets/assets/56.mp3 new file mode 100644 index 000000000..e1c0d338f Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/56.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/57.mp3 b/skills/featured/virtual-piano/assets/assets/57.mp3 new file mode 100644 index 000000000..4c05f9029 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/57.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/58.mp3 b/skills/featured/virtual-piano/assets/assets/58.mp3 new file mode 100644 index 000000000..eef75feb3 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/58.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/59.mp3 b/skills/featured/virtual-piano/assets/assets/59.mp3 new file mode 100644 index 000000000..dd3a6a871 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/59.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/6.mp3 b/skills/featured/virtual-piano/assets/assets/6.mp3 new file mode 100644 index 000000000..8eb2b7a06 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/6.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/60.mp3 b/skills/featured/virtual-piano/assets/assets/60.mp3 new file mode 100644 index 000000000..a85898029 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/60.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/61.mp3 b/skills/featured/virtual-piano/assets/assets/61.mp3 new file mode 100644 index 000000000..ebdbe2b69 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/61.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/62.mp3 b/skills/featured/virtual-piano/assets/assets/62.mp3 new file mode 100644 index 000000000..db976d1b2 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/62.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/63.mp3 b/skills/featured/virtual-piano/assets/assets/63.mp3 new file mode 100644 index 000000000..14d2f3992 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/63.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/64.mp3 b/skills/featured/virtual-piano/assets/assets/64.mp3 new file mode 100644 index 000000000..bf7b73531 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/64.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/65.mp3 b/skills/featured/virtual-piano/assets/assets/65.mp3 new file mode 100644 index 000000000..f49062f39 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/65.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/66.mp3 b/skills/featured/virtual-piano/assets/assets/66.mp3 new file mode 100644 index 000000000..0d78e08ba Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/66.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/67.mp3 b/skills/featured/virtual-piano/assets/assets/67.mp3 new file mode 100644 index 000000000..c04f57a87 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/67.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/68.mp3 b/skills/featured/virtual-piano/assets/assets/68.mp3 new file mode 100644 index 000000000..4f4f09c17 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/68.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/69.mp3 b/skills/featured/virtual-piano/assets/assets/69.mp3 new file mode 100644 index 000000000..6519b7082 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/69.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/7.mp3 b/skills/featured/virtual-piano/assets/assets/7.mp3 new file mode 100644 index 000000000..9e64aa40c Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/7.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/70.mp3 b/skills/featured/virtual-piano/assets/assets/70.mp3 new file mode 100644 index 000000000..f1e0456c9 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/70.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/71.mp3 b/skills/featured/virtual-piano/assets/assets/71.mp3 new file mode 100644 index 000000000..1cf1dfd98 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/71.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/72.mp3 b/skills/featured/virtual-piano/assets/assets/72.mp3 new file mode 100644 index 000000000..4e525ac22 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/72.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/73.mp3 b/skills/featured/virtual-piano/assets/assets/73.mp3 new file mode 100644 index 000000000..4fd195979 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/73.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/74.mp3 b/skills/featured/virtual-piano/assets/assets/74.mp3 new file mode 100644 index 000000000..da71daaf1 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/74.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/75.mp3 b/skills/featured/virtual-piano/assets/assets/75.mp3 new file mode 100644 index 000000000..e5a018b68 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/75.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/76.mp3 b/skills/featured/virtual-piano/assets/assets/76.mp3 new file mode 100644 index 000000000..898cedacf Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/76.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/77.mp3 b/skills/featured/virtual-piano/assets/assets/77.mp3 new file mode 100644 index 000000000..4fb40110e Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/77.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/78.mp3 b/skills/featured/virtual-piano/assets/assets/78.mp3 new file mode 100644 index 000000000..18157d7d9 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/78.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/79.mp3 b/skills/featured/virtual-piano/assets/assets/79.mp3 new file mode 100644 index 000000000..b0506747d Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/79.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/8.mp3 b/skills/featured/virtual-piano/assets/assets/8.mp3 new file mode 100644 index 000000000..39f9dafdc Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/8.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/80.mp3 b/skills/featured/virtual-piano/assets/assets/80.mp3 new file mode 100644 index 000000000..60bdc0a1d Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/80.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/81.mp3 b/skills/featured/virtual-piano/assets/assets/81.mp3 new file mode 100644 index 000000000..c0a8e2786 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/81.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/82.mp3 b/skills/featured/virtual-piano/assets/assets/82.mp3 new file mode 100644 index 000000000..f33b6dfe4 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/82.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/83.mp3 b/skills/featured/virtual-piano/assets/assets/83.mp3 new file mode 100644 index 000000000..12ed229f1 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/83.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/84.mp3 b/skills/featured/virtual-piano/assets/assets/84.mp3 new file mode 100644 index 000000000..44fa78ef3 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/84.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/85.mp3 b/skills/featured/virtual-piano/assets/assets/85.mp3 new file mode 100644 index 000000000..093b69cf9 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/85.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/86.mp3 b/skills/featured/virtual-piano/assets/assets/86.mp3 new file mode 100644 index 000000000..a8d7e2bdc Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/86.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/87.mp3 b/skills/featured/virtual-piano/assets/assets/87.mp3 new file mode 100644 index 000000000..db13d915f Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/87.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/88.mp3 b/skills/featured/virtual-piano/assets/assets/88.mp3 new file mode 100644 index 000000000..a83ecee68 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/88.mp3 differ diff --git a/skills/featured/virtual-piano/assets/assets/9.mp3 b/skills/featured/virtual-piano/assets/assets/9.mp3 new file mode 100644 index 000000000..82663fe81 Binary files /dev/null and b/skills/featured/virtual-piano/assets/assets/9.mp3 differ diff --git a/skills/featured/virtual-piano/assets/ui.html b/skills/featured/virtual-piano/assets/ui.html new file mode 100644 index 000000000..9eaf7faac --- /dev/null +++ b/skills/featured/virtual-piano/assets/ui.html @@ -0,0 +1,244 @@ + + + + + + + + 3D 88-Key Virtual Piano + + + +
+
+
+
⟷ SLIDE HERE TO SCROLL ⟷
+
+
+ + + + diff --git a/skills/featured/virtual-piano/scripts/index.html b/skills/featured/virtual-piano/scripts/index.html new file mode 100644 index 000000000..03efd9c19 --- /dev/null +++ b/skills/featured/virtual-piano/scripts/index.html @@ -0,0 +1,30 @@ + + + + + + + + Virtual Piano Skill + + + + + + diff --git a/skills/featured/virtual-piano/scripts/index.js b/skills/featured/virtual-piano/scripts/index.js new file mode 100644 index 000000000..6b26ce173 --- /dev/null +++ b/skills/featured/virtual-piano/scripts/index.js @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +window['ai_edge_gallery_get_result'] = async (dataStr) => { + try { + // Points the app directly to your local UI! + const fullUrl = `ui.html?v=${Date.now()}`; + + return JSON.stringify({ + webview: {url: fullUrl}, + result: + 'Success. Tell the user to tap the preview card to play the piano.' + }); + + } catch (e) { + console.error(e); + return JSON.stringify({error: `Failed to load piano: ${e.message}`}); + } +}; \ No newline at end of file