diff --git a/package.json b/package.json index b0ede2b0..6f6136d5 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,12 @@ "dependencies": { "@headlessui/react": "^2.1.10", "@react-oauth/google": "^0.12.1", - "@tauri-apps/api": ">=2.0.0", + "@tauri-apps/api": "^2.2.0", "@tauri-apps/plugin-autostart": "~2", "@tauri-apps/plugin-deep-link": "^2.2.0", "@tauri-apps/plugin-global-shortcut": "~2.0.0", "@tauri-apps/plugin-http": "~2.0.1", - "@tauri-apps/plugin-os": "^2.0.0", + "@tauri-apps/plugin-os": "^2.2.0", "@tauri-apps/plugin-shell": ">=2.0.0", "@tauri-apps/plugin-websocket": "~2", "@tauri-apps/plugin-window": "2.0.0-alpha.1", @@ -45,7 +45,7 @@ "zustand": "^5.0.0" }, "devDependencies": { - "@tauri-apps/cli": ">=2.0.0", + "@tauri-apps/cli": "^2.2.7", "@types/lodash": "^4.17.12", "@types/markdown-it": "^14.1.2", "@types/node": "^22.8.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76793841..1f689742 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^0.12.1 version: 0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tauri-apps/api': - specifier: '>=2.0.0' - version: 2.0.2 + specifier: ^2.2.0 + version: 2.2.0 '@tauri-apps/plugin-autostart': specifier: ~2 version: 2.2.0 @@ -30,7 +30,7 @@ importers: specifier: ~2.0.1 version: 2.0.1 '@tauri-apps/plugin-os': - specifier: ^2.0.0 + specifier: ^2.2.0 version: 2.2.0 '@tauri-apps/plugin-shell': specifier: '>=2.0.0' @@ -109,8 +109,8 @@ importers: version: 5.0.0(@types/react@18.3.11)(immer@10.1.1)(react@18.3.1) devDependencies: '@tauri-apps/cli': - specifier: '>=2.0.0' - version: 2.0.3 + specifier: ^2.2.7 + version: 2.2.7 '@types/lodash': specifier: ^4.17.12 version: 4.17.12 @@ -638,75 +638,75 @@ packages: resolution: {integrity: sha512-ZMOc3eu9amwvkC6M69h3hWt4/EsFaAXmtkiw4xd2LN59/lTb4ZQiVfq2QKlRcu1rj3n/Tcr7U30ZopvHwXBGIg==} engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} - '@tauri-apps/api@2.0.2': - resolution: {integrity: sha512-3wSwmG+1kr6WrgAFKK5ijkNFPp8TT3FLj3YHUb5EwMO+3FxX4uWlfSWkeeBy+Kc1RsKzugtYLuuya+98Flj+3w==} + '@tauri-apps/api@2.2.0': + resolution: {integrity: sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg==} - '@tauri-apps/cli-darwin-arm64@2.0.3': - resolution: {integrity: sha512-jIsbxGWS+As1ZN7umo90nkql/ZAbrDK0GBT6UsgHSz5zSwwArICsZFFwE1pLZip5yoiV5mn3TGG2c1+v+0puzQ==} + '@tauri-apps/cli-darwin-arm64@2.2.7': + resolution: {integrity: sha512-54kcpxZ3X1Rq+pPTzk3iIcjEVY4yv493uRx/80rLoAA95vAC0c//31Whz75UVddDjJfZvXlXZ3uSZ+bnCOnt0A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.0.3': - resolution: {integrity: sha512-ROITHtLTA1muyrwgyuwyasmaLCGtT4as/Kd1kerXaSDtFcYrnxiM984ZD0+FDUEDl5BgXtYa/sKKkKQFjgmM0A==} + '@tauri-apps/cli-darwin-x64@2.2.7': + resolution: {integrity: sha512-Vgu2XtBWemLnarB+6LqQeLanDlRj7CeFN//H8bVVdjbNzxcSxsvbLYMBP8+3boa7eBnjDrqMImRySSgL6IrwTw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.0.3': - resolution: {integrity: sha512-bQ3EZwCFfrLg/ZQ2I8sLuifSxESz4TP56SleTkKsPtTIZgNnKpM88PRDz4neiRroHVOq8NK0X276qi9LjGcXPw==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.2.7': + resolution: {integrity: sha512-+Clha2iQAiK9zoY/KKW0KLHkR0k36O78YLx5Sl98tWkwI3OBZFg5H5WT1plH/4sbZIS2aLFN6dw58/JlY9Bu/g==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.0.3': - resolution: {integrity: sha512-aLfAA8P9OTErVUk3sATxtXqpAtlfDPMPp4fGjDysEELG/MyekGhmh2k/kG/i32OdPeCfO+Nr37wJksARJKubGw==} + '@tauri-apps/cli-linux-arm64-gnu@2.2.7': + resolution: {integrity: sha512-Z/Lp4SQe6BUEOays9BQAEum2pvZF4w9igyXijP+WbkOejZx4cDvarFJ5qXrqSLmBh7vxrdZcLwoLk9U//+yQrg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-arm64-musl@2.0.3': - resolution: {integrity: sha512-I4MVD7nf6lLLRmNQPpe5beEIFM6q7Zkmh77ROA5BNu/+vHNL5kiTMD+bmd10ZL2r753A6pO7AvqkIxcBuIl0tg==} + '@tauri-apps/cli-linux-arm64-musl@2.2.7': + resolution: {integrity: sha512-+8HZ+txff/Y3YjAh80XcLXcX8kpGXVdr1P8AfjLHxHdS6QD4Md+acSxGTTNbplmHuBaSHJvuTvZf9tU1eDCTDg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@tauri-apps/cli-linux-x64-gnu@2.0.3': - resolution: {integrity: sha512-C6Jkx2zZGKkoi+sg5FK9GoH/0EvAaOgrZfF5azV5EALGba46g7VpWcZgp9zFUd7K2IzTi+0OOY8TQ2OVfKZgew==} + '@tauri-apps/cli-linux-x64-gnu@2.2.7': + resolution: {integrity: sha512-ahlSnuCnUntblp9dG7/w5ZWZOdzRFi3zl0oScgt7GF4KNAOEa7duADsxPA4/FT2hLRa0SvpqtD4IYFvCxoVv3Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-musl@2.0.3': - resolution: {integrity: sha512-qi4ghmTfSAl+EEUDwmwI9AJUiOLNSmU1RgiGgcPRE+7A/W+Am9UnxYySAiRbB/gJgTl9sj/pqH5Y9duP1/sqHg==} + '@tauri-apps/cli-linux-x64-musl@2.2.7': + resolution: {integrity: sha512-+qKAWnJRSX+pjjRbKAQgTdFY8ecdcu8UdJ69i7wn3ZcRn2nMMzOO2LOMOTQV42B7/Q64D1pIpmZj9yblTMvadA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@tauri-apps/cli-win32-arm64-msvc@2.0.3': - resolution: {integrity: sha512-UXxHkYmFesC97qVmZre4vY7oDxRDtC2OeKNv0bH+iSnuUp/ROxzJYGyaelnv9Ybvgl4YVqDCnxgB28qMM938TA==} + '@tauri-apps/cli-win32-arm64-msvc@2.2.7': + resolution: {integrity: sha512-aa86nRnrwT04u9D9fhf5JVssuAZlUCCc8AjqQjqODQjMd4BMA2+d4K9qBMpEG/1kVh95vZaNsLogjEaqSTTw4A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.0.3': - resolution: {integrity: sha512-D+xoaa35RGlkXDpnL5uDTpj29untuC5Wp6bN9snfgFDagD0wnFfC8+2ZQGu16bD0IteWqDI0OSoIXhNvy+F+wg==} + '@tauri-apps/cli-win32-ia32-msvc@2.2.7': + resolution: {integrity: sha512-EiJ5/25tLSQOSGvv+t6o3ZBfOTKB5S3vb+hHQuKbfmKdRF0XQu2YPdIi1CQw1DU97ZAE0Dq4frvnyYEKWgMzVQ==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.0.3': - resolution: {integrity: sha512-eWV9XWb4dSYHXl13OtYWLjX1JHphUEkHkkGwJrhr8qFBm7RbxXxQvrsUEprSi51ug/dwJenjJgM4zR8By4htfw==} + '@tauri-apps/cli-win32-x64-msvc@2.2.7': + resolution: {integrity: sha512-ZB8Kw90j8Ld+9tCWyD2fWCYfIrzbQohJ4DJSidNwbnehlZzP7wAz6Z3xjsvUdKtQ3ibtfoeTqVInzCCEpI+pWg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.0.3': - resolution: {integrity: sha512-JwEyhc5BAVpn4E8kxzY/h7+bVOiXQdudR1r3ODMfyyumZBfgIWqpD/WuTcPq6Yjchju1BSS+80jAE/oYwI/RKg==} + '@tauri-apps/cli@2.2.7': + resolution: {integrity: sha512-ZnsS2B4BplwXP37celanNANiIy8TCYhvg5RT09n72uR/o+navFZtGpFSqljV8fy1Y4ixIPds8FrGSXJCN2BerA==} engines: {node: '>= 10'} hasBin: true @@ -2727,78 +2727,78 @@ snapshots: '@tauri-apps/api@2.0.0-alpha.6': {} - '@tauri-apps/api@2.0.2': {} + '@tauri-apps/api@2.2.0': {} - '@tauri-apps/cli-darwin-arm64@2.0.3': + '@tauri-apps/cli-darwin-arm64@2.2.7': optional: true - '@tauri-apps/cli-darwin-x64@2.0.3': + '@tauri-apps/cli-darwin-x64@2.2.7': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.0.3': + '@tauri-apps/cli-linux-arm-gnueabihf@2.2.7': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.0.3': + '@tauri-apps/cli-linux-arm64-gnu@2.2.7': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.0.3': + '@tauri-apps/cli-linux-arm64-musl@2.2.7': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.0.3': + '@tauri-apps/cli-linux-x64-gnu@2.2.7': optional: true - '@tauri-apps/cli-linux-x64-musl@2.0.3': + '@tauri-apps/cli-linux-x64-musl@2.2.7': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.0.3': + '@tauri-apps/cli-win32-arm64-msvc@2.2.7': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.0.3': + '@tauri-apps/cli-win32-ia32-msvc@2.2.7': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.0.3': + '@tauri-apps/cli-win32-x64-msvc@2.2.7': optional: true - '@tauri-apps/cli@2.0.3': + '@tauri-apps/cli@2.2.7': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.0.3 - '@tauri-apps/cli-darwin-x64': 2.0.3 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.0.3 - '@tauri-apps/cli-linux-arm64-gnu': 2.0.3 - '@tauri-apps/cli-linux-arm64-musl': 2.0.3 - '@tauri-apps/cli-linux-x64-gnu': 2.0.3 - '@tauri-apps/cli-linux-x64-musl': 2.0.3 - '@tauri-apps/cli-win32-arm64-msvc': 2.0.3 - '@tauri-apps/cli-win32-ia32-msvc': 2.0.3 - '@tauri-apps/cli-win32-x64-msvc': 2.0.3 + '@tauri-apps/cli-darwin-arm64': 2.2.7 + '@tauri-apps/cli-darwin-x64': 2.2.7 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.2.7 + '@tauri-apps/cli-linux-arm64-gnu': 2.2.7 + '@tauri-apps/cli-linux-arm64-musl': 2.2.7 + '@tauri-apps/cli-linux-x64-gnu': 2.2.7 + '@tauri-apps/cli-linux-x64-musl': 2.2.7 + '@tauri-apps/cli-win32-arm64-msvc': 2.2.7 + '@tauri-apps/cli-win32-ia32-msvc': 2.2.7 + '@tauri-apps/cli-win32-x64-msvc': 2.2.7 '@tauri-apps/plugin-autostart@2.2.0': dependencies: - '@tauri-apps/api': 2.0.2 + '@tauri-apps/api': 2.2.0 '@tauri-apps/plugin-deep-link@2.2.0': dependencies: - '@tauri-apps/api': 2.0.2 + '@tauri-apps/api': 2.2.0 '@tauri-apps/plugin-global-shortcut@2.0.0': dependencies: - '@tauri-apps/api': 2.0.2 + '@tauri-apps/api': 2.2.0 '@tauri-apps/plugin-http@2.0.1': dependencies: - '@tauri-apps/api': 2.0.2 + '@tauri-apps/api': 2.2.0 '@tauri-apps/plugin-os@2.2.0': dependencies: - '@tauri-apps/api': 2.0.2 + '@tauri-apps/api': 2.2.0 '@tauri-apps/plugin-shell@2.0.0': dependencies: - '@tauri-apps/api': 2.0.2 + '@tauri-apps/api': 2.2.0 '@tauri-apps/plugin-websocket@2.0.0': dependencies: - '@tauri-apps/api': 2.0.2 + '@tauri-apps/api': 2.2.0 '@tauri-apps/plugin-window@2.0.0-alpha.1': dependencies: diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index bfa92750..a4d879dd 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -17,6 +17,18 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -41,6 +53,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -584,6 +602,13 @@ dependencies = [ name = "coco" version = "0.1.0" dependencies = [ + "futures", + "lazy_static", + "log", + "once_cell", + "ordered-float", + "pizza-common", + "reqwest", "serde", "serde_json", "tauri", @@ -598,6 +623,7 @@ dependencies = [ "tauri-plugin-store", "tauri-plugin-theme", "tauri-plugin-websocket", + "tokio", ] [[package]] @@ -611,7 +637,7 @@ dependencies = [ "cocoa-foundation", "core-foundation 0.10.0", "core-graphics", - "foreign-types", + "foreign-types 0.5.0", "libc", "objc", ] @@ -738,7 +764,7 @@ dependencies = [ "bitflags 2.6.0", "core-foundation 0.10.0", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -1236,6 +1262,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1243,7 +1278,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -1257,6 +1292,12 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1282,6 +1323,21 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1289,6 +1345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1371,6 +1428,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1518,8 +1576,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1723,6 +1783,10 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "hashbrown" @@ -1853,10 +1917,26 @@ dependencies = [ ] [[package]] -name = "hyper-util" -version = "0.1.9" +name = "hyper-tls" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", @@ -2321,6 +2401,23 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2677,12 +2774,65 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -2936,6 +3086,24 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pizza-common" +version = "0.1.0" +source = "git+https://github.com/infinilabs/pizza-common?branch=main#60ff2901bd302777b8c754cfd0bc436fac377838" +dependencies = [ + "bytes", + "camino", + "getrandom 0.2.15", + "hashbrown 0.14.5", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", + "serde_json", + "serde_with", + "uuid", +] + [[package]] name = "pkg-config" version = "0.3.31" @@ -3307,9 +3475,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.8" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "base64 0.22.1", "bytes", @@ -3324,11 +3492,13 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -3342,8 +3512,10 @@ dependencies = [ "sync_wrapper", "system-configuration", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util", + "tower", "tower-service", "url", "wasm-bindgen", @@ -3483,6 +3655,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "schemars" version = "0.8.21" @@ -3516,6 +3697,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.22.0" @@ -3796,7 +4000,7 @@ dependencies = [ "bytemuck", "cfg_aliases", "core-graphics", - "foreign-types", + "foreign-types 0.5.0", "js-sys", "log", "objc2", @@ -4606,6 +4810,16 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.0" @@ -4716,6 +4930,27 @@ dependencies = [ "winnow 0.6.22", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -4943,6 +5178,12 @@ dependencies = [ "serde", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 356d3cc9..e04e778b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -6,7 +6,6 @@ authors = ["INFINI Labs"] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [lib] # The `_lib` suffix may seem redundant but it is necessary # to make the lib name unique and wouldn't conflict with the bin name. @@ -18,6 +17,8 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2.0.0", features = [] } [dependencies] +pizza-common = { git = "https://github.com/infinilabs/pizza-common", branch = "main"} + tauri = { version = "2.0.6", features = ["macos-private-api", "tray-icon", "image-png", "unstable"] } tauri-plugin-shell = "2.0.0" serde = { version = "1", features = ["derive"] } @@ -31,8 +32,14 @@ tauri-plugin-oauth = { git = "https://github.com/FabianLars/tauri-plugin-oauth", tauri-plugin-deep-link = "2.0.0" tauri-plugin-single-instance = "2.0.0" tauri-plugin-store = "2.2.0" +reqwest = "0.12.12" +futures = "0.3.31" +ordered-float = { version = "4.6.0", default-features = false } +lazy_static = "1.5.0" +log = "0.4.22" +tokio = "1.40.0" +once_cell = "1.20.2" -# tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" } [profile.dev] incremental = true # Compile your binary in smaller steps. diff --git a/src-tauri/src/common/auth.rs b/src-tauri/src/common/auth.rs new file mode 100644 index 00000000..40c22494 --- /dev/null +++ b/src-tauri/src/common/auth.rs @@ -0,0 +1,9 @@ +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; +use crate::common::health::Status; + +#[derive(Debug,Clone, Serialize, Deserialize)] +pub struct RequestAccessTokenResponse { + pub access_token: String, + pub expire_at: u32, +} diff --git a/src-tauri/src/common/connector.rs b/src-tauri/src/common/connector.rs new file mode 100644 index 00000000..05a38c2f --- /dev/null +++ b/src-tauri/src/common/connector.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug,Clone, Serialize, Deserialize)] +pub struct Connector { + pub id: String, + pub created: Option, + pub updated: Option, + pub name: String, + pub description: Option, + pub category: Option, + pub icon: Option, + pub tags: Option>, + pub url: Option, + pub assets: Option, +} +#[derive(Debug,Clone, Serialize, Deserialize)] +pub struct ConnectorAssets { + pub icons: Option>, +} \ No newline at end of file diff --git a/src-tauri/src/common/datasource.rs b/src-tauri/src/common/datasource.rs new file mode 100644 index 00000000..efd36a9f --- /dev/null +++ b/src-tauri/src/common/datasource.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; +use crate::common::connector::Connector; + +// The DataSource struct representing a datasource, which includes the Connector +#[derive(Debug,Clone, Serialize, Deserialize)] +pub struct DataSource { + pub id: String, + pub created: Option, + pub updated: Option, + pub r#type: Option, // Using 'r#type' to escape the reserved keyword 'type' + pub name: Option, + pub connector: Option, + pub connector_info: Option, +} + +#[derive(Debug,Clone, Serialize, Deserialize)] +pub struct ConnectorConfig { + pub id: Option, + pub config: Option, // Using serde_json::Value to handle any type of config +} \ No newline at end of file diff --git a/src-tauri/src/common/health.rs b/src-tauri/src/common/health.rs new file mode 100644 index 00000000..14f117cc --- /dev/null +++ b/src-tauri/src/common/health.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug,Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Status { + Green, + Yellow, + Red, +} + +#[derive(Debug,Clone, Serialize, Deserialize)] +pub struct Health { + pub services: HashMap, + pub status: Status, +} diff --git a/src-tauri/src/common/mod.rs b/src-tauri/src/common/mod.rs new file mode 100644 index 00000000..40a1d04e --- /dev/null +++ b/src-tauri/src/common/mod.rs @@ -0,0 +1,7 @@ +pub mod health; +pub mod profile; +pub mod server; +pub mod auth; +pub mod datasource; +pub mod connector; +pub mod search_response; diff --git a/src-tauri/src/common/profile.rs b/src-tauri/src/common/profile.rs new file mode 100644 index 00000000..3891c3cc --- /dev/null +++ b/src-tauri/src/common/profile.rs @@ -0,0 +1,15 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Debug,Clone, Serialize, Deserialize)] +pub struct Preferences { + pub theme: String, + pub language: String, +} + +#[derive(Debug,Clone, Serialize, Deserialize)] +pub struct UserProfile { + pub name: String, + pub email: String, + pub avatar: String, + pub preferences: Preferences, +} \ No newline at end of file diff --git a/src-tauri/src/common/search_response.rs b/src-tauri/src/common/search_response.rs new file mode 100644 index 00000000..25e9fbd8 --- /dev/null +++ b/src-tauri/src/common/search_response.rs @@ -0,0 +1,80 @@ +use std::error::Error; +use reqwest::Response; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Serialize, Deserialize)] +pub struct SearchResponse { + pub took: u64, + pub timed_out: bool, + pub _shards: Shards, + pub hits: Hits, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Shards { + pub total: u64, + pub successful: u64, + pub skipped: u64, + pub failed: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Hits { + pub total: Total, + pub max_score: Option, + pub hits: Vec>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Total { + pub value: u64, + pub relation: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SearchHit { + pub _index: String, + pub _type: String, + pub _id: String, + pub _score: Option, + pub _source: T, // This will hold the type we pass in (e.g., DataSource) +} + +pub async fn parse_search_results(response: Response) -> Result, Box> +where + T: for<'de> Deserialize<'de> + std::fmt::Debug, +{ + // Log the response status and headers + // dbg!(&response.status()); + // dbg!(&response.headers()); + + // Parse the response body to a serde::Value + let body = response + .json::() + .await + .map_err(|e| format!("Failed to parse JSON: {}", e))?; + + // Log the raw body before further processing + // dbg!(&body); + + // Deserialize into the generic search response + let search_response: SearchResponse = serde_json::from_value(body) + .map_err(|e| format!("Failed to deserialize search response: {}", e))?; + + // Log the deserialized search response + // dbg!(&search_response); + + // Collect the _source part from all hits + let results: Vec = search_response + .hits + .hits + .into_iter() + .map(|hit| hit._source) + .collect(); + + // Log the final results before returning + // dbg!(&results); + + Ok(results) +} \ No newline at end of file diff --git a/src-tauri/src/common/server.rs b/src-tauri/src/common/server.rs new file mode 100644 index 00000000..64e40ce4 --- /dev/null +++ b/src-tauri/src/common/server.rs @@ -0,0 +1,116 @@ +use std::hash::{Hash, Hasher}; +use serde::{Deserialize, Serialize}; +use crate::common::profile::UserProfile; + +#[derive(Debug,Clone, Serialize, Deserialize)] +pub struct Provider { + pub name: String, + pub icon: String, + pub website: String, + pub eula: String, + pub privacy_policy: String, + pub banner: String, + pub description: String, +} + +#[derive(Debug,Clone, Serialize, Deserialize)] +pub struct Version { + pub number: String, +} + +#[derive(Debug, Clone,Serialize, Deserialize)] +pub struct Sso { + pub url: String, +} + +#[derive(Debug,Clone, Serialize, Deserialize)] +pub struct AuthProvider { + pub sso: Sso, +} + +#[derive(Debug,Clone, Serialize, Deserialize)] +pub struct Server { + #[serde(default = "default_empty_string")] // Custom default function for empty string + pub id: String, + #[serde(default = "default_bool_type")] + pub builtin: bool, + pub name: String, + pub endpoint: String, + pub provider: Provider, + pub version: Version, + pub updated: String, + #[serde(default = "default_bool_type")] + pub public: bool, + #[serde(default = "default_available_type")] + pub available: bool, + #[serde(default = "default_user_profile_type")] // Custom default function for empty string + pub profile: Option, + pub auth_provider: AuthProvider, + #[serde(default = "default_priority_type")] + pub priority: u32, +} + +impl PartialEq for Server { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Server {} + +impl Hash for Server { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + + +#[derive(Debug,Clone, Serialize, Deserialize)] +pub struct ServerAccessToken { + #[serde(default = "default_empty_string")] // Custom default function for empty string + pub id: String, + pub access_token: String, + pub expired_at: u32, //unix timestamp in seconds +} + +impl ServerAccessToken{ + pub fn new(id: String, access_token: String, expired_at: u32) -> Self { + Self { + id, + access_token, + expired_at: expired_at, + } + } +} + +impl PartialEq for ServerAccessToken { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for ServerAccessToken {} + +impl Hash for ServerAccessToken { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +fn default_empty_string() -> String { + "".to_string() // Default to empty string if not provided +} + +fn default_bool_type() -> bool { + false // Default to false if not provided +} + +fn default_available_type() -> bool { + true +} +fn default_priority_type() -> u32 { + 0 +} +fn default_user_profile_type() -> Option { + None +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2751e793..986dc297 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,12 +1,21 @@ mod autostart; +mod common; +mod server; mod shortcut; +mod util; +use crate::server::servers::{load_or_insert_default_server, load_servers_token}; use autostart::{change_autostart, enable_autostart}; #[cfg(target_os = "macos")] use tauri::ActivationPolicy; -use tauri::{AppHandle, Emitter, Listener, Manager, WebviewWindow}; +use tauri::{AppHandle, Emitter, Listener, Manager, Runtime, WebviewWindow}; use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_deep_link::DeepLinkExt; +use tokio::runtime::Runtime as RT; // Add this import +// Add this import + +/// Tauri store name +pub(crate) const COCO_TAURI_STORE: &str = "coco_tauri_store"; #[tauri::command] fn change_window_height(handle: AppHandle, height: u32) { @@ -80,12 +89,39 @@ pub fn run() { change_autostart, hide_coco, switch_tray_icon, - // show_panel, - // hide_panel, - // close_panel + server::servers::add_coco_server, + server::servers::remove_coco_server, + server::servers::list_coco_servers, + server::servers::logout_coco_server, + server::servers::refresh_coco_server_info, + server::auth::handle_sso_callback, + server::profile::get_user_profiles, + server::datasource::get_datasources_by_server, + server::connector::get_connectors_by_server, + server::search::query_coco_servers, + // server::get_coco_server_health_info, + // server::get_coco_servers_health_info, + // server::get_user_profiles, + // server::get_coco_server_datasources, + // server::get_coco_server_connectors ]) .setup(|app| { - init(app.app_handle()); + + + // Get app handle + let app_handle = app.handle().clone(); + + // Create a single Tokio runtime instance + let rt = RT::new().expect("Failed to create Tokio runtime"); + + // Use the runtime to spawn the async initialization tasks + rt.spawn(async move { + dbg!("Running async initialization tasks"); + init(&app_handle).await; // Pass a reference to `app_handle` + dbg!("Async initialization tasks completed"); + }); + + shortcut::enable_shortcut(app); enable_tray(app); @@ -108,13 +144,26 @@ pub fn run() { dbg!(event.urls()); }); + Ok(()) }) .run(ctx) .expect("error while running tauri application"); } -fn init(_app_handle: &AppHandle) { +pub async fn init(app_handle: &AppHandle) { + dbg!("Initializing..."); + // Await the async functions to load the servers and tokens + if let Err(err) = load_or_insert_default_server(app_handle).await { + eprintln!("Failed to load servers: {}", err); + } + + if let Err(err) = load_servers_token(app_handle).await { + eprintln!("Failed to load server tokens: {}", err); + } + + dbg!("Initialization completed"); + // let window: WebviewWindow = app_handle.get_webview_window("main").unwrap(); // let panel = window.to_panel().unwrap(); @@ -231,15 +280,15 @@ fn enable_tray(app: &mut tauri::App) { let quit_i = MenuItem::with_id(app, "quit", "Quit Coco", true, None::<&str>).unwrap(); let settings_i = MenuItem::with_id(app, "settings", "Settings...", true, None::<&str>).unwrap(); - let open_i = MenuItem::with_id(app, "open", "Open Coco", true, None::<&str>).unwrap(); - let about_i = MenuItem::with_id(app, "about", "About Coco", true, None::<&str>).unwrap(); + let open_i = MenuItem::with_id(app, "open", "Show Coco", true, None::<&str>).unwrap(); + // let about_i = MenuItem::with_id(app, "about", "About Coco", true, None::<&str>).unwrap(); // let hide_i = MenuItem::with_id(app, "hide", "Hide Coco", true, None::<&str>).unwrap(); let menu = MenuBuilder::new(app) .item(&open_i) .separator() // .item(&hide_i) - .item(&about_i) + // .item(&about_i) .item(&settings_i) .separator() .item(&quit_i) diff --git a/src-tauri/src/server/auth.rs b/src-tauri/src/server/auth.rs new file mode 100644 index 00000000..eb6acb72 --- /dev/null +++ b/src-tauri/src/server/auth.rs @@ -0,0 +1,92 @@ +use std::fmt::format; +use reqwest::StatusCode; +use tauri::{AppHandle, Runtime}; +use crate::common::auth::RequestAccessTokenResponse; +use crate::common::server::{Server, ServerAccessToken}; +use crate::server::http_client::HttpClient; +use crate::server::profile::get_user_profiles; +use crate::server::servers::{get_server_by_id, persist_servers, persist_servers_token, save_access_token, save_server}; +use crate::util; +fn request_access_token_url(request_id: &str) -> String { + // Remove the endpoint part and keep just the path for the request + format!("/auth/request_access_token?request_id={}", request_id) +} + +#[tauri::command] +pub async fn handle_sso_callback( + app_handle: AppHandle, + server_id: String, + request_id: String, + code: String, +) -> Result<(), String> { + // Retrieve the server details using the server ID + let server = get_server_by_id(&server_id); + + if let Some(mut server) = server { + // Prepare the URL for requesting the access token (endpoint is base URL, path is relative) + save_access_token(server_id.clone(), ServerAccessToken::new(server_id.clone(), code.clone(), 60*15)); + let path = request_access_token_url( &request_id); + + // Send the request for the access token using the util::http::HttpClient::get method + let response = HttpClient::get(&server_id, &path) + .await + .map_err(|e| format!("Failed to send request to the server: {}", e))?; + + if response.status() == StatusCode::OK { + // Check if the response has a valid content length + if let Some(content_length) = response.content_length() { + if content_length > 0 { + // Deserialize the response body to get the access token + let token_result: Result = response.json().await; + + match token_result { + Ok(token) => { + // Save the access token for the server + let access_token = ServerAccessToken::new( + server_id.clone(), + token.access_token.clone(), + token.expire_at, + ); + dbg!(&server_id, &request_id, &code, &token); + save_access_token(server_id.clone(), access_token); + persist_servers_token(&app_handle)?; + + // Update the server's profile using the util::http::HttpClient::get method + let profile = get_user_profiles(app_handle.clone(), server_id.clone()).await; + dbg!(&profile); + + match profile { + Ok(p) => { + server.profile = Some(p); + server.available = true; + save_server(&server); + persist_servers(&app_handle)?; + Ok(()) + } + Err(e) => Err(format!("Failed to get user profile: {}", e)), + } + } + Err(e) => Err(format!("Failed to deserialize the token response: {}", e)), + } + } else { + Err("Received empty response body.".to_string()) + } + } else { + Err("Could not determine the content length.".to_string()) + } + } else { + Err(format!( + "Request failed with status: {}, URL: {}, Code: {}, Response: {:?}", + response.status(), + path, + code, + response + )) + } + } else { + Err(format!( + "Server not found for ID: {}, Request ID: {}, Code: {}", + server_id, request_id, code + )) + } +} \ No newline at end of file diff --git a/src-tauri/src/server/connector.rs b/src-tauri/src/server/connector.rs new file mode 100644 index 00000000..df70aa55 --- /dev/null +++ b/src-tauri/src/server/connector.rs @@ -0,0 +1,147 @@ +use crate::common::connector::Connector; +use crate::common::search_response::parse_search_results; +use crate::server::http_client::HttpClient; +use crate::server::servers::{get_all_servers, list_coco_servers}; +use lazy_static::lazy_static; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use tauri::{AppHandle, Runtime}; + +lazy_static! { + static ref CONNECTOR_CACHE: Arc>>> = Arc::new(RwLock::new(HashMap::new())); +} + +pub fn save_connectors_to_cache(server_id: &str, connectors: Vec) { + let mut cache = CONNECTOR_CACHE.write().unwrap(); // Acquire write lock + let connectors_map: HashMap = connectors + .into_iter() + .map(|connector| (connector.id.clone(), connector)) + .collect(); + cache.insert(server_id.to_string(), connectors_map); +} + +pub fn get_connector_by_id(server_id: &str, connector_id: &str) -> Option { + let cache = CONNECTOR_CACHE.read().unwrap(); // Async read lock + let server_cache = cache.get(server_id)?; + let connector = server_cache.get(connector_id)?; + Some(connector.clone()) +} + +pub async fn refresh_all_connectors( + app_handle: &AppHandle, +) -> Result<(), String> { + dbg!("Attempting to refresh all connectors"); + + let servers = get_all_servers(); + + // Collect all the tasks for fetching and refreshing connectors + let mut serverMap = HashMap::new(); + for server in servers { + dbg!("start fetch connectors for server: {}", &server.id); + let connectors = match get_connectors_by_server(app_handle.clone(), server.id.clone()).await { + Ok(connectors) => { + let connectors_map: HashMap = connectors + .into_iter() + .map(|connector| (connector.id.clone(), connector)) + .collect(); + connectors_map + } + Err(e) => { + dbg!("Failed to get connectors for server {}: {}", &server.id, e); + HashMap::new() // Return empty map on failure + } + }; + + serverMap.insert(server.id.clone(), connectors); + dbg!("end fetch connectors for server: {}", &server.id); + } + + // After all tasks have finished, perform a read operation on the cache + let cache_size = { + // Insert connectors into the cache (async write lock) + let mut cache = CONNECTOR_CACHE.write().unwrap(); // Async write lock + cache.clear(); + cache.extend(serverMap); + // let cache = CONNECTOR_CACHE.read().await; // Async read lock + cache.len() + }; + + dbg!("finished refresh connectors: {:?}", cache_size); + + Ok(()) +} + +pub async fn get_connectors_from_cache_or_remote(server_id: &str) -> Result, String> { + // Acquire the read lock and check cache for connectors + let cache = CONNECTOR_CACHE.read().unwrap(); // Acquire read lock + if let Some(connectors) = cache.get(server_id).cloned() { + return Ok(connectors.values().cloned().collect()); + } + + // Drop the read lock before performing async operations + drop(cache); + + // If the cache is empty, fetch connectors from the remote server + let connectors = fetch_connectors_by_server(server_id).await?; + + // Convert the Vec into HashMap + let connectors_map: HashMap = connectors.clone() + .into_iter() + .map(|connector| (connector.id.clone(), connector)) // Assuming Connector has an `id` field + .collect(); + + // Optionally, update the cache after fetching data from remote + let mut cache = CONNECTOR_CACHE.write().unwrap(); // Acquire write lock + cache.insert(server_id.to_string(), connectors_map.clone()); + + Ok(connectors) +} + +pub async fn fetch_connectors_by_server(id: &str) -> Result, String> { + dbg!("start get_connectors_by_server: id =", &id); + + // Use the generic GET method from HttpClient + let resp = HttpClient::get(&id, "/connector/_search") + .await + .map_err(|e| { + dbg!("Error fetching connector for id {}: {}", &id, &e); + format!("Error fetching connector: {}", e) + })?; + + // Log the raw response status and headers + dbg!("Response status: {:?}", resp.status()); + dbg!("Response headers: {:?}", resp.headers()); + + // Ensure the response body is not empty or invalid + if resp.status().is_success() { + dbg!("Received successful response for id: {}", &id); + } else { + dbg!("Failed to fetch connectors. Response status: {}", resp.status()); + } + + // Parse the search results directly from the response body + let datasources: Vec = parse_search_results(resp).await.map_err(|e| { + dbg!("Error parsing search results for id {}: {}", &id, &e); + e.to_string() + })?; + + // Log the parsed results + dbg!("Parsed connectors: {:?}", &datasources); + + // Save the connectors to the cache + save_connectors_to_cache(&id, datasources.clone()); + + dbg!("end get_connectors_by_server: id =", &id); + return Ok(datasources); +} + +#[tauri::command] +pub async fn get_connectors_by_server( + app_handle: AppHandle, + id: String, +) -> Result, String> { + //fetch_connectors_by_server + + let connectors = fetch_connectors_by_server(&id).await?; + Ok(connectors) +} diff --git a/src-tauri/src/server/datasource.rs b/src-tauri/src/server/datasource.rs new file mode 100644 index 00000000..e50d2bc8 --- /dev/null +++ b/src-tauri/src/server/datasource.rs @@ -0,0 +1,138 @@ +use crate::common::datasource::DataSource; +use crate::common::search_response::parse_search_results; +use crate::server::connector::{fetch_connectors_by_server, get_connector_by_id, get_connectors_by_server, get_connectors_from_cache_or_remote}; +use crate::server::http_client::HttpClient; +use crate::server::servers::{get_all_servers, list_coco_servers}; +use lazy_static::lazy_static; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use tauri::{AppHandle, Runtime}; +use crate::common::connector::Connector; + +lazy_static! { + static ref DATASOURCE_CACHE: Arc>>> = Arc::new(RwLock::new(HashMap::new())); +} + +pub fn save_datasource_to_cache(server_id: &str, datasources: Vec) { + let mut cache = DATASOURCE_CACHE.write().unwrap(); // Acquire write lock + let datasources_map: HashMap = datasources + .into_iter() + .map(|datasource| { + (datasource.id.clone(), datasource) + }) + .collect(); + cache.insert(server_id.to_string(), datasources_map); +} + +pub fn get_datasources_from_cache(server_id: &str) -> Option> { + let cache = DATASOURCE_CACHE.read().unwrap(); // Acquire read lock + dbg!("cache: {:?}", &cache); + let server_cache = cache.get(server_id)?; // Get the server's cache + Some(server_cache.clone()) +} + +pub async fn refresh_all_datasources( + app_handle: &AppHandle, +) -> Result<(), String> { + dbg!("Attempting to refresh all datasources"); + + let servers = get_all_servers(); + + let mut serverMap = HashMap::new(); + + for server in servers { + dbg!("fetch datasources for server: {}", &server.id); + + // Attempt to get datasources by server, and continue even if it fails + let mut connectors = match get_datasources_by_server(app_handle.clone(), server.id.clone()).await { + Ok(connectors) => { + // Process connectors only after fetching them + let connectors_map: HashMap = connectors + .into_iter() + .map(|mut connector| { + (connector.id.clone(), connector) + }) + .collect(); + dbg!("connectors_map: {:?}", &connectors_map); + connectors_map + } + Err(e) => { + dbg!("Failed to get dataSources for server {}: {}", &server.id, e); + HashMap::new() + } + }; + + let mut new_map = HashMap::new(); + for (id, mut datasource) in connectors.iter() { + dbg!("connector: {:?}", &datasource); + if let Some(existing_connector) = get_connector_by_id(&server.id, &datasource.id) { + // If found in cache, update the connector's info + dbg!("Found connector in cache for {}: {:?}", &datasource.id, &existing_connector); + let mut obj = datasource.clone(); + obj.connector_info = Some(existing_connector); + new_map.insert(id.clone(), obj); + } + } + + serverMap.insert(server.id.clone(), new_map); + } + + // Perform a read operation after all writes are done + let cache_size = { + let mut cache = DATASOURCE_CACHE.write().unwrap(); + cache.clear(); + cache.extend(serverMap); + cache.len() + }; + dbg!("datasource_map size: {:?}", cache_size); + + Ok(()) +} + +#[tauri::command] +pub async fn get_datasources_by_server( + app_handle: AppHandle, + id: String, +) -> Result, String> { + dbg!("get_datasources_by_server: id = {}", &id); + + // Perform the async HTTP request outside the cache lock + let resp = HttpClient::get(&id, "/datasource/_search") + .await + .map_err(|e| { + dbg!("Error fetching datasource: {}", &e); + format!("Error fetching datasource: {}", e) + })?; + + // Parse the search results from the response + let mut datasources:Vec = parse_search_results(resp).await.map_err(|e| { + dbg!("Error parsing search results: {}", &e); + e.to_string() + })?; + + // let connectors=fetch_connectors_by_server(id.as_str()).await?; + // + // // Convert the Vec into HashMap + // let connectors_map: HashMap = connectors + // .into_iter() + // .map(|connector| (connector.id.clone(), connector)) // Assuming Connector has an `id` field + // .collect(); + // + // for datasource in datasources.iter_mut() { + // if let Some(connector) = &datasource.connector { + // if let Some(connector_id) = connector.id.as_ref() { + // if let Some(existing_connector) = connectors_map.get(connector_id) { + // // If found in cache, update the connector's info + // datasource.connector_info = Some(existing_connector.clone()); + // } + // } + // } + // } + + dbg!("Parsed datasources: {:?}", &datasources); + + // Save the updated datasources to cache + save_datasource_to_cache(&id, datasources.clone()); + + Ok(datasources) +} diff --git a/src-tauri/src/server/health.rs b/src-tauri/src/server/health.rs new file mode 100644 index 00000000..0459ca3b --- /dev/null +++ b/src-tauri/src/server/health.rs @@ -0,0 +1,79 @@ + +use crate::COCO_TAURI_STORE; +use core::panic; +use futures::stream::FuturesUnordered; +use futures::FutureExt; +use futures::StreamExt; +use futures::TryFutureExt; +use ordered_float::OrderedFloat; +use reqwest::Client; +use serde::Serialize; +use serde_json::Map as JsonMap; +use serde_json::Value as Json; +use std::collections::HashMap; +use std::sync::LazyLock; +use tauri::AppHandle; +use tauri::Runtime; +use tauri_plugin_store::StoreExt; +use crate::util::http::HTTP_CLIENT; + +fn health_url(endpoint: &str) -> String { + format!("{endpoint}/health") +} + +#[tauri::command] +pub async fn get_coco_server_health_info(endpoint: String) -> bool { + let response = match HTTP_CLIENT + .get(health_url(&endpoint)) + .send() + .map_err(|_request_err| ()) + .await + { + Ok(response) => response, + Err(_) => return false, + }; + let json: JsonMap = response.json().await.expect("invalid response"); + let status = json + .get("status") + .expect("response does not have a [status] field") + .as_str() + .expect("status field is not a string"); + + status != "red" +} + +#[tauri::command] +pub async fn get_coco_servers_health_info( + app_handle: AppHandle, +) -> Result, ()> { + // let coco_server_endpoints = _list_coco_server_endpoints(&app_handle).await?; + // + // let mut futures = FuturesUnordered::new(); + // for coco_server_endpoint in coco_server_endpoints { + // let request_future = HTTP_CLIENT.get(health_url(&coco_server_endpoint)).send(); + // futures.push(request_future.map(|request_result| (coco_server_endpoint, request_result))); + // } + // + // let mut health_info = HashMap::new(); + // + // while let Some((endpoint, res_response)) = futures.next().await { + // let healthy = match res_response { + // Ok(response) => { + // let json: JsonMap = response.json().await.expect("invalid response"); + // let status = json + // .get("status") + // .expect("response does not have a [status] field") + // .as_str() + // .expect("status field is not a string"); + // status != "red" + // } + // Err(_) => false, + // }; + // + // health_info.insert(endpoint, healthy); + // } + // + // Ok(health_info) + + Ok() +} diff --git a/src-tauri/src/server/http_client.rs b/src-tauri/src/server/http_client.rs new file mode 100644 index 00000000..611a6e37 --- /dev/null +++ b/src-tauri/src/server/http_client.rs @@ -0,0 +1,144 @@ +use std::future::Future; +use std::time::Duration; +use lazy_static::lazy_static; +use tauri::AppHandle; +use crate::server::servers::{get_server_by_id, get_server_token}; + +use once_cell::sync::Lazy; +use tokio::sync::Mutex; +use reqwest::{Client, Method, RequestBuilder, Response, StatusCode}; + +pub static HTTP_CLIENT: Lazy> = Lazy::new(|| { + let client = Client::builder() + .read_timeout(Duration::from_secs(3)) // Set a timeout of 3 second + .connect_timeout(Duration::from_secs(3)) // Set a timeout of 3 second + .timeout(Duration::from_secs(10)) // Set a timeout of 10 seconds + .danger_accept_invalid_certs(true) // example for self-signed certificates + .build() + .expect("Failed to build client"); + Mutex::new(client) +}); + +pub struct HttpClient; + +impl HttpClient { + // Utility function for properly joining paths + pub(crate) fn join_url(base: &str, path: &str) -> String { + let base = base.trim_end_matches('/'); + let path = path.trim_start_matches('/'); + format!("{}/{}", base, path) + } + + pub async fn send_raw_request( + method: Method, + url: &str, + headers: Option, + body: Option, + ) -> Result { + + let request_builder = Self::get_request_builder(method, url, headers, body).await; + + // Send the request + let response = match request_builder.send().await { + Ok(resp) => resp, + Err(e) => { + dbg!("Failed to send request: {}", &e); + return Err(format!("Failed to send request: {}", e)); + } + }; + Ok(response) + } + + pub async fn get_request_builder( + method: Method, + url: &str, + headers: Option, + body: Option, + ) -> RequestBuilder { + let client = HTTP_CLIENT.lock().await; // Acquire the lock on HTTP_CLIENT + + // Build the request + let mut request_builder = client.request(method.clone(), url); + + // Add headers if present + if let Some(h) = headers { + request_builder = request_builder.headers(h); + } + + // Add body if present + if let Some(b) = body { + request_builder = request_builder.body(b); + } + + request_builder + } + + pub async fn send_request( + server_id: &str, + method: Method, + path: &str, + body: Option, + ) -> Result { + // Fetch the server using the server_id + let server = get_server_by_id(server_id); + if let Some(s) = server { + // Construct the URL + let url = HttpClient::join_url(&s.endpoint, path); + + // dbg!(&url); + // dbg!(&server_id); + + // Retrieve the token for the server (token is optional) + let token = get_server_token(server_id).map(|t| t.access_token.clone()); + + + // Create headers map (optional "X-API-TOKEN" header) + let mut headers = reqwest::header::HeaderMap::new(); + if let Some(t) = token { + headers.insert("X-API-TOKEN", reqwest::header::HeaderValue::from_str(&t).unwrap()); + } + + // dbg!(&headers); + + Self::send_raw_request(method, &url, Some(headers), body).await + + } else { + Err("Server not found".to_string()) + } + } + + + // Convenience method for GET requests (as it's the most common) + pub async fn get( + server_id: &str, + path: &str, + ) -> Result { + HttpClient::send_request(server_id, Method::GET, path, None).await + } + + // Convenience method for POST requests + pub async fn post( + server_id: &str, + path: &str, + body: reqwest::Body, + ) -> Result { + HttpClient::send_request( server_id, Method::POST, path, Some(body)).await + } + + // Convenience method for PUT requests + pub async fn put( + server_id: &str, + path: &str, + body: reqwest::Body, + ) -> Result { + HttpClient::send_request( server_id, Method::PUT, path, Some(body)).await + } + + // Convenience method for DELETE requests + pub async fn delete( + server_id: &str, + path: &str, + ) -> Result { + HttpClient::send_request(server_id, Method::DELETE, path, None).await + } +} \ No newline at end of file diff --git a/src-tauri/src/server/mod.rs b/src-tauri/src/server/mod.rs new file mode 100644 index 00000000..9323b89d --- /dev/null +++ b/src-tauri/src/server/mod.rs @@ -0,0 +1,20 @@ +//! This file contains Rust APIs related to Coco Server management. + +use futures::FutureExt; +use futures::StreamExt; +use futures::TryFutureExt; +use reqwest::Client; +use serde::Serialize; +use std::sync::LazyLock; +use tauri::Runtime; +use tauri_plugin_store::StoreExt; + +pub mod servers; +pub mod auth; +// pub mod health; +pub mod datasource; +pub mod connector; +pub mod search; +pub mod profile; +pub mod http_client; + diff --git a/src-tauri/src/server/profile.rs b/src-tauri/src/server/profile.rs new file mode 100644 index 00000000..36fe655d --- /dev/null +++ b/src-tauri/src/server/profile.rs @@ -0,0 +1,26 @@ +use crate::common::profile::UserProfile; +use crate::server::http_client::HttpClient; +use tauri::{AppHandle, Runtime}; + +#[tauri::command] +pub async fn get_user_profiles( + app_handle: AppHandle, + server_id: String, +) -> Result { + // Use the generic GET method from HttpClient + let response = HttpClient::get( &server_id, "/account/profile") + .await + .map_err(|e| format!("Error fetching profile: {}", e))?; + + if let Some(content_length) = response.content_length() { + if content_length > 0 { + let profile: UserProfile = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + return Ok(profile); + } + } + + Err("Profile not found or empty response".to_string()) +} diff --git a/src-tauri/src/server/search.rs b/src-tauri/src/server/search.rs new file mode 100644 index 00000000..2e83c534 --- /dev/null +++ b/src-tauri/src/server/search.rs @@ -0,0 +1,182 @@ +use std::collections::HashMap; +use ordered_float::OrderedFloat; +use reqwest::Method; +use serde::Serialize; +use tauri::{ AppHandle, Runtime}; +use serde_json::Map as JsonMap; +use serde_json::Value as Json; +use crate::server::http_client::{HttpClient, HTTP_CLIENT}; +use crate::server::servers::{get_all_servers, get_server_token, get_servers_as_hashmap}; +use futures::stream::{FuturesUnordered, StreamExt}; + +struct DocumentsSizedCollector { + size: u64, + /// Documents and socres + /// + /// Sorted by score, in descending order. + docs: Vec<(JsonMap, OrderedFloat)>, +} + +impl DocumentsSizedCollector { + fn new(size: u64) -> Self { + // there will be size + 1 documents in docs at max + let docs = Vec::with_capacity((size + 1).try_into().expect("overflow")); + + Self { size, docs } + } + + fn push(&mut self, item: JsonMap, score: f64) { + let score = OrderedFloat(score); + let insert_idx = match self.docs.binary_search_by(|(_doc, s)| score.cmp(s)) { + Ok(idx) => idx, + Err(idx) => idx, + }; + + self.docs.insert(insert_idx, (item, score)); + + // cast usize to u64 is safe + if self.docs.len() as u64 > self.size { + self.docs.truncate(self.size.try_into().expect( + "self.size < a number of type usize, it can be expressed using usize, we are safe", + )); + } + } + + fn documents(self) -> impl ExactSizeIterator> { + self.docs.into_iter().map(|(doc, _score)| doc) + } +} + +#[derive(Debug, Serialize)] +pub struct QueryResponse { + failed_coco_servers: Vec, + documents: Vec>, + total_hits: u64, +} + + +fn get_name(provider_info: &JsonMap) -> &str { + provider_info + .get("name") + .expect("provider info does not have a [name] field") + .as_str() + .expect("field [name] should be a string") +} + +fn get_public(provider_info: &JsonMap) -> bool { + provider_info + .get("public") + .expect("provider info does not have a [public] field") + .as_bool() + .expect("field [public] should be a string") +} +#[tauri::command] +pub async fn query_coco_servers( + app_handle: AppHandle, + from: u64, + size: u64, + query_strings: HashMap, +) -> Result { + println!( + "DBG: query_coco_servers, from: {} size: {} query_strings {:?}", + from, size, query_strings + ); + + let coco_servers = get_servers_as_hashmap(); + let mut futures = FuturesUnordered::new(); + let size_for_each_request = (from + size).to_string(); + + for (_,server) in coco_servers { + let url = HttpClient::join_url(&server.endpoint, "/query/_search"); + let client = HTTP_CLIENT.lock().await; // Acquire the lock on HTTP_CLIENT + let mut request_builder = client.request(Method::GET, url); + + if !server.public { + if let Some(token) = get_server_token(&server.id).map(|t| t.access_token) { + request_builder = request_builder.header("X-API-TOKEN", token); + } + } + let query_strings_cloned = query_strings.clone(); // Clone for each iteration + + let size=size_for_each_request.clone(); + let future = async move { + let response = request_builder + .query(&[("from", "0"), ("size", size.as_str())]) + .query(&query_strings_cloned) // Use cloned instance + .send() + .await; + (server.id, response) + }; + + futures.push(future); + } + + let mut total_hits = 0; + let mut failed_coco_servers = Vec::new(); + let mut docs_collector = DocumentsSizedCollector::new(size); + + while let Some((name, res_response)) = futures.next().await { + match res_response { + Ok(response) => { + if let Ok(mut body) = response.json::>().await { + if let Some(Json::Object(mut hits)) = body.remove("hits") { + if let Some(Json::Number(hits_total_value)) = hits.get("total").and_then(|t| t.get("value")) { + if let Some(hits_total) = hits_total_value.as_u64() { + total_hits += hits_total; + } + } + if let Some(Json::Array(hits_hits)) = hits.remove("hits") { + for hit in hits_hits.into_iter().filter_map(|h| h.as_object().cloned()) { + if let (Some(Json::Number(score)), Some(Json::Object(source))) = (hit.get("_score"), hit.get("_source")) { + if let Some(score_value) = score.as_f64() { + docs_collector.push(source.clone(), score_value); + } + } + } + } + } + } + } + Err(_) => failed_coco_servers.push(name), + } + } + + let docs=docs_collector.documents().collect(); + + // dbg!(&total_hits); + // dbg!(&failed_coco_servers); + // dbg!(&docs); + + Ok(QueryResponse { + failed_coco_servers, + total_hits, + documents:docs , + }) +} + + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_docs_collector() { + let mut collector = DocumentsSizedCollector::new(3); + + for i in 0..10 { + collector.push(JsonMap::new(), i as f64); + } + + assert_eq!(collector.docs.len(), 3); + assert!(collector + .docs + .into_iter() + .map(|(_doc, score)| score) + .eq(vec![ + OrderedFloat(9.0), + OrderedFloat(8.0), + OrderedFloat(7.0) + ])); + } +} diff --git a/src-tauri/src/server/servers.rs b/src-tauri/src/server/servers.rs new file mode 100644 index 00000000..f7646b2d --- /dev/null +++ b/src-tauri/src/server/servers.rs @@ -0,0 +1,516 @@ +use crate::common::server::{AuthProvider, Provider, Server, ServerAccessToken, Sso, Version}; +use crate::server::http_client::HttpClient; +use crate::COCO_TAURI_STORE; +use lazy_static::lazy_static; +use reqwest::{Method, StatusCode}; +use serde_json::from_value; +use serde_json::Value as JsonValue; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::RwLock; +use tauri::AppHandle; +use tauri::Runtime; +use tauri_plugin_store::StoreExt; +use crate::server::connector::refresh_all_connectors; +use crate::server::datasource::refresh_all_datasources; +// Assuming you're using serde_json + +lazy_static! { + static ref SERVER_CACHE: Arc>> = Arc::new(RwLock::new(HashMap::new())); + static ref SERVER_TOKEN: Arc>> = Arc::new(RwLock::new(HashMap::new())); +} + +fn check_server_exists(id: &str) -> bool { + let cache = SERVER_CACHE.read().unwrap(); // Acquire read lock + cache.contains_key(id) +} + +pub fn get_server_by_id(id: &str) -> Option { + let cache = SERVER_CACHE.read().unwrap(); // Acquire read lock + cache.get(id).cloned() +} + +pub fn get_server_token(id: &str) -> Option { + let cache = SERVER_TOKEN.read().unwrap(); // Acquire read lock + cache.get(id).cloned() +} + +pub fn save_access_token(server_id: String, token: ServerAccessToken) -> bool { + let mut cache = SERVER_TOKEN.write().unwrap(); + cache.insert(server_id, token).is_none() +} + +fn check_endpoint_exists(endpoint: &str) -> bool { + let cache = SERVER_CACHE.read().unwrap(); + cache.values().any(|server| server.endpoint == endpoint) +} + +pub fn save_server(server: &Server) -> bool { + let mut cache = SERVER_CACHE.write().unwrap(); + cache.insert(server.id.clone(), server.clone()).is_none() // If the server id did not exist, `insert` will return `None` +} + +fn remove_server_by_id(id: String) -> bool { + dbg!("remove server by id:", &id); + let mut cache = SERVER_CACHE.write().unwrap(); + let deleted = cache.remove(id.as_str()); + deleted.is_some() +} + + +pub fn persist_servers(app_handle: &AppHandle) -> Result<(), String> { + let cache = SERVER_CACHE.read().unwrap(); // Acquire a read lock, not a write lock, since you're not modifying the cache + + // Convert HashMap to Vec for serialization (iterating over values of HashMap) + let servers: Vec = cache.values().cloned().collect(); + + // Serialize the servers into JSON automatically + let json_servers: Vec = servers + .into_iter() + .map(|server| serde_json::to_value(server).expect("Failed to serialize server")) // Automatically serialize all fields + .collect(); + + // dbg!(format!("persist servers: {:?}", &json_servers)); + + // Save the serialized servers to Tauri's store + app_handle + .store(COCO_TAURI_STORE) + .expect("create or load a store should never fail") + .set(COCO_SERVERS, json_servers); + + Ok(()) +} + +pub fn remove_server_token(id: &str) -> bool { + dbg!("remove server token by id:", &id); + let mut cache = SERVER_TOKEN.write().unwrap(); + cache.remove(id).is_some() +} + +pub fn persist_servers_token(app_handle: &AppHandle) -> Result<(), String> { + let cache = SERVER_TOKEN.read().unwrap(); // Acquire a read lock, not a write lock, since you're not modifying the cache + + // Convert HashMap to Vec for serialization (iterating over values of HashMap) + let servers: Vec = cache.values().cloned().collect(); + + // Serialize the servers into JSON automatically + let json_servers: Vec = servers + .into_iter() + .map(|server| serde_json::to_value(server).expect("Failed to serialize access_tokens")) // Automatically serialize all fields + .collect(); + + dbg!(format!("persist servers token: {:?}", &json_servers)); + + // Save the serialized servers to Tauri's store + app_handle + .store(COCO_TAURI_STORE) + .expect("create or load a store should never fail") + .set(COCO_SERVER_TOKENS, json_servers); + + Ok(()) +} + +// Function to get the default server if the request or parsing fails +fn get_default_server() -> Server { + Server { + id: "default_coco_server".to_string(), + builtin: true, + name: "Coco Cloud".to_string(), + endpoint: "https://coco.infini.cloud".to_string(), + provider: Provider { + name: "INFINI Labs".to_string(), + icon: "https://coco.infini.cloud/icon.png".to_string(), + website: "http://infinilabs.com".to_string(), + eula: "http://infinilabs.com/eula.txt".to_string(), + privacy_policy: "http://infinilabs.com/privacy_policy.txt".to_string(), + banner: "https://coco.infini.cloud/banner.jpg".to_string(), + description: "Coco AI Server - Search, Connect, Collaborate, AI-powered enterprise search, all in one space.".to_string(), + }, + version: Version { + number: "1.0.0_SNAPSHOT".to_string(), + }, + updated: "2025-01-24T12:12:17.326286927+08:00".to_string(), + public: false, + available: true, + profile: None, + auth_provider: AuthProvider { + sso: Sso { + url: "https://coco.infini.cloud/sso/login/".to_string(), + }, + }, + priority: 0, + } +} + +pub async fn load_servers_token(app_handle: &AppHandle) -> Result, String> { + + dbg!("Attempting to load servers token"); + + let store = app_handle + .store(COCO_TAURI_STORE) + .expect("create or load a store should not fail"); + + // Check if the servers key exists in the store + if !store.has(COCO_SERVER_TOKENS) { + return Err("Failed to read servers from store: No servers found".to_string()); + } + + // Load servers from store + let servers: Option = store.get(COCO_SERVER_TOKENS); + + // Handle the None case + let servers = servers.ok_or_else(|| "Failed to read servers from store: No servers found".to_string())?; + + // Convert each item in the JsonValue array to a Server + if let JsonValue::Array(servers_array) = servers { + // Deserialize each JsonValue into Server, filtering out any errors + let deserialized_tokens: Vec = servers_array + .into_iter() + .filter_map(|server_json| from_value(server_json).ok()) // Only keep valid Server instances + .collect(); + + if deserialized_tokens.is_empty() { + return Err("Failed to deserialize any servers from the store.".to_string()); + } + + for server in deserialized_tokens.iter() { + save_access_token(server.id.clone(), server.clone()); + } + + dbg!(format!("loaded {:?} servers's token", &deserialized_tokens.len())); + + Ok(deserialized_tokens) + } else { + Err("Failed to read servers from store: Invalid format".to_string()) + } +} + +pub async fn load_servers(app_handle: &AppHandle) -> Result, String> { + let store = app_handle + .store(COCO_TAURI_STORE) + .expect("create or load a store should not fail"); + + // Check if the servers key exists in the store + if !store.has(COCO_SERVERS) { + return Err("Failed to read servers from store: No servers found".to_string()); + } + + // Load servers from store + let servers: Option = store.get(COCO_SERVERS); + + // Handle the None case + let servers = servers.ok_or_else(|| "Failed to read servers from store: No servers found".to_string())?; + + // Convert each item in the JsonValue array to a Server + if let JsonValue::Array(servers_array) = servers { + // Deserialize each JsonValue into Server, filtering out any errors + let deserialized_servers: Vec = servers_array + .into_iter() + .filter_map(|server_json| from_value(server_json).ok()) // Only keep valid Server instances + .collect(); + + if deserialized_servers.is_empty() { + return Err("Failed to deserialize any servers from the store.".to_string()); + } + + for server in deserialized_servers.iter() { + save_server(&server); + } + + // dbg!(format!("load servers: {:?}", &deserialized_servers)); + + Ok(deserialized_servers) + } else { + Err("Failed to read servers from store: Invalid format".to_string()) + } +} + +/// Function to load servers or insert a default one if none exist +pub async fn load_or_insert_default_server(app_handle: &AppHandle) -> Result, String> { + + dbg!("Attempting to load or insert default server"); + + let exists_servers = load_servers(&app_handle).await; + if exists_servers.is_ok() && !exists_servers.as_ref()?.is_empty() { + dbg!(format!("loaded {} servers", &exists_servers.clone()?.len())); + return exists_servers; + } + + let default = get_default_server(); + save_server(&default); + + dbg!("loaded default servers"); + + Ok(vec![default]) +} + +#[tauri::command] +pub async fn list_coco_servers( + app_handle: AppHandle, +) -> Result, String> { + let servers: Vec =get_all_servers(); + Ok(servers) +} + +pub fn get_servers_as_hashmap() -> HashMap { + let cache = SERVER_CACHE.read().unwrap(); + cache.clone() +} + +pub fn get_all_servers() -> Vec { + let cache = SERVER_CACHE.read().unwrap(); + cache.values().cloned().collect() +} + +/// We store added Coco servers in the Tauri store using this key. +pub const COCO_SERVERS: &str = "coco_servers"; + +const COCO_SERVER_TOKENS: &str = "coco_server_tokens"; + +#[tauri::command] +pub async fn refresh_coco_server_info( + app_handle: AppHandle, + id: String, +) -> Result { + // Retrieve the server from the cache + let server = { + let cache = SERVER_CACHE.read().unwrap(); + cache.get(&id).cloned() + }; + + if let Some(server) = server { + let is_builtin = server.builtin; + let profile = server.profile; + + // Use the HttpClient to send the request + let response = HttpClient::get(&id, "/provider/_info") // Assuming "/provider-info" is the endpoint + .await + .map_err(|e| format!("Failed to send request to the server: {}", e))?; + + if response.status() == StatusCode::OK { + if let Some(content_length) = response.content_length() { + if content_length > 0 { + let new_coco_server: Result = response.json().await; + + match new_coco_server { + Ok(mut server) => { + server.id = id; + server.builtin = is_builtin; + server.available = true; + server.profile = profile; + trim_endpoint_last_forward_slash(&mut server); + save_server(&server); + persist_servers(&app_handle).expect("Failed to persist coco servers."); + + + //refresh connectors and datasources + if let Err(err) = refresh_all_connectors(&app_handle).await { + return Err(format!("Failed to load server connectors: {}", err)) + } + + if let Err(err) = refresh_all_datasources(&app_handle).await { + return Err(format!("Failed to load server datasources: {}", err)) + } + + Ok(server) + } + Err(e) => { + Err(format!("Failed to deserialize the response: {}", e)) + } + } + } else { + Err("Received empty response body.".to_string()) + } + } else { + Err("Could not determine the content length.".to_string()) + } + } else { + Err(format!("Request failed with status: {}", response.status())) + } + } else { + Err("Server not found.".to_string()) + } +} + +#[tauri::command] +pub async fn add_coco_server( + app_handle: AppHandle, + endpoint: String, +) -> Result { + load_or_insert_default_server(&app_handle).await + .expect("Failed to load default servers"); + + // Remove the trailing '/' from the endpoint to ensure correct URL construction + let endpoint = endpoint.trim_end_matches('/'); + + // Check if the server with this endpoint already exists + if check_endpoint_exists(endpoint) { + dbg!(format!("This Coco server has already been registered: {:?}", &endpoint)); + return Err("This Coco server has already been registered.".into()); + } + + let url = provider_info_url(&endpoint); + + // Use the HttpClient to fetch provider information + let response = HttpClient::send_raw_request(Method::GET, url.as_str(), None, None) + .await + .map_err(|e| format!("Failed to send request to the server: {}", e))?; + + dbg!(format!("Get provider info response: {:?}", &response)); + + // Check if the response status is OK (200) + if response.status() == StatusCode::OK { + if let Some(content_length) = response.content_length() { + if content_length > 0 { + let new_coco_server: Result = response.json().await; + + match new_coco_server { + Ok(mut server) => { + // Perform necessary checks and adjustments on the server data + trim_endpoint_last_forward_slash(&mut server); + + if server.id.is_empty() { + server.id = pizza_common::utils::uuid::Uuid::new().to_string(); + } + + if server.name.is_empty() { + server.name = "Coco Cloud".to_string(); + } + + // Save the new server to the cache + save_server(&server); + + // Persist the servers to the store + persist_servers(&app_handle) + .expect("Failed to persist Coco servers."); + + dbg!(format!("Successfully registered server: {:?}", &endpoint)); + Ok(server) + } + Err(e) => { + Err(format!("Failed to deserialize the response: {}", e)) + } + } + } else { + Err("Received empty response body.".to_string()) + } + } else { + Err("Could not determine the content length.".to_string()) + } + } else { + Err(format!("Request failed with status: {}", response.status())) + } +} + +#[tauri::command] +pub async fn remove_coco_server( + app_handle: AppHandle, + id: String, +) -> Result<(), ()> { + remove_server_token(id.as_str()); + remove_server_by_id(id); + persist_servers(&app_handle).expect("failed to save servers"); + persist_servers_token(&app_handle).expect("failed to save server tokens"); + Ok(()) +} + +#[tauri::command] +pub async fn logout_coco_server( + app_handle: AppHandle, + id: String, +) -> Result<(), String> { + + dbg!("Attempting to log out server by id:", &id); + + // Check if server token exists + if let Some(token) = get_server_token(id.as_str()) { + dbg!("Found server token for id:", &id); + + // Remove the server token from cache + remove_server_token(id.as_str()); + + // Persist the updated tokens + if let Err(e) = persist_servers_token(&app_handle) { + dbg!("Failed to save tokens for id: {}. Error: {:?}", &id, &e); + return Err(format!("Failed to save tokens: {}", &e)); + } + } else { + // Log the case where server token is not found + dbg!("No server token found for id: {}", &id); + } + + // Check if the server exists + if let Some(mut server) = get_server_by_id(id.as_str()) { + dbg!("Found server for id:", &id); + + // Clear server profile + server.profile = None; + + // Save the updated server data + save_server(&server); + + // Persist the updated server data + if let Err(e) = persist_servers(&app_handle) { + dbg!("Failed to save server for id: {}. Error: {:?}", &id, &e); + return Err(format!("Failed to save server: {}", &e)); + } + } else { + // Log the case where server is not found + dbg!("No server found for id: {}", &id); + return Err(format!("No server found for id: {}", id)); + } + + dbg!("Successfully logged out server with id:", &id); + Ok(()) +} + +/// Removes the trailing slash from the server's endpoint if present. +fn trim_endpoint_last_forward_slash(server: &mut Server) { + if server.endpoint.ends_with('/') { + server.endpoint.pop(); // Remove the last character + while server.endpoint.ends_with('/') { + server.endpoint.pop(); + } + } +} + +/// Helper function to construct the provider info URL. +fn provider_info_url(endpoint: &str) -> String { + format!("{endpoint}/provider/_info") +} + +#[test] +fn test_trim_endpoint_last_forward_slash() { + let mut server = Server { + id: "test".to_string(), + builtin: false, + name: "".to_string(), + endpoint: "https://example.com///".to_string(), + provider: Provider { + name: "".to_string(), + icon: "".to_string(), + website: "".to_string(), + eula: "".to_string(), + privacy_policy: "".to_string(), + banner: "".to_string(), + description: "".to_string(), + }, + version: Version { + number: "".to_string(), + }, + updated: "".to_string(), + public: false, + available: false, + profile: None, + auth_provider: AuthProvider { + sso: Sso { + url: "".to_string(), + }, + }, + priority: 0, + }; + + trim_endpoint_last_forward_slash(&mut server); + + assert_eq!(server.endpoint, "https://example.com"); +} diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index bb77de2b..2c8c0e2d 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -1,3 +1,4 @@ +use crate::COCO_TAURI_STORE; use tauri::App; use tauri::AppHandle; use tauri::Manager; @@ -8,9 +9,6 @@ use tauri_plugin_global_shortcut::ShortcutState; use tauri_plugin_store::JsonValue; use tauri_plugin_store::StoreExt; -/// Tauri store name -const COCO_TAURI_STORE: &str = "coco_tauri_store"; - /// Tauri's store is a key-value database, we use it to store our registered /// global shortcut. /// diff --git a/src-tauri/src/util/mod.rs b/src-tauri/src/util/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index 67a9cbf8..00000000 --- a/src/App.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { useTranslation } from "react-i18next"; - -import "./i18n"; -import CommandInput from "./components/CommandInput"; - -function App() { - const { t } = useTranslation(); - - return ( -
-
-
{t("welcome")}
- -
- -
-
-
- ); -} - -export default App; diff --git a/src/api/tauriFetchClient.ts b/src/api/tauriFetchClient.ts index cfc6b975..b556000e 100644 --- a/src/api/tauriFetchClient.ts +++ b/src/api/tauriFetchClient.ts @@ -60,7 +60,7 @@ export const tauriFetch = async ({ headers["Content-Type"] = "application/json"; } - headers["X-API-TOKEN"] = headers["X-API-TOKEN"] || (auth && auth[endpoint_http]?.token) || ""; + headers["X-API-TOKEN"] = headers["X-API-TOKEN"] || (auth && auth[endpoint_http]?.token) || undefined; // debug API const requestInfo = { diff --git a/src/assets/images/apple.png b/src/assets/images/apple.png deleted file mode 100644 index 05d54ec7..00000000 Binary files a/src/assets/images/apple.png and /dev/null differ diff --git a/src/assets/images/bg-login.png b/src/assets/images/bg-login.png deleted file mode 100644 index 3684a6de..00000000 Binary files a/src/assets/images/bg-login.png and /dev/null differ diff --git a/src/assets/images/file_efault.png b/src/assets/images/file_efault.png deleted file mode 100644 index dd6197e0..00000000 Binary files a/src/assets/images/file_efault.png and /dev/null differ diff --git a/src/assets/images/github.png b/src/assets/images/github.png deleted file mode 100644 index 6757e430..00000000 Binary files a/src/assets/images/github.png and /dev/null differ diff --git a/src/assets/images/google.png b/src/assets/images/google.png deleted file mode 100644 index ea325ee4..00000000 Binary files a/src/assets/images/google.png and /dev/null differ diff --git a/src/components/AppAI/DropdownList.tsx b/src/components/AppAI/DropdownList.tsx deleted file mode 100644 index 170fca5d..00000000 --- a/src/components/AppAI/DropdownList.tsx +++ /dev/null @@ -1,435 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { - CircleAlert, - Bolt, - X, - SquareArrowRight, - // UserRoundPen, -} from "lucide-react"; - -import { isTauri } from "@tauri-apps/api/core"; -import { open } from "@tauri-apps/plugin-shell"; - -import { useAppStore } from "@/stores/appStore"; -import { useSearchStore } from "@/stores/searchStore"; -import source_default_img from "@/assets/images/source_default.png"; -import source_default_dark_img from "@/assets/images/source_default_dark.png"; -import file_efault_img from "@/assets/images/file_efault.png"; -import { useTheme } from "@/contexts/ThemeContext"; -import { useConnectStore } from "@/stores/connectStore"; - -type ISearchData = Record; - -interface DropdownListProps { - selected: (item: any) => void; - suggests: any[]; - SearchData: ISearchData; - IsError: boolean; - isSearchComplete: boolean; - isChatMode: boolean; -} - -function DropdownList({ - selected, - suggests, - SearchData, - IsError, - isChatMode, -}: DropdownListProps) { - let globalIndex = 0; - const globalItemIndexMap: any[] = []; - // const letterFirstIndex: any = { - // a: 0, - // s: 0, - // d: 0, - // f: 0, - // }; - - const { theme } = useTheme(); - - const connector_data = useConnectStore((state) => state.connector_data); - const datasourceData = useConnectStore((state) => state.datasourceData); - - const endpoint_http = useAppStore((state) => state.endpoint_http); - const setSourceData = useSearchStore((state) => state.setSourceData); - - const [showError, setShowError] = useState(IsError); - const [selectedItem, setSelectedItem] = useState(null); - const [selectedName, setSelectedName] = useState(""); - const [showIndex, setShowIndex] = useState(false); - const containerRef = useRef(null); - const itemRefs = useRef<(HTMLDivElement | null)[]>([]); - - useEffect(() => { - isChatMode && setSelectedItem(null); - }, [isChatMode]); - - const handleOpenURL = async (url: string) => { - if (!url) return; - try { - if (isTauri()) { - await open(url); - // console.log("URL opened in default browser"); - } - } catch (error) { - console.error("Failed to open URL:", error); - } - }; - - const handleKeyDown = (e: KeyboardEvent) => { - // console.log( - // "handleKeyDown", - // e.key, - // showIndex, - // e.key >= "0" && e.key <= "9" && showIndex - // ); - if (!suggests.length) return; - - if (e.key === "ArrowUp") { - e.preventDefault(); - setSelectedItem((prev) => { - const res = - prev === null || prev === 0 ? suggests.length - 1 : prev - 1; - - return res; - }); - } else if (e.key === "ArrowDown") { - e.preventDefault(); - setSelectedItem((prev) => - prev === null || prev === suggests.length - 1 ? 0 : prev + 1 - ); - } else if (e.key === "Meta") { - e.preventDefault(); - if (selectedItem !== null) { - const item = globalItemIndexMap[selectedItem]; - setSelectedName(item?._source?.source?.name); - } - setShowIndex(true); - } - - if (e.key === "ArrowRight" && selectedItem !== null) { - e.preventDefault(); - const item = globalItemIndexMap[selectedItem]; - goToTwoPage(item); - } - - if (e.key === "Enter" && selectedItem !== null) { - // console.log("Enter key pressed", selectedItem); - const item = globalItemIndexMap[selectedItem]; - if (item?._source?.url) { - handleOpenURL(item?._source?.url); - } else { - selected(item); - } - } - - if (e.key >= "0" && e.key <= "9" && showIndex) { - // console.log(`number ${e.key}`); - const item = globalItemIndexMap[parseInt(e.key, 10)]; - if (item?._source?.url) { - handleOpenURL(item?._source?.url); - } else { - selected(item); - } - } - }; - - const handleKeyUp = (e: KeyboardEvent) => { - // console.log("handleKeyUp", e.key); - if (!suggests.length) return; - - if (!e.metaKey) { - setShowIndex(false); - } - }; - - useEffect(() => { - window.addEventListener("keydown", handleKeyDown); - window.addEventListener("keyup", handleKeyUp); - - return () => { - window.removeEventListener("keydown", handleKeyDown); - window.removeEventListener("keyup", handleKeyUp); - }; - }, [showIndex, selectedItem, suggests]); - - useEffect(() => { - if (selectedItem !== null && itemRefs.current[selectedItem]) { - itemRefs.current[selectedItem]?.scrollIntoView({ - behavior: "smooth", - block: "nearest", - }); - } - }, [selectedItem]); - - function findConnectorIcon(item: any) { - const id = item?._source?.source?.id || ""; - - const result_source = datasourceData[endpoint_http]?.find( - (data: any) => data._source.id === id - ); - - const connector_id = result_source?._source?.connector?.id; - - const result_connector = connector_data[endpoint_http]?.find( - (data: any) => data._source.id === connector_id - ); - - return result_connector?._source; - } - - function getTypeIcon(item: any) { - const connectorSource = findConnectorIcon(item); - const icons = connectorSource?.icon; - - if (!icons) { - return theme === "dark" ? source_default_dark_img : source_default_img; - } - - if (icons?.startsWith("http://") || icons?.startsWith("https://")) { - return icons; - } else { - return endpoint_http + icons; - } - } - - function getIcon(item: any) { - const connectorSource = findConnectorIcon(item); - const icons = connectorSource?.assets?.icons || {}; - - const selectedIcon = icons[item?._source?.icon]; - - if (!selectedIcon) { - return file_efault_img; - } - - if (selectedIcon?.startsWith("http://") || selectedIcon?.startsWith("https://")) { - return selectedIcon; - } else { - return endpoint_http + selectedIcon; - } - } - - function getRichIcon(item: any) { - const connectorSource = findConnectorIcon(item); - const icons = connectorSource?.assets?.icons || {}; - - const selectedIcon = icons[item?._source?.rich_categories?.[0]?.icon]; - - if (!selectedIcon) { - return theme === "dark" ? source_default_dark_img : source_default_img; - } - - if (selectedIcon?.startsWith("http://") || selectedIcon?.startsWith("https://")) { - return selectedIcon; - } else { - return endpoint_http + selectedIcon; - } - } - - function goToTwoPage(item: any) { - setSourceData(item); - selected && selected(item); - } - - // function numberToLetter(num: number): string { - // const mapping = ["A", "S", "D", "F"]; - // if (num >= 0 && num < mapping.length) { - // const letter = mapping[num]; - // letterFirstIndex[letter.toLocaleLowerCase()] = globalIndex - // return letter - // } else { - // return ""; - // } - // } - - return ( -
- {showError ? ( -
- - Coco server is unavailable, only local results and available services - are displayed. - - setShowError(false)} - /> -
- ) : null} - {Object.entries(SearchData).map(([sourceName, items]) => ( -
- {Object.entries(SearchData).length < 5 ? ( -
- icon - {sourceName} -
- goToTwoPage(items[0])} - /> - {showIndex && sourceName === selectedName ? ( -
- → -
- ) : null} -
- ) : null} - {items.map((item: any) => { - const isSelected = selectedItem === globalIndex; - const currentIndex = globalIndex; - globalItemIndexMap.push(item); - globalIndex++; - return ( -
(itemRefs.current[currentIndex] = el)} - onMouseEnter={() => setSelectedItem(currentIndex)} - onClick={() => { - if (item?._source?.url) { - handleOpenURL(item?._source?.url); - } else { - selected(item); - } - }} - className={`w-full px-2 py-2.5 text-sm flex gap-7 items-center justify-between rounded-lg transition-colors ${ - isSelected - ? "text-white bg-[#950599] hover:bg-[#950599]" - : "text-[#333] dark:text-[#d8d8d8]" - }`} - > -
- icon - - {item?._source?.title} - -
-
- {Object.entries(SearchData).length < 5 ? null : ( - icon { - e.stopPropagation(); - goToTwoPage(item); - }} - /> - )} - - {item?._source?.rich_categories ? ( -
- {Object.entries(SearchData).length < 5 ? ( - icon { - e.stopPropagation(); - goToTwoPage(item); - }} - /> - ) : null} - - {item?._source?.rich_categories?.map( - (rich_item: any, index: number) => { - if ( - item?._source?.rich_categories.length > 2 && - index === - item?._source?.rich_categories.length - 1 - ) - return ""; - else - return ( - (index !== 0 ? "/" : "") + rich_item?.label - ); - } - )} - - {item?._source?.rich_categories.length > 2 ? ( - - {"/" + item?._source?.rich_categories?.at(-1)?.label} - - ) : null} -
- ) : item?._source?.category || item?._source?.subcategory ? ( - - {(item?._source?.category || "") + - (item?._source?.subcategory - ? `/${item?._source?.subcategory}` - : "")} - - ) : ( - - {item?._source?.type || ""} - - )} - - {isSelected ? ( -
- ↩︎ -
- ) : null} - - {showIndex && currentIndex < 10 ? ( -
- {currentIndex} -
- ) : null} -
-
- ); - })} -
- ))} -
- ); -} - -export default DropdownList; diff --git a/src/components/ChatAI/AutoResizeTextarea.tsx b/src/components/Assistant/AutoResizeTextarea.tsx similarity index 100% rename from src/components/ChatAI/AutoResizeTextarea.tsx rename to src/components/Assistant/AutoResizeTextarea.tsx diff --git a/src/components/ChatAI/Chat.tsx b/src/components/Assistant/Chat.tsx similarity index 99% rename from src/components/ChatAI/Chat.tsx rename to src/components/Assistant/Chat.tsx index 834770c1..f7c0c950 100644 --- a/src/components/ChatAI/Chat.tsx +++ b/src/components/Assistant/Chat.tsx @@ -234,7 +234,7 @@ const ChatAI = forwardRef( skipTaskbar: true, decorations: true, closable: true, - url: "/ui/chat", + url: "/ui/app/chat", }); } } diff --git a/src/components/ChatAI/ChatInput.tsx b/src/components/Assistant/ChatInput.tsx similarity index 100% rename from src/components/ChatAI/ChatInput.tsx rename to src/components/Assistant/ChatInput.tsx diff --git a/src/components/ChatAI/ChatMessage.tsx b/src/components/Assistant/ChatMessage.tsx similarity index 100% rename from src/components/ChatAI/ChatMessage.tsx rename to src/components/Assistant/ChatMessage.tsx diff --git a/src/components/ChatAI/FullScreen.tsx b/src/components/Assistant/FullScreen.tsx similarity index 100% rename from src/components/ChatAI/FullScreen.tsx rename to src/components/Assistant/FullScreen.tsx diff --git a/src/components/ChatAI/Markdown.tsx b/src/components/Assistant/Markdown.tsx similarity index 100% rename from src/components/ChatAI/Markdown.tsx rename to src/components/Assistant/Markdown.tsx diff --git a/src/components/ChatAI/Sidebar.tsx b/src/components/Assistant/Sidebar.tsx similarity index 100% rename from src/components/ChatAI/Sidebar.tsx rename to src/components/Assistant/Sidebar.tsx diff --git a/src/components/ChatAI/highlight.css b/src/components/Assistant/highlight.css similarity index 100% rename from src/components/ChatAI/highlight.css rename to src/components/Assistant/highlight.css diff --git a/src/components/ChatAI/index.css b/src/components/Assistant/index.css similarity index 100% rename from src/components/ChatAI/index.css rename to src/components/Assistant/index.css diff --git a/src/components/ChatAI/index.tsx b/src/components/Assistant/index.tsx similarity index 100% rename from src/components/ChatAI/index.tsx rename to src/components/Assistant/index.tsx diff --git a/src/components/ChatAI/markdown.css b/src/components/Assistant/markdown.css similarity index 100% rename from src/components/ChatAI/markdown.css rename to src/components/Assistant/markdown.css diff --git a/src/components/ChatAI/types.ts b/src/components/Assistant/types.ts similarity index 100% rename from src/components/ChatAI/types.ts rename to src/components/Assistant/types.ts diff --git a/src/components/Auth/CocoCloud.tsx b/src/components/Auth/CocoCloud.tsx deleted file mode 100644 index 9530464e..00000000 --- a/src/components/Auth/CocoCloud.tsx +++ /dev/null @@ -1,335 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { - RefreshCcw, - Globe, - PackageOpen, - GitFork, - CalendarSync, - Trash2, -} from "lucide-react"; -import { v4 as uuidv4 } from "uuid"; -import { getCurrentWindow } from "@tauri-apps/api/window"; -import { - onOpenUrl, - getCurrent as getCurrentDeepLinkUrls, -} from "@tauri-apps/plugin-deep-link"; - -import { UserProfile } from "./UserProfile"; -import { DataSourcesList } from "./DataSourcesList"; -import { Sidebar } from "./Sidebar"; -import { ConnectService } from "./ConnectService"; -import { OpenBrowserURL } from "@/utils/index"; -import { useAppStore } from "@/stores/appStore"; -import { useAuthStore } from "@/stores/authStore"; -import { tauriFetch } from "@/api/tauriFetchClient"; -import { useConnectStore } from "@/stores/connectStore"; -import bannerImg from "@/assets/images/coco-cloud-banner.jpeg"; - -export default function CocoCloud() { - const [error, setError] = useState(null); - - const [isConnect, setIsConnect] = useState(true); - - const app_uid = useAppStore((state) => state.app_uid); - const setAppUid = useAppStore((state) => state.setAppUid); - const setEndpoint = useAppStore((state) => state.setEndpoint); - const endpoint = useAppStore((state) => state.endpoint); - - const auth = useAuthStore((state) => state.auth); - const setAuth = useAuthStore((state) => state.setAuth); - const userInfo = useAuthStore((state) => state.userInfo); - const setUserInfo = useAuthStore((state) => state.setUserInfo); - const defaultService = useConnectStore((state) => state.defaultService); - const currentService = useConnectStore((state) => state.currentService); - const setDefaultService = useConnectStore((state) => state.setDefaultService); - const setCurrentService = useConnectStore((state) => state.setCurrentService); - const deleteOtherService = useConnectStore( - (state) => state.deleteOtherService - ); - - const [loading, setLoading] = useState(false); - const [refreshLoading, setRefreshLoading] = useState(false); - - useEffect(() => { - console.log("currentService", currentService); - setLoading(false); - setRefreshLoading(false); - setError(null); - setEndpoint(currentService.endpoint); - setIsConnect(true); - }, [JSON.stringify(currentService)]); - - const getProfile = useCallback(async () => { - const response: any = await tauriFetch({ - url: `/account/profile`, - method: "GET", - }); - console.log("getProfile", response); - setUserInfo(response.data || {}, endpoint); - }, [endpoint]); - - const handleOAuthCallback = useCallback( - async (code: string | null, provider: string | null) => { - if (!code) { - setError("No authorization code received"); - return; - } - - try { - console.log("Handling OAuth callback:", { code, provider }); - const response: any = await tauriFetch({ - url: `/auth/request_access_token?request_id=${app_uid}`, - method: "GET", - headers: { - "X-API-TOKEN": code, - }, - }); - console.log( - "response", - `/auth/request_access_token?request_id=${app_uid}`, - code, - response - ); - - if (response.data?.access_token) { - setAuth( - { - token: response.data?.access_token, - expires: response.data?.expire_at, - plan: { upgraded: false, last_checked: 0 }, - }, - endpoint - ); - - getProfile(); - } else { - setAuth(undefined, endpoint); - setError("Sign in failed: " + response.data?.error?.reason); - } - - getCurrentWindow() - .setFocus() - .catch(() => {}); - } catch (e) { - console.error("Sign in failed:", error); - setError("Sign in failed: catch"); - setAuth(undefined, endpoint); - throw error; - } finally { - setLoading(false); - } - }, - [app_uid, endpoint] - ); - - const handleUrl = (url: string) => { - try { - // url = "coco://oauth_callback?code=cu8ag982sdb06e0j6k3g&provider=coco-cloud" - const urlObject = new URL(url); - console.log("urlObject:", urlObject); - - const code = urlObject.searchParams.get("code"); - const provider = urlObject.searchParams.get("provider"); - handleOAuthCallback(code, provider); - - // switch (urlObject.hostname) { - // case "/oauth_callback": - - // break; - - // default: - // console.log("Unhandled deep link path:", urlObject.pathname); - // } - } catch (err) { - console.error("Failed to parse URL:", err); - setError("Invalid URL format"); - } - }; - - // Fetch the initial deep link intent - useEffect(() => { - // handleUrl(""); - getCurrentDeepLinkUrls() - .then((urls) => { - console.log("URLs:", urls); - if (urls && urls.length > 0) { - handleUrl(urls[0]); - } - }) - .catch((err) => { - console.error("Failed to get initial URLs:", err); - setError("Failed to get initial URLs"); - }); - - const unlisten = onOpenUrl((urls) => handleUrl(urls[0])); - - return () => { - unlisten.then((fn) => fn()); - }; - }, [app_uid]); - - const LoginClick = useCallback(() => { - if (loading) return; - setAuth(undefined, endpoint); - - let uid = uuidv4(); - setAppUid(uid); - - console.log("LoginClick", uid, currentService.auth_provider.sso.url); - - OpenBrowserURL( - `${currentService.auth_provider.sso.url}/?provider=coco-cloud&product=coco&request_id=${uid}` - ); - - setLoading(true); - }, [JSON.stringify(currentService)]); - - function goToHref(url: string) { - OpenBrowserURL(url); - } - - const refreshClick = useCallback(() => { - setRefreshLoading(true); - tauriFetch({ - url: `/provider/_info`, - method: "GET", - }) - .then((res) => { - setEndpoint(res.data.endpoint); - setCurrentService(res.data || {}); - if (res.data?.endpoint === "https://coco.infini.cloud/") { - setDefaultService(res.data); - } - }) - .catch((err) => { - console.error(err); - }) - .finally(() => { - setRefreshLoading(false); - }); - }, [JSON.stringify(defaultService)]); - - function addService() { - setIsConnect(false); - } - - const deleteClick = useCallback(() => { - deleteOtherService(currentService); - setAuth(undefined, endpoint); - setUserInfo({}, endpoint); - }, [JSON.stringify(currentService), endpoint]); - - return ( -
- - -
- {/*
- {error && ( -
- Error: {error} -
- )} -
*/} - - {isConnect ? ( -
-
- banner -
-
-
-
- {currentService.name} -
-
-
- - - {currentService.endpoint !== defaultService.endpoint ? ( - - ) : null} -
-
- -
-
- - {" "} - {currentService.provider.name} - - | - - {" "} - {currentService.version.number} - - | - - {currentService.updated} - -
-

- {currentService.provider.description} -

-
- - {currentService.auth_provider.sso.url ? ( -
-

- Account Information -

- {auth && auth[endpoint] ? ( - - ) : ( -
- - -
- )} -
- ) : null} - - {auth && auth[endpoint] ? : null} -
- ) : ( - - )} -
-
- ); -} diff --git a/src/components/Auth/ConnectService.tsx b/src/components/Auth/ConnectService.tsx deleted file mode 100644 index 4d32d4e9..00000000 --- a/src/components/Auth/ConnectService.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useState, useCallback } from "react"; -import { ChevronLeft } from "lucide-react"; - -import { useConnectStore } from "@/stores/connectStore"; -import { tauriFetch } from "@/api/tauriFetchClient"; -import { useAppStore } from "@/stores/appStore"; - -interface ConnectServiceProps { - setIsConnect: (isConnect: boolean) => void; -} - -export function ConnectService({ setIsConnect }: ConnectServiceProps) { - const addOtherServices = useConnectStore((state) => state.addOtherServices); - const setCurrentService = useConnectStore((state) => state.setCurrentService); - const defaultService = useConnectStore((state) => state.defaultService); - const otherServices = useConnectStore((state) => state.otherServices); - - const setEndpoint = useAppStore((state) => state.setEndpoint); - - const [endpointLink, setEndpointLink] = useState(""); - const [refreshLoading, setRefreshLoading] = useState(false); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - console.log("Connecting Google Drive with name:", endpointLink); - }; - - const goBack = () => { - setIsConnect(true); - }; - - const addService = useCallback(() => { - if (!endpointLink) return; - if (!endpointLink.startsWith("http://") && !endpointLink.startsWith("https://")) { - return - } - setRefreshLoading(true); - // - let baseURL = endpointLink; - if (baseURL.endsWith("/")) { - baseURL = baseURL.slice(0, -1); - } - - tauriFetch({ - url: `${baseURL}/provider/_info`, - method: "GET", - }) - .then((res) => { - if ( - res.data?.endpoint === defaultService.endpoint || - otherServices.some( - (item: any) => item.endpoint === res.data?.endpoint - ) - ) { - console.error(`${res.data?.endpoint} Repeated`); - } else { - addOtherServices(res.data); - setCurrentService(res.data); - setEndpoint(res.data.endpoint); - setIsConnect(true); - } - }) - .catch((err) => { - console.error(err); - }) - .finally(() => { - setRefreshLoading(false); - }); - }, [endpointLink]); - - return ( -
-
- -
- Connecting to third-party services -
-
- -
-

- Third-party services are provided by other platforms or providers, and - users can integrate these services into Coco AI to expand the scope of - search data. -

-
- -
-
- -
- setEndpointLink(e.target.value)} - className="text-[#101010] dark:text-white flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800" - /> - -
-
-
-
- ); -} diff --git a/src/components/Auth/DataSourcesList.tsx b/src/components/Auth/DataSourcesList.tsx deleted file mode 100644 index 2ea705b6..00000000 --- a/src/components/Auth/DataSourcesList.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useEffect, useState } from "react"; -import { RefreshCcw } from "lucide-react"; - -import { DataSourceItem } from "./DataSourceItem"; -import { useConnectStore } from "@/stores/connectStore"; -import { tauriFetch } from "@/api/tauriFetchClient"; -import { useAppStore } from "@/stores/appStore"; - -export function DataSourcesList() { - const datasourceData = useConnectStore((state) => state.datasourceData); - const setDatasourceData = useConnectStore((state) => state.setDatasourceData); - - const endpoint_http = useAppStore((state) => state.endpoint_http); - - const [refreshLoading, setRefreshLoading] = useState(false); - - async function getDatasourceData() { - setRefreshLoading(true); - try { - const response = await tauriFetch({ - url: `/datasource/_search`, - method: "GET", - }); - console.log("datasource", response); - const data = response.data?.hits?.hits || []; - setDatasourceData(data, endpoint_http); - } catch (error) { - console.error("Failed to fetch user data:", error); - } - setRefreshLoading(false); - } - - useEffect(() => { - getDatasourceData() - }, []) - - return ( -
-

- Data Source - -

-
- {datasourceData[endpoint_http]?.map((source) => ( - - ))} -
-
- ); -} diff --git a/src/components/Auth/LoginForm.tsx b/src/components/Auth/LoginForm.tsx deleted file mode 100644 index 6178e19a..00000000 --- a/src/components/Auth/LoginForm.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useState } from 'react'; - -interface LoginFormProps { - onSubmit: (email: string, password: string) => void; -} - -export function LoginForm({ onSubmit }: LoginFormProps) { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - onSubmit(email, password); - }; - - return ( -
-
- - setEmail(e.target.value)} - className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Enter your email" - required - /> -
- -
- - setPassword(e.target.value)} - className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Enter your password" - required - /> -
- - -
- ); -} \ No newline at end of file diff --git a/src/components/Auth/Sidebar.tsx b/src/components/Auth/Sidebar.tsx deleted file mode 100644 index c2828ab3..00000000 --- a/src/components/Auth/Sidebar.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useState, useEffect } from "react"; -import { Plus } from "lucide-react"; - -import cocoLogoImg from "@/assets/app-icon.png"; -import { tauriFetch } from "@/api/tauriFetchClient"; -import { useConnectStore } from "@/stores/connectStore"; -import { useAppStore } from "@/stores/appStore"; - -interface SidebarProps { - addService: () => void; -} - -type StringBooleanMap = { - [key: string]: boolean; -}; - -export function Sidebar({ addService }: SidebarProps) { - const defaultService = useConnectStore((state) => state.defaultService); - const currentService = useConnectStore((state) => state.currentService); - const otherServices = useConnectStore((state) => state.otherServices); - const setCurrentService = useConnectStore((state) => state.setCurrentService); - - const setEndpoint = useAppStore((state) => state.setEndpoint); - - const [defaultHealth, setDefaultHealth] = useState(false); - const [otherHealth, setOtherHealth] = useState({}); - - const addServiceClick = () => { - addService(); - }; - - useEffect(() => { - getDefaultHealth(); - }, []); - - useEffect(() => { - getOtherHealth(currentService); - setEndpoint(currentService.endpoint); - }, [currentService.endpoint]); - - const getDefaultHealth = () => { - let baseURL = defaultService.endpoint - if (baseURL.endsWith("/")) { - baseURL = baseURL.slice(0, -1); - } - tauriFetch({ - url: `${baseURL}/health`, - method: "GET", - }) - .then((res) => { - // "services": { - // "system_cluster": "yellow" - // }, - // "status": "yellow" - setDefaultHealth(res.data?.status !== "red"); - }) - .catch((err) => { - console.error(err); - }); - }; - - const getOtherHealth = (item: any) => { - if (!item.endpoint) return; - // - let baseURL = item.endpoint - if (baseURL.endsWith("/")) { - baseURL = baseURL.slice(0, -1); - } - tauriFetch({ - url: `${baseURL}/health`, - method: "GET", - }) - .then((res) => { - let obj = { - ...otherHealth, - [item.endpoint]: res.data?.status !== "red", - }; - setOtherHealth(obj); - }) - .catch((err) => { - console.error(err); - }); - }; - - return ( -
-
-
{ - setCurrentService(defaultService); - setEndpoint(defaultService.endpoint); - getDefaultHealth(); - }} - > - cocoLogoImg - - {defaultService.name} -
- -
- -
- Third-party services -
- - {otherServices?.map((item, index) => ( -
{ - setEndpoint(item.endpoint); - setCurrentService(item); - getOtherHealth(item); - }} - > - LogoImg - - {item.name} -
- -
- ))} - -
- -
-
-
- ); -} diff --git a/src/components/Auth/callback.template.ts b/src/components/Auth/callback.template.ts deleted file mode 100644 index 9eed79bb..00000000 --- a/src/components/Auth/callback.template.ts +++ /dev/null @@ -1,59 +0,0 @@ -export default ` - - - - - - - - - Coco Auth - - - -
-

Coco

-

You are now signed in. Please re-open the Coco desktop app to continue.

-
-

- - -`; diff --git a/src/components/Auth/login2.tsx b/src/components/Auth/login2.tsx deleted file mode 100644 index 05d70ecd..00000000 --- a/src/components/Auth/login2.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useEffect } from 'react'; -import { Github, Mail, Apple } from 'lucide-react'; -import { useSearchParams } from "react-router-dom"; - -import { LoginForm } from '@/components/Auth/LoginForm'; -import { SocialButton } from '@/components/Auth/SocialButton'; -import { Divider } from '@/components/Auth/Divider'; -import { authWitheGithub } from '@/utils/index'; - -export default function LoginPage() { - const [searchParams] = useSearchParams(); - const uid = searchParams.get("uid"); - const code = searchParams.get("code"); - - useEffect(()=>{ - - }, [code]) - - function GithubClick() { - uid && authWitheGithub(uid) - } - - return ( -
-
-
-

Welcome Back

-

Sign in to continue to Coco

-
- -
- } - provider="GitHub" - onClick={() => GithubClick()} - /> - } - provider="Google" - onClick={() => console.log('Google login')} - /> - } - provider="Apple" - onClick={() => console.log('Apple login')} - /> -
- - - - console.log(email, password)} /> -
-
- ); -} \ No newline at end of file diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx deleted file mode 100644 index 4a0ee929..00000000 --- a/src/components/Chat.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -// import { invoke } from "@tauri-apps/api/core"; -import { useHotkeys } from "react-hotkeys-hook"; -import debounce from "lodash/debounce"; -import { Textarea } from "@headlessui/react"; -import clsx from "clsx"; -import { useTranslation } from "react-i18next"; -import { fetch } from "@tauri-apps/plugin-http"; - -import SendIcon from "../icons/Send"; - -export default function ChatInput() { - const { t } = useTranslation(); - - const inputRef = useRef(null); - const [message, setMessage] = useState(""); - const [info, setInfo] = useState(""); - const isMac = true; - - useEffect(() => { - const syncMessage = debounce(async () => { - try { - // await invoke("ask_sync", { message: JSON.stringify(message) }); - } catch (error) { - console.error("Error syncing message:", error); - } - }, 300); // Debounce by 300ms - - syncMessage(); - return () => syncMessage.cancel(); // Cleanup debounce on unmount - }, [message]); - - useHotkeys( - isMac ? "meta+enter" : "ctrl+enter", - async (event: KeyboardEvent) => { - event.preventDefault(); - await handleSend(); - }, - { - enableOnFormTags: true, - }, - [message] - ); - - const handleInput = (e: React.ChangeEvent) => { - setInfo(""); - setMessage(e.target.value); - }; - - const handleSend = async () => { - if (!message) return; - try { - // await invoke("ask_send", { message: JSON.stringify(message) }); - - // Send a GET request - const response = await fetch("https://test.tauri.app/data.json", { - method: "GET", - }); - setInfo(JSON.stringify(response)); - // console.log(response.status); // e.g. 200 - // console.log(response.statusText); // e.g. "OK" - } catch (error) { - console.error("Error sending message:", error); - setInfo(JSON.stringify(error)); - } - setMessage(""); - if (inputRef.current) { - inputRef.current.value = ""; - inputRef.current.focus(); - } - }; - - return ( -
-
{info}
-
-