diff --git a/packages/kbot/cat_gen_0.png b/packages/kbot/cat_gen_0.png new file mode 100644 index 00000000..f8701a11 Binary files /dev/null and b/packages/kbot/cat_gen_0.png differ diff --git a/packages/kbot/dist/win-64/tauri-app.exe b/packages/kbot/dist/win-64/tauri-app.exe index 634820e1..91823d8b 100644 Binary files a/packages/kbot/dist/win-64/tauri-app.exe and b/packages/kbot/dist/win-64/tauri-app.exe differ diff --git a/packages/kbot/gui/tauri-app/package-lock.json b/packages/kbot/gui/tauri-app/package-lock.json index bbbeadfd..bf9735af 100644 --- a/packages/kbot/gui/tauri-app/package-lock.json +++ b/packages/kbot/gui/tauri-app/package-lock.json @@ -9,6 +9,33 @@ "version": "0.1.0", "dependencies": { "@google/generative-ai": "^0.24.1", + "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-alert-dialog": "^1.1.1", + "@radix-ui/react-aspect-ratio": "^1.1.0", + "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-collapsible": "^1.1.0", + "@radix-ui/react-context-menu": "^2.2.1", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-hover-card": "^1.1.1", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-menubar": "^1.1.1", + "@radix-ui/react-navigation-menu": "^1.2.0", + "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-radio-group": "^1.2.0", + "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toast": "^1.2.1", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-toggle-group": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.4", "@tailwindcss/vite": "^4.1.13", "@tauri-apps/api": "^2.8.0", "@tauri-apps/cli": "^2.8.4", @@ -31,12 +58,24 @@ "@tauri-apps/plugin-store": "^2.4.0", "@tauri-apps/plugin-updater": "^2.9.0", "@tauri-apps/plugin-upload": "^2.3.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "input-otp": "^1.4.2", "lucide-react": "^0.544.0", "mime-types": "^2.1.35", + "next-themes": "^0.4.6", + "p-map": "^7.0.3", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-hook-form": "^7.63.0", + "react-resizable-panels": "^2.1.3", + "react-router-dom": "^7.9.1", "react-terminal": "^1.4.5", - "react-zoom-pan-pinch": "^3.7.0" + "react-zoom-pan-pinch": "^3.7.0", + "sonner": "^2.0.7", + "tailwind-merge": "^2.5.2", + "vaul": "^1.1.2" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.13", @@ -762,6 +801,44 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@google/generative-ai": { "version": "0.24.1", "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", @@ -828,6 +905,1360 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1832,7 +3263,7 @@ "version": "19.1.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1842,7 +3273,7 @@ "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -1869,6 +3300,18 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -1981,6 +3424,43 @@ "node": ">=18" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1988,11 +3468,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -2042,6 +3531,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.218", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", @@ -2168,12 +3663,31 @@ "node": ">=6.9.0" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/input-otp": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", + "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/is-standalone-pwa": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz", @@ -2574,6 +4088,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-releases": { "version": "2.0.21", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", @@ -2591,6 +4115,18 @@ "node": ">=0.10.0" } }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2665,6 +4201,22 @@ "react": "^19.1.1" } }, + "node_modules/react-hook-form": { + "version": "7.63.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", + "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -2675,6 +4227,123 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-resizable-panels": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.9.tgz", + "integrity": "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-router": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.1.tgz", + "integrity": "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.1.tgz", + "integrity": "sha512-U9WBQssBE9B1vmRjo9qTM7YRzfZ3lUxESIZnsf4VjR/lXYz9MHjvOxHzr/aUm4efpktbVOrF09rL/y4VHa8RMw==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-terminal": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/react-terminal/-/react-terminal-1.4.5.tgz", @@ -2756,6 +4425,22 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2765,6 +4450,16 @@ "node": ">=0.10.0" } }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", @@ -2826,6 +4521,12 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -2932,6 +4633,71 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/vite": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", diff --git a/packages/kbot/gui/tauri-app/package.json b/packages/kbot/gui/tauri-app/package.json index 0562408b..f91a0437 100644 --- a/packages/kbot/gui/tauri-app/package.json +++ b/packages/kbot/gui/tauri-app/package.json @@ -12,6 +12,33 @@ }, "dependencies": { "@google/generative-ai": "^0.24.1", + "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-alert-dialog": "^1.1.1", + "@radix-ui/react-aspect-ratio": "^1.1.0", + "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-collapsible": "^1.1.0", + "@radix-ui/react-context-menu": "^2.2.1", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-hover-card": "^1.1.1", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-menubar": "^1.1.1", + "@radix-ui/react-navigation-menu": "^1.2.0", + "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-radio-group": "^1.2.0", + "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toast": "^1.2.1", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-toggle-group": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.4", "@tailwindcss/vite": "^4.1.13", "@tauri-apps/api": "^2.8.0", "@tauri-apps/cli": "^2.8.4", @@ -34,12 +61,24 @@ "@tauri-apps/plugin-store": "^2.4.0", "@tauri-apps/plugin-updater": "^2.9.0", "@tauri-apps/plugin-upload": "^2.3.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "input-otp": "^1.4.2", "lucide-react": "^0.544.0", "mime-types": "^2.1.35", + "next-themes": "^0.4.6", + "p-map": "^7.0.3", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-hook-form": "^7.63.0", + "react-resizable-panels": "^2.1.3", + "react-router-dom": "^7.9.1", "react-terminal": "^1.4.5", - "react-zoom-pan-pinch": "^3.7.0" + "react-zoom-pan-pinch": "^3.7.0", + "sonner": "^2.0.7", + "tailwind-merge": "^2.5.2", + "vaul": "^1.1.2" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.13", diff --git a/packages/kbot/gui/tauri-app/src-tauri/2 b/packages/kbot/gui/tauri-app/src-tauri/2 new file mode 100644 index 00000000..dd71054c --- /dev/null +++ b/packages/kbot/gui/tauri-app/src-tauri/2 @@ -0,0 +1,7 @@ + +up to date, audited 333 packages in 1s + +38 packages are looking for funding + run `npm fund` for details + +found 0 vulnerabilities diff --git a/packages/kbot/gui/tauri-app/src-tauri/Cargo.lock b/packages/kbot/gui/tauri-app/src-tauri/Cargo.lock index 8bded590..8a78a7cc 100644 --- a/packages/kbot/gui/tauri-app/src-tauri/Cargo.lock +++ b/packages/kbot/gui/tauri-app/src-tauri/Cargo.lock @@ -5331,6 +5331,7 @@ dependencies = [ "tauri-plugin-log", "tauri-plugin-notification", "tauri-plugin-opener", + "tauri-plugin-os", "tauri-plugin-process", "tauri-plugin-shell", "tauri-plugin-store", @@ -5601,6 +5602,24 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-os" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a1c77ebf6f20417ab2a74e8c310820ba52151406d0c80fbcea7df232e3f6ba" +dependencies = [ + "gethostname", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "tauri-plugin", + "thiserror 2.0.16", +] + [[package]] name = "tauri-plugin-process" version = "2.3.0" diff --git a/packages/kbot/gui/tauri-app/src-tauri/Cargo.toml b/packages/kbot/gui/tauri-app/src-tauri/Cargo.toml index a2bca53d..8c1aedde 100644 --- a/packages/kbot/gui/tauri-app/src-tauri/Cargo.toml +++ b/packages/kbot/gui/tauri-app/src-tauri/Cargo.toml @@ -36,6 +36,7 @@ image = "0.25.8" glob = "0.3.3" tauri-plugin-notification = "2.3.1" log = "0.4" +tauri-plugin-os = "2" # Desktop-only dependencies (these may cause OpenSSL issues on Android) [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] diff --git a/packages/kbot/gui/tauri-app/src-tauri/capabilities/base.json b/packages/kbot/gui/tauri-app/src-tauri/capabilities/base.json index 013af15e..2f938157 100644 --- a/packages/kbot/gui/tauri-app/src-tauri/capabilities/base.json +++ b/packages/kbot/gui/tauri-app/src-tauri/capabilities/base.json @@ -102,6 +102,7 @@ "path": "$APPDATA/**" } ] - } + }, + "os:default" ] } \ No newline at end of file diff --git a/packages/kbot/gui/tauri-app/src-tauri/src/clipboard.rs b/packages/kbot/gui/tauri-app/src-tauri/src/clipboard.rs index 70717313..698cd513 100644 --- a/packages/kbot/gui/tauri-app/src-tauri/src/clipboard.rs +++ b/packages/kbot/gui/tauri-app/src-tauri/src/clipboard.rs @@ -33,11 +33,7 @@ fn save_rgba_image_to_temp( return Err(error_message.into()); }; - let file_name = format!( - "kbot_clipboard_{}.{}", - nanoid::nanoid!(), - candidate_format - ); + let file_name = format!("kbot_clipboard_{}.{}", nanoid::nanoid!(), candidate_format); let temp_path = temp_dir.join(file_name); img_buffer.save_with_format( @@ -71,7 +67,7 @@ async fn parse_clipboard_images( } Err(e) => { info!("[parse_clipboard_images] No image data found: {}", e); - + // Try to get file list (drag and drop from explorer/finder) match clipboard.get().file_list() { Ok(file_list) => { @@ -79,11 +75,12 @@ async fn parse_clipboard_images( for file in file_list { let path_str = file.to_string_lossy().to_string(); // Filter for image files only - if path_str.to_lowercase().ends_with(".png") + if path_str.to_lowercase().ends_with(".png") || path_str.to_lowercase().ends_with(".jpg") || path_str.to_lowercase().ends_with(".jpeg") || path_str.to_lowercase().ends_with(".gif") - || path_str.to_lowercase().ends_with(".webp") { + || path_str.to_lowercase().ends_with(".webp") + { result.push(path_str); } } diff --git a/packages/kbot/gui/tauri-app/src-tauri/src/handlers.rs b/packages/kbot/gui/tauri-app/src-tauri/src/handlers.rs index 40eb5863..99031e98 100644 --- a/packages/kbot/gui/tauri-app/src-tauri/src/handlers.rs +++ b/packages/kbot/gui/tauri-app/src-tauri/src/handlers.rs @@ -1,222 +1,260 @@ -use tauri::{Manager, Emitter}; -use serde::{Serialize, Deserialize}; -use dirs; - -use crate::{log_json, Counter, DebugMessages, DebugPayload}; - -#[derive(Serialize, Deserialize)] -pub struct Payload { - pub prompt: String, - pub files: Vec, - pub dst: String, -} - -#[derive(Serialize, Deserialize)] -pub struct IPCMessage { - #[serde(rename = "type")] - pub message_type: String, - pub data: serde_json::Value, - pub timestamp: Option, - pub id: Option, -} - -// Core command handlers -#[tauri::command] -pub fn submit_prompt(prompt: &str, files: Vec, dst: &str, window: tauri::Window) { - log_json("info", "submit_prompt command called", Some(serde_json::json!({ - "prompt": prompt, - "files": files, - "dst": dst - }))); - - let payload = Payload { - prompt: prompt.to_string(), - files, - dst: dst.to_string(), - }; - let json_payload = serde_json::to_string(&payload).unwrap(); - - log_json("info", "Sending JSON payload to stdout", Some(serde_json::json!({ - "payload_length": json_payload.len() - }))); - println!("{}", json_payload); - let _ = window.app_handle().exit(0); -} - -#[tauri::command] -pub fn log_error_to_console(error: &str) { - eprintln!("[WebView ERROR forwarded]: {}", error); -} - -#[tauri::command] -pub fn resolve_path_relative_to_home(absolute_path: String) -> Result { - let home_dir = dirs::home_dir().ok_or_else(|| "Could not find home directory".to_string())?; - let path_to_resolve = std::path::Path::new(&absolute_path); - let relative_path = pathdiff::diff_paths(path_to_resolve, home_dir) - .ok_or_else(|| "Failed to calculate relative path from home directory".to_string())?; - Ok(relative_path.to_string_lossy().to_string()) -} - -// Debug message handlers -#[tauri::command] -pub fn add_debug_message(message: String, level: String, data: Option, state: tauri::State<'_, DebugMessages>) -> Result<(), String> { - log_json(&level, &format!("Frontend: {}", message), data.clone()); - - let debug_payload = DebugPayload { - level, - message, - data, - }; - - let mut messages = state.0.lock().unwrap(); - messages.push(debug_payload); - - if messages.len() > 100 { - let len = messages.len(); - messages.drain(0..len - 100); - } - - Ok(()) -} - -#[tauri::command] -pub fn get_debug_messages(state: tauri::State<'_, DebugMessages>) -> Result, String> { - let messages = state.0.lock().unwrap(); - Ok(messages.clone()) -} - -#[tauri::command] -pub fn clear_debug_messages(state: tauri::State<'_, DebugMessages>) -> Result<(), String> { - let mut messages = state.0.lock().unwrap(); - messages.clear(); - Ok(()) -} - -// Counter handlers (legacy) -#[tauri::command] -pub fn increment_counter(state: tauri::State<'_, Counter>) -> Result { - let mut counter = state.0.lock().unwrap(); - *counter += 1; - Ok(*counter) -} - -#[tauri::command] -pub fn get_counter(state: tauri::State<'_, Counter>) -> Result { - let counter = state.0.lock().unwrap(); - Ok(*counter) -} - -#[tauri::command] -pub fn reset_counter(state: tauri::State<'_, Counter>) -> Result { - let mut counter = state.0.lock().unwrap(); - *counter = 0; - Ok(0) -} - -// IPC communication handlers -#[tauri::command] -pub fn send_ipc_message(message_type: String, data: serde_json::Value, _window: tauri::Window) -> Result<(), String> { - let ipc_message = IPCMessage { - message_type, - data, - timestamp: Some(std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis() as u64), - id: Some(format!("msg_{}_{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis(), - rand::random::())), - }; - - let json_message = serde_json::to_string(&ipc_message).unwrap(); - println!("{}", json_message); - - Ok(()) -} - -#[tauri::command] -pub fn send_message_to_stdout(message: String) -> Result<(), String> { - println!("{}", message); - Ok(()) -} - -// Image generation handlers -#[tauri::command] -pub fn generate_image_via_backend(prompt: String, files: Vec, dst: String) -> Result<(), String> { - let request = serde_json::json!({ - "type": "generate_request", - "prompt": prompt, - "files": files, - "dst": dst, - "timestamp": std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis() - }); - - println!("{}", serde_json::to_string(&request).unwrap()); - - Ok(()) -} - -#[tauri::command] -pub fn request_config_from_images(_app: tauri::AppHandle) -> Result<(), String> { - let request = serde_json::json!({ - "type": "config_request", - "timestamp": std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis() - }); - - println!("{}", serde_json::to_string(&request).unwrap()); - - Ok(()) -} - -#[tauri::command] -pub fn request_file_deletion(path: String) -> Result<(), String> { - let request = serde_json::json!({ - "type": "delete_request", - "path": path, - }); - - println!("{}", serde_json::to_string(&request).unwrap()); - - Ok(()) -} - -// Legacy direct handlers (not used via stdin) -#[tauri::command] -pub fn forward_config_to_frontend(prompt: Option, dst: Option, api_key: Option, files: Vec, app: tauri::AppHandle) -> Result<(), String> { - let config_data = serde_json::json!({ - "prompt": prompt, - "dst": dst, - "apiKey": api_key, - "files": files - }); - - if let Err(e) = app.emit("config-received", &config_data) { - return Err(format!("Failed to emit config: {}", e)); - } - - Ok(()) -} - -#[tauri::command] -pub fn forward_image_to_frontend(base64: String, mime_type: String, filename: String, app: tauri::AppHandle) -> Result<(), String> { - let image_data = serde_json::json!({ - "base64": base64, - "mimeType": mime_type, - "filename": filename - }); - - if let Err(e) = app.emit("image-received", &image_data) { - return Err(format!("Failed to emit image: {}", e)); - } - - Ok(()) -} +use dirs; +use serde::{Deserialize, Serialize}; +use tauri::{Emitter, Manager}; + +use crate::{log_json, Counter, DebugMessages, DebugPayload}; + +#[derive(Serialize, Deserialize)] +pub struct Payload { + pub prompt: String, + pub files: Vec, + pub dst: String, +} + +#[derive(Serialize, Deserialize)] +pub struct IPCMessage { + #[serde(rename = "type")] + pub message_type: String, + pub data: serde_json::Value, + pub timestamp: Option, + pub id: Option, +} + +// Core command handlers +#[tauri::command] +pub fn submit_prompt(prompt: &str, files: Vec, dst: &str, window: tauri::Window) { + log_json( + "info", + "submit_prompt command called", + Some(serde_json::json!({ + "prompt": prompt, + "files": files, + "dst": dst + })), + ); + + let payload = Payload { + prompt: prompt.to_string(), + files, + dst: dst.to_string(), + }; + let json_payload = serde_json::to_string(&payload).unwrap(); + + log_json( + "info", + "Sending JSON payload to stdout", + Some(serde_json::json!({ + "payload_length": json_payload.len() + })), + ); + println!("{}", json_payload); + let _ = window.app_handle().exit(0); +} + +#[tauri::command] +pub fn log_error_to_console(error: &str) { + eprintln!("[WebView ERROR forwarded]: {}", error); +} + +#[tauri::command] +pub fn resolve_path_relative_to_home(absolute_path: String) -> Result { + let home_dir = dirs::home_dir().ok_or_else(|| "Could not find home directory".to_string())?; + let path_to_resolve = std::path::Path::new(&absolute_path); + let relative_path = pathdiff::diff_paths(path_to_resolve, home_dir) + .ok_or_else(|| "Failed to calculate relative path from home directory".to_string())?; + Ok(relative_path.to_string_lossy().to_string()) +} + +// Debug message handlers +#[tauri::command] +pub fn add_debug_message( + message: String, + level: String, + data: Option, + state: tauri::State<'_, DebugMessages>, +) -> Result<(), String> { + log_json(&level, &format!("Frontend: {}", message), data.clone()); + + let debug_payload = DebugPayload { + level, + message, + data, + }; + + let mut messages = state.0.lock().unwrap(); + messages.push(debug_payload); + + if messages.len() > 100 { + let len = messages.len(); + messages.drain(0..len - 100); + } + + Ok(()) +} + +#[tauri::command] +pub fn get_debug_messages( + state: tauri::State<'_, DebugMessages>, +) -> Result, String> { + let messages = state.0.lock().unwrap(); + Ok(messages.clone()) +} + +#[tauri::command] +pub fn clear_debug_messages(state: tauri::State<'_, DebugMessages>) -> Result<(), String> { + let mut messages = state.0.lock().unwrap(); + messages.clear(); + Ok(()) +} + +// Counter handlers (legacy) +#[tauri::command] +pub fn increment_counter(state: tauri::State<'_, Counter>) -> Result { + let mut counter = state.0.lock().unwrap(); + *counter += 1; + Ok(*counter) +} + +#[tauri::command] +pub fn get_counter(state: tauri::State<'_, Counter>) -> Result { + let counter = state.0.lock().unwrap(); + Ok(*counter) +} + +#[tauri::command] +pub fn reset_counter(state: tauri::State<'_, Counter>) -> Result { + let mut counter = state.0.lock().unwrap(); + *counter = 0; + Ok(0) +} + +// IPC communication handlers +#[tauri::command] +pub fn send_ipc_message( + message_type: String, + data: serde_json::Value, + _window: tauri::Window, +) -> Result<(), String> { + let ipc_message = IPCMessage { + message_type, + data, + timestamp: Some( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64, + ), + id: Some(format!( + "msg_{}_{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(), + rand::random::() + )), + }; + + let json_message = serde_json::to_string(&ipc_message).unwrap(); + println!("{}", json_message); + + Ok(()) +} + +#[tauri::command] +pub fn send_message_to_stdout(message: String) -> Result<(), String> { + println!("{}", message); + Ok(()) +} + +// Image generation handlers +#[tauri::command] +pub fn generate_image_via_backend( + prompt: String, + files: Vec, + dst: String, +) -> Result<(), String> { + let request = serde_json::json!({ + "type": "generate_request", + "prompt": prompt, + "files": files, + "dst": dst, + "timestamp": std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() + }); + + println!("{}", serde_json::to_string(&request).unwrap()); + + Ok(()) +} + +#[tauri::command] +pub fn request_config_from_images(_app: tauri::AppHandle) -> Result<(), String> { + let request = serde_json::json!({ + "type": "config_request", + "timestamp": std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() + }); + + println!("{}", serde_json::to_string(&request).unwrap()); + + Ok(()) +} + +#[tauri::command] +pub fn request_file_deletion(path: String) -> Result<(), String> { + let request = serde_json::json!({ + "type": "delete_request", + "path": path, + }); + + println!("{}", serde_json::to_string(&request).unwrap()); + + Ok(()) +} + +// Legacy direct handlers (not used via stdin) +#[tauri::command] +pub fn forward_config_to_frontend( + prompt: Option, + dst: Option, + api_key: Option, + files: Vec, + app: tauri::AppHandle, +) -> Result<(), String> { + let config_data = serde_json::json!({ + "prompt": prompt, + "dst": dst, + "apiKey": api_key, + "files": files + }); + + if let Err(e) = app.emit("config-received", &config_data) { + return Err(format!("Failed to emit config: {}", e)); + } + + Ok(()) +} + +#[tauri::command] +pub fn forward_image_to_frontend( + base64: String, + mime_type: String, + filename: String, + app: tauri::AppHandle, +) -> Result<(), String> { + let image_data = serde_json::json!({ + "base64": base64, + "mimeType": mime_type, + "filename": filename + }); + + if let Err(e) = app.emit("image-received", &image_data) { + return Err(format!("Failed to emit image: {}", e)); + } + + Ok(()) +} diff --git a/packages/kbot/gui/tauri-app/src-tauri/src/lib.rs b/packages/kbot/gui/tauri-app/src-tauri/src/lib.rs index 4ca3c120..287c116d 100644 --- a/packages/kbot/gui/tauri-app/src-tauri/src/lib.rs +++ b/packages/kbot/gui/tauri-app/src-tauri/src/lib.rs @@ -1,11 +1,11 @@ -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; mod handlers; mod stdin_processor; // Desktop-only modules #[cfg(not(any(target_os = "android", target_os = "ios")))] mod clipboard; -use tauri::Manager; +use tauri::Manager; pub use handlers::*; @@ -32,7 +32,12 @@ pub fn log_json(level: &str, message: &str, data: Option) { .unwrap() .as_millis() as u64, }; - eprintln!("{}", serde_json::to_string(&log_msg).unwrap_or_else(|_| format!("{{\"level\":\"error\",\"message\":\"Failed to serialize log message\"}}"))); + eprintln!( + "{}", + serde_json::to_string(&log_msg).unwrap_or_else(|_| format!( + "{{\"level\":\"error\",\"message\":\"Failed to serialize log message\"}}" + )) + ); } // App state structures @@ -46,17 +51,17 @@ pub struct DebugPayload { pub data: Option, } - #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let mut builder = tauri::Builder::default() + .plugin(tauri_plugin_os::init()) .manage(Counter(std::sync::Mutex::new(0))) .manage(DebugMessages(std::sync::Mutex::new(Vec::new()))) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_http::init()); - + // Desktop-only plugins #[cfg(not(any(target_os = "android", target_os = "ios")))] { @@ -66,65 +71,63 @@ pub fn run() { .plugin(tauri_plugin_upload::init()) .plugin(tauri_plugin_clipboard_manager::init()); } - + // Desktop invoke handlers (includes clipboard functions) #[cfg(not(any(target_os = "android", target_os = "ios")))] - let app = builder - .invoke_handler(tauri::generate_handler![ - submit_prompt, - log_error_to_console, - resolve_path_relative_to_home, - increment_counter, - get_counter, - reset_counter, - add_debug_message, - get_debug_messages, - clear_debug_messages, - send_message_to_stdout, - send_ipc_message, - request_config_from_images, - forward_config_to_frontend, - forward_image_to_frontend, - generate_image_via_backend, - request_file_deletion, - ipc_parse_clipboard_images - ]); - + let app = builder.invoke_handler(tauri::generate_handler![ + submit_prompt, + log_error_to_console, + resolve_path_relative_to_home, + increment_counter, + get_counter, + reset_counter, + add_debug_message, + get_debug_messages, + clear_debug_messages, + send_message_to_stdout, + send_ipc_message, + request_config_from_images, + forward_config_to_frontend, + forward_image_to_frontend, + generate_image_via_backend, + request_file_deletion, + ipc_parse_clipboard_images + ]); + // Mobile invoke handlers (no clipboard functions) #[cfg(any(target_os = "android", target_os = "ios"))] - let app = builder - .invoke_handler(tauri::generate_handler![ - submit_prompt, - log_error_to_console, - resolve_path_relative_to_home, - increment_counter, - get_counter, - reset_counter, - add_debug_message, - get_debug_messages, - clear_debug_messages, - send_message_to_stdout, - send_ipc_message, - request_config_from_images, - forward_config_to_frontend, - forward_image_to_frontend, - generate_image_via_backend, - request_file_deletion - ]); - + let app = builder.invoke_handler(tauri::generate_handler![ + submit_prompt, + log_error_to_console, + resolve_path_relative_to_home, + increment_counter, + get_counter, + reset_counter, + add_debug_message, + get_debug_messages, + clear_debug_messages, + send_message_to_stdout, + send_ipc_message, + request_config_from_images, + forward_config_to_frontend, + forward_image_to_frontend, + generate_image_via_backend, + request_file_deletion + ]); + let app = app .setup(|app| { #[cfg(debug_assertions)] // only include this code on debug builds { - let window = app.get_webview_window("main").unwrap(); - window.open_devtools(); + let window = app.get_webview_window("main").unwrap(); + window.open_devtools(); } - + let app_handle = app.handle().clone(); - + // Start stdin listener in separate module stdin_processor::start_stdin_listener(app_handle); - + Ok(()) }) .build(tauri::generate_context!()) diff --git a/packages/kbot/gui/tauri-app/src-tauri/src/main.rs b/packages/kbot/gui/tauri-app/src-tauri/src/main.rs index 7f64dee7..2abccd9e 100644 --- a/packages/kbot/gui/tauri-app/src-tauri/src/main.rs +++ b/packages/kbot/gui/tauri-app/src-tauri/src/main.rs @@ -1,7 +1,6 @@ -// Prevents additional console window on Windows in release, DO NOT REMOVE!! -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -fn main() { - tauri_app_lib::run() -} - +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + tauri_app_lib::run() +} diff --git a/packages/kbot/gui/tauri-app/src-tauri/src/stdin_processor.rs b/packages/kbot/gui/tauri-app/src-tauri/src/stdin_processor.rs index 2283b442..a428a68f 100644 --- a/packages/kbot/gui/tauri-app/src-tauri/src/stdin_processor.rs +++ b/packages/kbot/gui/tauri-app/src-tauri/src/stdin_processor.rs @@ -1,147 +1,210 @@ -use tauri::Emitter; -use std::io::{BufRead, BufReader}; - -use crate::log_json; - -pub fn start_stdin_listener(app_handle: tauri::AppHandle) { - std::thread::spawn(move || { - let stdin = std::io::stdin(); - let reader = BufReader::new(stdin); - - - for line in reader.lines() { - if let Ok(line_content) = line { - if line_content.trim().is_empty() { - continue; - } - - // Parse command from images.ts - if let Ok(command) = serde_json::from_str::(&line_content) { - if let Some(cmd) = command.get("cmd").and_then(|v| v.as_str()) { - - match cmd { - "forward_config_to_frontend" => { - handle_config_forward(&command, &app_handle); - } - "forward_image_to_frontend" => { - handle_image_forward(&command, &app_handle); - } - "generation_result" => { - if let Err(e) = app_handle.emit("generation-result", &command) { - log_json("error", "Failed to emit generation-result", Some(serde_json::json!({ - "error": e.to_string() - }))); - } - } - "generation_error" => { - if let Err(e) = app_handle.emit("generation-error", &command) { - log_json("error", "Failed to emit generation-error", Some(serde_json::json!({ - "error": e.to_string() - }))); - } - } - "generation_complete" => { - if let Err(e) = app_handle.emit("generation-complete", &command) { - log_json("error", "Failed to emit generation-complete", Some(serde_json::json!({ - "error": e.to_string() - }))); - } - } - "file_deleted_successfully" => { - if let Some(path) = command.get("path").and_then(|v| v.as_str()) { - if let Err(e) = app_handle.emit("file-deleted-successfully", &serde_json::json!({ "path": path })) { - log_json("error", "Failed to emit file-deleted-successfully", Some(serde_json::json!({ - "error": e.to_string() - }))); - } - } - } - "file_deletion_error" => { - if let (Some(path), Some(error)) = ( - command.get("path").and_then(|v| v.as_str()), - command.get("error").and_then(|v| v.as_str()) - ) { - if let Err(e) = app_handle.emit("file-deletion-error", &serde_json::json!({ "path": path, "error": error })) { - log_json("error", "Failed to emit file-deletion-error", Some(serde_json::json!({ - "error": e.to_string() - }))); - } - } - } - _ => { - log_json("warn", "Unknown command received", Some(serde_json::json!({ - "command": cmd - }))); - } - } - } - } else { - log_json("warn", "Failed to parse stdin as JSON", Some(serde_json::json!({ - "content_length": line_content.len() - }))); - } - } - } - - log_json("info", "Stdin listener thread ended", None); - }); -} - -fn handle_config_forward(command: &serde_json::Value, app_handle: &tauri::AppHandle) { - log_json("info", "Forwarding config to frontend", Some(serde_json::json!({ - "has_prompt": command.get("prompt").is_some(), - "has_dst": command.get("dst").is_some(), - "has_api_key": command.get("apiKey").is_some(), - "file_count": command.get("files").and_then(|f| f.as_array()).map(|a| a.len()).unwrap_or(0) - }))); - - // Extract values, handling both null and undefined - let prompt = command.get("prompt") - .and_then(|v| if v.is_null() { None } else { v.as_str().map(|s| s.to_string()) }); - let dst = command.get("dst") - .and_then(|v| if v.is_null() { None } else { v.as_str().map(|s| s.to_string()) }); - let api_key = command.get("apiKey") - .and_then(|v| if v.is_null() { None } else { v.as_str().map(|s| s.to_string()) }); - let files = command.get("files") - .and_then(|v| v.as_array()) - .map(|arr| arr.iter().filter_map(|f| f.as_str().map(|s| s.to_string())).collect::>()) - .unwrap_or_else(Vec::new); - - let config_data = serde_json::json!({ - "prompt": prompt, - "dst": dst, - "apiKey": api_key, - "files": files - }); - - if let Err(e) = app_handle.emit("config-received", &config_data) { - log_json("error", "Failed to emit config-received", Some(serde_json::json!({ - "error": e.to_string() - }))); - } else { - log_json("info", "Config emitted successfully to frontend", None); - } -} - -fn handle_image_forward(command: &serde_json::Value, app_handle: &tauri::AppHandle) { - if let (Some(filename), Some(base64), Some(mime_type)) = ( - command.get("filename").and_then(|v| v.as_str()), - command.get("base64").and_then(|v| v.as_str()), - command.get("mimeType").and_then(|v| v.as_str()) - ) { - let image_data = serde_json::json!({ - "base64": base64, - "mimeType": mime_type, - "filename": filename - }); - - if let Err(e) = app_handle.emit("image-received", &image_data) { - log_json("error", "Failed to emit image-received", Some(serde_json::json!({ - "error": e.to_string(), - "filename": filename - }))); - } else { - - } - } -} +use std::io::{BufRead, BufReader}; +use tauri::Emitter; + +use crate::log_json; + +pub fn start_stdin_listener(app_handle: tauri::AppHandle) { + std::thread::spawn(move || { + let stdin = std::io::stdin(); + let reader = BufReader::new(stdin); + + for line in reader.lines() { + if let Ok(line_content) = line { + if line_content.trim().is_empty() { + continue; + } + + // Parse command from images.ts + if let Ok(command) = serde_json::from_str::(&line_content) { + if let Some(cmd) = command.get("cmd").and_then(|v| v.as_str()) { + match cmd { + "forward_config_to_frontend" => { + handle_config_forward(&command, &app_handle); + } + "forward_image_to_frontend" => { + handle_image_forward(&command, &app_handle); + } + "generation_result" => { + if let Err(e) = app_handle.emit("generation-result", &command) { + log_json( + "error", + "Failed to emit generation-result", + Some(serde_json::json!({ + "error": e.to_string() + })), + ); + } + } + "generation_error" => { + if let Err(e) = app_handle.emit("generation-error", &command) { + log_json( + "error", + "Failed to emit generation-error", + Some(serde_json::json!({ + "error": e.to_string() + })), + ); + } + } + "generation_complete" => { + if let Err(e) = app_handle.emit("generation-complete", &command) { + log_json( + "error", + "Failed to emit generation-complete", + Some(serde_json::json!({ + "error": e.to_string() + })), + ); + } + } + "file_deleted_successfully" => { + if let Some(path) = command.get("path").and_then(|v| v.as_str()) { + if let Err(e) = app_handle.emit( + "file-deleted-successfully", + &serde_json::json!({ "path": path }), + ) { + log_json( + "error", + "Failed to emit file-deleted-successfully", + Some(serde_json::json!({ + "error": e.to_string() + })), + ); + } + } + } + "file_deletion_error" => { + if let (Some(path), Some(error)) = ( + command.get("path").and_then(|v| v.as_str()), + command.get("error").and_then(|v| v.as_str()), + ) { + if let Err(e) = app_handle.emit( + "file-deletion-error", + &serde_json::json!({ "path": path, "error": error }), + ) { + log_json( + "error", + "Failed to emit file-deletion-error", + Some(serde_json::json!({ + "error": e.to_string() + })), + ); + } + } + } + _ => { + log_json( + "warn", + "Unknown command received", + Some(serde_json::json!({ + "command": cmd + })), + ); + } + } + } + } else { + log_json( + "warn", + "Failed to parse stdin as JSON", + Some(serde_json::json!({ + "content_length": line_content.len() + })), + ); + } + } + } + + log_json("info", "Stdin listener thread ended", None); + }); +} + +fn handle_config_forward(command: &serde_json::Value, app_handle: &tauri::AppHandle) { + log_json( + "info", + "Forwarding config to frontend", + Some(serde_json::json!({ + "has_prompt": command.get("prompt").is_some(), + "has_dst": command.get("dst").is_some(), + "has_api_key": command.get("apiKey").is_some(), + "file_count": command.get("files").and_then(|f| f.as_array()).map(|a| a.len()).unwrap_or(0) + })), + ); + + // Extract values, handling both null and undefined + let prompt = command.get("prompt").and_then(|v| { + if v.is_null() { + None + } else { + v.as_str().map(|s| s.to_string()) + } + }); + let dst = command.get("dst").and_then(|v| { + if v.is_null() { + None + } else { + v.as_str().map(|s| s.to_string()) + } + }); + let api_key = command.get("apiKey").and_then(|v| { + if v.is_null() { + None + } else { + v.as_str().map(|s| s.to_string()) + } + }); + let files = command + .get("files") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|f| f.as_str().map(|s| s.to_string())) + .collect::>() + }) + .unwrap_or_else(Vec::new); + + let config_data = serde_json::json!({ + "prompt": prompt, + "dst": dst, + "apiKey": api_key, + "files": files + }); + + if let Err(e) = app_handle.emit("config-received", &config_data) { + log_json( + "error", + "Failed to emit config-received", + Some(serde_json::json!({ + "error": e.to_string() + })), + ); + } else { + log_json("info", "Config emitted successfully to frontend", None); + } +} + +fn handle_image_forward(command: &serde_json::Value, app_handle: &tauri::AppHandle) { + if let (Some(filename), Some(base64), Some(mime_type)) = ( + command.get("filename").and_then(|v| v.as_str()), + command.get("base64").and_then(|v| v.as_str()), + command.get("mimeType").and_then(|v| v.as_str()), + ) { + let image_data = serde_json::json!({ + "base64": base64, + "mimeType": mime_type, + "filename": filename + }); + + if let Err(e) = app_handle.emit("image-received", &image_data) { + log_json( + "error", + "Failed to emit image-received", + Some(serde_json::json!({ + "error": e.to_string(), + "filename": filename + })), + ); + } else { + } + } +} diff --git a/packages/kbot/gui/tauri-app/src/App.tsx b/packages/kbot/gui/tauri-app/src/App.tsx index 3d617e5f..1b82c891 100644 --- a/packages/kbot/gui/tauri-app/src/App.tsx +++ b/packages/kbot/gui/tauri-app/src/App.tsx @@ -1,553 +1,11 @@ import { useState, useEffect } from "react"; -import { ImageFile, PromptTemplate } from "./types"; -import { useTauriListeners } from "./hooks/useTauriListeners"; -import { tauriApi } from "./lib/tauriApi"; -import { saveToStore } from "./lib/init"; -import { QUICK_STYLES, QUICK_ACTIONS } from "./constants"; -import log from "./lib/log"; -import Header from "./components/Header"; -import PromptForm from "./components/PromptForm"; -import DebugPanel from "./components/DebugPanel"; - -function arrayBufferToBase64(buffer: number[]) { - let binary = ''; - const bytes = new Uint8Array(buffer); - const len = bytes.byteLength; - for (let i = 0; i < len; i++) { - binary += String.fromCharCode(bytes[i]); - } - return window.btoa(binary); -} +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import ImageWizard from "./components/ImageWizard"; +import Settings from "./components/Settings"; function App() { - const [prompt, setPrompt] = useState(""); - const [files, setFiles] = useState([]); - const [dst, setDst] = useState(""); - const [isGenerating, setIsGenerating] = useState(false); const [apiKey, setApiKey] = useState(""); const [isDarkMode, setIsDarkMode] = useState(false); - const [debugMessages, setDebugMessages] = useState([]); - const [showDebugPanel, setShowDebugPanel] = useState(false); // Hidden in production - const [errorMessage, setErrorMessage] = useState(null); - - // Initialize logging system and connect to UI - useEffect(() => { - log.setMessageCallback((message) => { - setDebugMessages(prev => [...prev.slice(-99), message]); // Keep last 100 messages - }); - - // Load existing messages - setDebugMessages(log.getLocalMessages()); - }, []); - const [ipcInitialized, setIpcInitialized] = useState(false); - const [messageToSend, setMessageToSend] = useState(""); - const [generationTimeoutId, setGenerationTimeoutId] = useState(null); - const [currentIndex, setCurrentIndex] = useState(0); - const [prompts, setPrompts] = useState([]); - const [promptHistory, setPromptHistory] = useState([]); - const [historyIndex, setHistoryIndex] = useState(-1); - const [fileHistory, setFileHistory] = useState([]); - - // Debug wrapper for setFileHistory - const setFileHistoryWithLogging = (history: string[]) => { - log.debug(`🔧 setFileHistory called with ${history.length} files`, { - files: history.map(f => f.split(/[/\\]/).pop()) - }); - setFileHistory(history); - }; - const [showFileHistory, setShowFileHistory] = useState(false); - - - const appendStyle = (style: string) => { - setPrompt(prev => { - const trimmed = prev.trim(); - - // Remove any existing styles from QUICK_STYLES - let cleanPrompt = trimmed; - QUICK_STYLES.forEach(existingStyle => { - // Remove style if it exists (with or without comma) - const patterns = [ - new RegExp(`,\\s*${existingStyle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*`, 'gi'), - new RegExp(`^${existingStyle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*,?\\s*`, 'gi'), - new RegExp(`\\s*,\\s*${existingStyle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'gi') - ]; - patterns.forEach(pattern => { - cleanPrompt = cleanPrompt.replace(pattern, ''); - }); - }); - - // Clean up any double commas or trailing/leading commas - cleanPrompt = cleanPrompt.replace(/,\s*,/g, ',').replace(/^,\s*|,\s*$/g, '').trim(); - - // Add the new style - if (cleanPrompt) { - return `${cleanPrompt}, ${style}`; - } else { - return style; - } - }); - log.info(`🎨 Style replaced with: ${style}`); - }; - - const executeQuickAction = async (action: { name: string; prompt: string; iconName: string }) => { - const selectedImages = getSelectedImages(); - - if (selectedImages.length === 0) { - log.warn('Please select an image first to use quick actions'); - return; - } - - // Use the first selected image - const targetImage = selectedImages[0]; - - // Set the action prompt - setPrompt(action.prompt); - - // Generate with the selected image - log.info(`🚀 Executing quick action: ${action.name} on selected image ${targetImage.path}`); - await generateImage(action.prompt, [targetImage]); - }; - - const addToHistory = async (promptText: string) => { - if (promptText.trim() && !promptHistory.includes(promptText.trim())) { - const newHistory = [...promptHistory, promptText.trim()]; - setPromptHistory(newHistory); - setHistoryIndex(-1); // Reset to end of history - log.info(`📝 Added to history: "${promptText.substring(0, 50)}..."`); - - // Auto-save to store - try { - await saveToStore({ history: newHistory }); - log.info('💾 History saved to store'); - } catch (error) { - log.error('Failed to save history', { error: (error as Error).message }); - } - } - }; - - const navigateHistory = (direction: 'up' | 'down') => { - if (promptHistory.length === 0) return; - - let newIndex = historyIndex; - - if (direction === 'up') { - newIndex = historyIndex === -1 ? promptHistory.length - 1 : Math.max(0, historyIndex - 1); - } else { - newIndex = historyIndex === -1 ? -1 : historyIndex + 1; - if (newIndex >= promptHistory.length) newIndex = -1; - } - - setHistoryIndex(newIndex); - - if (newIndex === -1) { - setPrompt(''); - } else { - setPrompt(promptHistory[newIndex]); - log.info(`📜 History: ${newIndex + 1}/${promptHistory.length}`); - } - }; - - const addToFileHistory = async (filePath: string) => { - if (!filePath) return; - - // Use functional update to get current state - setFileHistory(currentHistory => { - // Remove if already exists, then add to front, then limit to 8 - const filteredHistory = currentHistory.filter(path => path !== filePath); - const newFileHistory = [filePath, ...filteredHistory].slice(0, 8); - - log.info(`📁 Adding to file history: ${filePath.split(/[/\\]/).pop()}`, { - currentHistoryLength: currentHistory.length, - newHistoryLength: newFileHistory.length, - fullPath: filePath, - maxEntries: 8 - }); - - // Auto-save to store - saveToStore({ fileHistory: newFileHistory }).then(() => { - log.info('💾 File history saved to store', { - savedCount: newFileHistory.length, - savedFiles: newFileHistory.map(f => f.split(/[/\\]/).pop()) - }); - }).catch(error => { - log.error('Failed to save file history', { error: error.message }); - }); - - return newFileHistory; - }); - }; - - const openFileFromHistory = async (filePath: string) => { - try { - if (await tauriApi.fs.readFile(filePath)) { - await addFiles([filePath]); - setShowFileHistory(false); - log.info(`📂 Reopened from history: ${filePath.split(/[/\\]/).pop()}`); - } - } catch (error) { - log.error(`File no longer exists: ${filePath.split(/[/\\]/).pop()}`); - // Remove from history if file doesn't exist - const updatedHistory = fileHistory.filter(f => f !== filePath); - setFileHistoryWithLogging(updatedHistory); - await saveToStore({ fileHistory: updatedHistory }); - } - }; - - const onFileHistoryCleanup = async (validFiles: string[]) => { - setFileHistoryWithLogging(validFiles); - await saveToStore({ fileHistory: validFiles }); - }; - - const handleLightboxPromptSubmit = async (promptText: string, imagePath: string) => { - // Set the prompt and select the image for editing - setPrompt(promptText); - - // Find and select the image - setFiles(prev => prev.map(file => ({ - ...file, - selected: file.path === imagePath - }))); - - // Add to history and generate - await addToHistory(promptText); - await generateImage(promptText, [{ path: imagePath, src: '', isGenerated: false }]); - - log.info(`🎨 Lightbox edit: "${promptText}" on ${imagePath.split(/[/\\]/).pop()}`); - }; - - const importPrompts = async () => { - try { - const selected = await tauriApi.dialog.open({ - multiple: false, - filters: [{ name: 'JSON', extensions: ['json'] }] - }); - if (typeof selected === 'string') { - const contents = await tauriApi.fs.readTextFile(selected); - const newPrompts = JSON.parse(contents); - if (newPrompts.prompts && Array.isArray(newPrompts.prompts)) { - setPrompts(newPrompts.prompts); - savePrompts(newPrompts.prompts); - log.info(`✅ Prompts imported successfully from: ${selected}`); - } else { - log.error('Invalid prompts file format.'); - } - } - } catch (error) { - log.error('Failed to import prompts', { error: (error as Error).message }); - } - }; - - const exportPrompts = async () => { - log.info('Attempting to export prompts...'); - try { - const path = await tauriApi.dialog.save({ - defaultPath: 'kbot-prompts.json', - filters: [{ name: 'JSON', extensions: ['json'] }] - }); - - if (path) { - log.debug(`📂 Export path selected: ${path}`); - const dataToWrite = JSON.stringify({ prompts }, null, 2); - log.debug('📋 Data to be exported:', { promptCount: prompts.length, dataLength: dataToWrite.length }); - log.debug('💾 About to call writeTextFile...'); - await tauriApi.fs.writeTextFile(path, dataToWrite); - log.info(`✅ Prompts exported successfully to: ${path}`); - } else { - log.info('Export dialog was cancelled.'); - } - } catch (error) { - log.error('Failed to export prompts', { error: (error as Error).message }); - } - }; - - const deleteFilePermanently = async (pathToDelete: string) => { - log.info(`Requesting deletion of file: ${pathToDelete}`); - // This will be the new tauri command - await tauriApi.requestFileDeletion({ path: pathToDelete }); - }; - - const saveImageAs = async (imagePath: string) => { - const imageFile = files.find(f => f.path === imagePath); - if (!imageFile) { - log.error(`Could not find image to save: ${imagePath}`); - return; - } - - try { - const defaultPath = imagePath.split(/[/\\]/).pop() || 'saved_image.png'; - const newPath = await tauriApi.dialog.save({ - defaultPath, - filters: [{ name: 'Images', extensions: ['png', 'jpg', 'jpeg'] }] - }); - - if (newPath) { - // Convert data URL to binary using fetch - const response = await fetch(imageFile.src); - const blob = await response.blob(); - const buffer = await blob.arrayBuffer(); - const uint8Array = new Uint8Array(buffer); - - await tauriApi.fs.writeFile(newPath, uint8Array); - log.info(`✅ Image saved successfully to: ${newPath}`); - } else { - log.info('Save dialog was cancelled.'); - } - } catch (error) { - log.error(`Failed to save image: ${(error as Error).message}`); - } - }; - - const generateDefaultDst = (fileCount: number, firstFilePath?: string) => { - if (fileCount === 1 && firstFilePath) { - const parsedPath = firstFilePath.split(/[/\\]/).pop() || 'image'; - const nameWithoutExt = parsedPath.replace(/\.[^/.]+$/, ""); - return `${nameWithoutExt}_out.png`; - } else { - const now = new Date(); - const hours = now.getHours().toString().padStart(2, '0'); - const minutes = now.getMinutes().toString().padStart(2, '0'); - return `data_${hours}_${minutes}.png`; - } - }; - - - const addImageFromUrl = async (url: string) => { - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch image: ${response.statusText}`); - } - const blob = await response.blob(); - const reader = new FileReader(); - reader.readAsDataURL(blob); - reader.onloadend = () => { - const base64data = reader.result as string; - const newImageFile: ImageFile = { - path: `url_${Date.now()}.jpg`, - src: base64data, - }; - setFiles(prevFiles => [...prevFiles, newImageFile]); - }; - } catch (error) { - console.error('Failed to add image from URL:', error); - } - }; - - useTauriListeners({ - setPrompt, - setDst, - setApiKey, - setIpcInitialized, - setPrompts, - setFiles, - isGenerating, - generationTimeoutId, - setGenerationTimeoutId, - setIsGenerating, - prompt, - setCurrentIndex, - setPromptHistory, - setFileHistory: setFileHistoryWithLogging, - addToFileHistory, - setErrorMessage, - }); - - const addFiles = async (newPaths: string[]) => { - const uniqueNewPaths = newPaths.filter(newPath => !files.some(f => f.path === newPath)); - - if (uniqueNewPaths.length === 0) { - return; - } - - // Update destination directory - const firstPath = uniqueNewPaths[0]; - - // Check if this is a clipboard image (from temp directory) - const isClipboardImage = firstPath.includes('kbot_clipboard_') || - firstPath.includes('\\Temp\\') || // Windows temp - firstPath.includes('/tmp/') || // Unix/Linux temp - firstPath.includes('AppData\\Local\\Temp') || // Windows user temp - firstPath.includes('/var/folders/'); // macOS temp - - let newDir: string; - if (isClipboardImage) { - // For clipboard images, use current working directory or user's Documents folder - newDir = './'; // Use current working directory - log.info('📋 Clipboard image detected, using current working directory for output'); - } else { - // For regular files, use the directory of the first file - const lastSeparatorIndex = Math.max(firstPath.lastIndexOf('/'), firstPath.lastIndexOf('\\')); - newDir = firstPath.substring(0, lastSeparatorIndex); - } - - const currentFilename = dst.split(/[/\\]/).pop() || generateDefaultDst(1, firstPath); - const separator = firstPath.includes('\\') ? '\\' : '/'; - const newDst = newDir === './' ? currentFilename : `${newDir}${separator}${currentFilename}`; - setDst(newDst); - - // Read files - const newImageFiles: ImageFile[] = []; - for (const path of uniqueNewPaths) { - try { - const buffer = await tauriApi.fs.readFile(path); - const base64 = arrayBufferToBase64(Array.from(buffer)); - const mimeType = path.toLowerCase().endsWith('.png') ? 'image/png' : - path.toLowerCase().endsWith('.jpg') || path.toLowerCase().endsWith('.jpeg') ? 'image/jpeg' : - 'image/png'; - const src = `data:${mimeType};base64,${base64}`; - newImageFiles.push({ path, src, selected: false, isGenerated: false }); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); - console.error(`Failed to read file: ${path}`, e); - tauriApi.logErrorToConsole(`[Frontend Error] Failed to read file ${path}: ${errorMessage}`); - } - } - - const lastPath = uniqueNewPaths[uniqueNewPaths.length - 1]; - - setFiles(prevFiles => { - const combinedFiles = [...prevFiles, ...newImageFiles]; - const newIndex = combinedFiles.findIndex(f => f.path === lastPath); - if (newIndex !== -1) { - setCurrentIndex(newIndex); - } - - return combinedFiles.map(file => ({ - ...file, - selected: file.path === lastPath - })); - }); - }; - - const removeFile = (pathToRemove: string) => { - setFiles(prevFiles => prevFiles.filter(file => file.path !== pathToRemove)); - }; - - const clearAllFiles = () => { - setFiles([]); - }; - - const handleImageSelection = (imagePath: string, isMultiSelect: boolean) => { - setFiles(prev => - prev.map(file => { - if (file.path === imagePath) { - // For multi-select, toggle the current state. For single-select, always select it. - return { ...file, selected: isMultiSelect ? !file.selected : true }; - } - // For single-select, deselect all other images. - if (!isMultiSelect) { - return { ...file, selected: false }; - } - // For multi-select, leave other images as they are. - return file; - }) - ); - }; - - const getSelectedImages = () => { - return files.filter(file => file.selected); - }; - - const saveAndClose = async () => { - // Find the last generated image - const generatedFiles = files.filter(file => file.isGenerated); - if (generatedFiles.length === 0) { - log.warn('No generated images to save'); - return; - } - - const lastGenerated = generatedFiles[generatedFiles.length - 1]; - log.info(`💾 Saving and closing with: ${lastGenerated.path}`); - - try { - // Send the final result back to images.ts for saving - const result = { - prompt, - files: files.filter(f => !f.isGenerated).map(f => f.path), - dst, - generatedImage: { - src: lastGenerated.src, - filename: lastGenerated.path - } - }; - - await tauriApi.submitPrompt(result); - - log.info('✅ Final result sent, closing app'); - } catch (error) { - log.error('Failed to save and close', { error: (error as Error).message }); - } - }; - - const generateImage = async (promptText: string, includeImages: ImageFile[] = []) => { - if (!apiKey) { - log.error('No API key available for image generation'); - return; - } - - setIsGenerating(true); - setErrorMessage(null); // Clear any previous error messages - log.info(`🎨 Starting image generation via backend: "${promptText}"`); - - // Add placeholder image with spinner to the files grid - const placeholderFile: ImageFile = { - path: `generating_${Date.now()}`, - src: 'data:image/svg+xml;base64,' + btoa(` - - - - - - - Generating... - - `) - }; - - setFiles(prev => [...prev, placeholderFile]); - - try { - // Use the images.ts backend instead of direct API calls - const filePaths = includeImages.map(img => img.path); - const genDst = dst || `generated_${Date.now()}.png`; - - log.info('Sending generation request to images.ts backend', { - prompt: promptText, - files: filePaths, - dst: genDst - }); - - // Send generation request via Tauri command - await tauriApi.generateImageViaBackend({ - prompt: promptText, - files: filePaths, - dst: genDst - }); - - log.info('📤 Generation request sent to backend'); - - // Clear any existing timeout - if (generationTimeoutId) { - clearTimeout(generationTimeoutId); - } - - const timeoutId = setTimeout(() => { - log.warn('⏰ Generation timeout - resetting state'); - setIsGenerating(false); - setFiles(prev => prev.filter(file => !file.path.startsWith('generating_'))); - setGenerationTimeoutId(null); - }, 30000); - - setGenerationTimeoutId(timeoutId); - - } catch (error) { - log.error('Failed to send generation request', { - error: error instanceof Error ? error.message : JSON.stringify(error) - }); - setIsGenerating(false); - setFiles(prev => prev.filter(file => !file.path.startsWith('generating_'))); - } - }; // Theme management useEffect(() => { @@ -582,273 +40,33 @@ function App() { } }; - // Auto-generate default destination file when files change - useEffect(() => { - if (files.length > 0 && !dst) { - const defaultDst = generateDefaultDst(files.length, files[0]?.path); - setDst(defaultDst); - } - }, [files, dst]); - - useEffect(() => { - const logBaseDirectories = async () => { - }; - logBaseDirectories(); - }, []); - - async function submit() { - if (!prompt.trim()) { - log.warn('Please enter a prompt first'); - return; - } - - if (apiKey) { - // Add to history before generating - await addToHistory(prompt); - - // Generate image via backend (always chat mode now) - // Only use explicitly selected images. If none are selected, generate from prompt alone. - const imagesToUse = getSelectedImages(); - - await generateImage(prompt, imagesToUse); - // Don't clear prompt - let user iterate - } else { - log.error('API key required for image generation'); - } - } - - const clearDebugMessages = async () => { - setDebugMessages([]); - log.clearLocalMessages(); - await tauriApi.clearDebugMessages(); - }; - - const sendIPCMessage = async (messageType: string, data: any) => { - await tauriApi.sendIPCMessage(messageType, data); - log.info(`IPC message sent: ${messageType}`, data); - }; - - const sendMessageToImages = async () => { - if (!messageToSend.trim()) return; - - const message = { - message: messageToSend, - timestamp: Date.now(), - source: 'gui' - }; - - try { - await tauriApi.sendMessageToStdout(JSON.stringify(message)); - log.info(`📤 Sent to images.ts: ${messageToSend}`, message); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : JSON.stringify(error); - log.error(`Failed to send message: ${errorMessage}`); - } - - // Clear the input - setMessageToSend(''); - }; - - const savePrompts = async (promptsToSave: PromptTemplate[]) => { - try { - await saveToStore({ prompts: promptsToSave }); - } catch (error) { - log.error('Failed to save prompts', { - error: (error as Error).message - }); - } - }; - - async function openFilePicker() { - const { isTauri: isTauriEnv } = await tauriApi.ensureTauriApi(); - if (!isTauriEnv) { - // Browser fallback: create file input - const input = document.createElement('input'); - input.type = 'file'; - input.multiple = true; - input.accept = 'image/*'; - input.onchange = (e) => { - const target = e.target as HTMLInputElement; - if (target.files) { - const fileArray = Array.from(target.files); - const newImageFiles: ImageFile[] = []; - let loadedCount = 0; - - fileArray.forEach(file => { - const reader = new FileReader(); - reader.onload = (e) => { - if (e.target?.result) { - newImageFiles.push({ - path: file.name, - src: e.target.result as string - }); - loadedCount++; - if (loadedCount === fileArray.length) { - setFiles(prevFiles => [...prevFiles, ...newImageFiles]); - } - } - }; - reader.readAsDataURL(file); - }); - } - }; - input.click(); - return; - } - - try { - if (!tauriApi.dialog.open) { - console.error('Open function not available'); - return; - } - - const selected = await tauriApi.dialog.open({ - multiple: true, - filters: [{ - name: 'Images', - extensions: ['png', 'jpeg', 'jpg'] - }] - }); - if (Array.isArray(selected)) { - addFiles(selected); - } - } catch (e) { - console.error('File picker error:', e); - tauriApi.logErrorToConsole(`[Frontend Error] File picker error: ${JSON.stringify(e)}`); - } - } - - async function openSaveDialog() { - try { - // Extract current filename from dst for default, or use smart default - const currentFilename = dst.split(/[/\\]/).pop() || generateDefaultDst(files.length, files[0]?.path); - - const selected = await tauriApi.dialog.save({ - defaultPath: currentFilename, - filters: [{ - name: 'Images', - extensions: ['png', 'jpg'] - }] - }); - - if (selected) { - setDst(selected); - } - } catch (e) { - console.error('Save dialog error:', e); - tauriApi.logErrorToConsole(`[Frontend Error] Save dialog error: ${JSON.stringify(e)}`); - } - } - return ( -
- {/* Background decoration */} -
-
-
-
- -
-
+ + + } /> - - {/* Error Message Display */} - {errorMessage && ( -
-
-
-
- - - -
-
-

- Generation Error -

-
-

{errorMessage}

-
-
-
-
- -
-
-
- )} - + } /> - - {/* Debug Panel */} - {showDebugPanel && ( - - )} -
- -
+ + ); } diff --git a/packages/kbot/gui/tauri-app/src/components/Header.tsx b/packages/kbot/gui/tauri-app/src/components/Header.tsx index 6df85ec8..74be72f1 100644 --- a/packages/kbot/gui/tauri-app/src/components/Header.tsx +++ b/packages/kbot/gui/tauri-app/src/components/Header.tsx @@ -1,11 +1,11 @@ import React from 'react'; +import { useNavigate } from 'react-router-dom'; interface HeaderProps { showDebugPanel: boolean; setShowDebugPanel: (show: boolean) => void; isDarkMode: boolean; toggleTheme: () => void; - apiKey: string; } const Header: React.FC = ({ @@ -13,58 +13,66 @@ const Header: React.FC = ({ setShowDebugPanel, isDarkMode, toggleTheme, - apiKey, }) => { + const navigate = useNavigate(); return ( -
-

Image Prompt

-
- {/* Debug Panel Toggle */} - - - {/* Theme Toggle */} - + - {/* Status Indicator */} -
- Interactive Mode -
- {!apiKey && ( - - (API key required) - - )} + {/* Theme Toggle */} +
+ + {/* Settings Button */} +
); diff --git a/packages/kbot/gui/tauri-app/src/components/ImageWizard.tsx b/packages/kbot/gui/tauri-app/src/components/ImageWizard.tsx new file mode 100644 index 00000000..a2e33f4c --- /dev/null +++ b/packages/kbot/gui/tauri-app/src/components/ImageWizard.tsx @@ -0,0 +1,831 @@ +import React, { useState, useEffect } from "react"; +import { ImageFile, PromptTemplate } from "../types"; +import { useTauriListeners } from "../hooks/useTauriListeners"; +import { tauriApi } from "../lib/tauriApi"; +import { saveToStore } from "../lib/init"; +import { QUICK_STYLES, QUICK_ACTIONS } from "../constants"; +import log from "../lib/log"; +import Header from "./Header"; +import PromptForm from "./PromptForm"; +import DebugPanel from "./DebugPanel"; + +function arrayBufferToBase64(buffer: number[]) { + let binary = ''; + const bytes = new Uint8Array(buffer); + const len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); +} + +interface ImageWizardProps { + apiKey: string; + setApiKey: (key: string) => void; + isDarkMode: boolean; + toggleTheme: () => void; +} + +const ImageWizard: React.FC = ({ + apiKey, + setApiKey, + isDarkMode, + toggleTheme, +}) => { + const [prompt, setPrompt] = useState(""); + const [files, setFiles] = useState([]); + const [dst, setDst] = useState(""); + const [isGenerating, setIsGenerating] = useState(false); + const [debugMessages, setDebugMessages] = useState([]); + const [showDebugPanel, setShowDebugPanel] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + // Initialize logging system and connect to UI + useEffect(() => { + log.setMessageCallback((message) => { + setDebugMessages(prev => [...prev.slice(-99), message]); + }); + + // Load existing messages + setDebugMessages(log.getLocalMessages()); + }, []); + + const [ipcInitialized, setIpcInitialized] = useState(false); + const [messageToSend, setMessageToSend] = useState(""); + const [generationTimeoutId, setGenerationTimeoutId] = useState(null); + const [currentIndex, setCurrentIndex] = useState(0); + const [prompts, setPrompts] = useState([]); + const [promptHistory, setPromptHistory] = useState([]); + const [historyIndex, setHistoryIndex] = useState(-1); + const [fileHistory, setFileHistory] = useState([]); + + // Debug wrapper for setFileHistory + const setFileHistoryWithLogging = (history: string[]) => { + log.debug(`🔧 setFileHistory called with ${history.length} files`, { + files: history.map(f => f.split(/[/\\]/).pop()) + }); + setFileHistory(history); + }; + const [showFileHistory, setShowFileHistory] = useState(false); + + // All the existing functions from App.tsx... + const appendStyle = (style: string) => { + setPrompt(prev => { + const trimmed = prev.trim(); + + // Remove any existing styles from QUICK_STYLES + let cleanPrompt = trimmed; + QUICK_STYLES.forEach(existingStyle => { + // Remove style if it exists (with or without comma) + const patterns = [ + new RegExp(`,\\s*${existingStyle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*`, 'gi'), + new RegExp(`^${existingStyle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*,?\\s*`, 'gi'), + new RegExp(`\\s*,\\s*${existingStyle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'gi') + ]; + patterns.forEach(pattern => { + cleanPrompt = cleanPrompt.replace(pattern, ''); + }); + }); + + // Clean up any double commas or trailing/leading commas + cleanPrompt = cleanPrompt.replace(/,\s*,/g, ',').replace(/^,\s*|,\s*$/g, '').trim(); + + // Add the new style + if (cleanPrompt) { + return `${cleanPrompt}, ${style}`; + } else { + return style; + } + }); + log.info(`🎨 Style replaced with: ${style}`); + }; + + const executeQuickAction = async (action: { name: string; prompt: string; iconName: string }) => { + const selectedImages = getSelectedImages(); + + if (selectedImages.length === 0) { + log.warn('Please select an image first to use quick actions'); + return; + } + + // Use the first selected image + const targetImage = selectedImages[0]; + + // Set the action prompt + setPrompt(action.prompt); + + // Generate with the selected image + log.info(`🚀 Executing quick action: ${action.name} on selected image ${targetImage.path}`); + await generateImage(action.prompt, [targetImage]); + }; + + const addToHistory = async (promptText: string) => { + if (promptText.trim() && !promptHistory.includes(promptText.trim())) { + const newHistory = [...promptHistory, promptText.trim()]; + setPromptHistory(newHistory); + setHistoryIndex(-1); // Reset to end of history + log.info(`📝 Added to history: "${promptText.substring(0, 50)}..."`); + + // Auto-save to store + try { + await saveToStore({ history: newHistory }); + log.info('💾 History saved to store'); + } catch (error) { + log.error('Failed to save history', { error: (error as Error).message }); + } + } + }; + + const navigateHistory = (direction: 'up' | 'down') => { + if (promptHistory.length === 0) return; + + let newIndex = historyIndex; + + if (direction === 'up') { + newIndex = historyIndex === -1 ? promptHistory.length - 1 : Math.max(0, historyIndex - 1); + } else { + newIndex = historyIndex === -1 ? -1 : historyIndex + 1; + if (newIndex >= promptHistory.length) newIndex = -1; + } + + setHistoryIndex(newIndex); + + if (newIndex === -1) { + setPrompt(''); + } else { + setPrompt(promptHistory[newIndex]); + log.info(`📜 History: ${newIndex + 1}/${promptHistory.length}`); + } + }; + + const addToFileHistory = async (filePath: string) => { + if (!filePath) return; + + // Use functional update to get current state + setFileHistory(currentHistory => { + // Remove if already exists, then add to front, then limit to 8 + const filteredHistory = currentHistory.filter(path => path !== filePath); + const newFileHistory = [filePath, ...filteredHistory].slice(0, 8); + + log.info(`📁 Adding to file history: ${filePath.split(/[/\\]/).pop()}`, { + currentHistoryLength: currentHistory.length, + newHistoryLength: newFileHistory.length, + fullPath: filePath, + maxEntries: 8 + }); + + // Auto-save to store + saveToStore({ fileHistory: newFileHistory }).then(() => { + log.info('💾 File history saved to store', { + savedCount: newFileHistory.length, + savedFiles: newFileHistory.map(f => f.split(/[/\\]/).pop()) + }); + }).catch(error => { + log.error('Failed to save file history', { error: error.message }); + }); + + return newFileHistory; + }); + }; + + const openFileFromHistory = async (filePath: string) => { + try { + if (await tauriApi.fs.readFile(filePath)) { + await addFiles([filePath]); + setShowFileHistory(false); + log.info(`📂 Reopened from history: ${filePath.split(/[/\\]/).pop()}`); + } + } catch (error) { + log.error(`File no longer exists: ${filePath.split(/[/\\]/).pop()}`); + // Remove from history if file doesn't exist + const updatedHistory = fileHistory.filter(f => f !== filePath); + setFileHistoryWithLogging(updatedHistory); + await saveToStore({ fileHistory: updatedHistory }); + } + }; + + const onFileHistoryCleanup = async (validFiles: string[]) => { + setFileHistoryWithLogging(validFiles); + await saveToStore({ fileHistory: validFiles }); + }; + + const handleLightboxPromptSubmit = async (promptText: string, imagePath: string) => { + // Set the prompt and select the image for editing + setPrompt(promptText); + + // Find and select the image + setFiles(prev => prev.map(file => ({ + ...file, + selected: file.path === imagePath + }))); + + // Add to history and generate + await addToHistory(promptText); + await generateImage(promptText, [{ path: imagePath, src: '', isGenerated: false }]); + + log.info(`🎨 Lightbox edit: "${promptText}" on ${imagePath.split(/[/\\]/).pop()}`); + }; + + const importPrompts = async () => { + try { + const selected = await tauriApi.dialog.open({ + multiple: false, + filters: [{ name: 'JSON', extensions: ['json'] }] + }); + if (typeof selected === 'string') { + const contents = await tauriApi.fs.readTextFile(selected); + const newPrompts = JSON.parse(contents); + if (newPrompts.prompts && Array.isArray(newPrompts.prompts)) { + setPrompts(newPrompts.prompts); + savePrompts(newPrompts.prompts); + log.info(`✅ Prompts imported successfully from: ${selected}`); + } else { + log.error('Invalid prompts file format.'); + } + } + } catch (error) { + log.error('Failed to import prompts', { error: (error as Error).message }); + } + }; + + const exportPrompts = async () => { + log.info('Attempting to export prompts...'); + try { + const path = await tauriApi.dialog.save({ + defaultPath: 'kbot-prompts.json', + filters: [{ name: 'JSON', extensions: ['json'] }] + }); + + if (path) { + log.debug(`📂 Export path selected: ${path}`); + const dataToWrite = JSON.stringify({ prompts }, null, 2); + log.debug('📋 Data to be exported:', { promptCount: prompts.length, dataLength: dataToWrite.length }); + log.debug('💾 About to call writeTextFile...'); + await tauriApi.fs.writeTextFile(path, dataToWrite); + log.info(`✅ Prompts exported successfully to: ${path}`); + } else { + log.info('Export dialog was cancelled.'); + } + } catch (error) { + log.error('Failed to export prompts', { error: (error as Error).message }); + } + }; + + const deleteFilePermanently = async (pathToDelete: string) => { + log.info(`Requesting deletion of file: ${pathToDelete}`); + // This will be the new tauri command + await tauriApi.requestFileDeletion({ path: pathToDelete }); + }; + + const saveImageAs = async (imagePath: string) => { + const imageFile = files.find(f => f.path === imagePath); + if (!imageFile) { + log.error(`Could not find image to save: ${imagePath}`); + return; + } + + try { + const defaultPath = imagePath.split(/[/\\]/).pop() || 'saved_image.png'; + const newPath = await tauriApi.dialog.save({ + defaultPath, + filters: [{ name: 'Images', extensions: ['png', 'jpg', 'jpeg'] }] + }); + + if (newPath) { + // Convert data URL to binary using fetch + const response = await fetch(imageFile.src); + const blob = await response.blob(); + const buffer = await blob.arrayBuffer(); + const uint8Array = new Uint8Array(buffer); + + await tauriApi.fs.writeFile(newPath, uint8Array); + log.info(`✅ Image saved successfully to: ${newPath}`); + } else { + log.info('Save dialog was cancelled.'); + } + } catch (error) { + log.error(`Failed to save image: ${(error as Error).message}`); + } + }; + + const generateDefaultDst = (fileCount: number, firstFilePath?: string) => { + if (fileCount === 1 && firstFilePath) { + const parsedPath = firstFilePath.split(/[/\\]/).pop() || 'image'; + const nameWithoutExt = parsedPath.replace(/\.[^/.]+$/, ""); + return `${nameWithoutExt}_out.png`; + } else { + const now = new Date(); + const hours = now.getHours().toString().padStart(2, '0'); + const minutes = now.getMinutes().toString().padStart(2, '0'); + return `data_${hours}_${minutes}.png`; + } + }; + + const addImageFromUrl = async (url: string) => { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch image: ${response.statusText}`); + } + const blob = await response.blob(); + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => { + const base64data = reader.result as string; + const newImageFile: ImageFile = { + path: `url_${Date.now()}.jpg`, + src: base64data, + }; + setFiles(prevFiles => [...prevFiles, newImageFile]); + }; + } catch (error) { + console.error('Failed to add image from URL:', error); + } + }; + + useTauriListeners({ + setPrompt, + setDst, + setApiKey, + setIpcInitialized, + setPrompts, + setFiles, + isGenerating, + generationTimeoutId, + setGenerationTimeoutId, + setIsGenerating, + prompt, + setCurrentIndex, + setPromptHistory, + setFileHistory: setFileHistoryWithLogging, + addToFileHistory, + setErrorMessage, + }); + + const addFiles = async (newPaths: string[]) => { + const uniqueNewPaths = newPaths.filter(newPath => !files.some(f => f.path === newPath)); + + if (uniqueNewPaths.length === 0) { + return; + } + + // Update destination directory + const firstPath = uniqueNewPaths[0]; + + // Check if this is a clipboard image (from temp directory) + const isClipboardImage = firstPath.includes('kbot_clipboard_') || + firstPath.includes('\\Temp\\') || // Windows temp + firstPath.includes('/tmp/') || // Unix/Linux temp + firstPath.includes('AppData\\Local\\Temp') || // Windows user temp + firstPath.includes('/var/folders/'); // macOS temp + + let newDir: string; + if (isClipboardImage) { + // For clipboard images, use current working directory or user's Documents folder + newDir = './'; // Use current working directory + log.info('📋 Clipboard image detected, using current working directory for output'); + } else { + // For regular files, use the directory of the first file + const lastSeparatorIndex = Math.max(firstPath.lastIndexOf('/'), firstPath.lastIndexOf('\\')); + newDir = firstPath.substring(0, lastSeparatorIndex); + } + + const currentFilename = dst.split(/[/\\]/).pop() || generateDefaultDst(1, firstPath); + const separator = firstPath.includes('\\') ? '\\' : '/'; + const newDst = newDir === './' ? currentFilename : `${newDir}${separator}${currentFilename}`; + setDst(newDst); + + // Read files + const newImageFiles: ImageFile[] = []; + for (const path of uniqueNewPaths) { + try { + const buffer = await tauriApi.fs.readFile(path); + const base64 = arrayBufferToBase64(Array.from(buffer)); + const mimeType = path.toLowerCase().endsWith('.png') ? 'image/png' : + path.toLowerCase().endsWith('.jpg') || path.toLowerCase().endsWith('.jpeg') ? 'image/jpeg' : + 'image/png'; + const src = `data:${mimeType};base64,${base64}`; + newImageFiles.push({ path, src, selected: false, isGenerated: false }); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); + console.error(`Failed to read file: ${path}`, e); + tauriApi.logErrorToConsole(`[Frontend Error] Failed to read file ${path}: ${errorMessage}`); + } + } + + const lastPath = uniqueNewPaths[uniqueNewPaths.length - 1]; + + setFiles(prevFiles => { + const combinedFiles = [...prevFiles, ...newImageFiles]; + const newIndex = combinedFiles.findIndex(f => f.path === lastPath); + if (newIndex !== -1) { + setCurrentIndex(newIndex); + } + + return combinedFiles.map(file => ({ + ...file, + selected: file.path === lastPath + })); + }); + }; + + const removeFile = (pathToRemove: string) => { + setFiles(prevFiles => prevFiles.filter(file => file.path !== pathToRemove)); + }; + + const clearAllFiles = () => { + setFiles([]); + }; + + const handleImageSelection = (imagePath: string, isMultiSelect: boolean) => { + setFiles(prev => + prev.map(file => { + if (file.path === imagePath) { + // For multi-select, toggle the current state. For single-select, always select it. + return { ...file, selected: isMultiSelect ? !file.selected : true }; + } + // For single-select, deselect all other images. + if (!isMultiSelect) { + return { ...file, selected: false }; + } + // For multi-select, leave other images as they are. + return file; + }) + ); + }; + + const getSelectedImages = () => { + return files.filter(file => file.selected); + }; + + const saveAndClose = async () => { + // Find the last generated image + const generatedFiles = files.filter(file => file.isGenerated); + if (generatedFiles.length === 0) { + log.warn('No generated images to save'); + return; + } + + const lastGenerated = generatedFiles[generatedFiles.length - 1]; + log.info(`💾 Saving and closing with: ${lastGenerated.path}`); + + try { + // Send the final result back to images.ts for saving + const result = { + prompt, + files: files.filter(f => !f.isGenerated).map(f => f.path), + dst, + generatedImage: { + src: lastGenerated.src, + filename: lastGenerated.path + } + }; + + await tauriApi.submitPrompt(result); + + log.info('✅ Final result sent, closing app'); + } catch (error) { + log.error('Failed to save and close', { error: (error as Error).message }); + } + }; + + const generateImage = async (promptText: string, includeImages: ImageFile[] = []) => { + if (!apiKey) { + log.error('No API key available for image generation'); + return; + } + + setIsGenerating(true); + setErrorMessage(null); // Clear any previous error messages + log.info(`🎨 Starting image generation via backend: "${promptText}"`); + + // Add placeholder image with spinner to the files grid + const placeholderFile: ImageFile = { + path: `generating_${Date.now()}`, + src: 'data:image/svg+xml;base64,' + btoa(` + + + + + + + Generating... + + `) + }; + + setFiles(prev => [...prev, placeholderFile]); + + try { + // Use the images.ts backend instead of direct API calls + const filePaths = includeImages.map(img => img.path); + const genDst = dst || `generated_${Date.now()}.png`; + + log.info('Sending generation request to images.ts backend', { + prompt: promptText, + files: filePaths, + dst: genDst + }); + + // Send generation request via Tauri command + await tauriApi.generateImageViaBackend({ + prompt: promptText, + files: filePaths, + dst: genDst + }); + + log.info('📤 Generation request sent to backend'); + + // Clear any existing timeout + if (generationTimeoutId) { + clearTimeout(generationTimeoutId); + } + + const timeoutId = setTimeout(() => { + log.warn('⏰ Generation timeout - resetting state'); + setIsGenerating(false); + setFiles(prev => prev.filter(file => !file.path.startsWith('generating_'))); + setGenerationTimeoutId(null); + }, 30000); + + setGenerationTimeoutId(timeoutId); + + } catch (error) { + log.error('Failed to send generation request', { + error: error instanceof Error ? error.message : JSON.stringify(error) + }); + setIsGenerating(false); + setFiles(prev => prev.filter(file => !file.path.startsWith('generating_'))); + } + }; + + // Auto-generate default destination file when files change + useEffect(() => { + if (files.length > 0 && !dst) { + const defaultDst = generateDefaultDst(files.length, files[0]?.path); + setDst(defaultDst); + } + }, [files, dst]); + + useEffect(() => { + const logBaseDirectories = async () => { + }; + logBaseDirectories(); + }, []); + + async function submit() { + if (!prompt.trim()) { + log.warn('Please enter a prompt first'); + return; + } + + if (apiKey) { + // Add to history before generating + await addToHistory(prompt); + + // Generate image via backend (always chat mode now) + // Only use explicitly selected images. If none are selected, generate from prompt alone. + const imagesToUse = getSelectedImages(); + + await generateImage(prompt, imagesToUse); + // Don't clear prompt - let user iterate + } else { + log.error('API key required for image generation'); + } + } + + const clearDebugMessages = async () => { + setDebugMessages([]); + log.clearLocalMessages(); + await tauriApi.clearDebugMessages(); + }; + + const sendIPCMessage = async (messageType: string, data: any) => { + await tauriApi.sendIPCMessage(messageType, data); + log.info(`IPC message sent: ${messageType}`, data); + }; + + const sendMessageToImages = async () => { + if (!messageToSend.trim()) return; + + const message = { + message: messageToSend, + timestamp: Date.now(), + source: 'gui' + }; + + try { + await tauriApi.sendMessageToStdout(JSON.stringify(message)); + log.info(`📤 Sent to images.ts: ${messageToSend}`, message); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : JSON.stringify(error); + log.error(`Failed to send message: ${errorMessage}`); + } + + // Clear the input + setMessageToSend(''); + }; + + const savePrompts = async (promptsToSave: PromptTemplate[]) => { + try { + await saveToStore({ prompts: promptsToSave }); + } catch (error) { + log.error('Failed to save prompts', { + error: (error as Error).message + }); + } + }; + + async function openFilePicker() { + const { isTauri: isTauriEnv } = await tauriApi.ensureTauriApi(); + if (!isTauriEnv) { + // Browser fallback: create file input + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = true; + input.accept = 'image/*'; + input.onchange = (e) => { + const target = e.target as HTMLInputElement; + if (target.files) { + const fileArray = Array.from(target.files); + const newImageFiles: ImageFile[] = []; + let loadedCount = 0; + + fileArray.forEach(file => { + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target?.result) { + newImageFiles.push({ + path: file.name, + src: e.target.result as string + }); + loadedCount++; + if (loadedCount === fileArray.length) { + setFiles(prevFiles => [...prevFiles, ...newImageFiles]); + } + } + }; + reader.readAsDataURL(file); + }); + } + }; + input.click(); + return; + } + + try { + if (!tauriApi.dialog.open) { + console.error('Open function not available'); + return; + } + + const selected = await tauriApi.dialog.open({ + multiple: true, + filters: [{ + name: 'Images', + extensions: ['png', 'jpeg', 'jpg'] + }] + }); + if (Array.isArray(selected)) { + addFiles(selected); + } + } catch (e) { + console.error('File picker error:', e); + tauriApi.logErrorToConsole(`[Frontend Error] File picker error: ${JSON.stringify(e)}`); + } + } + + async function openSaveDialog() { + try { + // Extract current filename from dst for default, or use smart default + const currentFilename = dst.split(/[/\\]/).pop() || generateDefaultDst(files.length, files[0]?.path); + + const selected = await tauriApi.dialog.save({ + defaultPath: currentFilename, + filters: [{ + name: 'Images', + extensions: ['png', 'jpg'] + }] + }); + + if (selected) { + setDst(selected); + } + } catch (e) { + console.error('Save dialog error:', e); + tauriApi.logErrorToConsole(`[Frontend Error] Save dialog error: ${JSON.stringify(e)}`); + } + } + + return ( +
+ {/* Background decoration */} +
+
+
+
+ +
+
+ + {/* Error Message Display */} + {errorMessage && ( +
+
+
+
+ + + +
+
+

+ Generation Error +

+
+

{errorMessage}

+
+
+
+
+ +
+
+
+ )} + + + {/* Debug Panel */} + {showDebugPanel && ( + + )} +
+ +
+ ); +}; + +export default ImageWizard; diff --git a/packages/kbot/gui/tauri-app/src/components/Settings.tsx b/packages/kbot/gui/tauri-app/src/components/Settings.tsx new file mode 100644 index 00000000..e236a349 --- /dev/null +++ b/packages/kbot/gui/tauri-app/src/components/Settings.tsx @@ -0,0 +1,149 @@ +import React, { useState, useEffect } from 'react'; +import { ArrowLeft } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import { saveToStore } from '../lib/init'; + +interface SettingsProps { + apiKey: string; + setApiKey: (key: string) => void; + isDarkMode: boolean; + toggleTheme: () => void; +} + +const Settings: React.FC = ({ + apiKey, + setApiKey, + isDarkMode, + toggleTheme, +}) => { + const navigate = useNavigate(); + const [localApiKey, setLocalApiKey] = useState(apiKey); + + useEffect(() => { + setLocalApiKey(apiKey); + }, [apiKey]); + + const handleSave = async () => { + console.log('💾 Saving API key:', localApiKey ? '***configured***' : 'empty'); + setApiKey(localApiKey); + + // Save API key to persistent store + try { + await saveToStore({ apiKey: localApiKey }); + console.log('✅ API key saved to store successfully'); + } catch (error) { + console.error('❌ Failed to save API key:', error); + // Don't navigate if save failed + return; + } + + // Navigate back to main view + navigate('/'); + }; + + return ( +
+ {/* Background decoration */} +
+
+
+
+ +
+ {/* Header */} +
+
+ +

Settings

+
+
+ + {/* Settings Form */} +
+ {/* API Key Section */} +
+
+ +
+ setLocalApiKey(e.target.value)} + placeholder="Enter your Google AI API key" + className="w-full glass-input p-4 rounded-xl" + /> +
+
+ + {localApiKey ? 'API key configured' : 'API key required for image generation'} + +
+
+

+ Get your API key from{' '} + + Google AI Studio + +

+
+
+ + {/* Theme Section */} +
+
+
+

Theme

+

Choose your preferred color scheme

+
+ +
+
+ + {/* Save Button */} +
+ +
+
+
+
+ ); +}; + +export default Settings; diff --git a/packages/kbot/gui/tauri-app/src/components/ui/LedBar.tsx b/packages/kbot/gui/tauri-app/src/components/ui/LedBar.tsx new file mode 100644 index 00000000..30bb3356 --- /dev/null +++ b/packages/kbot/gui/tauri-app/src/components/ui/LedBar.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { cn } from '@/lib/utils'; + +interface LedBarProps extends React.HTMLAttributes { + value: number; + max?: number; +} + +const LedBar = React.forwardRef( + ({ className, value, max = 100, ...props }, ref) => { + const percentage = max > 0 ? (value / max) * 100 : 0; + const isOverload = percentage > 100; + const displayPercentage = Math.min(percentage, 200); + + const normalWidth = isOverload ? '100%' : `${displayPercentage}%`; + const overloadWidth = isOverload ? `${Math.min(displayPercentage - 100, 100)}%` : '0%'; + + return ( +
+
+ {isOverload && ( +
+
+
+ )} +
+ ); + } +); + +LedBar.displayName = 'LedBar'; + +export { LedBar }; \ No newline at end of file diff --git a/packages/kbot/gui/tauri-app/src/components/ui/accordion.tsx b/packages/kbot/gui/tauri-app/src/components/ui/accordion.tsx new file mode 100644 index 00000000..e6a723d0 --- /dev/null +++ b/packages/kbot/gui/tauri-app/src/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/packages/kbot/gui/tauri-app/src/components/ui/alert-dialog.tsx b/packages/kbot/gui/tauri-app/src/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..8722561c --- /dev/null +++ b/packages/kbot/gui/tauri-app/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/packages/kbot/gui/tauri-app/src/components/ui/alert.tsx b/packages/kbot/gui/tauri-app/src/components/ui/alert.tsx new file mode 100644 index 00000000..41fa7e05 --- /dev/null +++ b/packages/kbot/gui/tauri-app/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/packages/kbot/gui/tauri-app/src/components/ui/aspect-ratio.tsx b/packages/kbot/gui/tauri-app/src/components/ui/aspect-ratio.tsx new file mode 100644 index 00000000..c4abbf37 --- /dev/null +++ b/packages/kbot/gui/tauri-app/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/packages/kbot/gui/tauri-app/src/components/ui/avatar.tsx b/packages/kbot/gui/tauri-app/src/components/ui/avatar.tsx new file mode 100644 index 00000000..991f56ec --- /dev/null +++ b/packages/kbot/gui/tauri-app/src/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/packages/kbot/gui/tauri-app/src/components/ui/badge.tsx b/packages/kbot/gui/tauri-app/src/components/ui/badge.tsx new file mode 100644 index 00000000..f000e3ef --- /dev/null +++ b/packages/kbot/gui/tauri-app/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/packages/kbot/gui/tauri-app/src/components/ui/breadcrumb.tsx b/packages/kbot/gui/tauri-app/src/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..71a5c325 --- /dev/null +++ b/packages/kbot/gui/tauri-app/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>