mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 03:27:43 +01:00
refactor: refactoring Coco App (#112)
* feat: impl Coco server related APIs * chore: remove unused method * fix: invoke Rust interfaces in tauri::run() * chore: add invoke * feat: add add_coco_server * fix: trim the tailing forward slash * feat: interface get_user_profiles * chore: add * fix: store the servers in add interface * chore: ass * fix: skip non-publich servers with no token * feat: add * feat: get datasources and connectors * fix: invoke interfaces in tauri::run() * chore: add SidebarRef * refactor: refactoring coco-app * refactor: refactoring coco app * refactor: refactoring project layout * refactor: refactoring server management * chore: cleanup code * chore: display error when connect failed * refactor: refactoring refresh server's info * refactor: refactoring how to connect the coco serverg * chore: rename to cloud * refactor: refactoring remove coco server * fix: refresh current selected server * fix: reset server selection * chore: update login status * feat: add error message tips * fix: fix login and logout * refactor: refactoring http client * fix: fix the datasources * chore: minor fix * refactor: refactoring code * fix: fix search api * chore: optimize part of icons * chore: fix build * refactor: search list icon * refactor: search list icon * chore: lib * feat: add plugin-os --------- Co-authored-by: rain <15911122312@163.com> Co-authored-by: medcl <m@medcl.net>
This commit is contained in:
@@ -12,12 +12,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.1.10",
|
"@headlessui/react": "^2.1.10",
|
||||||
"@react-oauth/google": "^0.12.1",
|
"@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-autostart": "~2",
|
||||||
"@tauri-apps/plugin-deep-link": "^2.2.0",
|
"@tauri-apps/plugin-deep-link": "^2.2.0",
|
||||||
"@tauri-apps/plugin-global-shortcut": "~2.0.0",
|
"@tauri-apps/plugin-global-shortcut": "~2.0.0",
|
||||||
"@tauri-apps/plugin-http": "~2.0.1",
|
"@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-shell": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-websocket": "~2",
|
"@tauri-apps/plugin-websocket": "~2",
|
||||||
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"zustand": "^5.0.0"
|
"zustand": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": ">=2.0.0",
|
"@tauri-apps/cli": "^2.2.7",
|
||||||
"@types/lodash": "^4.17.12",
|
"@types/lodash": "^4.17.12",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^22.8.4",
|
"@types/node": "^22.8.4",
|
||||||
|
|||||||
116
pnpm-lock.yaml
generated
116
pnpm-lock.yaml
generated
@@ -15,8 +15,8 @@ importers:
|
|||||||
specifier: ^0.12.1
|
specifier: ^0.12.1
|
||||||
version: 0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: '>=2.0.0'
|
specifier: ^2.2.0
|
||||||
version: 2.0.2
|
version: 2.2.0
|
||||||
'@tauri-apps/plugin-autostart':
|
'@tauri-apps/plugin-autostart':
|
||||||
specifier: ~2
|
specifier: ~2
|
||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
@@ -30,7 +30,7 @@ importers:
|
|||||||
specifier: ~2.0.1
|
specifier: ~2.0.1
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
'@tauri-apps/plugin-os':
|
'@tauri-apps/plugin-os':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.2.0
|
||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
'@tauri-apps/plugin-shell':
|
'@tauri-apps/plugin-shell':
|
||||||
specifier: '>=2.0.0'
|
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)
|
version: 5.0.0(@types/react@18.3.11)(immer@10.1.1)(react@18.3.1)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tauri-apps/cli':
|
'@tauri-apps/cli':
|
||||||
specifier: '>=2.0.0'
|
specifier: ^2.2.7
|
||||||
version: 2.0.3
|
version: 2.2.7
|
||||||
'@types/lodash':
|
'@types/lodash':
|
||||||
specifier: ^4.17.12
|
specifier: ^4.17.12
|
||||||
version: 4.17.12
|
version: 4.17.12
|
||||||
@@ -638,75 +638,75 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZMOc3eu9amwvkC6M69h3hWt4/EsFaAXmtkiw4xd2LN59/lTb4ZQiVfq2QKlRcu1rj3n/Tcr7U30ZopvHwXBGIg==}
|
resolution: {integrity: sha512-ZMOc3eu9amwvkC6M69h3hWt4/EsFaAXmtkiw4xd2LN59/lTb4ZQiVfq2QKlRcu1rj3n/Tcr7U30ZopvHwXBGIg==}
|
||||||
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
|
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
|
||||||
|
|
||||||
'@tauri-apps/api@2.0.2':
|
'@tauri-apps/api@2.2.0':
|
||||||
resolution: {integrity: sha512-3wSwmG+1kr6WrgAFKK5ijkNFPp8TT3FLj3YHUb5EwMO+3FxX4uWlfSWkeeBy+Kc1RsKzugtYLuuya+98Flj+3w==}
|
resolution: {integrity: sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg==}
|
||||||
|
|
||||||
'@tauri-apps/cli-darwin-arm64@2.0.3':
|
'@tauri-apps/cli-darwin-arm64@2.2.7':
|
||||||
resolution: {integrity: sha512-jIsbxGWS+As1ZN7umo90nkql/ZAbrDK0GBT6UsgHSz5zSwwArICsZFFwE1pLZip5yoiV5mn3TGG2c1+v+0puzQ==}
|
resolution: {integrity: sha512-54kcpxZ3X1Rq+pPTzk3iIcjEVY4yv493uRx/80rLoAA95vAC0c//31Whz75UVddDjJfZvXlXZ3uSZ+bnCOnt0A==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@tauri-apps/cli-darwin-x64@2.0.3':
|
'@tauri-apps/cli-darwin-x64@2.2.7':
|
||||||
resolution: {integrity: sha512-ROITHtLTA1muyrwgyuwyasmaLCGtT4as/Kd1kerXaSDtFcYrnxiM984ZD0+FDUEDl5BgXtYa/sKKkKQFjgmM0A==}
|
resolution: {integrity: sha512-Vgu2XtBWemLnarB+6LqQeLanDlRj7CeFN//H8bVVdjbNzxcSxsvbLYMBP8+3boa7eBnjDrqMImRySSgL6IrwTw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-arm-gnueabihf@2.0.3':
|
'@tauri-apps/cli-linux-arm-gnueabihf@2.2.7':
|
||||||
resolution: {integrity: sha512-bQ3EZwCFfrLg/ZQ2I8sLuifSxESz4TP56SleTkKsPtTIZgNnKpM88PRDz4neiRroHVOq8NK0X276qi9LjGcXPw==}
|
resolution: {integrity: sha512-+Clha2iQAiK9zoY/KKW0KLHkR0k36O78YLx5Sl98tWkwI3OBZFg5H5WT1plH/4sbZIS2aLFN6dw58/JlY9Bu/g==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-arm64-gnu@2.0.3':
|
'@tauri-apps/cli-linux-arm64-gnu@2.2.7':
|
||||||
resolution: {integrity: sha512-aLfAA8P9OTErVUk3sATxtXqpAtlfDPMPp4fGjDysEELG/MyekGhmh2k/kG/i32OdPeCfO+Nr37wJksARJKubGw==}
|
resolution: {integrity: sha512-Z/Lp4SQe6BUEOays9BQAEum2pvZF4w9igyXijP+WbkOejZx4cDvarFJ5qXrqSLmBh7vxrdZcLwoLk9U//+yQrg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-arm64-musl@2.0.3':
|
'@tauri-apps/cli-linux-arm64-musl@2.2.7':
|
||||||
resolution: {integrity: sha512-I4MVD7nf6lLLRmNQPpe5beEIFM6q7Zkmh77ROA5BNu/+vHNL5kiTMD+bmd10ZL2r753A6pO7AvqkIxcBuIl0tg==}
|
resolution: {integrity: sha512-+8HZ+txff/Y3YjAh80XcLXcX8kpGXVdr1P8AfjLHxHdS6QD4Md+acSxGTTNbplmHuBaSHJvuTvZf9tU1eDCTDg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-x64-gnu@2.0.3':
|
'@tauri-apps/cli-linux-x64-gnu@2.2.7':
|
||||||
resolution: {integrity: sha512-C6Jkx2zZGKkoi+sg5FK9GoH/0EvAaOgrZfF5azV5EALGba46g7VpWcZgp9zFUd7K2IzTi+0OOY8TQ2OVfKZgew==}
|
resolution: {integrity: sha512-ahlSnuCnUntblp9dG7/w5ZWZOdzRFi3zl0oScgt7GF4KNAOEa7duADsxPA4/FT2hLRa0SvpqtD4IYFvCxoVv3Q==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-x64-musl@2.0.3':
|
'@tauri-apps/cli-linux-x64-musl@2.2.7':
|
||||||
resolution: {integrity: sha512-qi4ghmTfSAl+EEUDwmwI9AJUiOLNSmU1RgiGgcPRE+7A/W+Am9UnxYySAiRbB/gJgTl9sj/pqH5Y9duP1/sqHg==}
|
resolution: {integrity: sha512-+qKAWnJRSX+pjjRbKAQgTdFY8ecdcu8UdJ69i7wn3ZcRn2nMMzOO2LOMOTQV42B7/Q64D1pIpmZj9yblTMvadA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@tauri-apps/cli-win32-arm64-msvc@2.0.3':
|
'@tauri-apps/cli-win32-arm64-msvc@2.2.7':
|
||||||
resolution: {integrity: sha512-UXxHkYmFesC97qVmZre4vY7oDxRDtC2OeKNv0bH+iSnuUp/ROxzJYGyaelnv9Ybvgl4YVqDCnxgB28qMM938TA==}
|
resolution: {integrity: sha512-aa86nRnrwT04u9D9fhf5JVssuAZlUCCc8AjqQjqODQjMd4BMA2+d4K9qBMpEG/1kVh95vZaNsLogjEaqSTTw4A==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@tauri-apps/cli-win32-ia32-msvc@2.0.3':
|
'@tauri-apps/cli-win32-ia32-msvc@2.2.7':
|
||||||
resolution: {integrity: sha512-D+xoaa35RGlkXDpnL5uDTpj29untuC5Wp6bN9snfgFDagD0wnFfC8+2ZQGu16bD0IteWqDI0OSoIXhNvy+F+wg==}
|
resolution: {integrity: sha512-EiJ5/25tLSQOSGvv+t6o3ZBfOTKB5S3vb+hHQuKbfmKdRF0XQu2YPdIi1CQw1DU97ZAE0Dq4frvnyYEKWgMzVQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@tauri-apps/cli-win32-x64-msvc@2.0.3':
|
'@tauri-apps/cli-win32-x64-msvc@2.2.7':
|
||||||
resolution: {integrity: sha512-eWV9XWb4dSYHXl13OtYWLjX1JHphUEkHkkGwJrhr8qFBm7RbxXxQvrsUEprSi51ug/dwJenjJgM4zR8By4htfw==}
|
resolution: {integrity: sha512-ZB8Kw90j8Ld+9tCWyD2fWCYfIrzbQohJ4DJSidNwbnehlZzP7wAz6Z3xjsvUdKtQ3ibtfoeTqVInzCCEpI+pWg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@tauri-apps/cli@2.0.3':
|
'@tauri-apps/cli@2.2.7':
|
||||||
resolution: {integrity: sha512-JwEyhc5BAVpn4E8kxzY/h7+bVOiXQdudR1r3ODMfyyumZBfgIWqpD/WuTcPq6Yjchju1BSS+80jAE/oYwI/RKg==}
|
resolution: {integrity: sha512-ZnsS2B4BplwXP37celanNANiIy8TCYhvg5RT09n72uR/o+navFZtGpFSqljV8fy1Y4ixIPds8FrGSXJCN2BerA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -2727,78 +2727,78 @@ snapshots:
|
|||||||
|
|
||||||
'@tauri-apps/api@2.0.0-alpha.6': {}
|
'@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
|
optional: true
|
||||||
|
|
||||||
'@tauri-apps/cli-darwin-x64@2.0.3':
|
'@tauri-apps/cli-darwin-x64@2.2.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-arm-gnueabihf@2.0.3':
|
'@tauri-apps/cli-linux-arm-gnueabihf@2.2.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-arm64-gnu@2.0.3':
|
'@tauri-apps/cli-linux-arm64-gnu@2.2.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-arm64-musl@2.0.3':
|
'@tauri-apps/cli-linux-arm64-musl@2.2.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-x64-gnu@2.0.3':
|
'@tauri-apps/cli-linux-x64-gnu@2.2.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-x64-musl@2.0.3':
|
'@tauri-apps/cli-linux-x64-musl@2.2.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tauri-apps/cli-win32-arm64-msvc@2.0.3':
|
'@tauri-apps/cli-win32-arm64-msvc@2.2.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tauri-apps/cli-win32-ia32-msvc@2.0.3':
|
'@tauri-apps/cli-win32-ia32-msvc@2.2.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tauri-apps/cli-win32-x64-msvc@2.0.3':
|
'@tauri-apps/cli-win32-x64-msvc@2.2.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tauri-apps/cli@2.0.3':
|
'@tauri-apps/cli@2.2.7':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@tauri-apps/cli-darwin-arm64': 2.0.3
|
'@tauri-apps/cli-darwin-arm64': 2.2.7
|
||||||
'@tauri-apps/cli-darwin-x64': 2.0.3
|
'@tauri-apps/cli-darwin-x64': 2.2.7
|
||||||
'@tauri-apps/cli-linux-arm-gnueabihf': 2.0.3
|
'@tauri-apps/cli-linux-arm-gnueabihf': 2.2.7
|
||||||
'@tauri-apps/cli-linux-arm64-gnu': 2.0.3
|
'@tauri-apps/cli-linux-arm64-gnu': 2.2.7
|
||||||
'@tauri-apps/cli-linux-arm64-musl': 2.0.3
|
'@tauri-apps/cli-linux-arm64-musl': 2.2.7
|
||||||
'@tauri-apps/cli-linux-x64-gnu': 2.0.3
|
'@tauri-apps/cli-linux-x64-gnu': 2.2.7
|
||||||
'@tauri-apps/cli-linux-x64-musl': 2.0.3
|
'@tauri-apps/cli-linux-x64-musl': 2.2.7
|
||||||
'@tauri-apps/cli-win32-arm64-msvc': 2.0.3
|
'@tauri-apps/cli-win32-arm64-msvc': 2.2.7
|
||||||
'@tauri-apps/cli-win32-ia32-msvc': 2.0.3
|
'@tauri-apps/cli-win32-ia32-msvc': 2.2.7
|
||||||
'@tauri-apps/cli-win32-x64-msvc': 2.0.3
|
'@tauri-apps/cli-win32-x64-msvc': 2.2.7
|
||||||
|
|
||||||
'@tauri-apps/plugin-autostart@2.2.0':
|
'@tauri-apps/plugin-autostart@2.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.0.2
|
'@tauri-apps/api': 2.2.0
|
||||||
|
|
||||||
'@tauri-apps/plugin-deep-link@2.2.0':
|
'@tauri-apps/plugin-deep-link@2.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.0.2
|
'@tauri-apps/api': 2.2.0
|
||||||
|
|
||||||
'@tauri-apps/plugin-global-shortcut@2.0.0':
|
'@tauri-apps/plugin-global-shortcut@2.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.0.2
|
'@tauri-apps/api': 2.2.0
|
||||||
|
|
||||||
'@tauri-apps/plugin-http@2.0.1':
|
'@tauri-apps/plugin-http@2.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.0.2
|
'@tauri-apps/api': 2.2.0
|
||||||
|
|
||||||
'@tauri-apps/plugin-os@2.2.0':
|
'@tauri-apps/plugin-os@2.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.0.2
|
'@tauri-apps/api': 2.2.0
|
||||||
|
|
||||||
'@tauri-apps/plugin-shell@2.0.0':
|
'@tauri-apps/plugin-shell@2.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.0.2
|
'@tauri-apps/api': 2.2.0
|
||||||
|
|
||||||
'@tauri-apps/plugin-websocket@2.0.0':
|
'@tauri-apps/plugin-websocket@2.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.0.2
|
'@tauri-apps/api': 2.2.0
|
||||||
|
|
||||||
'@tauri-apps/plugin-window@2.0.0-alpha.1':
|
'@tauri-apps/plugin-window@2.0.0-alpha.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
259
src-tauri/Cargo.lock
generated
259
src-tauri/Cargo.lock
generated
@@ -17,6 +17,18 @@ version = "2.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
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]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@@ -41,6 +53,12 @@ dependencies = [
|
|||||||
"alloc-no-stdlib",
|
"alloc-no-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allocator-api2"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android-tzdata"
|
name = "android-tzdata"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -584,6 +602,13 @@ dependencies = [
|
|||||||
name = "coco"
|
name = "coco"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"ordered-float",
|
||||||
|
"pizza-common",
|
||||||
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
@@ -598,6 +623,7 @@ dependencies = [
|
|||||||
"tauri-plugin-store",
|
"tauri-plugin-store",
|
||||||
"tauri-plugin-theme",
|
"tauri-plugin-theme",
|
||||||
"tauri-plugin-websocket",
|
"tauri-plugin-websocket",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -611,7 +637,7 @@ dependencies = [
|
|||||||
"cocoa-foundation",
|
"cocoa-foundation",
|
||||||
"core-foundation 0.10.0",
|
"core-foundation 0.10.0",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
"foreign-types",
|
"foreign-types 0.5.0",
|
||||||
"libc",
|
"libc",
|
||||||
"objc",
|
"objc",
|
||||||
]
|
]
|
||||||
@@ -738,7 +764,7 @@ dependencies = [
|
|||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"core-foundation 0.10.0",
|
"core-foundation 0.10.0",
|
||||||
"core-graphics-types",
|
"core-graphics-types",
|
||||||
"foreign-types",
|
"foreign-types 0.5.0",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1236,6 +1262,15 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
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]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -1243,7 +1278,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"foreign-types-macros",
|
"foreign-types-macros",
|
||||||
"foreign-types-shared",
|
"foreign-types-shared 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1257,6 +1292,12 @@ dependencies = [
|
|||||||
"syn 2.0.90",
|
"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]]
|
[[package]]
|
||||||
name = "foreign-types-shared"
|
name = "foreign-types-shared"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -1282,6 +1323,21 @@ dependencies = [
|
|||||||
"new_debug_unreachable",
|
"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]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -1289,6 +1345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1371,6 +1428,7 @@ version = "0.3.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
@@ -1518,8 +1576,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1723,6 +1783,10 @@ name = "hashbrown"
|
|||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"allocator-api2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
@@ -1853,10 +1917,26 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-tls"
|
||||||
version = "0.1.9"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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 = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@@ -2321,6 +2401,23 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -2677,12 +2774,65 @@ dependencies = [
|
|||||||
"pathdiff",
|
"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]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
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]]
|
[[package]]
|
||||||
name = "ordered-multimap"
|
name = "ordered-multimap"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@@ -2936,6 +3086,24 @@ dependencies = [
|
|||||||
"futures-io",
|
"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]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -3307,9 +3475,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.8"
|
version = "0.12.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
|
checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -3324,11 +3492,13 @@ dependencies = [
|
|||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-rustls",
|
"hyper-rustls",
|
||||||
|
"hyper-tls",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
|
"native-tls",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -3342,8 +3512,10 @@ dependencies = [
|
|||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"system-configuration",
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
"tower",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -3483,6 +3655,15 @@ dependencies = [
|
|||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "0.8.21"
|
version = "0.8.21"
|
||||||
@@ -3516,6 +3697,29 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
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]]
|
[[package]]
|
||||||
name = "selectors"
|
name = "selectors"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
@@ -3796,7 +4000,7 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
"foreign-types",
|
"foreign-types 0.5.0",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"objc2",
|
"objc2",
|
||||||
@@ -4606,6 +4810,16 @@ dependencies = [
|
|||||||
"syn 2.0.90",
|
"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]]
|
[[package]]
|
||||||
name = "tokio-rustls"
|
name = "tokio-rustls"
|
||||||
version = "0.26.0"
|
version = "0.26.0"
|
||||||
@@ -4716,6 +4930,27 @@ dependencies = [
|
|||||||
"winnow 0.6.22",
|
"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]]
|
[[package]]
|
||||||
name = "tower-service"
|
name = "tower-service"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@@ -4943,6 +5178,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ authors = ["INFINI Labs"]
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
# The `_lib` suffix may seem redundant but it is necessary
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
# 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 = [] }
|
tauri-build = { version = "2.0.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[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 = { version = "2.0.6", features = ["macos-private-api", "tray-icon", "image-png", "unstable"] }
|
||||||
tauri-plugin-shell = "2.0.0"
|
tauri-plugin-shell = "2.0.0"
|
||||||
serde = { version = "1", features = ["derive"] }
|
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-deep-link = "2.0.0"
|
||||||
tauri-plugin-single-instance = "2.0.0"
|
tauri-plugin-single-instance = "2.0.0"
|
||||||
tauri-plugin-store = "2.2.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]
|
[profile.dev]
|
||||||
incremental = true # Compile your binary in smaller steps.
|
incremental = true # Compile your binary in smaller steps.
|
||||||
|
|||||||
9
src-tauri/src/common/auth.rs
Normal file
9
src-tauri/src/common/auth.rs
Normal file
@@ -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,
|
||||||
|
}
|
||||||
19
src-tauri/src/common/connector.rs
Normal file
19
src-tauri/src/common/connector.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug,Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Connector {
|
||||||
|
pub id: String,
|
||||||
|
pub created: Option<String>,
|
||||||
|
pub updated: Option<String>,
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub category: Option<String>,
|
||||||
|
pub icon: Option<String>,
|
||||||
|
pub tags: Option<Vec<String>>,
|
||||||
|
pub url: Option<String>,
|
||||||
|
pub assets: Option<ConnectorAssets>,
|
||||||
|
}
|
||||||
|
#[derive(Debug,Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ConnectorAssets {
|
||||||
|
pub icons: Option<std::collections::HashMap<String, String>>,
|
||||||
|
}
|
||||||
20
src-tauri/src/common/datasource.rs
Normal file
20
src-tauri/src/common/datasource.rs
Normal file
@@ -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<String>,
|
||||||
|
pub updated: Option<String>,
|
||||||
|
pub r#type: Option<String>, // Using 'r#type' to escape the reserved keyword 'type'
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub connector: Option<ConnectorConfig>,
|
||||||
|
pub connector_info: Option<Connector>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug,Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ConnectorConfig {
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub config: Option<serde_json::Value>, // Using serde_json::Value to handle any type of config
|
||||||
|
}
|
||||||
16
src-tauri/src/common/health.rs
Normal file
16
src-tauri/src/common/health.rs
Normal file
@@ -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<String, Status>,
|
||||||
|
pub status: Status,
|
||||||
|
}
|
||||||
7
src-tauri/src/common/mod.rs
Normal file
7
src-tauri/src/common/mod.rs
Normal file
@@ -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;
|
||||||
15
src-tauri/src/common/profile.rs
Normal file
15
src-tauri/src/common/profile.rs
Normal file
@@ -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,
|
||||||
|
}
|
||||||
80
src-tauri/src/common/search_response.rs
Normal file
80
src-tauri/src/common/search_response.rs
Normal file
@@ -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<T> {
|
||||||
|
pub took: u64,
|
||||||
|
pub timed_out: bool,
|
||||||
|
pub _shards: Shards,
|
||||||
|
pub hits: Hits<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<T> {
|
||||||
|
pub total: Total,
|
||||||
|
pub max_score: Option<f32>,
|
||||||
|
pub hits: Vec<SearchHit<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Total {
|
||||||
|
pub value: u64,
|
||||||
|
pub relation: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SearchHit<T> {
|
||||||
|
pub _index: String,
|
||||||
|
pub _type: String,
|
||||||
|
pub _id: String,
|
||||||
|
pub _score: Option<f32>,
|
||||||
|
pub _source: T, // This will hold the type we pass in (e.g., DataSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn parse_search_results<T>(response: Response) -> Result<Vec<T>, Box<dyn Error>>
|
||||||
|
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::<Value>()
|
||||||
|
.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<T> = 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<T> = search_response
|
||||||
|
.hits
|
||||||
|
.hits
|
||||||
|
.into_iter()
|
||||||
|
.map(|hit| hit._source)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Log the final results before returning
|
||||||
|
// dbg!(&results);
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
116
src-tauri/src/common/server.rs
Normal file
116
src-tauri/src/common/server.rs
Normal file
@@ -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<UserProfile>,
|
||||||
|
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<H: Hasher>(&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<H: Hasher>(&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<UserProfile> {
|
||||||
|
None
|
||||||
|
}
|
||||||
@@ -1,12 +1,21 @@
|
|||||||
mod autostart;
|
mod autostart;
|
||||||
|
mod common;
|
||||||
|
mod server;
|
||||||
mod shortcut;
|
mod shortcut;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
use crate::server::servers::{load_or_insert_default_server, load_servers_token};
|
||||||
use autostart::{change_autostart, enable_autostart};
|
use autostart::{change_autostart, enable_autostart};
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use tauri::ActivationPolicy;
|
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_autostart::MacosLauncher;
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
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]
|
#[tauri::command]
|
||||||
fn change_window_height(handle: AppHandle, height: u32) {
|
fn change_window_height(handle: AppHandle, height: u32) {
|
||||||
@@ -80,12 +89,39 @@ pub fn run() {
|
|||||||
change_autostart,
|
change_autostart,
|
||||||
hide_coco,
|
hide_coco,
|
||||||
switch_tray_icon,
|
switch_tray_icon,
|
||||||
// show_panel,
|
server::servers::add_coco_server,
|
||||||
// hide_panel,
|
server::servers::remove_coco_server,
|
||||||
// close_panel
|
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| {
|
.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);
|
shortcut::enable_shortcut(app);
|
||||||
enable_tray(app);
|
enable_tray(app);
|
||||||
@@ -108,13 +144,26 @@ pub fn run() {
|
|||||||
dbg!(event.urls());
|
dbg!(event.urls());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.run(ctx)
|
.run(ctx)
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(_app_handle: &AppHandle) {
|
pub async fn init<R: Runtime>(app_handle: &AppHandle<R>) {
|
||||||
|
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 window: WebviewWindow = app_handle.get_webview_window("main").unwrap();
|
||||||
|
|
||||||
// let panel = window.to_panel().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 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 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 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 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 hide_i = MenuItem::with_id(app, "hide", "Hide Coco", true, None::<&str>).unwrap();
|
||||||
|
|
||||||
let menu = MenuBuilder::new(app)
|
let menu = MenuBuilder::new(app)
|
||||||
.item(&open_i)
|
.item(&open_i)
|
||||||
.separator()
|
.separator()
|
||||||
// .item(&hide_i)
|
// .item(&hide_i)
|
||||||
.item(&about_i)
|
// .item(&about_i)
|
||||||
.item(&settings_i)
|
.item(&settings_i)
|
||||||
.separator()
|
.separator()
|
||||||
.item(&quit_i)
|
.item(&quit_i)
|
||||||
|
|||||||
92
src-tauri/src/server/auth.rs
Normal file
92
src-tauri/src/server/auth.rs
Normal file
@@ -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<R: Runtime>(
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
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<RequestAccessTokenResponse, _> = 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
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
147
src-tauri/src/server/connector.rs
Normal file
147
src-tauri/src/server/connector.rs
Normal file
@@ -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<RwLock<HashMap<String, HashMap<String, Connector>>>> = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_connectors_to_cache(server_id: &str, connectors: Vec<Connector>) {
|
||||||
|
let mut cache = CONNECTOR_CACHE.write().unwrap(); // Acquire write lock
|
||||||
|
let connectors_map: HashMap<String, Connector> = 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<Connector> {
|
||||||
|
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<R: Runtime>(
|
||||||
|
app_handle: &AppHandle<R>,
|
||||||
|
) -> 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<String, Connector> = 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<Vec<Connector>, 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<Connector> into HashMap<String, Connector>
|
||||||
|
let connectors_map: HashMap<String, Connector> = 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<Vec<Connector>, 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<Connector> = 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<R: Runtime>(
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<Vec<Connector>, String> {
|
||||||
|
//fetch_connectors_by_server
|
||||||
|
|
||||||
|
let connectors = fetch_connectors_by_server(&id).await?;
|
||||||
|
Ok(connectors)
|
||||||
|
}
|
||||||
138
src-tauri/src/server/datasource.rs
Normal file
138
src-tauri/src/server/datasource.rs
Normal file
@@ -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<RwLock<HashMap<String,HashMap<String,DataSource>>>> = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_datasource_to_cache(server_id: &str, datasources: Vec<DataSource>) {
|
||||||
|
let mut cache = DATASOURCE_CACHE.write().unwrap(); // Acquire write lock
|
||||||
|
let datasources_map: HashMap<String, DataSource> = 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<HashMap<String, DataSource>> {
|
||||||
|
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<R: Runtime>(
|
||||||
|
app_handle: &AppHandle<R>,
|
||||||
|
) -> 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<String, DataSource> = 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<R: Runtime>(
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<Vec<DataSource>, 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<DataSource> = 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<Connector> into HashMap<String, Connector>
|
||||||
|
// let connectors_map: HashMap<String, Connector> = 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)
|
||||||
|
}
|
||||||
79
src-tauri/src/server/health.rs
Normal file
79
src-tauri/src/server/health.rs
Normal file
@@ -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<String, Json> = 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<R: Runtime>(
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
) -> Result<HashMap<String, bool>, ()> {
|
||||||
|
// 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<String, Json> = 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()
|
||||||
|
}
|
||||||
144
src-tauri/src/server/http_client.rs
Normal file
144
src-tauri/src/server/http_client.rs
Normal file
@@ -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<Mutex<Client>> = 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<reqwest::header::HeaderMap>,
|
||||||
|
body: Option<reqwest::Body>,
|
||||||
|
) -> Result<reqwest::Response, String> {
|
||||||
|
|
||||||
|
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<reqwest::header::HeaderMap>,
|
||||||
|
body: Option<reqwest::Body>,
|
||||||
|
) -> 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<reqwest::Body>,
|
||||||
|
) -> Result<reqwest::Response, String> {
|
||||||
|
// 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<reqwest::Response, String> {
|
||||||
|
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<reqwest::Response, String> {
|
||||||
|
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<reqwest::Response, String> {
|
||||||
|
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<reqwest::Response, String> {
|
||||||
|
HttpClient::send_request(server_id, Method::DELETE, path, None).await
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src-tauri/src/server/mod.rs
Normal file
20
src-tauri/src/server/mod.rs
Normal file
@@ -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;
|
||||||
|
|
||||||
26
src-tauri/src/server/profile.rs
Normal file
26
src-tauri/src/server/profile.rs
Normal file
@@ -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<R: Runtime>(
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
server_id: String,
|
||||||
|
) -> Result<UserProfile, String> {
|
||||||
|
// 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())
|
||||||
|
}
|
||||||
182
src-tauri/src/server/search.rs
Normal file
182
src-tauri/src/server/search.rs
Normal file
@@ -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<String, Json>, OrderedFloat<f64>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String, Json>, 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<Item = JsonMap<String, Json>> {
|
||||||
|
self.docs.into_iter().map(|(doc, _score)| doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct QueryResponse {
|
||||||
|
failed_coco_servers: Vec<String>,
|
||||||
|
documents: Vec<JsonMap<String, Json>>,
|
||||||
|
total_hits: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn get_name(provider_info: &JsonMap<String, Json>) -> &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<String, Json>) -> 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<R: Runtime>(
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
from: u64,
|
||||||
|
size: u64,
|
||||||
|
query_strings: HashMap<String, String>,
|
||||||
|
) -> Result<QueryResponse, ()> {
|
||||||
|
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::<JsonMap<String, 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)
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
516
src-tauri/src/server/servers.rs
Normal file
516
src-tauri/src/server/servers.rs
Normal file
@@ -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<RwLock<HashMap<String,Server>>> = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
static ref SERVER_TOKEN: Arc<RwLock<HashMap<String,ServerAccessToken>>> = 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<Server> {
|
||||||
|
let cache = SERVER_CACHE.read().unwrap(); // Acquire read lock
|
||||||
|
cache.get(id).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_server_token(id: &str) -> Option<ServerAccessToken> {
|
||||||
|
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<R: Runtime>(app_handle: &AppHandle<R>) -> 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<Server> = cache.values().cloned().collect();
|
||||||
|
|
||||||
|
// Serialize the servers into JSON automatically
|
||||||
|
let json_servers: Vec<JsonValue> = 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<R: Runtime>(app_handle: &AppHandle<R>) -> 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<ServerAccessToken> = cache.values().cloned().collect();
|
||||||
|
|
||||||
|
// Serialize the servers into JSON automatically
|
||||||
|
let json_servers: Vec<JsonValue> = 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<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Vec<ServerAccessToken>, 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<JsonValue> = 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<ServerAccessToken> = 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<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Vec<Server>, 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<JsonValue> = 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<Server> = 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<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Vec<Server>, 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<R: Runtime>(
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
) -> Result<Vec<Server>, String> {
|
||||||
|
let servers: Vec<Server> =get_all_servers();
|
||||||
|
Ok(servers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_servers_as_hashmap() -> HashMap<String, Server> {
|
||||||
|
let cache = SERVER_CACHE.read().unwrap();
|
||||||
|
cache.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_servers() -> Vec<Server> {
|
||||||
|
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<R: Runtime>(
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<Server, String> {
|
||||||
|
// 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<Server, _> = 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<R: Runtime>(
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
endpoint: String,
|
||||||
|
) -> Result<Server, String> {
|
||||||
|
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<Server, _> = 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<R: Runtime>(
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
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<R: Runtime>(
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
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");
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::COCO_TAURI_STORE;
|
||||||
use tauri::App;
|
use tauri::App;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
@@ -8,9 +9,6 @@ use tauri_plugin_global_shortcut::ShortcutState;
|
|||||||
use tauri_plugin_store::JsonValue;
|
use tauri_plugin_store::JsonValue;
|
||||||
use tauri_plugin_store::StoreExt;
|
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
|
/// Tauri's store is a key-value database, we use it to store our registered
|
||||||
/// global shortcut.
|
/// global shortcut.
|
||||||
///
|
///
|
||||||
|
|||||||
0
src-tauri/src/util/mod.rs
Normal file
0
src-tauri/src/util/mod.rs
Normal file
27
src/App.tsx
27
src/App.tsx
@@ -1,27 +0,0 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import "./i18n";
|
|
||||||
import CommandInput from "./components/CommandInput";
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`
|
|
||||||
bg-background
|
|
||||||
w-screen h-screen
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<main className="w-[100%] h-[100%] flex flex-col items-center justify-center">
|
|
||||||
<div className="text-xl text-primary">{t("welcome")}</div>
|
|
||||||
|
|
||||||
<div className="mx-0 mt-5 w-[100%]">
|
|
||||||
<CommandInput />
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -60,7 +60,7 @@ export const tauriFetch = async <T = any>({
|
|||||||
headers["Content-Type"] = "application/json";
|
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
|
// debug API
|
||||||
const requestInfo = {
|
const requestInfo = {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -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<string, any[]>;
|
|
||||||
|
|
||||||
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<boolean>(IsError);
|
|
||||||
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
|
||||||
const [selectedName, setSelectedName] = useState<string>("");
|
|
||||||
const [showIndex, setShowIndex] = useState<boolean>(false);
|
|
||||||
const containerRef = useRef<HTMLDivElement>(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 (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="h-[458px] w-full p-2 flex flex-col overflow-y-auto custom-scrollbar focus:outline-none"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
{showError ? (
|
|
||||||
<div className="flex items-center gap-2 text-sm text-[#333] p-2">
|
|
||||||
<CircleAlert className="text-[#FF0000] w-[14px] h-[14px]" />
|
|
||||||
Coco server is unavailable, only local results and available services
|
|
||||||
are displayed.
|
|
||||||
<Bolt className="text-[#000] w-[14px] h-[14px] cursor-pointer" />
|
|
||||||
<X
|
|
||||||
className="text-[#666] w-[16px] h-[16px] cursor-pointer"
|
|
||||||
onClick={() => setShowError(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{Object.entries(SearchData).map(([sourceName, items]) => (
|
|
||||||
<div key={sourceName}>
|
|
||||||
{Object.entries(SearchData).length < 5 ? (
|
|
||||||
<div className="p-2 text-xs text-[#999] dark:text-[#666] flex items-center gap-2.5 relative">
|
|
||||||
<img className="w-4 h-4" src={getTypeIcon(items[0])} alt="icon" />
|
|
||||||
{sourceName}
|
|
||||||
<div className="flex-1 border-b border-b-[#e6e6e6] dark:border-b-[#272626]"></div>
|
|
||||||
<SquareArrowRight
|
|
||||||
className="w-4 h-4 cursor-pointer"
|
|
||||||
onClick={() => goToTwoPage(items[0])}
|
|
||||||
/>
|
|
||||||
{showIndex && sourceName === selectedName ? (
|
|
||||||
<div
|
|
||||||
className={`absolute right-2
|
|
||||||
w-4 h-4 flex items-end justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
|
|
||||||
>
|
|
||||||
→
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{items.map((item: any) => {
|
|
||||||
const isSelected = selectedItem === globalIndex;
|
|
||||||
const currentIndex = globalIndex;
|
|
||||||
globalItemIndexMap.push(item);
|
|
||||||
globalIndex++;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item._id}
|
|
||||||
ref={(el) => (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]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex gap-2 items-center justify-start max-w-[450px]">
|
|
||||||
<img className="w-5 h-5" src={getIcon(item)} alt="icon" />
|
|
||||||
<span
|
|
||||||
className={`text-sm truncate text-left ${
|
|
||||||
isSelected ? "font-medium" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item?._source?.title}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 text-right min-w-[160px] h-full pl-5 text-[12px] flex gap-2 items-center justify-end relative">
|
|
||||||
{Object.entries(SearchData).length < 5 ? null : (
|
|
||||||
<img
|
|
||||||
className="w-4 h-4 cursor-pointer"
|
|
||||||
src={getTypeIcon(item)}
|
|
||||||
alt="icon"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
goToTwoPage(item);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{item?._source?.rich_categories ? (
|
|
||||||
<div className="flex items-center justify-end max-w-[calc(100%-20px)] whitespace-nowrap">
|
|
||||||
{Object.entries(SearchData).length < 5 ? (
|
|
||||||
<img
|
|
||||||
className="w-4 h-4 mr-2 cursor-pointer"
|
|
||||||
src={getRichIcon(item)}
|
|
||||||
alt="icon"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
goToTwoPage(item);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
isSelected ? "text-[#C8C8C8]" : "text-[#666]"
|
|
||||||
} text-right truncate`}
|
|
||||||
>
|
|
||||||
{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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
{item?._source?.rich_categories.length > 2 ? (
|
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
isSelected ? "text-[#C8C8C8]" : "text-[#666]"
|
|
||||||
} text-right truncate`}
|
|
||||||
>
|
|
||||||
{"/" + item?._source?.rich_categories?.at(-1)?.label}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : item?._source?.category || item?._source?.subcategory ? (
|
|
||||||
<span
|
|
||||||
className={`text-[12px] truncate ${
|
|
||||||
isSelected
|
|
||||||
? "text-[#DCDCDC]"
|
|
||||||
: "text-[#999] dark:text-[#666]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{(item?._source?.category || "") +
|
|
||||||
(item?._source?.subcategory
|
|
||||||
? `/${item?._source?.subcategory}`
|
|
||||||
: "")}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className={`text-[12px] truncate ${
|
|
||||||
isSelected
|
|
||||||
? "text-[#DCDCDC]"
|
|
||||||
: "text-[#999] dark:text-[#666]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item?._source?.type || ""}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isSelected ? (
|
|
||||||
<div
|
|
||||||
className={`absolute ${
|
|
||||||
showIndex && currentIndex < 10 ? "right-7" : "right-0"
|
|
||||||
} w-4 h-4 flex items-end justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md ${
|
|
||||||
isSelected
|
|
||||||
? "shadow-[-6px_0px_6px_2px_#950599]"
|
|
||||||
: "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
↩︎
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showIndex && currentIndex < 10 ? (
|
|
||||||
<div
|
|
||||||
className={`absolute right-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md ${
|
|
||||||
isSelected
|
|
||||||
? "shadow-[-6px_0px_6px_2px_#950599]"
|
|
||||||
: "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{currentIndex}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DropdownList;
|
|
||||||
@@ -234,7 +234,7 @@ const ChatAI = forwardRef<ChatAIRef, ChatAIProps>(
|
|||||||
skipTaskbar: true,
|
skipTaskbar: true,
|
||||||
decorations: true,
|
decorations: true,
|
||||||
closable: true,
|
closable: true,
|
||||||
url: "/ui/chat",
|
url: "/ui/app/chat",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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<string | null>(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 (
|
|
||||||
<div className="flex bg-gray-50 dark:bg-gray-900">
|
|
||||||
<Sidebar addService={addService} />
|
|
||||||
|
|
||||||
<main className="flex-1 p-4 py-8">
|
|
||||||
{/* <div>
|
|
||||||
{error && (
|
|
||||||
<div className="text-red-500 dark:text-red-400 mb-4">
|
|
||||||
Error: {error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
{isConnect ? (
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<div className="w-full rounded-[4px] bg-[rgba(229,229,229,1)] dark:bg-gray-800 mb-6">
|
|
||||||
<img
|
|
||||||
width="100%"
|
|
||||||
src={currentService.provider.banner || bannerImg}
|
|
||||||
alt="banner"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className="flex items-center text-gray-900 dark:text-white font-medium">
|
|
||||||
{currentService.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
|
||||||
onClick={() => goToHref(currentService.provider.website)}
|
|
||||||
>
|
|
||||||
<Globe className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
|
||||||
onClick={() => refreshClick()}
|
|
||||||
>
|
|
||||||
<RefreshCcw
|
|
||||||
className={`w-3.5 h-3.5 ${
|
|
||||||
refreshLoading ? "animate-spin" : ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
{currentService.endpoint !== defaultService.endpoint ? (
|
|
||||||
<button
|
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
|
||||||
onClick={() => deleteClick()}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]" />
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400 mb-2 flex">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<PackageOpen className="w-4 h-4" />{" "}
|
|
||||||
{currentService.provider.name}
|
|
||||||
</span>
|
|
||||||
<span className="mx-4">|</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<GitFork className="w-4 h-4" />{" "}
|
|
||||||
{currentService.version.number}
|
|
||||||
</span>
|
|
||||||
<span className="mx-4">|</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<CalendarSync className="w-4 h-4" /> {currentService.updated}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
|
||||||
{currentService.provider.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{currentService.auth_provider.sso.url ? (
|
|
||||||
<div className="mb-8">
|
|
||||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
|
||||||
Account Information
|
|
||||||
</h2>
|
|
||||||
{auth && auth[endpoint] ? (
|
|
||||||
<UserProfile userInfo={userInfo[endpoint]} />
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
|
||||||
onClick={LoginClick}
|
|
||||||
>
|
|
||||||
{loading ? "Login..." : "Login"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="text-xs text-[#0096FB] dark:text-blue-400 block"
|
|
||||||
onClick={() =>
|
|
||||||
goToHref(currentService.provider.privacy_policy)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
EULA | Privacy Policy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{auth && auth[endpoint] ? <DataSourcesList /> : null}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<ConnectService setIsConnect={setIsConnect} />
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<div className="max-w-4xl">
|
|
||||||
<div className="flex items-center gap-2 mb-8">
|
|
||||||
<button
|
|
||||||
className=" text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 border border-[rgba(228,229,239,1)] dark:border-gray-700 p-1"
|
|
||||||
onClick={goBack}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<div className="text-xl text-[#101010] dark:text-white">
|
|
||||||
Connecting to third-party services
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-8">
|
|
||||||
<p className="text-gray-600 dark:text-gray-400">
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="endpoint"
|
|
||||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2.5"
|
|
||||||
>
|
|
||||||
Server address
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="endpoint"
|
|
||||||
value={endpointLink}
|
|
||||||
placeholder="For example: https://coco.infini.cloud/"
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
|
||||||
onClick={addService}
|
|
||||||
>
|
|
||||||
{refreshLoading ? "Connecting..." : "Connect"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="flex justify-between text-xl font-medium text-gray-900 dark:text-white">
|
|
||||||
Data Source
|
|
||||||
<button
|
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
|
||||||
onClick={() => getDatasourceData()}
|
|
||||||
>
|
|
||||||
<RefreshCcw
|
|
||||||
className={`w-3.5 h-3.5 ${refreshLoading ? "animate-spin" : ""}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{datasourceData[endpoint_http]?.map((source) => (
|
|
||||||
<DataSourceItem key={source._id} {...source._source} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-1">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => 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
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-1">
|
|
||||||
Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => 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
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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<StringBooleanMap>({});
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className="w-64 min-h-[550px] border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
|
||||||
<div className="p-4 py-8">
|
|
||||||
<div
|
|
||||||
className={`flex items-center space-x-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-lg mb-6 ${
|
|
||||||
currentService.endpoint === defaultService.endpoint
|
|
||||||
? "border border-[rgba(0,135,255,1)]"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentService(defaultService);
|
|
||||||
setEndpoint(defaultService.endpoint);
|
|
||||||
getDefaultHealth();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={defaultService.provider.icon || cocoLogoImg}
|
|
||||||
alt="cocoLogoImg"
|
|
||||||
className="w-5 h-5"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className="font-medium">{defaultService.name}</span>
|
|
||||||
<div className="flex-1" />
|
|
||||||
<button className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
|
|
||||||
{defaultHealth ? (
|
|
||||||
<div className="w-3 h-3 rounded-full bg-[#00DB5E]"></div>
|
|
||||||
) : (
|
|
||||||
<div className="w-3 h-3 rounded-full bg-[#FF4747]"></div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
|
||||||
Third-party services
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{otherServices?.map((item, index) => (
|
|
||||||
<div
|
|
||||||
key={item.name + index}
|
|
||||||
className={`flex items-center space-x-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-lg mb-2 ${
|
|
||||||
currentService.endpoint === item.endpoint
|
|
||||||
? "border border-[rgba(0,135,255,1)]"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
setEndpoint(item.endpoint);
|
|
||||||
setCurrentService(item);
|
|
||||||
getOtherHealth(item);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={item.provider.icon || cocoLogoImg}
|
|
||||||
alt="LogoImg"
|
|
||||||
className="w-5 h-5"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className="font-medium">{item.name}</span>
|
|
||||||
<div className="flex-1" />
|
|
||||||
<button className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
|
|
||||||
{otherHealth[item.endpoint] ? (
|
|
||||||
<div className="w-3 h-3 rounded-full bg-[#00DB5E]"></div>
|
|
||||||
) : (
|
|
||||||
<div className="w-3 h-3 rounded-full bg-[#FF4747]"></div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<button
|
|
||||||
className="w-full flex items-center justify-center p-2 border-2 border-dashed border-gray-200 dark:border-gray-700 rounded-lg text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"
|
|
||||||
onClick={addServiceClick}
|
|
||||||
>
|
|
||||||
<Plus className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
export default `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate">
|
|
||||||
<meta http-equiv="Pragma" content="no-cache">
|
|
||||||
<title>Coco Auth</title>
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: sans-serif;
|
|
||||||
text-align: center;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
padding: 30px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.logo {
|
|
||||||
width: 130px;
|
|
||||||
height: auto;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
font-size: 21px;
|
|
||||||
line-height: 26px;
|
|
||||||
color: #12161F;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
color: #dc2626;
|
|
||||||
margin-top: 12px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Coco<h1>
|
|
||||||
<p id="message">You are now signed in. Please re-open the Coco desktop app to continue.</p>
|
|
||||||
<div id="error-container"></div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
@@ -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 (
|
|
||||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
|
||||||
<div className="w-full max-w-md bg-gray-800 rounded-xl p-8 shadow-2xl">
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<h1 className="text-2xl font-bold text-white mb-2">Welcome Back</h1>
|
|
||||||
<p className="text-gray-400">Sign in to continue to Coco</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4 mb-6">
|
|
||||||
<SocialButton
|
|
||||||
icon={<Github className="w-5 h-5" />}
|
|
||||||
provider="GitHub"
|
|
||||||
onClick={() => GithubClick()}
|
|
||||||
/>
|
|
||||||
<SocialButton
|
|
||||||
icon={<Mail className="w-5 h-5" />}
|
|
||||||
provider="Google"
|
|
||||||
onClick={() => console.log('Google login')}
|
|
||||||
/>
|
|
||||||
<SocialButton
|
|
||||||
icon={<Apple className="w-5 h-5" />}
|
|
||||||
provider="Apple"
|
|
||||||
onClick={() => console.log('Apple login')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<LoginForm onSubmit={(email, password) => console.log(email, password)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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<HTMLTextAreaElement>(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<HTMLTextAreaElement>) => {
|
|
||||||
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 (
|
|
||||||
<div className="w-[100%] h-[100%]">
|
|
||||||
<div>{info}</div>
|
|
||||||
<div className="relative flex dark:bg-app-gray-2/[0.98] dark:text-slate-200 items-center gap-1">
|
|
||||||
<Textarea
|
|
||||||
ref={inputRef}
|
|
||||||
onChange={handleInput}
|
|
||||||
spellCheck="false"
|
|
||||||
autoFocus
|
|
||||||
className={clsx(
|
|
||||||
"mt-3 block w-full resize-none rounded-xl border border-transparent bg-white/10",
|
|
||||||
"py-3 px-4 text-sm text-black placeholder-gray-500 shadow-md",
|
|
||||||
// Transition for smoother appearance changes
|
|
||||||
"transition-colors duration-300 ease-in-out",
|
|
||||||
// Dark mode styles
|
|
||||||
"dark:bg-gray-800 dark:text-white dark:placeholder-gray-400",
|
|
||||||
"focus:border-blue-400 focus:ring-2 focus:ring-blue-400 focus:ring-opacity-50",
|
|
||||||
"dark:focus:border-white dark:focus:ring-white/25",
|
|
||||||
"focus:outline-none"
|
|
||||||
)}
|
|
||||||
placeholder={t("InputMessage")}
|
|
||||||
/>
|
|
||||||
<SendIcon
|
|
||||||
size={30}
|
|
||||||
className="absolute right-2 text-gray-400/80 dark:text-gray-600 cursor-pointer"
|
|
||||||
onClick={handleSend}
|
|
||||||
title={`Send message (${isMac ? "⌘⏎" : "⌃⏎"})`}
|
|
||||||
aria-label="Send message"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
476
src/components/Cloud/Cloud.tsx
Normal file
476
src/components/Cloud/Cloud.tsx
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
|
import {
|
||||||
|
RefreshCcw,
|
||||||
|
Globe,
|
||||||
|
PackageOpen,
|
||||||
|
GitFork,
|
||||||
|
CalendarSync,
|
||||||
|
Trash2,
|
||||||
|
Copy,
|
||||||
|
} 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 { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
import { UserProfile } from "./UserProfile";
|
||||||
|
import { DataSourcesList } from "./DataSourcesList";
|
||||||
|
import { Sidebar } from "./Sidebar";
|
||||||
|
import { Connect } from "./Connect.tsx";
|
||||||
|
import { OpenURLWithBrowser } from "@/utils";
|
||||||
|
import { useAppStore } from "@/stores/appStore";
|
||||||
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
|
import bannerImg from "@/assets/images/coco-cloud-banner.jpeg";
|
||||||
|
|
||||||
|
export default function Cloud() {
|
||||||
|
const SidebarRef = useRef<{ refreshData: () => void; }>(null);
|
||||||
|
|
||||||
|
// const [error, setError] = useState<string | null>(null);
|
||||||
|
const error = useAppStore((state) => state.error);
|
||||||
|
const setError = useAppStore((state) => state.setError);
|
||||||
|
|
||||||
|
const [isConnect, setIsConnect] = useState(true);
|
||||||
|
// const [ssoRequestID, setSSORequestID] = useState("");
|
||||||
|
const ssoRequestID = useAppStore((state) => state.ssoRequestID);
|
||||||
|
const setSSORequestID = useAppStore((state) => state.setSSORequestID);
|
||||||
|
|
||||||
|
// const ssoServerID = useAppStore((state) => state.ssoServerID);
|
||||||
|
// const setSSOServerID = useAppStore((state) => state.setSSOServerID);
|
||||||
|
|
||||||
|
const endpoint = useAppStore((state) => state.endpoint);
|
||||||
|
|
||||||
|
const currentService = useConnectStore((state) => state.currentService);
|
||||||
|
const setCurrentService = useConnectStore((state) => state.setCurrentService);
|
||||||
|
|
||||||
|
const serverList = useConnectStore((state) => state.serverList);
|
||||||
|
const setServerList = useConnectStore((state) => state.setServerList);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [refreshLoading, setRefreshLoading] = useState(false);
|
||||||
|
// const [profiles, setProfiles] = useState<any>({});
|
||||||
|
// const [userInfo, setUserInfo] = useState<any>({});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//fetch the servers
|
||||||
|
useEffect(() => {
|
||||||
|
fetchServers(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("currentService", currentService);
|
||||||
|
setLoading(false);
|
||||||
|
setRefreshLoading(false);
|
||||||
|
setError("");
|
||||||
|
// setEndpoint(currentService.endpoint);
|
||||||
|
setIsConnect(true);
|
||||||
|
// setUserInfo(profiles[endpoint] || {})
|
||||||
|
}, [JSON.stringify(currentService)]);
|
||||||
|
|
||||||
|
// const get_user_profiles = useCallback(() => {
|
||||||
|
// invoke("get_user_profiles")
|
||||||
|
// .then((res: any) => {
|
||||||
|
// console.log("get_user_profiles", res);
|
||||||
|
// setProfiles(res);
|
||||||
|
// console.log("setUserInfo", res[endpoint]);
|
||||||
|
// setUserInfo(res[endpoint] || {})
|
||||||
|
// })
|
||||||
|
// .catch((err: any) => {
|
||||||
|
// console.error(err);
|
||||||
|
// });
|
||||||
|
// }, [endpoint]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// get_user_profiles()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchServers = async (resetSelection: boolean) => {
|
||||||
|
invoke("list_coco_servers")
|
||||||
|
.then((res: any) => {
|
||||||
|
console.log("list_coco_servers", res);
|
||||||
|
setServerList(res);
|
||||||
|
if (resetSelection && res.length > 0) {
|
||||||
|
console.log("setCurrentService", res[res.length - 1]);
|
||||||
|
setCurrentService(res[res.length - 1]);
|
||||||
|
} else {
|
||||||
|
console.warn("Service list is empty or last item has no id");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
setError(err);
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const add_coco_server = (endpointLink: string) => {
|
||||||
|
if (!endpointLink) {
|
||||||
|
throw new Error('Endpoint is required');
|
||||||
|
}
|
||||||
|
if (!endpointLink.startsWith("http://") && !endpointLink.startsWith("https://")) {
|
||||||
|
throw new Error('Invalid Endpoint');
|
||||||
|
}
|
||||||
|
|
||||||
|
setRefreshLoading(true);
|
||||||
|
|
||||||
|
return invoke("add_coco_server", { endpoint: endpointLink })
|
||||||
|
.then((res: any) => {
|
||||||
|
console.log("add_coco_server", res);
|
||||||
|
fetchServers(false)
|
||||||
|
.then((r) => {
|
||||||
|
console.log("fetchServers", r);
|
||||||
|
setCurrentService(res);
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
console.error("fetchServers failed:", err);
|
||||||
|
setError(err);
|
||||||
|
throw err; // Propagate error back up to outer promise chain
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
// Handle the invoke error
|
||||||
|
console.error("add coco server failed:", err);
|
||||||
|
setError(err);
|
||||||
|
throw err; // Propagate error back up
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setRefreshLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOAuthCallback = useCallback(
|
||||||
|
async (code: string | null, serverId: string | null) => {
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
setError("No authorization code received");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Handling OAuth callback:", { code, serverId });
|
||||||
|
await invoke("handle_sso_callback", {
|
||||||
|
serverId: serverId, // Make sure 'server_id' is the correct argument
|
||||||
|
requestId: ssoRequestID, // Make sure 'request_id' is the correct argument
|
||||||
|
code: code
|
||||||
|
});
|
||||||
|
|
||||||
|
if (serverId != null) {
|
||||||
|
refreshClick(serverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentWindow()
|
||||||
|
.setFocus()
|
||||||
|
.catch((err) => {
|
||||||
|
setError(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Sign in failed:", e);
|
||||||
|
setError("SSO login failed: " + e);
|
||||||
|
// setAuth(undefined, endpoint);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[ssoRequestID, endpoint]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUrl = (url: string) => {
|
||||||
|
try {
|
||||||
|
// url = "coco://oauth_callback?code=cuhhi8o2sdbbbcoe0g10ktmht6aky3jmd4xkwsgvzf748i4zdgr898bfeu3kze7ffdusdtbgtnpke8ng3fe6&provider=coco-cloud/"
|
||||||
|
const urlObject = new URL(url);
|
||||||
|
console.log("handle urlObject:", urlObject);
|
||||||
|
|
||||||
|
//TODO, pass request_id and check with local, if the request_id are same, then continue
|
||||||
|
const reqId = urlObject.searchParams.get("request_id");
|
||||||
|
const code = urlObject.searchParams.get("code");
|
||||||
|
|
||||||
|
if (reqId != ssoRequestID) {
|
||||||
|
console.log("Request ID not matched, skip");
|
||||||
|
setError("Request ID not matched, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverId = currentService?.id;
|
||||||
|
handleOAuthCallback(code, serverId);
|
||||||
|
|
||||||
|
// 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: " + err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Fetch the initial deep link intent
|
||||||
|
useEffect(() => {
|
||||||
|
// Function to handle pasted URL
|
||||||
|
const handlePaste = (event: any) => {
|
||||||
|
const pastedText = event.clipboardData.getData('text');
|
||||||
|
console.log("handle paste text:", pastedText);
|
||||||
|
if (isValidCallbackUrl(pastedText)) {
|
||||||
|
// Handle the URL as if it's a deep link
|
||||||
|
console.log("handle callback on paste:", pastedText);
|
||||||
|
handleUrl(pastedText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to check if the pasted URL is valid for our deep link scheme
|
||||||
|
const isValidCallbackUrl = (url: string) => {
|
||||||
|
return url && url.startsWith('coco://oauth_callback');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adding event listener for paste events
|
||||||
|
document.addEventListener('paste', handlePaste);
|
||||||
|
|
||||||
|
getCurrentDeepLinkUrls()
|
||||||
|
.then((urls) => {
|
||||||
|
console.log("URLs:", urls);
|
||||||
|
if (urls && urls.length > 0) {
|
||||||
|
if (isValidCallbackUrl(urls[0])) {
|
||||||
|
handleUrl(urls[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to get initial URLs:", err);
|
||||||
|
setError("Failed to get initial URLs: " + err);
|
||||||
|
});
|
||||||
|
|
||||||
|
const unlisten = onOpenUrl((urls) => handleUrl(urls[0]));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then((fn) => fn());
|
||||||
|
document.removeEventListener('paste', handlePaste);
|
||||||
|
};
|
||||||
|
}, [ssoRequestID]);
|
||||||
|
|
||||||
|
// const generateLogin = () => {
|
||||||
|
// const requestID = uuidv4();
|
||||||
|
// setSSORequestID(requestID);
|
||||||
|
// setSSOServerID(currentService?.id); // Set server ID
|
||||||
|
//
|
||||||
|
// // The URL is now updated when ssoRequestID and ssoServerID are both set
|
||||||
|
// };
|
||||||
|
|
||||||
|
const LoginClick = useCallback(() => {
|
||||||
|
if (loading) return; // Prevent multiple clicks if already loading
|
||||||
|
|
||||||
|
// If the appUid doesn't exist, generate one
|
||||||
|
// if (!ssoRequestID) {
|
||||||
|
let requestID = uuidv4();
|
||||||
|
setSSORequestID(requestID);
|
||||||
|
// setSSOServerID(currentService?.id);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Generate the login URL with the current appUid
|
||||||
|
const url = `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${requestID}`;
|
||||||
|
|
||||||
|
console.log("Open SSO link, requestID:", ssoRequestID, url);
|
||||||
|
|
||||||
|
// Open the URL in a browser
|
||||||
|
OpenURLWithBrowser(url);
|
||||||
|
|
||||||
|
// Start loading state
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
}, [ssoRequestID, loading, currentService]);
|
||||||
|
|
||||||
|
const refreshClick = (id: string) => {
|
||||||
|
setRefreshLoading(true);
|
||||||
|
invoke("refresh_coco_server_info", { id })
|
||||||
|
.then((res: any) => {
|
||||||
|
console.log("refresh_coco_server_info", id, JSON.stringify(res));
|
||||||
|
fetchServers(false).then(r => {
|
||||||
|
console.log("fetchServers", r);
|
||||||
|
});
|
||||||
|
//update currentService
|
||||||
|
setCurrentService(res);
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
setError(err);
|
||||||
|
console.error(err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setRefreshLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function onAddServer() {
|
||||||
|
setIsConnect(false);
|
||||||
|
}
|
||||||
|
function onLogout(id: string) {
|
||||||
|
console.log("onLogout", id);
|
||||||
|
setRefreshLoading(true);
|
||||||
|
invoke("logout_coco_server", { id })
|
||||||
|
.then((res: any) => {
|
||||||
|
console.log("logout_coco_server", id, JSON.stringify(res));
|
||||||
|
refreshClick(id);
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
setError(err);
|
||||||
|
console.error(err);
|
||||||
|
}).finally(() => {
|
||||||
|
setRefreshLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const remove_coco_server = (id: string) => {
|
||||||
|
invoke("remove_coco_server", { id })
|
||||||
|
.then((res: any) => {
|
||||||
|
console.log("remove_coco_server", id, JSON.stringify(res));
|
||||||
|
fetchServers(true).then(r => {
|
||||||
|
console.log("fetchServers", r);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
//TODO display the error message
|
||||||
|
setError(err);
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex bg-gray-50 dark:bg-gray-900">
|
||||||
|
<Sidebar ref={SidebarRef} onAddServer={onAddServer} serverList={serverList} />
|
||||||
|
|
||||||
|
<main className="flex-1 p-4 py-8">
|
||||||
|
|
||||||
|
{isConnect ? (
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="w-full rounded-[4px] bg-[rgba(229,229,229,1)] dark:bg-gray-800 mb-6">
|
||||||
|
<img
|
||||||
|
width="100%"
|
||||||
|
src={currentService?.provider?.banner || bannerImg}
|
||||||
|
alt="banner"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="flex items-center text-gray-900 dark:text-white font-medium">
|
||||||
|
{currentService?.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||||
|
onClick={() => OpenURLWithBrowser(currentService?.provider?.website)}
|
||||||
|
>
|
||||||
|
<Globe className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||||
|
onClick={() => refreshClick(currentService?.id)}
|
||||||
|
>
|
||||||
|
<RefreshCcw
|
||||||
|
className={`w-3.5 h-3.5 ${refreshLoading ? "animate-spin" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{!currentService?.builtin && (
|
||||||
|
<button
|
||||||
|
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||||
|
onClick={() => remove_coco_server(currentService?.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400 mb-2 flex">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<PackageOpen className="w-4 h-4" />{" "}
|
||||||
|
{currentService?.provider?.name}
|
||||||
|
</span>
|
||||||
|
<span className="mx-4">|</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<GitFork className="w-4 h-4" />{" "}
|
||||||
|
{currentService?.version?.number}
|
||||||
|
</span>
|
||||||
|
<span className="mx-4">|</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<CalendarSync className="w-4 h-4" /> {currentService?.updated}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||||
|
{currentService?.provider?.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{currentService?.auth_provider?.sso?.url ? (
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||||
|
Account Information
|
||||||
|
</h2>
|
||||||
|
{currentService?.profile ? (
|
||||||
|
<UserProfile server={currentService?.id} userInfo={currentService?.profile} onLogout={onLogout} />
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{/* Login Button (conditionally rendered when not loading) */}
|
||||||
|
{!loading && (
|
||||||
|
<button
|
||||||
|
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
||||||
|
onClick={LoginClick}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cancel Button and Copy URL button while loading */}
|
||||||
|
{loading && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
className="px-6 py-2 text-white bg-red-500 rounded-md hover:bg-red-600 transition-colors mb-3"
|
||||||
|
onClick={() => setLoading(false)} // Reset loading state
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}`
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className="text-xl text-blue-500 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
<Copy className="inline mr-2" /> {/* Lucide Copy Icon */}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Privacy Policy Link */}
|
||||||
|
<button
|
||||||
|
className="text-xs text-[#0096FB] dark:text-blue-400 block"
|
||||||
|
onClick={() =>
|
||||||
|
OpenURLWithBrowser(currentService?.provider?.privacy_policy)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
EULA | Privacy Policy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{currentService?.profile ? <DataSourcesList server={currentService?.id} /> : null}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Connect setIsConnect={setIsConnect} onAddServer={add_coco_server} />
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
124
src/components/Cloud/Connect.tsx
Normal file
124
src/components/Cloud/Connect.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ChevronLeft } from "lucide-react";
|
||||||
|
import {useAppStore} from "@/stores/appStore";
|
||||||
|
|
||||||
|
|
||||||
|
interface ConnectServiceProps {
|
||||||
|
setIsConnect: (isConnect: boolean) => void;
|
||||||
|
onAddServer: (endpoint: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
|
||||||
|
const [endpointLink, setEndpointLink] = useState("");
|
||||||
|
const [refreshLoading, ] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState(''); // State to store the error message
|
||||||
|
|
||||||
|
const setError = useAppStore((state) => state.setError);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
setIsConnect(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddServerClick = async (endpoint: string) => {
|
||||||
|
console.log("onAddServer", endpoint);
|
||||||
|
try {
|
||||||
|
await onAddServer(endpoint);
|
||||||
|
setIsConnect(true); // Only set as connected if the server is added successfully
|
||||||
|
} catch (err: any) {
|
||||||
|
// Handle the error if something goes wrong
|
||||||
|
const errorMessage = typeof err === 'string' ? err : err?.message || 'An unknown error occurred.';
|
||||||
|
setErrorMessage("ERR:"+errorMessage);
|
||||||
|
setError(errorMessage);
|
||||||
|
console.error('Error:', errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to close the error message
|
||||||
|
const closeError = () => {
|
||||||
|
setErrorMessage('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl">
|
||||||
|
<div className="flex items-center gap-2 mb-8">
|
||||||
|
<button
|
||||||
|
className=" text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 border border-[rgba(228,229,239,1)] dark:border-gray-700 p-1"
|
||||||
|
onClick={goBack}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<div className="text-xl text-[#101010] dark:text-white">
|
||||||
|
Connecting to Your Coco-Server
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Running your own private instance of coco-server ensures complete control over
|
||||||
|
your data, keeping it secure and accessible only within your environment.
|
||||||
|
Enjoy enhanced privacy, better performance, and seamless integration with your
|
||||||
|
internal systems.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="endpoint"
|
||||||
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2.5"
|
||||||
|
>
|
||||||
|
Server address
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="endpoint"
|
||||||
|
value={endpointLink}
|
||||||
|
placeholder="For example: https://coco.infini.cloud/"
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||||
|
onClick={()=>onAddServerClick(endpointLink)}
|
||||||
|
>
|
||||||
|
{refreshLoading ? "Connecting..." : "Connect"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/*//TODO move to outer container, move error state to global*/}
|
||||||
|
{errorMessage && (
|
||||||
|
<div
|
||||||
|
className="mb-8"
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
color: 'red',
|
||||||
|
marginTop: '10px',
|
||||||
|
display: 'block', // Makes sure the error message starts on a new line
|
||||||
|
marginBottom: '10px',
|
||||||
|
}}>
|
||||||
|
<span>{errorMessage}</span>
|
||||||
|
<button
|
||||||
|
onClick={closeError}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
color: 'red',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -29,10 +29,10 @@ export function DataSourceItem({ name, connector }: DataSourceItemProps) {
|
|||||||
const connector_id = connector?.id;
|
const connector_id = connector?.id;
|
||||||
|
|
||||||
const result_connector = connector_data[endpoint_http]?.find(
|
const result_connector = connector_data[endpoint_http]?.find(
|
||||||
(data: any) => data._source.id === connector_id
|
(data: any) => data.id === connector_id
|
||||||
);
|
);
|
||||||
|
|
||||||
return result_connector?._source;
|
return result_connector;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTypeIcon() {
|
function getTypeIcon() {
|
||||||
80
src/components/Cloud/DataSourcesList.tsx
Normal file
80
src/components/Cloud/DataSourcesList.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {RefreshCcw} from "lucide-react";
|
||||||
|
|
||||||
|
import {DataSourceItem} from "./DataSourceItem";
|
||||||
|
import {useConnectStore} from "@/stores/connectStore";
|
||||||
|
import {useAppStore} from "@/stores/appStore";
|
||||||
|
import {invoke} from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
export function DataSourcesList({server}: { server: string }) {
|
||||||
|
const datasourceData = useConnectStore((state) => state.datasourceData);
|
||||||
|
const setError = useAppStore((state) => state.setError);
|
||||||
|
const [refreshLoading, setRefreshLoading] = useState(false);
|
||||||
|
const setDatasourceData = useConnectStore((state) => state.setDatasourceData);
|
||||||
|
const setConnectorData = useConnectStore((state) => state.setConnectorData);
|
||||||
|
|
||||||
|
function initServerAppData({server}: { server: string }) {
|
||||||
|
//fetch datasource data
|
||||||
|
invoke("get_connectors_by_server", {id: server})
|
||||||
|
.then((res: any) => {
|
||||||
|
console.log("get_connectors_by_server", res);
|
||||||
|
setConnectorData(res, server);
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
setError(err);
|
||||||
|
throw err; // Propagate error back up
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
//fetch datasource data
|
||||||
|
invoke("get_datasources_by_server", {id: server})
|
||||||
|
.then((res: any) => {
|
||||||
|
console.log("get_datasources_by_server", res);
|
||||||
|
setDatasourceData(res, server);
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
setError(err);
|
||||||
|
throw err; // Propagate error back up
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDatasourceData() {
|
||||||
|
setRefreshLoading(true);
|
||||||
|
try {
|
||||||
|
initServerAppData({server});
|
||||||
|
} catch (e) {
|
||||||
|
setError(e);
|
||||||
|
} finally {
|
||||||
|
setRefreshLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getDatasourceData()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="flex justify-between text-xl font-medium text-gray-900 dark:text-white">
|
||||||
|
Data Source
|
||||||
|
<button
|
||||||
|
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||||
|
onClick={() => getDatasourceData()}
|
||||||
|
>
|
||||||
|
<RefreshCcw
|
||||||
|
className={`w-3.5 h-3.5 ${refreshLoading ? "animate-spin" : ""}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{datasourceData[server]?.map((source) => (
|
||||||
|
<DataSourceItem key={source.id} {...source} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
83
src/components/Cloud/Sidebar.tsx
Normal file
83
src/components/Cloud/Sidebar.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { forwardRef } from "react";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
|
||||||
|
import cocoLogoImg from "@/assets/app-icon.png";
|
||||||
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
onAddServer: () => void;
|
||||||
|
serverList: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sidebar = forwardRef<{ refreshData: () => void; }, SidebarProps>(
|
||||||
|
({ onAddServer, serverList }, _ref) => {
|
||||||
|
const currentService = useConnectStore((state) => state.currentService);
|
||||||
|
const setCurrentService = useConnectStore((state) => state.setCurrentService);
|
||||||
|
|
||||||
|
const onAddServerClick = () => {
|
||||||
|
onAddServer();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extracted server item rendering
|
||||||
|
const renderServerItem = (item: any) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item?.id}
|
||||||
|
className={`flex cursor-pointer items-center space-x-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-lg mb-2 ${
|
||||||
|
currentService?.id === item?.id ? "dark:bg-blue-900/20 dark:bg-blue-900" // Apply background color when selected
|
||||||
|
: "bg-gray-50 dark:bg-gray-900" // Default background color when not selected
|
||||||
|
}`}
|
||||||
|
onClick={() => setCurrentService(item)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={item?.provider?.icon || cocoLogoImg}
|
||||||
|
alt="LogoImg"
|
||||||
|
className="w-5 h-5"
|
||||||
|
/>
|
||||||
|
<span className="font-medium">{item?.name}</span>
|
||||||
|
<div className="flex-1" />
|
||||||
|
<button className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
|
||||||
|
{item?.available ? (
|
||||||
|
<div className="w-3 h-3 rounded-full bg-[#00DB5E]" />
|
||||||
|
) : (
|
||||||
|
<div className="w-3 h-3 rounded-full bg-[#FF4747]" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-64 min-h-[550px] border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||||
|
<div className="p-4 py-8">
|
||||||
|
{/* Render Built-in Servers */}
|
||||||
|
<div>
|
||||||
|
{serverList
|
||||||
|
.filter((item) => item?.builtin)
|
||||||
|
.map((item) => renderServerItem(item))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||||
|
Your Coco-Servers
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Render Non-Built-in Servers */}
|
||||||
|
<div>
|
||||||
|
{serverList
|
||||||
|
.filter((item) => !item?.builtin)
|
||||||
|
.map((item) => renderServerItem(item))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<button
|
||||||
|
className="w-full flex items-center justify-center p-2 border-2 border-dashed border-gray-200 dark:border-gray-700 rounded-lg text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"
|
||||||
|
onClick={onAddServerClick}
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import { User, LogOut } from "lucide-react";
|
import { User, LogOut } from "lucide-react";
|
||||||
|
|
||||||
import { useAuthStore } from "@/stores/authStore";
|
|
||||||
import { useAppStore } from "@/stores/appStore";
|
|
||||||
|
|
||||||
interface UserPreferences {
|
interface UserPreferences {
|
||||||
theme: "dark" | "light";
|
theme: "dark" | "light";
|
||||||
language: string;
|
language: string;
|
||||||
@@ -16,17 +13,15 @@ interface UserInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UserProfileProps {
|
interface UserProfileProps {
|
||||||
|
server: string; //server's id
|
||||||
userInfo: UserInfo;
|
userInfo: UserInfo;
|
||||||
|
onLogout: (server: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserProfile({ userInfo }: UserProfileProps) {
|
export function UserProfile({ server,userInfo,onLogout }: UserProfileProps) {
|
||||||
const setAuth = useAuthStore((state) => state.setAuth);
|
|
||||||
const setUserInfo = useAuthStore((state) => state.setUserInfo);
|
|
||||||
const endpoint = useAppStore((state) => state.endpoint);
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
setAuth(undefined, endpoint);
|
onLogout(server);
|
||||||
setUserInfo({}, endpoint);
|
console.log("Logout",server);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { Command } from "lucide-react";
|
|
||||||
import { CommandPalette } from "./CommandPalette";
|
|
||||||
|
|
||||||
function CommandInput() {
|
|
||||||
return (
|
|
||||||
<div className="h-[100%] w-[100%]">
|
|
||||||
<div className="mx-auto px-4 py-16">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="inline-flex items-center gap-2 bg-white px-4 py-2 rounded-lg shadow-sm border border-gray-200">
|
|
||||||
<Command className="w-5 h-5 text-gray-500" />
|
|
||||||
<span className="text-gray-600">Press</span>
|
|
||||||
<kbd className="px-2 py-1 text-sm font-semibold text-gray-700 bg-gray-100 border border-gray-200 rounded-md">
|
|
||||||
⌘
|
|
||||||
</kbd>
|
|
||||||
<kbd className="px-2 py-1 text-sm font-semibold text-gray-700 bg-gray-100 border border-gray-200 rounded-md">
|
|
||||||
K
|
|
||||||
</kbd>
|
|
||||||
<span className="text-gray-600">to open command palette</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CommandPalette />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CommandInput;
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
|
||||||
import {
|
|
||||||
Search,
|
|
||||||
Settings,
|
|
||||||
Calculator,
|
|
||||||
Calendar,
|
|
||||||
Mail,
|
|
||||||
Music,
|
|
||||||
User,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
interface CommandItem {
|
|
||||||
id: string;
|
|
||||||
icon: React.ReactNode;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
action: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CommandPalette() {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
||||||
|
|
||||||
const commands: CommandItem[] = [
|
|
||||||
{
|
|
||||||
id: "settings",
|
|
||||||
icon: <Settings className="w-5 h-5" />,
|
|
||||||
title: "Settings",
|
|
||||||
description: "Adjust your preferences",
|
|
||||||
action: () => console.log("Settings clicked"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "calculator",
|
|
||||||
icon: <Calculator className="w-5 h-5" />,
|
|
||||||
title: "Calculator",
|
|
||||||
description: "Perform quick calculations",
|
|
||||||
action: () => console.log("Calculator clicked"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "calendar",
|
|
||||||
icon: <Calendar className="w-5 h-5" />,
|
|
||||||
title: "Calendar",
|
|
||||||
description: "View your schedule",
|
|
||||||
action: () => console.log("Calendar clicked"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "mail",
|
|
||||||
icon: <Mail className="w-5 h-5" />,
|
|
||||||
title: "Mail",
|
|
||||||
description: "Check your inbox",
|
|
||||||
action: () => console.log("Mail clicked"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "music",
|
|
||||||
icon: <Music className="w-5 h-5" />,
|
|
||||||
title: "Music",
|
|
||||||
description: "Control playback",
|
|
||||||
action: () => console.log("Music clicked"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "profile",
|
|
||||||
icon: <User className="w-5 h-5" />,
|
|
||||||
title: "Profile",
|
|
||||||
description: "View your profile",
|
|
||||||
action: () => console.log("Profile clicked"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const filteredCommands = commands.filter(
|
|
||||||
(command) =>
|
|
||||||
command.title.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
command.description.toLowerCase().includes(search.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
|
||||||
(e: KeyboardEvent) => {
|
|
||||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsOpen((prev) => !prev);
|
|
||||||
} else if (e.key === "Escape") {
|
|
||||||
setIsOpen(false);
|
|
||||||
} else if (e.key === "ArrowDown") {
|
|
||||||
e.preventDefault();
|
|
||||||
setSelectedIndex((prev) =>
|
|
||||||
prev < filteredCommands.length - 1 ? prev + 1 : prev
|
|
||||||
);
|
|
||||||
} else if (e.key === "ArrowUp") {
|
|
||||||
e.preventDefault();
|
|
||||||
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
|
|
||||||
} else if (e.key === "Enter" && filteredCommands[selectedIndex]) {
|
|
||||||
filteredCommands[selectedIndex].action();
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[filteredCommands, selectedIndex]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [handleKeyDown]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedIndex(0);
|
|
||||||
}, [search]);
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
|
||||||
<div className="min-h-screen px-4 text-center">
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 bg-black/50 transition-opacity"
|
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="inline-block w-full max-w-2xl my-16 text-left align-middle transition-all transform">
|
|
||||||
<div className="relative bg-white rounded-xl shadow-2xl overflow-hidden">
|
|
||||||
<div className="flex items-center px-4 border-b border-gray-200">
|
|
||||||
<Search className="w-5 h-5 text-gray-400" />
|
|
||||||
<input
|
|
||||||
autoFocus
|
|
||||||
type="text"
|
|
||||||
className="w-full px-4 py-4 text-gray-700 bg-transparent border-none focus:outline-none focus:ring-0"
|
|
||||||
placeholder="Search commands..."
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
/>
|
|
||||||
<div className="w-32 flex items-center gap-1">
|
|
||||||
<kbd className="px-2 py-1 text-xs font-semibold text-gray-500 bg-gray-100 border border-gray-200 rounded-md">
|
|
||||||
Esc
|
|
||||||
</kbd>
|
|
||||||
<span className="text-gray-400">to close</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-h-[60vh] overflow-y-auto">
|
|
||||||
{filteredCommands.length === 0 ? (
|
|
||||||
<div className="px-4 py-14 text-center text-gray-500">
|
|
||||||
No results found.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="py-2">
|
|
||||||
{filteredCommands.map((command, index) => (
|
|
||||||
<div
|
|
||||||
key={command.id}
|
|
||||||
className={`px-4 py-3 flex items-center gap-3 cursor-pointer transition-colors ${
|
|
||||||
selectedIndex === index
|
|
||||||
? "bg-blue-50 text-blue-700"
|
|
||||||
: "text-gray-700 hover:bg-gray-50"
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
command.action();
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
selectedIndex === index
|
|
||||||
? "text-blue-600"
|
|
||||||
: "text-gray-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{command.icon}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">{command.title}</div>
|
|
||||||
<div
|
|
||||||
className={`text-sm ${
|
|
||||||
selectedIndex === index
|
|
||||||
? "text-blue-500"
|
|
||||||
: "text-gray-500"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{command.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -53,8 +53,8 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
|
|||||||
if (e.key === "Enter" && selectedItem !== null) {
|
if (e.key === "Enter" && selectedItem !== null) {
|
||||||
// console.log("Enter key pressed", selectedItem);
|
// console.log("Enter key pressed", selectedItem);
|
||||||
const item = suggests[selectedItem];
|
const item = suggests[selectedItem];
|
||||||
if (item?._source?.url) {
|
if (item?.url) {
|
||||||
handleOpenURL(item?._source?.url);
|
handleOpenURL(item?.url);
|
||||||
} else {
|
} else {
|
||||||
selected(item);
|
selected(item);
|
||||||
}
|
}
|
||||||
@@ -63,8 +63,8 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
|
|||||||
if (e.key >= "0" && e.key <= "9" && showIndex) {
|
if (e.key >= "0" && e.key <= "9" && showIndex) {
|
||||||
// console.log(`number ${e.key}`);
|
// console.log(`number ${e.key}`);
|
||||||
const item = suggests[parseInt(e.key, 10)];
|
const item = suggests[parseInt(e.key, 10)];
|
||||||
if (item?._source?.url) {
|
if (item?.url) {
|
||||||
handleOpenURL(item?._source?.url);
|
handleOpenURL(item?.url);
|
||||||
} else {
|
} else {
|
||||||
selected(item);
|
selected(item);
|
||||||
}
|
}
|
||||||
@@ -121,8 +121,8 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
|
|||||||
ref={(el) => (itemRefs.current[index] = el)}
|
ref={(el) => (itemRefs.current[index] = el)}
|
||||||
onMouseEnter={() => setSelectedItem(index)}
|
onMouseEnter={() => setSelectedItem(index)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item?._source?.url) {
|
if (item?.url) {
|
||||||
handleOpenURL(item?._source?.url);
|
handleOpenURL(item?.url);
|
||||||
} else {
|
} else {
|
||||||
selected(item);
|
selected(item);
|
||||||
}
|
}
|
||||||
@@ -134,14 +134,14 @@ function DropdownList({ selected, suggests }: DropdownListProps) {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<img className="w-5 h-5" src={item?._source?.icon} alt="icon" />
|
<img className="w-5 h-5" src={item?.icon} alt="icon" />
|
||||||
<span className="text-[#333] dark:text-[#d8d8d8] truncate w-80 text-left">
|
<span className="text-[#333] dark:text-[#d8d8d8] truncate w-80 text-left">
|
||||||
{item?._source?.title}
|
{item?.title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center relative">
|
<div className="flex gap-2 items-center relative">
|
||||||
<span className="text-sm text-[#666] dark:text-[#666] truncate w-52 text-right">
|
<span className="text-sm text-[#666] dark:text-[#666] truncate w-52 text-right">
|
||||||
{item?._source?.source}
|
{item?.source}
|
||||||
</span>
|
</span>
|
||||||
{showIndex && index < 10 ? (
|
{showIndex && index < 10 ? (
|
||||||
<div
|
<div
|
||||||
23
src/components/Common/Icons/IconWrapper.tsx
Normal file
23
src/components/Common/Icons/IconWrapper.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface IconWrapperProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
onClick: React.MouseEventHandler<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function IconWrapper({ children, className="", onClick }: IconWrapperProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClick(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IconWrapper;
|
||||||
50
src/components/Common/Icons/ItemIcon.tsx
Normal file
50
src/components/Common/Icons/ItemIcon.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import {
|
||||||
|
File,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import IconWrapper from './IconWrapper';
|
||||||
|
import ThemedIcon from './ThemedIcon';
|
||||||
|
import { useFindConnectorIcon } from "./hooks"
|
||||||
|
import { useAppStore } from "@/stores/appStore";
|
||||||
|
|
||||||
|
interface ItemIconProps {
|
||||||
|
item: any;
|
||||||
|
className?: string;
|
||||||
|
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemIcon({
|
||||||
|
item,
|
||||||
|
className = "w-5 h-5 flex-shrink-0",
|
||||||
|
onClick = () => {}
|
||||||
|
}: ItemIconProps) {
|
||||||
|
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||||
|
|
||||||
|
const connectorSource = useFindConnectorIcon(item);
|
||||||
|
const icons = connectorSource?.assets?.icons || {};
|
||||||
|
const selectedIcon = icons[item?.icon];
|
||||||
|
|
||||||
|
if (!selectedIcon) {
|
||||||
|
return (
|
||||||
|
<IconWrapper className={className} onClick={onClick}>
|
||||||
|
<ThemedIcon component={File} className={className} />
|
||||||
|
</IconWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedIcon.startsWith("http://") || selectedIcon.startsWith("https://")) {
|
||||||
|
return (
|
||||||
|
<IconWrapper className={className} onClick={onClick}>
|
||||||
|
<img className={className} src={selectedIcon} alt="icon" />
|
||||||
|
</IconWrapper>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<IconWrapper className={className} onClick={onClick}>
|
||||||
|
<img className={className} src={`${endpoint_http}${selectedIcon}`} alt="icon" />
|
||||||
|
</IconWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ItemIcon;
|
||||||
47
src/components/Common/Icons/RichIcon.tsx
Normal file
47
src/components/Common/Icons/RichIcon.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import {
|
||||||
|
Folder,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import IconWrapper from './IconWrapper';
|
||||||
|
import ThemedIcon from './ThemedIcon';
|
||||||
|
import { useFindConnectorIcon } from "./hooks"
|
||||||
|
import { useAppStore } from "@/stores/appStore";
|
||||||
|
|
||||||
|
interface RichIconProps {
|
||||||
|
item: any;
|
||||||
|
className?: string;
|
||||||
|
onClick: (e: React.MouseEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RichIcon({ item, className, onClick }: RichIconProps) {
|
||||||
|
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||||
|
|
||||||
|
const connectorSource = useFindConnectorIcon(item);
|
||||||
|
const icons = connectorSource?.assets?.icons || {};
|
||||||
|
|
||||||
|
const selectedIcon = icons[item?.rich_categories?.[0]?.icon];
|
||||||
|
|
||||||
|
if (!selectedIcon) {
|
||||||
|
return (
|
||||||
|
<IconWrapper className={className} onClick={onClick}>
|
||||||
|
<ThemedIcon component={Folder} className={className} />
|
||||||
|
</IconWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedIcon.startsWith("http://") || selectedIcon.startsWith("https://")) {
|
||||||
|
return (
|
||||||
|
<IconWrapper className={className} onClick={onClick}>
|
||||||
|
<img className={className} src={selectedIcon} alt="icon" />
|
||||||
|
</IconWrapper>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<IconWrapper className={className} onClick={onClick}>
|
||||||
|
<img className={className} src={`${endpoint_http}${selectedIcon}`} alt="icon" />
|
||||||
|
</IconWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RichIcon;
|
||||||
27
src/components/Common/Icons/ThemedIcon.tsx
Normal file
27
src/components/Common/Icons/ThemedIcon.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface ThemedIconProps {
|
||||||
|
component: React.ElementType;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ThemedIcon({ component: Component, className = "" }: ThemedIconProps) {
|
||||||
|
const [color, setColor] = useState("#000");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateTheme = () => {
|
||||||
|
const isDark = document.body.classList.contains("dark");
|
||||||
|
setColor(isDark ? "#DCDCDC" : "#999");
|
||||||
|
};
|
||||||
|
|
||||||
|
updateTheme();
|
||||||
|
const observer = new MutationObserver(updateTheme);
|
||||||
|
observer.observe(document.body, { attributes: true, attributeFilter: ["class"] });
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <Component className={className} color={color} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThemedIcon;
|
||||||
48
src/components/Common/Icons/TypeIcon.tsx
Normal file
48
src/components/Common/Icons/TypeIcon.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box } from "lucide-react";
|
||||||
|
|
||||||
|
import IconWrapper from './IconWrapper';
|
||||||
|
import ThemedIcon from './ThemedIcon';
|
||||||
|
import { useFindConnectorIcon } from "./hooks"
|
||||||
|
import { useAppStore } from "@/stores/appStore";
|
||||||
|
|
||||||
|
interface TypeIconProps {
|
||||||
|
item: any;
|
||||||
|
className?: string;
|
||||||
|
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TypeIcon({
|
||||||
|
item,
|
||||||
|
className = "w-5 h-5 flex-shrink-0",
|
||||||
|
onClick = () => { }
|
||||||
|
}: TypeIconProps) {
|
||||||
|
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
||||||
|
const connectorSource = useFindConnectorIcon(item);
|
||||||
|
const selectedIcon = connectorSource?.icon;
|
||||||
|
|
||||||
|
if (!selectedIcon) {
|
||||||
|
// console.log("go default folder:");
|
||||||
|
return (
|
||||||
|
<IconWrapper className={className} onClick={onClick}>
|
||||||
|
<ThemedIcon component={Box} className={className} />
|
||||||
|
</IconWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedIcon.startsWith("http://") || selectedIcon.startsWith("https://")) {
|
||||||
|
return (
|
||||||
|
<IconWrapper className={className} onClick={onClick}>
|
||||||
|
<img className={className} src={selectedIcon} alt="icon" />
|
||||||
|
</IconWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconWrapper className={className} onClick={onClick}>
|
||||||
|
<img className={className} src={`${endpoint_http}${selectedIcon}`} alt="icon" />
|
||||||
|
</IconWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TypeIcon;
|
||||||
21
src/components/Common/Icons/hooks.ts
Normal file
21
src/components/Common/Icons/hooks.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useConnectStore } from "@/stores/connectStore";
|
||||||
|
|
||||||
|
export function useFindConnectorIcon(item: any) {
|
||||||
|
const connector_data = useConnectStore((state) => state.connector_data);
|
||||||
|
const datasourceData = useConnectStore((state) => state.datasourceData);
|
||||||
|
const currentService = useConnectStore((state) => state.currentService);
|
||||||
|
|
||||||
|
const id = item?.source?.id || "";
|
||||||
|
|
||||||
|
const result_source = datasourceData[currentService?.id]?.find(
|
||||||
|
(data: any) => data.id === id
|
||||||
|
);
|
||||||
|
|
||||||
|
const connector_id = result_source?.connector?.id;
|
||||||
|
|
||||||
|
const result_connector = connector_data[currentService?.id]?.find(
|
||||||
|
(data: any) => data.id === connector_id
|
||||||
|
);
|
||||||
|
|
||||||
|
return result_connector;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { Library, Mic, Send, Plus, AudioLines, Image } from "lucide-react";
|
|||||||
import { useRef, useState, useEffect, useCallback } from "react";
|
import { useRef, useState, useEffect, useCallback } from "react";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
import ChatSwitch from "../SearchChat/ChatSwitch";
|
import ChatSwitch from "@/components/Common/ChatSwitch";
|
||||||
import AutoResizeTextarea from "./AutoResizeTextarea";
|
import AutoResizeTextarea from "./AutoResizeTextarea";
|
||||||
import { useChatStore } from "../../stores/chatStore";
|
import { useChatStore } from "../../stores/chatStore";
|
||||||
import StopIcon from "../../icons/Stop";
|
import StopIcon from "../../icons/Stop";
|
||||||
@@ -5,7 +5,7 @@ import { LogicalSize } from "@tauri-apps/api/dpi";
|
|||||||
|
|
||||||
import DropdownList from "./DropdownList";
|
import DropdownList from "./DropdownList";
|
||||||
import { Footer } from "./Footer";
|
import { Footer } from "./Footer";
|
||||||
import { SearchResults } from "./SearchResults";
|
import { SearchResults } from "../Search/SearchResults";
|
||||||
import { tauriFetch } from "../../api/tauriFetchClient";
|
import { tauriFetch } from "../../api/tauriFetchClient";
|
||||||
import { useAppStore } from '@/stores/appStore';
|
import { useAppStore } from '@/stores/appStore';
|
||||||
interface SearchProps {
|
interface SearchProps {
|
||||||
@@ -8,7 +8,7 @@ import { LogicalSize } from "@tauri-apps/api/dpi";
|
|||||||
|
|
||||||
import InputBox from "./InputBox";
|
import InputBox from "./InputBox";
|
||||||
import Search from "./Search";
|
import Search from "./Search";
|
||||||
import ChatAI, { ChatAIRef } from "../ChatAI/Chat";
|
import ChatAI, { ChatAIRef } from "@/components/Assistant/Chat";
|
||||||
import { useWindows } from "../../hooks/useWindows";
|
import { useWindows } from "../../hooks/useWindows";
|
||||||
|
|
||||||
// const appWindow = new Window("main");
|
// const appWindow = new Window("main");
|
||||||
@@ -1,30 +1,54 @@
|
|||||||
import {
|
import {
|
||||||
Menu,
|
Menu,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
// MenuItems, MenuItem
|
// MenuItems, MenuItem
|
||||||
} from "@headlessui/react";
|
} from "@headlessui/react";
|
||||||
// import { Settings, LogOut, User, ChevronUp, Home } from "lucide-react";
|
// import { Settings, LogOut, User, ChevronUp, Home } from "lucide-react";
|
||||||
// import { Link } from "react-router-dom";
|
// import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import logoImg from "../assets/32x32.png";
|
import logoImg from "../assets/32x32.png";
|
||||||
|
import {useAppStore} from "@/stores/appStore";
|
||||||
|
import {OctagonAlert} from 'lucide-react';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
return (
|
|
||||||
<div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t dark:border-gray-700">
|
const error = useAppStore((state) => state.error);
|
||||||
<div className="max-w-6xl mx-auto px-4 h-8 flex items-center justify-between">
|
const setError = useAppStore((state) => state.setError);
|
||||||
<Menu as="div" className="relative">
|
|
||||||
<MenuButton className="h-7 flex items-center space-x-2 px-1 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
return (
|
||||||
<img
|
<div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t dark:border-gray-700">
|
||||||
src={logoImg}
|
{/* Move the warning message outside the border */}
|
||||||
className="w-5 h-5 text-gray-600 dark:text-gray-400"
|
{error && (
|
||||||
/>
|
<div className="fixed bottom-6 left-0 right-0 bg-yellow-100 dark:bg-yellow-900 border-l-4 border-yellow-500 rounded-lg shadow-lg p-4 m-4">
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<div className="flex items-center space-x-4">
|
||||||
|
<OctagonAlert size={32} color="red" className="mr-2" />
|
||||||
|
<span className="text-xs text-red-500 dark:text-red-400 flex-1">{error}</span>
|
||||||
|
<X
|
||||||
|
className="cursor-pointer ml-2"
|
||||||
|
onClick={() => setError("")}
|
||||||
|
size={32}
|
||||||
|
color="gray"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="max-w-6xl mx-auto px-4 h-8 flex items-center justify-between">
|
||||||
|
<Menu as="div" className="relative">
|
||||||
|
<MenuButton
|
||||||
|
className="h-7 flex items-center space-x-2 px-1 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<img
|
||||||
|
src={logoImg}
|
||||||
|
className="w-5 h-5 text-gray-600 dark:text-gray-400"
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Coco
|
Coco
|
||||||
</span>
|
</span>
|
||||||
{/* <ChevronUp className="w-4 h-4 text-gray-500 dark:text-gray-400" /> */}
|
{/* <ChevronUp className="w-4 h-4 text-gray-500 dark:text-gray-400" /> */}
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
|
|
||||||
{/* <MenuItems className="absolute bottom-full mb-2 left-0 w-64 rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
{/* <MenuItems className="absolute bottom-full mb-2 left-0 w-64 rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
@@ -85,20 +109,22 @@ const Footer = () => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</MenuItems> */}
|
</MenuItems> */}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
Version 1.0.0
|
Version 1.0.0
|
||||||
</span>
|
</span>
|
||||||
{/* <div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
|
{/* <div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
|
||||||
<button className="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors">
|
<button className="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors">
|
||||||
Check for Updates
|
Check for Updates
|
||||||
</button> */}
|
</button> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
|
|
||||||
interface ChatInterfaceProps {
|
|
||||||
query: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Message {
|
|
||||||
id: string;
|
|
||||||
content: string;
|
|
||||||
isUser: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChatInterface: React.FC<ChatInterfaceProps> = ({ query }) => {
|
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
|
||||||
const [isTyping, setIsTyping] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (query) {
|
|
||||||
setMessages((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
id: Date.now().toString(),
|
|
||||||
content: query,
|
|
||||||
isUser: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
setIsTyping(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
setMessages((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
id: (Date.now() + 1).toString(),
|
|
||||||
content: "hello world...",
|
|
||||||
isUser: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
setIsTyping(false);
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{messages.map((message) => (
|
|
||||||
<div
|
|
||||||
key={message.id}
|
|
||||||
className={`flex ${message.isUser ? "justify-end" : "justify-start"}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`max-w-[80%] p-3 rounded-lg ${
|
|
||||||
message.isUser
|
|
||||||
? "bg-blue-500 text-white"
|
|
||||||
: "bg-gray-100 text-gray-900"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{message.content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{isTyping && (
|
|
||||||
<div className="flex justify-start">
|
|
||||||
<div className="bg-gray-100 p-3 rounded-lg">...</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
|
||||||
import { SearchIcon, MessageCircleIcon } from "lucide-react";
|
|
||||||
|
|
||||||
import { SearchResults } from "./SearchResults";
|
|
||||||
import { ChatInterface } from "./ChatInterface";
|
|
||||||
|
|
||||||
interface CommandPaletteProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
setIsOpen: (isOpen: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Mode = "search" | "chat";
|
|
||||||
|
|
||||||
export const CommandPalette: React.FC<CommandPaletteProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
setIsOpen,
|
|
||||||
}) => {
|
|
||||||
const [mode, setMode] = useState<Mode>("search");
|
|
||||||
const [query, setQuery] = useState("");
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
isOpen ? onClose() : setIsOpen(true);
|
|
||||||
}
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [isOpen, onClose]);
|
|
||||||
|
|
||||||
const toggleMode = () => {
|
|
||||||
setMode((prev) => (prev === "search" ? "chat" : "search"));
|
|
||||||
setQuery("");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{isOpen && (
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 z-50 flex items-start justify-center pt-20"
|
|
||||||
>
|
|
||||||
<div className="w-full max-w-2xl bg-white rounded-lg shadow-2xl">
|
|
||||||
<div className="flex items-center p-4 border-b">
|
|
||||||
<button
|
|
||||||
onClick={toggleMode}
|
|
||||||
className={`p-2 rounded-md transition-colors ${
|
|
||||||
mode === "search" ? "bg-blue-100 text-blue-600" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<SearchIcon size={20} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={toggleMode}
|
|
||||||
className={`p-2 ml-2 rounded-md transition-colors ${
|
|
||||||
mode === "chat" ? "bg-blue-100 text-blue-600" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<MessageCircleIcon size={20} />
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
type="text"
|
|
||||||
value={query}
|
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
|
||||||
placeholder={mode === "search" ? "Search for files..." : "Input Message..."}
|
|
||||||
className="flex-1 ml-4 p-2 outline-none"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-h-[60vh] overflow-y-auto p-4">
|
|
||||||
{mode === "search" ? (
|
|
||||||
<SearchResults query={query} />
|
|
||||||
) : (
|
|
||||||
<ChatInterface query={query} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { FileIcon, FolderIcon } from "lucide-react";
|
|
||||||
|
|
||||||
interface SearchResultsProps {
|
|
||||||
query: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SearchResults: React.FC<SearchResultsProps> = ({ query }) => {
|
|
||||||
const [results, setResults] = useState<
|
|
||||||
Array<{ name: string; path: string; type: "file" | "folder" }>
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const searchFiles = async () => {
|
|
||||||
setResults([
|
|
||||||
{ name: "Document.pdf", path: "/documents/Document.pdf", type: "file" },
|
|
||||||
{ name: "Projects", path: "/documents/projects", type: "folder" },
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (query) {
|
|
||||||
searchFiles();
|
|
||||||
}
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{results.map((result, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="flex items-center p-3 hover:bg-gray-100 rounded-md cursor-pointer"
|
|
||||||
>
|
|
||||||
{result.type === "file" ? (
|
|
||||||
<FileIcon size={16} />
|
|
||||||
) : (
|
|
||||||
<FolderIcon size={16} />
|
|
||||||
)}
|
|
||||||
<div className="ml-3">
|
|
||||||
<div className="font-medium">{result.name}</div>
|
|
||||||
<div className="text-sm text-gray-500">{result.path}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { CommandPalette } from "./CommandPalette";
|
|
||||||
|
|
||||||
export default function MySearch() {
|
|
||||||
const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsCommandPaletteOpen(true)}
|
|
||||||
className="fixed bottom-4 right-4 p-4 bg-blue-500 text-white rounded-full"
|
|
||||||
>
|
|
||||||
Open the Command Palette
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<CommandPalette
|
|
||||||
isOpen={isCommandPaletteOpen}
|
|
||||||
onClose={() => setIsCommandPaletteOpen(false)}
|
|
||||||
setIsOpen={setIsCommandPaletteOpen}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -20,19 +20,19 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
|
|||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
function findConnectorIcon(item: any) {
|
function findConnectorIcon(item: any) {
|
||||||
const id = item?._source?.source?.id || "";
|
const id = item?.source?.id || "";
|
||||||
|
|
||||||
const result_source = datasourceData[endpoint_http]?.find(
|
const result_source = datasourceData[endpoint_http]?.find(
|
||||||
(data: any) => data._source.id === id
|
(data: any) => data.id === id
|
||||||
);
|
);
|
||||||
|
|
||||||
const connector_id = result_source?._source?.connector?.id;
|
const connector_id = result_source?.connector?.id;
|
||||||
|
|
||||||
const result_connector = connector_data[endpoint_http]?.find(
|
const result_connector = connector_data[endpoint_http]?.find(
|
||||||
(data: any) => data._source.id === connector_id
|
(data: any) => data.id === connector_id
|
||||||
);
|
);
|
||||||
|
|
||||||
return result_connector?._source;
|
return result_connector;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTypeIcon(item: any) {
|
function getTypeIcon(item: any) {
|
||||||
@@ -58,7 +58,7 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
|
|||||||
|
|
||||||
{/* <div className="mb-4">
|
{/* <div className="mb-4">
|
||||||
<iframe
|
<iframe
|
||||||
src={document?._source?.metadata?.web_view_link}
|
src={document?.metadata?.web_view_link}
|
||||||
style={{ width: "100%", height: "500px" }}
|
style={{ width: "100%", height: "500px" }}
|
||||||
title="Text Preview"
|
title="Text Preview"
|
||||||
/>
|
/>
|
||||||
@@ -74,7 +74,7 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
|
|||||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||||
<div className="text-[#666]">Name</div>
|
<div className="text-[#666]">Name</div>
|
||||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-60 break-words">
|
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-60 break-words">
|
||||||
{document?._source?.title || "-"}
|
{document?.title || "-"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
|
|||||||
src={getTypeIcon(document)}
|
src={getTypeIcon(document)}
|
||||||
alt="icon"
|
alt="icon"
|
||||||
/>
|
/>
|
||||||
{document?._source?.source?.name || "-"}
|
{document?.source?.name || "-"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="flex justify-between font-normal text-xs mb-2.5">
|
{/* <div className="flex justify-between font-normal text-xs mb-2.5">
|
||||||
@@ -95,43 +95,43 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
|
|||||||
-
|
-
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
{document?._source?.updated ? (
|
{document?.updated ? (
|
||||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||||
<div className="text-[#666]">Updated at</div>
|
<div className="text-[#666]">Updated at</div>
|
||||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||||
{document?._source?.updated || "-"}
|
{document?.updated || "-"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{document?._source?.last_updated_by?.user?.username ? (
|
{document?.last_updated_by?.user?.username ? (
|
||||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||||
<div className="text-[#666]">Update by</div>
|
<div className="text-[#666]">Update by</div>
|
||||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||||
{document?._source?.last_updated_by?.user?.username || "-"}
|
{document?.last_updated_by?.user?.username || "-"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{document?._source?.owner?.username ? (
|
{document?.owner?.username ? (
|
||||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||||
<div className="text-[#666]">Created by</div>
|
<div className="text-[#666]">Created by</div>
|
||||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||||
{document?._source?.owner?.username || "-"}
|
{document?.owner?.username || "-"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{document?._source?.type ? (
|
{document?.type ? (
|
||||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||||
<div className="text-[#666]">Type</div>
|
<div className="text-[#666]">Type</div>
|
||||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||||
{document?._source?.type || "-"}
|
{document?.type || "-"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{document?._source?.size ? (
|
{document?.size ? (
|
||||||
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
<div className="flex justify-between flex-wrap font-normal text-xs mb-2.5">
|
||||||
<div className="text-[#666]">Size</div>
|
<div className="text-[#666]">Size</div>
|
||||||
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
<div className="text-[#333] dark:text-[#D8D8D8] text-right w-56 break-words">
|
||||||
{formatter.bytes(document?._source?.size || 0)}
|
{formatter.bytes(document?.size || 0)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import { useInfiniteScroll } from "ahooks";
|
import { useInfiniteScroll } from "ahooks";
|
||||||
import { isTauri } from "@tauri-apps/api/core";
|
import { isTauri, invoke } from "@tauri-apps/api/core";
|
||||||
import { open } from "@tauri-apps/plugin-shell";
|
import { open } from "@tauri-apps/plugin-shell";
|
||||||
|
|
||||||
import { useAppStore } from "@/stores/appStore";
|
|
||||||
import { tauriFetch } from "@/api/tauriFetchClient";
|
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
import { SearchHeader } from "./SearchHeader";
|
import { SearchHeader } from "./SearchHeader";
|
||||||
import file_efault_img from "@/assets/images/file_efault.png";
|
|
||||||
import noDataImg from "@/assets/coconut-tree.png";
|
import noDataImg from "@/assets/coconut-tree.png";
|
||||||
import { useConnectStore } from "@/stores/connectStore";
|
import ItemIcon from "@/components/Common/Icons/ItemIcon";
|
||||||
|
|
||||||
interface DocumentListProps {
|
interface DocumentListProps {
|
||||||
onSelectDocument: (id: string) => void;
|
onSelectDocument: (id: string) => void;
|
||||||
@@ -26,11 +23,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
getDocDetail,
|
getDocDetail,
|
||||||
isChatMode,
|
isChatMode,
|
||||||
}) => {
|
}) => {
|
||||||
const connector_data = useConnectStore((state) => state.connector_data);
|
|
||||||
const datasourceData = useConnectStore((state) => state.datasourceData);
|
|
||||||
|
|
||||||
const sourceData = useSearchStore((state) => state.sourceData);
|
const sourceData = useSearchStore((state) => state.sourceData);
|
||||||
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
|
||||||
|
|
||||||
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
@@ -39,26 +32,30 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
|
|
||||||
const { data, loading } = useInfiniteScroll(
|
const { data, loading } = useInfiniteScroll(
|
||||||
async (d) => {
|
async (d) => {
|
||||||
const page = d ? Math.ceil(d.list.length / PAGE_SIZE) + 1 : 1;
|
const from = d?.list.length || 0;
|
||||||
|
|
||||||
const from = (page - 1) * PAGE_SIZE;
|
let queryStrings: any = {
|
||||||
|
query: input,
|
||||||
|
datasource: sourceData?.source?.id,
|
||||||
|
};
|
||||||
|
|
||||||
let url = `/query/_search?query=${input}&datasource=${sourceData?._source?.source?.id}&from=${from}&size=${PAGE_SIZE}`;
|
if (sourceData?.rich_categories) {
|
||||||
|
queryStrings = {
|
||||||
if (sourceData?._source?.rich_categories) {
|
query: input,
|
||||||
url = `/query/_search?query=${input}&rich_category=${sourceData?._source?.rich_categories[0]?.key}&from=${from}&size=${PAGE_SIZE}`;
|
rich_category: sourceData?.rich_categories[0]?.key,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await tauriFetch({
|
const response: any = await invoke("query_coco_servers", {
|
||||||
url,
|
from: from,
|
||||||
method: "GET",
|
size: PAGE_SIZE,
|
||||||
|
queryStrings,
|
||||||
});
|
});
|
||||||
|
const list = response?.documents || [];
|
||||||
|
const total = response?.total_hits || 0;
|
||||||
|
|
||||||
const list = response.data?.hits?.hits || [];
|
console.log("doc", response?.documents);
|
||||||
const total = response.data?.hits?.total?.value || 0;
|
|
||||||
|
|
||||||
console.log("doc", url, response.data?.hits)
|
|
||||||
|
|
||||||
setTotal(total);
|
setTotal(total);
|
||||||
|
|
||||||
@@ -84,10 +81,11 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const parentRef = containerRef.current;
|
const parentRef = containerRef.current;
|
||||||
if (parentRef && parentRef.childElementCount > 10) {
|
if (parentRef && parentRef.childElementCount > 10) {
|
||||||
const itemHeight = (parentRef.firstChild as HTMLElement)?.offsetHeight || 80;
|
const itemHeight =
|
||||||
|
(parentRef.firstChild as HTMLElement)?.offsetHeight || 80;
|
||||||
parentRef.scrollTo({
|
parentRef.scrollTo({
|
||||||
top: (parentRef.lastChild as HTMLElement)?.offsetTop - itemHeight,
|
top: (parentRef.lastChild as HTMLElement)?.offsetTop - itemHeight,
|
||||||
behavior: 'instant',
|
behavior: "instant",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -104,43 +102,10 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
parentRef.scrollTo({
|
parentRef.scrollTo({
|
||||||
top:
|
top:
|
||||||
parentRef.lastChild?.offsetTop - (data?.list?.length + 1) * itemHeight,
|
parentRef.lastChild?.offsetTop - (data?.list?.length + 1) * itemHeight,
|
||||||
behavior: 'instant',
|
behavior: "instant",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
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 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 onMouseEnter(index: number, item: any) {
|
function onMouseEnter(index: number, item: any) {
|
||||||
getDocDetail(item);
|
getDocDetail(item);
|
||||||
setSelectedItem(index);
|
setSelectedItem(index);
|
||||||
@@ -179,8 +144,8 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
|
|
||||||
if (e.key === "Enter" && selectedItem !== null) {
|
if (e.key === "Enter" && selectedItem !== null) {
|
||||||
const item = data?.list?.[selectedItem];
|
const item = data?.list?.[selectedItem];
|
||||||
if (item?._source?.url) {
|
if (item?.url) {
|
||||||
handleOpenURL(item?._source?.url);
|
handleOpenURL(item?.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -216,13 +181,13 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
const isSelected = selectedItem === index;
|
const isSelected = selectedItem === index;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item._id + index}
|
key={item.id + index}
|
||||||
ref={(el) => (itemRefs.current[index] = el)}
|
ref={(el) => (itemRefs.current[index] = el)}
|
||||||
onMouseEnter={() => onMouseEnter(index, item)}
|
onMouseEnter={() => onMouseEnter(index, item)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item?._source?.url) {
|
if (item?.url) {
|
||||||
handleOpenURL(item?._source?.url);
|
handleOpenURL(item?.url);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`w-full px-2 py-2.5 text-sm flex items-center gap-3 rounded-lg transition-colors cursor-pointer ${
|
className={`w-full px-2 py-2.5 text-sm flex items-center gap-3 rounded-lg transition-colors cursor-pointer ${
|
||||||
isSelected
|
isSelected
|
||||||
@@ -231,17 +196,14 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex gap-2 items-center flex-1 min-w-0">
|
<div className="flex gap-2 items-center flex-1 min-w-0">
|
||||||
<img
|
|
||||||
className="w-5 h-5 flex-shrink-0"
|
<ItemIcon item={item} />
|
||||||
src={getIcon(item)}
|
|
||||||
alt="icon"
|
|
||||||
/>
|
|
||||||
<span
|
<span
|
||||||
className={`text-sm truncate ${
|
className={`text-sm truncate ${
|
||||||
isSelected ? "font-medium" : ""
|
isSelected ? "font-medium" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item?._source?.title}
|
{item?.title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -256,14 +218,14 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
|
|
||||||
{!loading && data?.list.length === 0 && (
|
{!loading && data?.list.length === 0 && (
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="h-full w-full flex flex-col items-center"
|
className="h-full w-full flex flex-col items-center"
|
||||||
>
|
>
|
||||||
<img src={noDataImg} alt="no-data" className="w-16 h-16 mt-24" />
|
<img src={noDataImg} alt="no-data" className="w-16 h-16 mt-24" />
|
||||||
<div className="mt-4 text-sm text-[#999] dark:text-[#666]">
|
<div className="mt-4 text-sm text-[#999] dark:text-[#666]">
|
||||||
No Results
|
No Results
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
245
src/components/Search/DropdownList.tsx
Normal file
245
src/components/Search/DropdownList.tsx
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import { useEffect, useRef, useState, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
CircleAlert,
|
||||||
|
Bolt,
|
||||||
|
X,
|
||||||
|
ArrowBigRight,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { isTauri } from "@tauri-apps/api/core";
|
||||||
|
import { open } from "@tauri-apps/plugin-shell";
|
||||||
|
|
||||||
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
|
import ThemedIcon from "@/components/Common/Icons/ThemedIcon";
|
||||||
|
import IconWrapper from "@/components/Common/Icons/IconWrapper";
|
||||||
|
import TypeIcon from "@/components/Common/Icons/TypeIcon";
|
||||||
|
import ItemIcon from "@/components/Common/Icons/ItemIcon";
|
||||||
|
import ListRight from "./ListRight";
|
||||||
|
|
||||||
|
type ISearchData = Record<string, any[]>;
|
||||||
|
|
||||||
|
interface DropdownListProps {
|
||||||
|
selected: (item: any) => void;
|
||||||
|
suggests: any[];
|
||||||
|
SearchData: ISearchData;
|
||||||
|
IsError: boolean;
|
||||||
|
isSearchComplete: boolean;
|
||||||
|
isChatMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownList({
|
||||||
|
selected,
|
||||||
|
suggests,
|
||||||
|
SearchData,
|
||||||
|
IsError,
|
||||||
|
isChatMode,
|
||||||
|
}: DropdownListProps) {
|
||||||
|
const globalIndex = useRef(0);
|
||||||
|
const globalItemIndexMap = useRef<any[]>([]);
|
||||||
|
|
||||||
|
const setSourceData = useSearchStore((state: { setSourceData: any; }) => state.setSourceData);
|
||||||
|
|
||||||
|
const [showError, setShowError] = useState<boolean>(IsError);
|
||||||
|
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
||||||
|
const [selectedName, setSelectedName] = useState<string>("");
|
||||||
|
const [showIndex, setShowIndex] = useState<boolean>(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (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 = useCallback((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.current[selectedItem];
|
||||||
|
setSelectedName(item?.source?.name);
|
||||||
|
}
|
||||||
|
setShowIndex(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "ArrowRight" && selectedItem !== null) {
|
||||||
|
e.preventDefault();
|
||||||
|
const item = globalItemIndexMap.current[selectedItem];
|
||||||
|
goToTwoPage(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Enter" && selectedItem !== null) {
|
||||||
|
// console.log("Enter key pressed", selectedItem);
|
||||||
|
const item = globalItemIndexMap.current[selectedItem];
|
||||||
|
if (item?.url) {
|
||||||
|
handleOpenURL(item?.url);
|
||||||
|
} else {
|
||||||
|
selected(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key >= "0" && e.key <= "9" && showIndex) {
|
||||||
|
// console.log(`number ${e.key}`);
|
||||||
|
const item = globalItemIndexMap.current[parseInt(e.key, 10)];
|
||||||
|
if (item?.url) {
|
||||||
|
handleOpenURL(item?.url);
|
||||||
|
} else {
|
||||||
|
selected(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [suggests, selectedItem, showIndex, selected, handleOpenURL, globalItemIndexMap]);
|
||||||
|
|
||||||
|
const handleKeyUp = useCallback((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);
|
||||||
|
};
|
||||||
|
}, [handleKeyDown, handleKeyUp]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedItem !== null && itemRefs.current[selectedItem]) {
|
||||||
|
itemRefs.current[selectedItem]?.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "nearest",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [selectedItem]);
|
||||||
|
|
||||||
|
function goToTwoPage(item: any) {
|
||||||
|
setSourceData(item);
|
||||||
|
selected && selected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="h-[458px] w-full p-2 flex flex-col overflow-y-auto custom-scrollbar focus:outline-none"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{showError ? (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-[#333] p-2">
|
||||||
|
<CircleAlert className="text-[#FF0000] w-[14px] h-[14px]" />
|
||||||
|
Coco server is unavailable, only local results and available services
|
||||||
|
are displayed.
|
||||||
|
<Bolt className="text-[#000] w-[14px] h-[14px] cursor-pointer" />
|
||||||
|
<X
|
||||||
|
className="text-[#666] w-[16px] h-[16px] cursor-pointer"
|
||||||
|
onClick={() => setShowError(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{Object.entries(SearchData).map(([sourceName, items]) => (
|
||||||
|
<div key={sourceName}>
|
||||||
|
{Object.entries(SearchData).length < 5 ? (
|
||||||
|
<div className="p-2 text-xs text-[#999] dark:text-[#666] flex items-center gap-2.5 relative">
|
||||||
|
<TypeIcon item={items[0]} className="w-4 h-4" />
|
||||||
|
{sourceName}
|
||||||
|
<div className="flex-1 border-b border-b-[#e6e6e6] dark:border-b-[#272626]"></div>
|
||||||
|
|
||||||
|
<IconWrapper
|
||||||
|
className="w-4 h-4 cursor-pointer"
|
||||||
|
onClick={(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
goToTwoPage(items[0]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThemedIcon component={ArrowBigRight} className="w-4 h-4" />
|
||||||
|
</IconWrapper>
|
||||||
|
|
||||||
|
{showIndex && sourceName === selectedName ? (
|
||||||
|
<div
|
||||||
|
className={`absolute right-2
|
||||||
|
w-4 h-4 flex items-end justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{items.map((item: any, index: number) => {
|
||||||
|
const isSelected = selectedItem === globalIndex.current;
|
||||||
|
const currentIndex = globalIndex.current;
|
||||||
|
globalItemIndexMap.current.push(item);
|
||||||
|
globalIndex.current++;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id + index}
|
||||||
|
ref={(el) => (itemRefs.current[currentIndex] = el)}
|
||||||
|
onMouseEnter={() => setSelectedItem(currentIndex)}
|
||||||
|
onClick={() => {
|
||||||
|
if (item?.url) {
|
||||||
|
handleOpenURL(item?.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]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center justify-start max-w-[450px]">
|
||||||
|
<ItemIcon item={item} />
|
||||||
|
<span
|
||||||
|
className={`text-sm truncate text-left ${isSelected ? "font-medium" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item?.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ListRight goToTwoPage={goToTwoPage} item={item} isSelected={isSelected} showIndex={showIndex} currentIndex={currentIndex}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DropdownList;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
ArrowDown01,
|
ArrowDown01,
|
||||||
AppWindowMac,
|
// AppWindowMac,
|
||||||
CornerDownLeft,
|
CornerDownLeft,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ interface FooterProps {
|
|||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Footer({ name }: FooterProps) {
|
export default function Footer({ }: FooterProps) {
|
||||||
const sourceData = useSearchStore((state) => state.sourceData);
|
const sourceData = useSearchStore((state) => state.sourceData);
|
||||||
|
|
||||||
const connector_data = useConnectStore((state) => state.connector_data);
|
const connector_data = useConnectStore((state) => state.connector_data);
|
||||||
@@ -29,19 +29,19 @@ export default function Footer({ name }: FooterProps) {
|
|||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
function findConnectorIcon(item: any) {
|
function findConnectorIcon(item: any) {
|
||||||
const id = item?._source?.source?.id || "";
|
const id = item?.source?.id || "";
|
||||||
|
|
||||||
const result_source = datasourceData[endpoint_http]?.find(
|
const result_source = datasourceData[endpoint_http]?.find(
|
||||||
(data: any) => data._source.id === id
|
(data: any) => data.id === id
|
||||||
);
|
);
|
||||||
|
|
||||||
const connector_id = result_source?._source?.connector?.id;
|
const connector_id = result_source?.connector?.id;
|
||||||
|
|
||||||
const result_connector = connector_data[endpoint_http]?.find(
|
const result_connector = connector_data[endpoint_http]?.find(
|
||||||
(data: any) => data._source.id === connector_id
|
(data: any) => data.id === connector_id
|
||||||
);
|
);
|
||||||
|
|
||||||
return result_connector?._source;
|
return result_connector;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTypeIcon(item: any) {
|
function getTypeIcon(item: any) {
|
||||||
@@ -66,21 +66,21 @@ export default function Footer({ name }: FooterProps) {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{sourceData?._source?.source?.name ? (
|
{sourceData?.source?.name ? (
|
||||||
<img className="w-5 h-5" src={getTypeIcon(sourceData)} alt="icon" />
|
<img className="w-5 h-5" src={getTypeIcon(sourceData)} alt="icon" />
|
||||||
) : (
|
) : (
|
||||||
<img src={logoImg} className="w-5 h-5" />
|
<img src={logoImg} className="w-5 h-5" />
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{sourceData?._source?.source?.name || "Version 1.0.0"}
|
{sourceData?.source?.name || "Version 1.0.0"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{name ? (
|
{/* {name ? (
|
||||||
<div className="flex gap-2 items-center text-[#666] text-xs">
|
<div className="flex gap-2 items-center text-[#666] text-xs">
|
||||||
<AppWindowMac className="w-5 h-5" /> {name}
|
<AppWindowMac className="w-5 h-5" /> {name}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null} */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -5,18 +5,19 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
AudioLines,
|
AudioLines,
|
||||||
Image,
|
Image,
|
||||||
SquareArrowLeft,
|
ArrowBigLeft,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useRef, useState, useEffect, useCallback } from "react";
|
import { useRef, useState, useEffect, useCallback } from "react";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { isTauri } from "@tauri-apps/api/core";
|
import { isTauri } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
import ChatSwitch from "../SearchChat/ChatSwitch";
|
import ChatSwitch from "@/components/Common/ChatSwitch";
|
||||||
import AutoResizeTextarea from "./AutoResizeTextarea";
|
import AutoResizeTextarea from "./AutoResizeTextarea";
|
||||||
import { useChatStore } from "@/stores/chatStore";
|
import { useChatStore } from "@/stores/chatStore";
|
||||||
import StopIcon from "@/icons/Stop";
|
import StopIcon from "@/icons/Stop";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
|
import { isMac } from "@/utils/platform";
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
onSend: (message: string) => void;
|
onSend: (message: string) => void;
|
||||||
@@ -39,10 +40,10 @@ export default function ChatInput({
|
|||||||
disabledChange,
|
disabledChange,
|
||||||
reconnect,
|
reconnect,
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const showTooltip = useAppStore((state) => state.showTooltip);
|
const showTooltip = useAppStore((state: { showTooltip: boolean }) => state.showTooltip);
|
||||||
|
|
||||||
const sourceData = useSearchStore((state) => state.sourceData);
|
const sourceData = useSearchStore((state: { sourceData: any; }) => state.sourceData);
|
||||||
const setSourceData = useSearchStore((state) => state.setSourceData);
|
const setSourceData = useSearchStore((state: { setSourceData: any; }) => state.setSourceData);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSourceData(undefined);
|
setSourceData(undefined);
|
||||||
@@ -76,11 +77,13 @@ export default function ChatInput({
|
|||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
pressedKeys.add(e.code);
|
pressedKeys.add(e.code);
|
||||||
|
|
||||||
if (e.code === "MetaLeft" || e.code === "MetaRight") {
|
if ((isMac && (e.code === "MetaLeft" || e.code === "MetaRight")) ||
|
||||||
|
(!isMac && (e.code === "ControlLeft" || e.code === "ControlRight"))) {
|
||||||
setIsCommandPressed(true);
|
setIsCommandPressed(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pressedKeys.has("MetaLeft") || pressedKeys.has("MetaRight")) {
|
if ((isMac && (pressedKeys.has("MetaLeft") || pressedKeys.has("MetaRight"))) ||
|
||||||
|
(!isMac && (pressedKeys.has("ControlLeft") || pressedKeys.has("ControlRight")))) {
|
||||||
// e.preventDefault();
|
// e.preventDefault();
|
||||||
switch (e.code) {
|
switch (e.code) {
|
||||||
case "Comma":
|
case "Comma":
|
||||||
@@ -128,7 +131,8 @@ export default function ChatInput({
|
|||||||
|
|
||||||
const handleKeyUp = useCallback((e: KeyboardEvent) => {
|
const handleKeyUp = useCallback((e: KeyboardEvent) => {
|
||||||
pressedKeys.delete(e.code);
|
pressedKeys.delete(e.code);
|
||||||
if (e.code === "MetaLeft" || e.code === "MetaRight") {
|
if ((isMac && (e.code === "MetaLeft" || e.code === "MetaRight")) ||
|
||||||
|
(!isMac && (e.code === "ControlLeft" || e.code === "ControlRight"))) {
|
||||||
setIsCommandPressed(false);
|
setIsCommandPressed(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -198,7 +202,7 @@ export default function ChatInput({
|
|||||||
<div className="p-2 flex items-center dark:text-[#D8D8D8] bg-[#ededed] dark:bg-[#202126] rounded transition-all relative">
|
<div className="p-2 flex items-center dark:text-[#D8D8D8] bg-[#ededed] dark:bg-[#202126] rounded transition-all relative">
|
||||||
<div className="flex flex-wrap gap-2 flex-1 items-center relative">
|
<div className="flex flex-wrap gap-2 flex-1 items-center relative">
|
||||||
{!isChatMode && sourceData ? (
|
{!isChatMode && sourceData ? (
|
||||||
<SquareArrowLeft
|
<ArrowBigLeft
|
||||||
className="w-4 h-4 text-[#000] dark:text-[#d8d8d8] cursor-pointer"
|
className="w-4 h-4 text-[#000] dark:text-[#d8d8d8] cursor-pointer"
|
||||||
onClick={() => setSourceData(undefined)}
|
onClick={() => setSourceData(undefined)}
|
||||||
/>
|
/>
|
||||||
@@ -242,9 +246,8 @@ export default function ChatInput({
|
|||||||
) : null}
|
) : null}
|
||||||
{showTooltip && isCommandPressed ? (
|
{showTooltip && isCommandPressed ? (
|
||||||
<div
|
<div
|
||||||
className={`absolute ${
|
className={`absolute ${!isChatMode && sourceData ? "left-7" : ""
|
||||||
!isChatMode && sourceData ? "left-7" : ""
|
} w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#ededed] dark:shadow-[-6px_0px_6px_2px_#202126]`}
|
||||||
} w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#ededed] dark:shadow-[-6px_0px_6px_2px_#202126]`}
|
|
||||||
>
|
>
|
||||||
I
|
I
|
||||||
</div>
|
</div>
|
||||||
@@ -262,11 +265,10 @@ export default function ChatInput({
|
|||||||
|
|
||||||
{isChatMode && curChatEnd ? (
|
{isChatMode && curChatEnd ? (
|
||||||
<button
|
<button
|
||||||
className={`ml-1 p-1 ${
|
className={`ml-1 p-1 ${inputValue
|
||||||
inputValue
|
? "bg-[#0072FF]"
|
||||||
? "bg-[#0072FF]"
|
: "bg-[#E4E5F0] dark:bg-[rgb(84,84,84)]"
|
||||||
: "bg-[#E4E5F0] dark:bg-[rgb(84,84,84)]"
|
} rounded-full transition-colors`}
|
||||||
} rounded-full transition-colors`}
|
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => onSend(inputValue.trim())}
|
onClick={() => onSend(inputValue.trim())}
|
||||||
>
|
>
|
||||||
@@ -386,7 +388,7 @@ export default function ChatInput({
|
|||||||
) : null}
|
) : null}
|
||||||
<ChatSwitch
|
<ChatSwitch
|
||||||
isChatMode={isChatMode}
|
isChatMode={isChatMode}
|
||||||
onChange={(value) => {
|
onChange={(value: boolean) => {
|
||||||
value && disabledChange();
|
value && disabledChange();
|
||||||
changeMode(value);
|
changeMode(value);
|
||||||
setSourceData(undefined);
|
setSourceData(undefined);
|
||||||
111
src/components/Search/ListRight.tsx
Normal file
111
src/components/Search/ListRight.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import TypeIcon from "@/components/Common/Icons/TypeIcon";
|
||||||
|
import RichIcon from "@/components/Common/Icons/RichIcon";
|
||||||
|
|
||||||
|
interface ListRightProps {
|
||||||
|
item: any;
|
||||||
|
isSelected: boolean;
|
||||||
|
showIndex: boolean;
|
||||||
|
currentIndex: number;
|
||||||
|
goToTwoPage: (item: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ListRight({
|
||||||
|
item,
|
||||||
|
isSelected,
|
||||||
|
showIndex,
|
||||||
|
currentIndex,
|
||||||
|
goToTwoPage
|
||||||
|
}: ListRightProps) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 text-right min-w-[160px] h-full pl-5 text-[12px] flex gap-2 items-center justify-end relative">
|
||||||
|
{item?.rich_categories ? null : (
|
||||||
|
<div
|
||||||
|
className="w-4 h-4 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
goToTwoPage(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TypeIcon
|
||||||
|
item={item}
|
||||||
|
className="w-4 h-4 cursor-pointer"
|
||||||
|
onClick={(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
goToTwoPage(item);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{item?.rich_categories ? (
|
||||||
|
<div className="flex items-center justify-end max-w-[calc(100%-20px)] whitespace-nowrap">
|
||||||
|
<RichIcon
|
||||||
|
item={item}
|
||||||
|
className="w-4 h-4 mr-2 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
goToTwoPage(item);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`${isSelected ? "text-[#C8C8C8]" : "text-[#666]"} text-right truncate`}
|
||||||
|
>
|
||||||
|
{item?.rich_categories?.map(
|
||||||
|
(rich_item: any, index: number) => {
|
||||||
|
if (
|
||||||
|
item?.rich_categories.length > 2 &&
|
||||||
|
index === item?.rich_categories.length - 1
|
||||||
|
)
|
||||||
|
return "";
|
||||||
|
return (index !== 0 ? "/" : "") + rich_item?.label;
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{item?.rich_categories.length > 2 ? (
|
||||||
|
<span className={`${isSelected ? "text-[#C8C8C8]" : "text-[#666]"} text-right truncate`}>
|
||||||
|
{"/" + item?.rich_categories?.at(-1)?.label}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : item?.category || item?.subcategory ? (
|
||||||
|
<span
|
||||||
|
className={`text-[12px] truncate ${isSelected ? "text-[#DCDCDC]" : "text-[#999] dark:text-[#666]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{(item?.category || "") + (item?.subcategory ? `/${item?.subcategory}` : "")}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className={`text-[12px] truncate ${isSelected ? "text-[#DCDCDC]" : "text-[#999] dark:text-[#666]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item?.last_updated_by?.user?.username || item?.owner?.username || item?.updated || item?.created || item?.type || ""}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSelected ? (
|
||||||
|
<div
|
||||||
|
className={`absolute ${showIndex && currentIndex < 10 ? "right-7" : "right-0"
|
||||||
|
} w-4 h-4 flex items-end justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md ${isSelected
|
||||||
|
? "shadow-[-6px_0px_6px_2px_#950599]"
|
||||||
|
: "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
↩︎
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showIndex && currentIndex < 10 ? (
|
||||||
|
<div
|
||||||
|
className={`absolute right-0 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md ${isSelected
|
||||||
|
? "shadow-[-6px_0px_6px_2px_#950599]"
|
||||||
|
: "shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{currentIndex}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import { useEffect, useState, useCallback, useRef } from "react";
|
import { useEffect, useState, useCallback, useRef } from "react";
|
||||||
import { Command } from "lucide-react";
|
import { Command } from "lucide-react";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
// import { isTauri } from "@tauri-apps/api/core";
|
// import { isTauri } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
import DropdownList from "./DropdownList";
|
import DropdownList from "./DropdownList";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
import { tauriFetch } from "@/api/tauriFetchClient";
|
|
||||||
import noDataImg from "@/assets/coconut-tree.png";
|
import noDataImg from "@/assets/coconut-tree.png";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
|
||||||
// import { res_search2 } from "@/mock/index";
|
// import { res_search2 } from "@/mock/index";
|
||||||
import { SearchResults } from "../SearchChat/SearchResults";
|
import { SearchResults } from "@/components/Search/SearchResults";
|
||||||
import { useSearchStore } from "@/stores/searchStore";
|
import { useSearchStore } from "@/stores/searchStore";
|
||||||
|
|
||||||
interface SearchProps {
|
interface SearchProps {
|
||||||
@@ -19,8 +17,6 @@ interface SearchProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Search({ isChatMode, input }: SearchProps) {
|
function Search({ isChatMode, input }: SearchProps) {
|
||||||
const appStore = useAppStore();
|
|
||||||
|
|
||||||
const sourceData = useSearchStore((state) => state.sourceData);
|
const sourceData = useSearchStore((state) => state.sourceData);
|
||||||
|
|
||||||
const [IsError, setIsError] = useState<boolean>(false);
|
const [IsError, setIsError] = useState<boolean>(false);
|
||||||
@@ -76,17 +72,20 @@ function Search({ isChatMode, input }: SearchProps) {
|
|||||||
// return;
|
// return;
|
||||||
//
|
//
|
||||||
try {
|
try {
|
||||||
const response = await tauriFetch({
|
// const response = await tauriFetch({
|
||||||
url: `/query/_search?query=${input}`,
|
// url: `/query/_search?query=${input}`,
|
||||||
method: "GET",
|
// method: "GET",
|
||||||
baseURL: appStore.endpoint_http,
|
// baseURL: appStore.endpoint_http,
|
||||||
});
|
// });
|
||||||
|
|
||||||
|
const response: any = await invoke("query_coco_servers", { from: 0, size: 10, queryStrings: { query: input } });
|
||||||
|
// failed_coco_servers documents
|
||||||
|
|
||||||
console.log("_suggest", input, response);
|
console.log("_suggest", input, response);
|
||||||
let data = response.data?.hits?.hits || [];
|
let data = response?.documents || [];
|
||||||
setSuggests(data);
|
setSuggests(data);
|
||||||
const search_data = data.reduce((acc: any, item: any) => {
|
const search_data = data.reduce((acc: any, item: any) => {
|
||||||
const name = item?._source?.source?.name;
|
const name = item?.source?.name;
|
||||||
if (!acc[name]) {
|
if (!acc[name]) {
|
||||||
acc[name] = [];
|
acc[name] = [];
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { DocumentList } from "./DocumentList";
|
import { DocumentList } from "./DocumentList";
|
||||||
import { DocumentDetail } from "./DocumentDetail";
|
import { DocumentDetail } from "./DocumentDetail";
|
||||||
|
|
||||||
@@ -21,16 +22,16 @@ export function SearchResults({ input, isChatMode }: SearchResultsProps) {
|
|||||||
<div className="h-full flex">
|
<div className="h-full flex">
|
||||||
{/* Left Panel */}
|
{/* Left Panel */}
|
||||||
<DocumentList
|
<DocumentList
|
||||||
onSelectDocument={setSelectedDocumentId}
|
onSelectDocument={setSelectedDocumentId}
|
||||||
selectedId={selectedDocumentId}
|
selectedId={selectedDocumentId}
|
||||||
input={input}
|
input={input}
|
||||||
getDocDetail={getDocDetail}
|
getDocDetail={getDocDetail}
|
||||||
isChatMode={isChatMode}
|
isChatMode={isChatMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Right Panel */}
|
{/* Right Panel */}
|
||||||
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
||||||
<DocumentDetail document={detailData}/>
|
<DocumentDetail document={detailData} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Globe, Github } from "lucide-react";
|
import { Globe, Github } from "lucide-react";
|
||||||
|
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { OpenBrowserURL } from "@/utils";
|
import { OpenURLWithBrowser } from "@/utils";
|
||||||
import logoLight from "@/assets/images/logo-text-light.svg";
|
import logoLight from "@/assets/images/logo-text-light.svg";
|
||||||
import logoDark from "@/assets/images/logo-text-dark.svg";
|
import logoDark from "@/assets/images/logo-text-dark.svg";
|
||||||
|
|
||||||
@@ -20,8 +20,8 @@ export default function AboutView(){
|
|||||||
Search, Connect, Collaborate—All in one place
|
Search, Connect, Collaborate—All in one place
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center items-center mt-10">
|
<div className="flex justify-center items-center mt-10">
|
||||||
<button onClick={() => OpenBrowserURL('https://coco.rs')} className="w-6 h-6 mr-2.5 flex justify-center rounded-[6px] border-[1px] gray-200 dark:border-gray-700"><Globe className="w-3 text-blue-500"/></button>
|
<button onClick={() => OpenURLWithBrowser('https://coco.rs')} className="w-6 h-6 mr-2.5 flex justify-center rounded-[6px] border-[1px] gray-200 dark:border-gray-700"><Globe className="w-3 text-blue-500"/></button>
|
||||||
<button onClick={() => OpenBrowserURL('https://github.com/infinilabs/coco-app')} className="w-6 h-6 flex justify-center rounded-[6px] border-[1px] gray-200 dark:border-gray-700" ><Github className="w-3 text-blue-500"/></button>
|
<button onClick={() => OpenURLWithBrowser('https://github.com/infinilabs/coco-app')} className="w-6 h-6 flex justify-center rounded-[6px] border-[1px] gray-200 dark:border-gray-700" ><Github className="w-3 text-blue-500"/></button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
<div className="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Version 1.0.0
|
Version 1.0.0
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
import { listen } from "@tauri-apps/api/event";
|
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
||||||
import * as shell from "@tauri-apps/plugin-shell";
|
|
||||||
|
|
||||||
import { useAppStore } from "@/stores/appStore";
|
|
||||||
import { useAuthStore } from "@/stores/authStore";
|
|
||||||
import { OpenBrowserURL } from "@/utils/index";
|
|
||||||
import logoImg from "@/assets/32x32.png";
|
|
||||||
import callbackTemplate from "@/components/Auth/callback.template";
|
|
||||||
import { clientEnv } from "@/utils/env";
|
|
||||||
|
|
||||||
export default function Account() {
|
|
||||||
const app_uid = useAppStore((state) => state.app_uid);
|
|
||||||
const setAppUid = useAppStore((state) => state.setAppUid);
|
|
||||||
const endpoint_http = useAppStore((state) => state.endpoint_http);
|
|
||||||
|
|
||||||
const { auth, setAuth } = useAuthStore();
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let unsubscribe: (() => void) | undefined;
|
|
||||||
|
|
||||||
const setupAuthListener = async () => {
|
|
||||||
try {
|
|
||||||
if (!(auth && auth[endpoint_http])) {
|
|
||||||
// Replace the current route with signin
|
|
||||||
// navigate("/signin", { replace: true });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to set up auth listener:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setupAuthListener();
|
|
||||||
|
|
||||||
// Clean up logic on unmount
|
|
||||||
return () => {
|
|
||||||
const cleanup = async () => {
|
|
||||||
try {
|
|
||||||
await invoke("plugin:oauth|stop");
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore errors if no server is running
|
|
||||||
}
|
|
||||||
if (unsubscribe) {
|
|
||||||
unsubscribe();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cleanup();
|
|
||||||
};
|
|
||||||
}, [JSON.stringify(auth)]);
|
|
||||||
|
|
||||||
async function signIn() {
|
|
||||||
let res: (url: URL) => void;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stopListening = await listen(
|
|
||||||
"oauth://url",
|
|
||||||
(data: { payload: string }) => {
|
|
||||||
if (!data.payload.includes("token")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlObject = new URL(data.payload);
|
|
||||||
res(urlObject);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Stop any existing OAuth server first
|
|
||||||
try {
|
|
||||||
await invoke("plugin:oauth|stop");
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore errors if no server is running
|
|
||||||
}
|
|
||||||
|
|
||||||
const port: string = await invoke("plugin:oauth|start", {
|
|
||||||
config: {
|
|
||||||
response: callbackTemplate,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "text/html; charset=utf-8",
|
|
||||||
"Cache-Control": "no-store, no-cache, must-revalidate",
|
|
||||||
Pragma: "no-cache",
|
|
||||||
},
|
|
||||||
// Add a cleanup function to stop the server after handling the request
|
|
||||||
cleanup: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await shell.open(
|
|
||||||
`${endpoint_http || clientEnv.COCO_SERVER_URL}/api/desktop/session/request?port=${port}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const url = await new Promise<URL>((r) => {
|
|
||||||
res = r;
|
|
||||||
});
|
|
||||||
stopListening();
|
|
||||||
|
|
||||||
const token = url.searchParams.get("token");
|
|
||||||
const user_id = url.searchParams.get("user_id");
|
|
||||||
const expires = Number(url.searchParams.get("expires"));
|
|
||||||
if (!token || !expires || !user_id) {
|
|
||||||
throw new Error("Invalid token or expires");
|
|
||||||
}
|
|
||||||
|
|
||||||
await setAuth({
|
|
||||||
token,
|
|
||||||
user_id,
|
|
||||||
expires,
|
|
||||||
plan: { upgraded: false, last_checked: 0 },
|
|
||||||
}, endpoint_http);
|
|
||||||
|
|
||||||
getCurrentWindow()
|
|
||||||
.setFocus()
|
|
||||||
.catch(() => {});
|
|
||||||
|
|
||||||
return navigate("/");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Sign in failed:", error);
|
|
||||||
await setAuth(undefined, endpoint_http);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initializeUser() {
|
|
||||||
let uid = app_uid;
|
|
||||||
if (!uid) {
|
|
||||||
uid = uuidv4();
|
|
||||||
setAppUid(uid);
|
|
||||||
}
|
|
||||||
// const response = await fetch("/api/register", {
|
|
||||||
// method: "POST",
|
|
||||||
// headers: { "Content-Type": "application/json" },
|
|
||||||
// body: JSON.stringify({ uid }),
|
|
||||||
// });
|
|
||||||
// const { token } = await response.json();
|
|
||||||
// localStorage.setItem("auth_token", token);
|
|
||||||
OpenBrowserURL(`http://localhost:1420/login?uid=${uid}`);
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
await signIn();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function LoginClick(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
|
|
||||||
event.preventDefault();
|
|
||||||
initializeUser();
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="h-[450px] bg-gradient-to-br from-purple-100 via-purple-200 to-gray-200 dark:from-gray-800 dark:via-gray-900 dark:to-black flex flex-col items-center justify-center p-6">
|
|
||||||
<div className="w-full max-w-md flex flex-col items-center text-center space-y-8">
|
|
||||||
<div className="animate-pulse">
|
|
||||||
<img
|
|
||||||
src={logoImg}
|
|
||||||
alt="logo"
|
|
||||||
className="w-12 h-12 text-red-500 dark:text-red-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h1 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
Get Started
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 text-sm leading-relaxed max-w-sm dark:text-gray-300">
|
|
||||||
You need to log in or create an account to view your organizations,
|
|
||||||
manage your custom extensions.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-10 flex items-center justify-center gap-x-6">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
|
||||||
>
|
|
||||||
Sign Up
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="text-sm/6 font-semibold text-gray-900 dark:text-gray-100"
|
|
||||||
onClick={LoginClick}
|
|
||||||
>
|
|
||||||
{loading ? "Signing In..." : "Sign In"}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -20,11 +20,45 @@ import SettingsToggle from "./SettingsToggle";
|
|||||||
import { ShortcutItem } from "./ShortcutItem";
|
import { ShortcutItem } from "./ShortcutItem";
|
||||||
import { Shortcut } from "./shortcut";
|
import { Shortcut } from "./shortcut";
|
||||||
import { useShortcutEditor } from "@/hooks/useShortcutEditor";
|
import { useShortcutEditor } from "@/hooks/useShortcutEditor";
|
||||||
import { ThemeOption } from "./index2";
|
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
|
import {AppTheme} from "@/utils/tauri.ts";
|
||||||
|
import {useTheme} from "@/contexts/ThemeContext.tsx";
|
||||||
// import { useAuthStore } from "@/stores/authStore";
|
// import { useAuthStore } from "@/stores/authStore";
|
||||||
// import { useConnectStore } from "@/stores/connectStore";
|
// import { useConnectStore } from "@/stores/connectStore";
|
||||||
|
|
||||||
|
|
||||||
|
export function ThemeOption({
|
||||||
|
icon: Icon,
|
||||||
|
title,
|
||||||
|
theme,
|
||||||
|
}: {
|
||||||
|
icon: any;
|
||||||
|
title: string;
|
||||||
|
theme: AppTheme;
|
||||||
|
}) {
|
||||||
|
const { theme: currentTheme, changeTheme } = useTheme();
|
||||||
|
|
||||||
|
const isSelected = currentTheme === theme;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => changeTheme(theme)}
|
||||||
|
className={`p-4 rounded-lg border-2 ${
|
||||||
|
isSelected
|
||||||
|
? "border-blue-500 bg-blue-50 dark:bg-blue-900/20"
|
||||||
|
: "border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
|
||||||
|
} flex flex-col items-center justify-center space-y-2 transition-all`}
|
||||||
|
>
|
||||||
|
<Icon className={`w-6 h-6 ${isSelected ? "text-blue-500" : ""}`} />
|
||||||
|
<span
|
||||||
|
className={`text-sm font-medium ${isSelected ? "text-blue-500" : ""}`}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function GeneralSettings() {
|
export default function GeneralSettings() {
|
||||||
const [launchAtLogin, setLaunchAtLogin] = useState(true);
|
const [launchAtLogin, setLaunchAtLogin] = useState(true);
|
||||||
|
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import {
|
|
||||||
Settings,
|
|
||||||
Search,
|
|
||||||
Command,
|
|
||||||
Keyboard,
|
|
||||||
Globe,
|
|
||||||
Zap,
|
|
||||||
ChevronRight,
|
|
||||||
Moon,
|
|
||||||
Sun,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
function NavItem({ icon: Icon, label, active, onClick }: any) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
className={`w-full flex items-center px-3 py-2 rounded-lg text-sm ${
|
|
||||||
active
|
|
||||||
? "bg-indigo-50 text-indigo-600"
|
|
||||||
: "text-gray-700 hover:bg-gray-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon className="w-4 h-4 mr-3" />
|
|
||||||
<span className="font-medium">{label}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SettingItem({ icon: Icon, title, description, action }: any) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between py-4 border-b border-gray-100">
|
|
||||||
<div className="flex items-start space-x-3">
|
|
||||||
<div className="p-2 bg-gray-50 rounded-lg">
|
|
||||||
<Icon className="w-5 h-5 text-gray-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-medium text-gray-900">{title}</h3>
|
|
||||||
<p className="text-sm text-gray-500">{description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{action}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AppSettings() {
|
|
||||||
const [activeSection, setActiveSection] = useState("general");
|
|
||||||
const [darkMode, setDarkMode] = useState(false);
|
|
||||||
|
|
||||||
const sections = [
|
|
||||||
{ id: "general", label: "General", icon: Settings },
|
|
||||||
{ id: "appearance", label: "Appearance", icon: Search },
|
|
||||||
{ id: "extensions", label: "Extensions", icon: Command },
|
|
||||||
{ id: "keyboard", label: "Keyboard", icon: Keyboard },
|
|
||||||
{ id: "advanced", label: "Advanced", icon: Zap },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
|
||||||
<div className="bg-white rounded-2xl border border-gray-200 flex min-h-[500px]">
|
|
||||||
<div className="w-64 p-4 border-r border-gray-100">
|
|
||||||
<div className="space-y-1">
|
|
||||||
{sections.map((section) => (
|
|
||||||
<NavItem
|
|
||||||
key={section.id}
|
|
||||||
icon={section.icon}
|
|
||||||
label={section.label}
|
|
||||||
active={activeSection === section.id}
|
|
||||||
onClick={() => setActiveSection(section.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<div className="flex-1 p-6">
|
|
||||||
<div className="max-w-3xl">
|
|
||||||
<h1 className="text-2xl font-semibold text-gray-900 mb-6">
|
|
||||||
Settings
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
<SettingItem
|
|
||||||
icon={Globe}
|
|
||||||
title="Language"
|
|
||||||
description="Choose your preferred language for the interface"
|
|
||||||
action={
|
|
||||||
<button className="flex items-center text-sm text-gray-600 hover:text-gray-900">
|
|
||||||
English
|
|
||||||
<ChevronRight className="w-4 h-4 ml-1" />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingItem
|
|
||||||
icon={darkMode ? Moon : Sun}
|
|
||||||
title="Appearance"
|
|
||||||
description="Switch between light and dark mode"
|
|
||||||
action={
|
|
||||||
<button
|
|
||||||
onClick={() => setDarkMode(!darkMode)}
|
|
||||||
className="relative inline-flex h-6 w-11 items-center rounded-full bg-gray-200 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`${
|
|
||||||
darkMode ? "translate-x-6" : "translate-x-1"
|
|
||||||
} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingItem
|
|
||||||
icon={Command}
|
|
||||||
title="Keyboard Shortcuts"
|
|
||||||
description="Customize your keyboard shortcuts"
|
|
||||||
action={
|
|
||||||
<button className="px-3 py-1 text-sm text-gray-600 hover:text-gray-900 border border-gray-200 rounded-md hover:border-gray-300">
|
|
||||||
Configure
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingItem
|
|
||||||
icon={Zap}
|
|
||||||
title="Performance Mode"
|
|
||||||
description="Optimize for better performance"
|
|
||||||
action={
|
|
||||||
<button className="relative inline-flex h-6 w-11 items-center rounded-full bg-indigo-600 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
|
|
||||||
<span className="translate-x-6 inline-block h-4 w-4 transform rounded-full bg-white transition-transform" />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AppSettings;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user