mirror of
https://github.com/coaidev/coai.git
synced 2025-05-28 09:20:18 +09:00
feat: channel alpha
This commit is contained in:
parent
7e7798d213
commit
db7acee643
@ -17,6 +17,7 @@
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-progress": "^1.0.3",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
@ -31,6 +32,7 @@
|
||||
"chart.js": "^4.4.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"i18next": "^23.4.6",
|
||||
"localforage": "^1.10.0",
|
||||
"lucide-react": "^0.289.0",
|
||||
|
334
app/pnpm-lock.yaml
generated
334
app/pnpm-lock.yaml
generated
@ -23,6 +23,9 @@ dependencies:
|
||||
'@radix-ui/react-label':
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-popover':
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-progress':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
@ -65,6 +68,9 @@ dependencies:
|
||||
clsx:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
cmdk:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
i18next:
|
||||
specifier: ^23.4.6
|
||||
version: 23.6.0
|
||||
@ -549,6 +555,12 @@ packages:
|
||||
'@babel/runtime': 7.23.2
|
||||
dev: false
|
||||
|
||||
/@radix-ui/primitive@1.0.0:
|
||||
resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
dev: false
|
||||
|
||||
/@radix-ui/primitive@1.0.1:
|
||||
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
|
||||
dependencies:
|
||||
@ -654,6 +666,15 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-compose-refs@1.0.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.33)(react@18.2.0):
|
||||
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
|
||||
peerDependencies:
|
||||
@ -694,6 +715,15 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-context@1.0.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-context@1.0.1(@types/react@18.2.33)(react@18.2.0):
|
||||
resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
|
||||
peerDependencies:
|
||||
@ -708,6 +738,33 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-dialog@1.0.0(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
'@radix-ui/primitive': 1.0.0
|
||||
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.0.0(react@18.2.0)
|
||||
'@radix-ui/react-dismissable-layer': 1.0.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-focus-guards': 1.0.0(react@18.2.0)
|
||||
'@radix-ui/react-focus-scope': 1.0.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-id': 1.0.0(react@18.2.0)
|
||||
'@radix-ui/react-portal': 1.0.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-presence': 1.0.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-slot': 1.0.0(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.0.0(react@18.2.0)
|
||||
aria-hidden: 1.2.3
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-remove-scroll: 2.5.4(@types/react@18.2.33)(react@18.2.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-dialog@1.0.5(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
|
||||
peerDependencies:
|
||||
@ -756,6 +813,22 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-dismissable-layer@1.0.0(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
'@radix-ui/primitive': 1.0.0
|
||||
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
|
||||
'@radix-ui/react-use-escape-keydown': 1.0.0(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==}
|
||||
peerDependencies:
|
||||
@ -808,6 +881,15 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-focus-guards@1.0.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.33)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
|
||||
peerDependencies:
|
||||
@ -822,6 +904,20 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-focus-scope@1.0.0(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==}
|
||||
peerDependencies:
|
||||
@ -845,6 +941,16 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-id@1.0.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
'@radix-ui/react-use-layout-effect': 1.0.0(react@18.2.0)
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-id@1.0.1(@types/react@18.2.33)(react@18.2.0):
|
||||
resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==}
|
||||
peerDependencies:
|
||||
@ -919,6 +1025,41 @@ packages:
|
||||
react-remove-scroll: 2.5.5(@types/react@18.2.33)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-popover@1.0.7(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
'@radix-ui/primitive': 1.0.1
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||
'@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||
'@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-id': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||
'@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.33)(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||
'@types/react': 18.2.33
|
||||
'@types/react-dom': 18.2.14
|
||||
aria-hidden: 1.2.3
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-remove-scroll: 2.5.5(@types/react@18.2.33)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==}
|
||||
peerDependencies:
|
||||
@ -949,6 +1090,18 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-portal@1.0.0(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
'@radix-ui/react-primitive': 1.0.0(react-dom@18.2.0)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==}
|
||||
peerDependencies:
|
||||
@ -970,6 +1123,19 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-presence@1.0.0(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.0.0(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==}
|
||||
peerDependencies:
|
||||
@ -992,6 +1158,18 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-primitive@1.0.0(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
'@radix-ui/react-slot': 1.0.0(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
|
||||
peerDependencies:
|
||||
@ -1155,6 +1333,16 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-slot@1.0.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-slot@1.0.2(@types/react@18.2.33)(react@18.2.0):
|
||||
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
|
||||
peerDependencies:
|
||||
@ -1284,6 +1472,15 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-callback-ref@1.0.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.33)(react@18.2.0):
|
||||
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
|
||||
peerDependencies:
|
||||
@ -1298,6 +1495,16 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-controllable-state@1.0.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.33)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==}
|
||||
peerDependencies:
|
||||
@ -1313,6 +1520,16 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-escape-keydown@1.0.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.33)(react@18.2.0):
|
||||
resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
|
||||
peerDependencies:
|
||||
@ -1328,6 +1545,15 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-layout-effect@1.0.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.2
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.33)(react@18.2.0):
|
||||
resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==}
|
||||
peerDependencies:
|
||||
@ -1681,22 +1907,22 @@ packages:
|
||||
'@types/ms': 0.7.33
|
||||
dev: false
|
||||
|
||||
/@types/eslint-scope@3.7.6:
|
||||
resolution: {integrity: sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==}
|
||||
/@types/eslint-scope@3.7.7:
|
||||
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
||||
dependencies:
|
||||
'@types/eslint': 8.44.6
|
||||
'@types/estree': 1.0.3
|
||||
'@types/eslint': 8.44.8
|
||||
'@types/estree': 1.0.5
|
||||
dev: true
|
||||
|
||||
/@types/eslint@8.44.6:
|
||||
resolution: {integrity: sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==}
|
||||
/@types/eslint@8.44.8:
|
||||
resolution: {integrity: sha512-4K8GavROwhrYl2QXDXm0Rv9epkA8GBFu0EI+XrrnnuCl7u8CWBRusX7fXJfanhZTDWSAL24gDI/UqXyUM0Injw==}
|
||||
dependencies:
|
||||
'@types/estree': 1.0.3
|
||||
'@types/json-schema': 7.0.14
|
||||
'@types/estree': 1.0.5
|
||||
'@types/json-schema': 7.0.15
|
||||
dev: true
|
||||
|
||||
/@types/estree@1.0.3:
|
||||
resolution: {integrity: sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==}
|
||||
/@types/estree@1.0.5:
|
||||
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
||||
dev: true
|
||||
|
||||
/@types/hast@2.3.7:
|
||||
@ -1716,6 +1942,10 @@ packages:
|
||||
resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==}
|
||||
dev: true
|
||||
|
||||
/@types/json-schema@7.0.15:
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
dev: true
|
||||
|
||||
/@types/katex@0.14.0:
|
||||
resolution: {integrity: sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==}
|
||||
dev: false
|
||||
@ -1740,6 +1970,12 @@ packages:
|
||||
resolution: {integrity: sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ==}
|
||||
dev: false
|
||||
|
||||
/@types/node@20.10.1:
|
||||
resolution: {integrity: sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==}
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
dev: true
|
||||
|
||||
/@types/node@20.8.9:
|
||||
resolution: {integrity: sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==}
|
||||
dependencies:
|
||||
@ -2050,12 +2286,12 @@ packages:
|
||||
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
|
||||
dev: true
|
||||
|
||||
/acorn-import-assertions@1.9.0(acorn@8.10.0):
|
||||
/acorn-import-assertions@1.9.0(acorn@8.11.2):
|
||||
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
|
||||
peerDependencies:
|
||||
acorn: ^8
|
||||
dependencies:
|
||||
acorn: 8.10.0
|
||||
acorn: 8.11.2
|
||||
dev: true
|
||||
|
||||
/acorn-jsx@5.3.2(acorn@8.10.0):
|
||||
@ -2072,6 +2308,12 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/acorn@8.11.2:
|
||||
resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/ajv-keywords@3.5.2(ajv@6.12.6):
|
||||
resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==}
|
||||
peerDependencies:
|
||||
@ -2304,6 +2546,20 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/cmdk@0.2.0(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-JQpKvEOb86SnvMZbYaFKYhvzFntWBeSZdyii0rZPhKJj9uwJBxu4DaVYDrRN7r3mPop56oPhRw+JYWTKs66TYw==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
dependencies:
|
||||
'@radix-ui/react-dialog': 1.0.0(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
command-score: 0.1.2
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
dev: false
|
||||
|
||||
/color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@ -2334,6 +2590,10 @@ packages:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
dev: false
|
||||
|
||||
/command-score@0.1.2:
|
||||
resolution: {integrity: sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w==}
|
||||
dev: false
|
||||
|
||||
/commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
dev: true
|
||||
@ -2557,8 +2817,8 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/es-module-lexer@1.3.1:
|
||||
resolution: {integrity: sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==}
|
||||
/es-module-lexer@1.4.1:
|
||||
resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==}
|
||||
dev: true
|
||||
|
||||
/esbuild@0.18.20:
|
||||
@ -3218,7 +3478,7 @@ packages:
|
||||
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
dependencies:
|
||||
'@types/node': 20.8.9
|
||||
'@types/node': 20.10.1
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
dev: true
|
||||
@ -4340,6 +4600,25 @@ packages:
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/react-remove-scroll@2.5.4(@types/react@18.2.33)(react@18.2.0):
|
||||
resolution: {integrity: sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/react': 18.2.33
|
||||
react: 18.2.0
|
||||
react-remove-scroll-bar: 2.3.4(@types/react@18.2.33)(react@18.2.0)
|
||||
react-style-singleton: 2.2.1(@types/react@18.2.33)(react@18.2.0)
|
||||
tslib: 2.6.2
|
||||
use-callback-ref: 1.3.0(@types/react@18.2.33)(react@18.2.0)
|
||||
use-sidecar: 1.1.2(@types/react@18.2.33)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-remove-scroll@2.5.5(@types/react@18.2.33)(react@18.2.0):
|
||||
resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==}
|
||||
engines: {node: '>=10'}
|
||||
@ -4597,7 +4876,7 @@ packages:
|
||||
resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.14
|
||||
'@types/json-schema': 7.0.15
|
||||
ajv: 6.12.6
|
||||
ajv-keywords: 3.5.2(ajv@6.12.6)
|
||||
dev: true
|
||||
@ -4786,7 +5065,7 @@ packages:
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 3.3.0
|
||||
serialize-javascript: 6.0.1
|
||||
terser: 5.22.0
|
||||
terser: 5.24.0
|
||||
webpack: 5.89.0
|
||||
dev: true
|
||||
|
||||
@ -4801,6 +5080,17 @@ packages:
|
||||
source-map-support: 0.5.21
|
||||
dev: true
|
||||
|
||||
/terser@5.24.0:
|
||||
resolution: {integrity: sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.5
|
||||
acorn: 8.11.2
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
dev: true
|
||||
|
||||
/text-table@0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
dev: true
|
||||
@ -5163,17 +5453,17 @@ packages:
|
||||
webpack-cli:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.6
|
||||
'@types/estree': 1.0.3
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.5
|
||||
'@webassemblyjs/ast': 1.11.6
|
||||
'@webassemblyjs/wasm-edit': 1.11.6
|
||||
'@webassemblyjs/wasm-parser': 1.11.6
|
||||
acorn: 8.10.0
|
||||
acorn-import-assertions: 1.9.0(acorn@8.10.0)
|
||||
acorn: 8.11.2
|
||||
acorn-import-assertions: 1.9.0(acorn@8.11.2)
|
||||
browserslist: 4.22.1
|
||||
chrome-trace-event: 1.0.3
|
||||
enhanced-resolve: 5.15.0
|
||||
es-module-lexer: 1.3.1
|
||||
es-module-lexer: 1.4.1
|
||||
eslint-scope: 5.1.1
|
||||
events: 3.3.0
|
||||
glob-to-regexp: 0.4.1
|
||||
|
177
app/src/admin/channel.ts
Normal file
177
app/src/admin/channel.ts
Normal file
@ -0,0 +1,177 @@
|
||||
export type Channel = {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
models: string[];
|
||||
priority: number;
|
||||
weight: number;
|
||||
retry: number;
|
||||
secret: string;
|
||||
endpoint: string;
|
||||
mapper: string;
|
||||
state: boolean;
|
||||
};
|
||||
|
||||
export type ChannelEditProps = {
|
||||
type: string;
|
||||
name: string;
|
||||
models: string[];
|
||||
priority: number;
|
||||
weight: number;
|
||||
retry: number;
|
||||
secret: string;
|
||||
endpoint: string;
|
||||
mapper: string;
|
||||
};
|
||||
|
||||
export type ChannelInfo = {
|
||||
id: number;
|
||||
description?: string;
|
||||
endpoint: string;
|
||||
format: string;
|
||||
models: string[];
|
||||
};
|
||||
|
||||
export const ChannelTypes: Record<string, string> = {
|
||||
openai: "OpenAI",
|
||||
claude: "Claude",
|
||||
slack: "Slack",
|
||||
sparkdesk: "讯飞星火",
|
||||
chatglm: "智谱 ChatGLM",
|
||||
qwen: "通义千问",
|
||||
hunyuan: "腾讯混元",
|
||||
zhinao: "360 智脑",
|
||||
baichuan: "百川 AI",
|
||||
skylark: "火山方舟",
|
||||
bing: "New Bing",
|
||||
palm: "Google PaLM2",
|
||||
midjourney: "Midjourney",
|
||||
oneapi: "One API",
|
||||
};
|
||||
|
||||
export const ChannelInfos: Record<string, ChannelInfo> = {
|
||||
openai: {
|
||||
id: 0,
|
||||
endpoint: "https://api.openai.com",
|
||||
format: "<api-key>",
|
||||
models: [
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-instruct",
|
||||
"gpt-3.5-turbo-0613",
|
||||
"gpt-3.5-turbo-0301",
|
||||
"gpt-3.5-turbo-1106",
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
"gpt-3.5-turbo-16k-0301",
|
||||
"gpt-4",
|
||||
"gpt-4-0314",
|
||||
"gpt-4-0613",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4-vision-preview",
|
||||
"gpt-4-32k",
|
||||
"gpt-4-32k-0314",
|
||||
"gpt-4-32k-0613",
|
||||
"dall-e-2",
|
||||
"dall-e-3",
|
||||
],
|
||||
},
|
||||
claude: {
|
||||
id: 1,
|
||||
endpoint: "https://api.anthropic.com",
|
||||
format: "<x-api-key>",
|
||||
models: ["claude-instant-1", "claude-2"],
|
||||
},
|
||||
slack: {
|
||||
id: 2,
|
||||
endpoint: "your-channel",
|
||||
format: "<bot-id>|<xoxp-token>",
|
||||
models: ["claude-slack"],
|
||||
},
|
||||
sparkdesk: {
|
||||
id: 3,
|
||||
endpoint: "wss://spark-api.xf-yun.com",
|
||||
format: "<app-id>|<app-secret>|<api-key>",
|
||||
models: ["spark-desk-v1.5", "spark-desk-v2", "spark-desk-v3"],
|
||||
},
|
||||
chatglm: {
|
||||
id: 4,
|
||||
endpoint: "https://open.bigmodel.cn",
|
||||
format: "<api-key>",
|
||||
models: [
|
||||
"zhipu-chatglm-turbo",
|
||||
"zhipu-chatglm-pro",
|
||||
"zhipu-chatglm-std",
|
||||
"zhipu-chatglm-lite",
|
||||
],
|
||||
},
|
||||
qwen: {
|
||||
id: 5,
|
||||
endpoint: "https://dashscope.aliyuncs.com",
|
||||
format: "<api-key>",
|
||||
models: ["qwen-turbo", "qwen-plus", "qwen-turbo-net", "qwen-plus-net"],
|
||||
},
|
||||
hunyuan: {
|
||||
id: 6,
|
||||
endpoint: "https://hunyuan.cloud.tencent.com",
|
||||
format: "<app-id>|<secret-id>|<secret-key>",
|
||||
models: ["hunyuan"],
|
||||
// endpoint
|
||||
},
|
||||
zhinao: {
|
||||
id: 7,
|
||||
endpoint: "https://api.360.cn",
|
||||
format: "<api-key>",
|
||||
models: ["360-gpt-v9"],
|
||||
},
|
||||
baichuan: {
|
||||
id: 8,
|
||||
endpoint: "https://api.baichuan-ai.com",
|
||||
format: "<api-key>",
|
||||
models: ["baichuan-53b"],
|
||||
},
|
||||
skylark: {
|
||||
id: 9,
|
||||
endpoint: "https://maas-api.ml-platform-cn-beijing.volces.com",
|
||||
format: "<access-key>|<secret-key>",
|
||||
models: [
|
||||
"skylark-lite-public",
|
||||
"skylark-plus-public",
|
||||
"skylark-pro-public",
|
||||
"skylark-chat",
|
||||
],
|
||||
},
|
||||
bing: {
|
||||
id: 10,
|
||||
endpoint: "wss://your.bing.service",
|
||||
format: "<secret>",
|
||||
models: ["bing-creative", "bing-balanced", "bing-precise"],
|
||||
description:
|
||||
"> Bing 服务需要自行搭建,详情请参考 [chatnio-bing-service](https://github.com/Deeptrain-Community/chatnio-bing-service) (如为 bing2api 可直接使用 OpenAI 格式映射)",
|
||||
},
|
||||
palm: {
|
||||
id: 11,
|
||||
endpoint: "https://generativelanguage.googleapis.com",
|
||||
format: "<api-key>",
|
||||
models: ["chat-bison-001"],
|
||||
},
|
||||
midjourney: {
|
||||
id: 12,
|
||||
endpoint: "https://your.midjourney.proxy",
|
||||
format: "<mj-api-secret>|<white-list>",
|
||||
models: ["midjourney", "midjourney-fast", "midjourney-turbo"],
|
||||
description:
|
||||
"> 请参考 [midjourney-proxy](https://github.com/novicezk/midjourney-proxy) 项目填入参数,可设置白名单 *white-list* 以限制回调 IP \n" +
|
||||
"> 密钥举例: password|localhost,127.0.0.1,196.128.0.31\n" +
|
||||
"> 注意:**请在系统设置中设置后端的公网 IP / 域名,否则无法接收回调**",
|
||||
},
|
||||
oneapi: {
|
||||
id: 13,
|
||||
endpoint: "https://openai.justsong.cn/api",
|
||||
format: "<api-key>",
|
||||
models: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const ChannelModels: string[] = Object.values(ChannelInfos).flatMap(
|
||||
(info) => info.models,
|
||||
);
|
@ -2,6 +2,7 @@
|
||||
@import "dashboard";
|
||||
@import "management";
|
||||
@import "broadcast";
|
||||
@import "channel";
|
||||
|
||||
.admin-page {
|
||||
position: relative;
|
||||
|
98
app/src/assets/admin/channel.less
Normal file
98
app/src/assets/admin/channel.less
Normal file
@ -0,0 +1,98 @@
|
||||
.channel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.channel-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 20vh;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
& > * {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
|
||||
.channel-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: 0.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channel-model-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
height: max-content;
|
||||
width: 100%;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--background));
|
||||
padding: 1rem;
|
||||
min-height: 5rem;
|
||||
|
||||
.channel-model-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
transition: .25s;
|
||||
height: max-content;
|
||||
|
||||
&:hover {
|
||||
border-color: hsl(var(--border-hover));
|
||||
}
|
||||
|
||||
.remove-action {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
cursor: pointer;
|
||||
margin-left: 0.5rem;
|
||||
color: hsl(var(--text-secondary));
|
||||
transition: .25s;
|
||||
|
||||
&:hover {
|
||||
color: hsl(var(--text-primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channel-model-action {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.channel-description {
|
||||
white-space: break-spaces;
|
||||
line-height: 1em;
|
||||
}
|
@ -62,7 +62,7 @@
|
||||
|
||||
input {
|
||||
text-align: center;
|
||||
max-width: 3.5rem;
|
||||
max-width: 4rem;
|
||||
max-height: 1.75rem;
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,54 @@
|
||||
}
|
||||
}
|
||||
|
||||
.model-select-group {
|
||||
transform: translateX(-26px) !important;
|
||||
width: calc(100vw - 72px) !important;
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.thin-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-webkit-appearance: textfield;
|
||||
margin: 0;
|
||||
|
||||
&::-webkit-inner-spin-button,
|
||||
&::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '>';
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 2px;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '<';
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 20px;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,14 @@ import { useDispatch } from "react-redux";
|
||||
import { openDialog as openQuotaDialog } from "@/store/quota.ts";
|
||||
import { openDialog as openSubscriptionDialog } from "@/store/subscription.ts";
|
||||
import { AppDispatch } from "@/store";
|
||||
import { Copy } from "lucide-react";
|
||||
import {
|
||||
Codepen,
|
||||
Codesandbox,
|
||||
Copy,
|
||||
Github,
|
||||
Twitter,
|
||||
Youtube,
|
||||
} from "lucide-react";
|
||||
import { copyClipboard } from "@/utils/dom.ts";
|
||||
import { useToast } from "./ui/use-toast.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@ -43,6 +50,21 @@ const LanguageMap: Record<string, string> = {
|
||||
rs: "rust",
|
||||
};
|
||||
|
||||
function getSocialIcon(url: string) {
|
||||
const { hostname } = new URL(url);
|
||||
|
||||
if (hostname.includes("github.com"))
|
||||
return <Github className="h-4 w-4 inline-block mr-0.5" />;
|
||||
if (hostname.includes("twitter.com"))
|
||||
return <Twitter className="h-4 w-4 inline-block mr-0.5" />;
|
||||
if (hostname.includes("youtube.com"))
|
||||
return <Youtube className="h-4 w-4 inline-block mr-0.5" />;
|
||||
if (hostname.includes("codepen.io"))
|
||||
return <Codepen className="h-4 w-4 inline-block mr-0.5" />;
|
||||
if (hostname.includes("codesandbox.io"))
|
||||
return <Codesandbox className="h-4 w-4 inline-block mr-0.5" />;
|
||||
}
|
||||
|
||||
function MarkdownContent({ children, className }: MarkdownProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
@ -75,6 +97,7 @@ function MarkdownContent({ children, className }: MarkdownProps) {
|
||||
if (doAction(dispatch, url)) e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{getSocialIcon(url)}
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
|
73
app/src/components/OperationAction.tsx
Normal file
73
app/src/components/OperationAction.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip.tsx";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
|
||||
type ActionProps = {
|
||||
tooltip?: string;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => any;
|
||||
variant?:
|
||||
| "secondary"
|
||||
| "default"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "ghost"
|
||||
| "link"
|
||||
| null
|
||||
| undefined;
|
||||
};
|
||||
function OperationAction({ tooltip, children, onClick, variant }: ActionProps) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{variant === "destructive" ? (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
size={`icon`}
|
||||
className={`mx-1 w-8 h-8`}
|
||||
variant={variant}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className={`w-max`}>
|
||||
<Button
|
||||
className={`flex flex-row items-center mx-1`}
|
||||
onClick={onClick}
|
||||
variant={variant}
|
||||
>
|
||||
{children}
|
||||
<p className={`ml-1 translate-y-[-1px]`}>{tooltip}</p>
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Button
|
||||
size={`icon`}
|
||||
className={`mx-1 w-8 h-8`}
|
||||
onClick={onClick}
|
||||
variant={variant}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default OperationAction;
|
5
app/src/components/Require.tsx
Normal file
5
app/src/components/Require.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
function Required() {
|
||||
return <span className={`text-red-500 mr-0.5`}>*</span>;
|
||||
}
|
||||
|
||||
export default Required;
|
@ -114,8 +114,6 @@ function SelectGroupMobile(props: SelectGroupProps) {
|
||||
<SelectValue placeholder={props.current.value} />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
position={`item-aligned`}
|
||||
side={props.side}
|
||||
className={`${props.className} ${props.classNameMobile}`}
|
||||
>
|
||||
{props.list.map((select: SelectItemProps, idx: number) => (
|
||||
|
15
app/src/components/admin/ChannelSettings.tsx
Normal file
15
app/src/components/admin/ChannelSettings.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import ChannelTable from "@/components/admin/assemblies/ChannelTable.tsx";
|
||||
import ChannelEditor from "@/components/admin/assemblies/ChannelEditor.tsx";
|
||||
|
||||
function ChannelSettings() {
|
||||
const [enabled, setEnabled] = useState<boolean>(false);
|
||||
|
||||
return !enabled ? (
|
||||
<ChannelTable setEnabled={setEnabled} />
|
||||
) : (
|
||||
<ChannelEditor setEnabled={setEnabled} />
|
||||
);
|
||||
}
|
||||
|
||||
export default ChannelSettings;
|
@ -1,7 +1,13 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { closeMenu, selectMenu } from "@/store/menu.ts";
|
||||
import React, { useMemo } from "react";
|
||||
import {CandlestickChart, LayoutDashboard, Radio, Settings, Users} from "lucide-react";
|
||||
import {
|
||||
CandlestickChart,
|
||||
LayoutDashboard,
|
||||
Radio,
|
||||
Settings,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import router from "@/router.tsx";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
384
app/src/components/admin/assemblies/ChannelEditor.tsx
Normal file
384
app/src/components/admin/assemblies/ChannelEditor.tsx
Normal file
@ -0,0 +1,384 @@
|
||||
import Tips from "@/components/Tips.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectGroup,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select.tsx";
|
||||
import {
|
||||
ChannelEditProps,
|
||||
ChannelInfos,
|
||||
ChannelModels,
|
||||
ChannelTypes,
|
||||
} from "@/admin/channel.ts";
|
||||
import { Textarea } from "@/components/ui/textarea.tsx";
|
||||
import { NumberInput } from "@/components/ui/number-input.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo, useReducer, useState } from "react";
|
||||
import Required from "@/components/Require.tsx";
|
||||
import { X } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu.tsx";
|
||||
import {
|
||||
Command,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import Markdown from "@/components/Markdown.tsx";
|
||||
|
||||
const initialState: ChannelEditProps = {
|
||||
type: "openai",
|
||||
name: "",
|
||||
models: [],
|
||||
priority: 0,
|
||||
weight: 1,
|
||||
retry: 3,
|
||||
secret: "",
|
||||
endpoint: ChannelInfos["openai"].endpoint,
|
||||
mapper: "",
|
||||
};
|
||||
|
||||
type CustomActionProps = {
|
||||
onPost: (model: string) => void;
|
||||
};
|
||||
function CustomAction({ onPost }: CustomActionProps) {
|
||||
const { t } = useTranslation();
|
||||
const [model, setModel] = useState("");
|
||||
|
||||
function post() {
|
||||
const data = model.trim();
|
||||
if (data === "") return;
|
||||
onPost(data);
|
||||
setModel("");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex flex-row grow gap-0 custom-action`}>
|
||||
<Input
|
||||
value={model}
|
||||
placeholder={t("admin.channels.add-custom-model")}
|
||||
className={`rounded-r-none`}
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") post();
|
||||
}}
|
||||
/>
|
||||
<Button className={`rounded-l-none`} onClick={post}>
|
||||
{t("add")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function reducer(state: ChannelEditProps, action: any) {
|
||||
switch (action.type) {
|
||||
case "type":
|
||||
const isChanged = ChannelInfos[state.type].endpoint !== state.endpoint;
|
||||
const endpoint = isChanged
|
||||
? state.endpoint
|
||||
: ChannelInfos[action.value].endpoint;
|
||||
return { ...state, endpoint, type: action.value };
|
||||
case "name":
|
||||
return { ...state, name: action.value };
|
||||
case "models":
|
||||
return { ...state, models: action.value };
|
||||
case "add-model":
|
||||
if (state.models.includes(action.value) || action.value === "") {
|
||||
return state;
|
||||
}
|
||||
return { ...state, models: [...state.models, action.value] };
|
||||
case "add-models":
|
||||
const models = action.value.filter(
|
||||
(model: string) => !state.models.includes(model) && model !== "",
|
||||
);
|
||||
return { ...state, models: [...state.models, ...models] };
|
||||
case "remove-model":
|
||||
return {
|
||||
...state,
|
||||
models: state.models.filter((model) => model !== action.value),
|
||||
};
|
||||
case "clear-models":
|
||||
return { ...state, models: [] };
|
||||
case "priority":
|
||||
return { ...state, priority: action.value };
|
||||
case "weight":
|
||||
return { ...state, weight: action.value };
|
||||
case "secret":
|
||||
return { ...state, secret: action.value };
|
||||
case "endpoint":
|
||||
return { ...state, endpoint: action.value };
|
||||
case "mapper":
|
||||
return { ...state, mapper: action.value };
|
||||
case "retry":
|
||||
return { ...state, retry: action.value };
|
||||
case "clear":
|
||||
return { ...initialState };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function validator(state: ChannelEditProps): boolean {
|
||||
return (
|
||||
state.name.trim() !== "" &&
|
||||
state.models.length > 0 &&
|
||||
state.secret.trim() !== "" &&
|
||||
state.endpoint.trim() !== ""
|
||||
);
|
||||
}
|
||||
|
||||
function handler(data: ChannelEditProps): ChannelEditProps {
|
||||
data.models = data.models.filter((model) => model.trim() !== "");
|
||||
data.name = data.name.trim();
|
||||
data.secret = data.secret
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() !== "")
|
||||
.join("\n");
|
||||
data.endpoint = data.endpoint.trim();
|
||||
data.mapper = data.mapper
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((line) => {
|
||||
if (line.trim() === "") return false;
|
||||
const values = line.split(">");
|
||||
return (
|
||||
values.length === 2 &&
|
||||
values[0].trim() !== "" &&
|
||||
values[1].trim() !== ""
|
||||
);
|
||||
})
|
||||
.join("\n");
|
||||
return data;
|
||||
}
|
||||
|
||||
type ChannelEditorProps = {
|
||||
setEnabled: (enabled: boolean) => void;
|
||||
};
|
||||
|
||||
function ChannelEditor({ setEnabled }: ChannelEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const [edit, dispatch] = useReducer(reducer, { ...initialState });
|
||||
const info = useMemo(() => {
|
||||
return ChannelInfos[edit.type];
|
||||
}, [edit.type]);
|
||||
const unusedModels = useMemo(() => {
|
||||
return ChannelModels.filter(
|
||||
(model) => !edit.models.includes(model) && model !== "",
|
||||
);
|
||||
}, [edit.models]);
|
||||
const enabled = useMemo(() => validator(edit), [edit]);
|
||||
|
||||
function post() {
|
||||
const data = handler(edit);
|
||||
console.debug(`[channel] preflight channel data`, data);
|
||||
// setEnabled(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`channel-editor`}>
|
||||
<div className={`channel-wrapper w-full h-max`}>
|
||||
<div className={`channel-row`}>
|
||||
<div className={`channel-content`}>
|
||||
<Required />
|
||||
{t("admin.channels.name")}
|
||||
<Tips content={t("admin.channels.name-tip")} />
|
||||
</div>
|
||||
<Input
|
||||
value={edit.name}
|
||||
placeholder={t("admin.channels.name-placeholder")}
|
||||
onChange={(e) => dispatch({ type: "name", value: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className={`channel-row`}>
|
||||
<div className={`channel-content`}>
|
||||
<Required />
|
||||
{t("admin.channels.type")}
|
||||
</div>
|
||||
<Select
|
||||
value={edit.type}
|
||||
onValueChange={(value) => dispatch({ type: "type", value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("admin.channels.type")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{Object.entries(ChannelTypes).map(([key, value], idx) => (
|
||||
<SelectItem key={idx} value={key}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{info.description && (
|
||||
<Markdown className={`channel-description mt-4 mb-1`}>
|
||||
{info.description}
|
||||
</Markdown>
|
||||
)}
|
||||
</div>
|
||||
<div className={`channel-row`}>
|
||||
<div className={`channel-content`}>
|
||||
<Required />
|
||||
{t("admin.channels.model")}
|
||||
</div>
|
||||
<div className={`channel-model-wrapper`}>
|
||||
{edit.models.map((model: string, idx: number) => (
|
||||
<div className={`channel-model-item`} key={idx}>
|
||||
{model}
|
||||
<X
|
||||
className={`remove-action`}
|
||||
onClick={() =>
|
||||
dispatch({ type: "remove-model", value: model })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={`channel-model-action mt-4`}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>{t("admin.channels.add-model")}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent asChild>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder={t("admin.channels.search-model")}
|
||||
/>
|
||||
<CommandList className={`thin-scrollbar`}>
|
||||
{unusedModels.map((model, idx) => (
|
||||
<CommandItem
|
||||
key={idx}
|
||||
value={model}
|
||||
onSelect={() =>
|
||||
dispatch({ type: "add-model", value: model })
|
||||
}
|
||||
className={`px-2`}
|
||||
>
|
||||
{model}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<CustomAction
|
||||
onPost={(model) => {
|
||||
dispatch({ type: "add-model", value: model });
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
dispatch({ type: "add-models", value: info.models })
|
||||
}
|
||||
>
|
||||
{t("admin.channels.fill-template-models", {
|
||||
number: info.models.length,
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
onClick={() => dispatch({ type: "clear-models" })}
|
||||
>
|
||||
{t("admin.channels.clear-models")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`channel-row`}>
|
||||
<div className={`channel-content`}>
|
||||
<Required />
|
||||
{t("admin.channels.secret")}
|
||||
</div>
|
||||
<Textarea
|
||||
value={edit.secret}
|
||||
placeholder={t("admin.channels.secret-placeholder", {
|
||||
format: info.format,
|
||||
})}
|
||||
onChange={(e) =>
|
||||
dispatch({ type: "secret", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={`channel-row`}>
|
||||
<div className={`channel-content`}>
|
||||
<Required />
|
||||
{t("admin.channels.endpoint")}
|
||||
</div>
|
||||
<Input
|
||||
value={edit.endpoint}
|
||||
placeholder={t("admin.channels.endpoint-placeholder")}
|
||||
onChange={(e) =>
|
||||
dispatch({ type: "endpoint", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={`channel-row`}>
|
||||
<div className={`channel-content`}>
|
||||
{t("admin.channels.priority")}
|
||||
<Tips content={t("admin.channels.priority-tip")} />
|
||||
</div>
|
||||
<NumberInput
|
||||
value={edit.priority}
|
||||
acceptNegative={true}
|
||||
onValueChange={(value) => dispatch({ type: "priority", value })}
|
||||
/>
|
||||
</div>
|
||||
<div className={`channel-row`}>
|
||||
<div className={`channel-content`}>
|
||||
{t("admin.channels.weight")}
|
||||
<Tips content={t("admin.channels.weight-tip")} />
|
||||
</div>
|
||||
<NumberInput
|
||||
value={edit.weight}
|
||||
min={1}
|
||||
onValueChange={(value) => dispatch({ type: "weight", value })}
|
||||
/>
|
||||
</div>
|
||||
<div className={`channel-row`}>
|
||||
<div className={`channel-content`}>
|
||||
{t("admin.channels.retry")}
|
||||
<Tips content={t("admin.channels.retry-tip")} />
|
||||
</div>
|
||||
<NumberInput
|
||||
value={edit.retry}
|
||||
min={1}
|
||||
onValueChange={(value) => dispatch({ type: "retry", value })}
|
||||
/>
|
||||
</div>
|
||||
<div className={`channel-row`}>
|
||||
<div className={`channel-content`}>
|
||||
{t("admin.channels.mapper")}
|
||||
<Tips content={t("admin.channels.mapper-tip")} />
|
||||
</div>
|
||||
<Textarea
|
||||
value={edit.mapper}
|
||||
placeholder={t("admin.channels.mapper-placeholder")}
|
||||
onChange={(e) =>
|
||||
dispatch({ type: "mapper", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`mt-4 flex flex-row w-full h-max pr-2`}>
|
||||
<div className={`grow`} />
|
||||
<Button variant={`outline`} onClick={() => setEnabled(false)}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button className={`ml-2`} onClick={post} disabled={!enabled}>
|
||||
{t("confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChannelEditor;
|
96
app/src/components/admin/assemblies/ChannelTable.tsx
Normal file
96
app/src/components/admin/assemblies/ChannelTable.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table.tsx";
|
||||
import { Badge } from "@/components/ui/badge.tsx";
|
||||
import { Check, Plus, RotateCw, Settings2, Trash, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import OperationAction from "@/components/OperationAction.tsx";
|
||||
import { useState } from "react";
|
||||
import { Channel } from "@/admin/channel.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type ChannelTableProps = {
|
||||
setEnabled: (enabled: boolean) => void;
|
||||
};
|
||||
|
||||
function ChannelTable({ setEnabled }: ChannelTableProps) {
|
||||
const { t } = useTranslation();
|
||||
const [data, setData] = useState<Channel[]>([]);
|
||||
|
||||
return (
|
||||
<div className={`channel-table`}>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className={`select-none whitespace-nowrap`}>
|
||||
<TableCell>{t("admin.channels.id")}</TableCell>
|
||||
<TableCell>{t("admin.channels.name")}</TableCell>
|
||||
<TableCell>{t("admin.channels.type")}</TableCell>
|
||||
<TableCell>{t("admin.channels.priority")}</TableCell>
|
||||
<TableCell>{t("admin.channels.weight")}</TableCell>
|
||||
<TableCell>{t("admin.channels.state")}</TableCell>
|
||||
<TableCell>{t("admin.channels.action")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(data || []).map((chan, idx) => (
|
||||
<TableRow key={idx}>
|
||||
<TableCell>{chan.id}</TableCell>
|
||||
<TableCell>{chan.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={`select-none w-max`}>{chan.type}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{chan.priority}</TableCell>
|
||||
<TableCell>{chan.weight}</TableCell>
|
||||
<TableCell>
|
||||
{chan.state ? (
|
||||
<Check className={`h-4 w-4 text-green-500`} />
|
||||
) : (
|
||||
<X className={`h-4 w-4 text-red-500`} />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<OperationAction tooltip={t("admin.channels.edit")}>
|
||||
<Settings2 className={`h-4 w-4`} />
|
||||
</OperationAction>
|
||||
{chan.state ? (
|
||||
<OperationAction
|
||||
tooltip={t("admin.channels.disable")}
|
||||
variant={`destructive`}
|
||||
>
|
||||
<X className={`h-4 w-4`} />
|
||||
</OperationAction>
|
||||
) : (
|
||||
<OperationAction tooltip={t("admin.channels.enable")}>
|
||||
<Check className={`h-4 w-4`} />
|
||||
</OperationAction>
|
||||
)}
|
||||
<OperationAction
|
||||
tooltip={t("admin.channels.delete")}
|
||||
variant={`destructive`}
|
||||
>
|
||||
<Trash className={`h-4 w-4`} />
|
||||
</OperationAction>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className={`mt-6 pr-2 flex flex-row w-full h-max`}>
|
||||
<div className={`grow`} />
|
||||
<Button variant={`outline`} size={`icon`} className={`mr-2`}>
|
||||
<RotateCw className={`h-4 w-4`} />
|
||||
</Button>
|
||||
<Button onClick={() => setEnabled(true)}>
|
||||
<Plus className={`h-4 w-4 mr-1`} />
|
||||
{t("admin.channels.create")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChannelTable;
|
153
app/src/components/ui/command.tsx
Normal file
153
app/src/components/ui/command.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import * as React from "react";
|
||||
import { type DialogProps } from "@radix-ui/react-dialog";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
import { cn } from "@/components/ui/lib/utils";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CommandShortcut.displayName = "CommandShortcut";
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
@ -22,6 +22,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
type={"number"}
|
||||
className={`number-input ${className}`}
|
||||
id={props.id}
|
||||
value={value}
|
||||
@ -34,6 +35,11 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
||||
value = props.min;
|
||||
props.onValueChange(value);
|
||||
}}
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
onWheel={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
29
app/src/components/ui/popover.tsx
Normal file
29
app/src/components/ui/popover.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "@/components/ui/lib/utils";
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
@ -1,12 +1,12 @@
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronUp, ChevronDown } from "lucide-react";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
import { cn } from "@/components/ui/lib/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectList = SelectPrimitive.Group;
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@ -30,52 +30,84 @@ const SelectTrigger = React.forwardRef<
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
avoidCollisions={true}
|
||||
ref={(ref) => {
|
||||
if (!ref) return;
|
||||
ref.ontouchend = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
}}
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper"
|
||||
? "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1"
|
||||
: "w-[80vw] max-w-[30rem] max-h-[80vh]",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
className={`flex items-center justify-center h-[25px] cursor-pointer`}
|
||||
>
|
||||
<ChevronUp />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
className={`flex items-center justify-center h-[25px] cursor-pointer`}
|
||||
>
|
||||
<ChevronDown />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
>(
|
||||
(
|
||||
{ className, children, position = "popper", ...props },
|
||||
ref: React.ForwardedRef<HTMLDivElement>,
|
||||
) => {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={(ref) => {
|
||||
if (!ref) return;
|
||||
ref.ontouchend = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
}}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
},
|
||||
);
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
@ -127,11 +159,13 @@ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectList,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
|
@ -34,7 +34,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select.tsx";
|
||||
import { langs, setLanguage } from "@/i18n.ts";
|
||||
import {cn} from "@/components/ui/lib/utils.ts";
|
||||
import { cn } from "@/components/ui/lib/utils.ts";
|
||||
|
||||
function SettingsDialog() {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
119
app/src/i18n.ts
119
app/src/i18n.ts
@ -15,6 +15,7 @@ const resources = {
|
||||
cn: {
|
||||
translation: {
|
||||
end: "",
|
||||
add: "添加",
|
||||
"not-found": "页面未找到",
|
||||
home: "首页",
|
||||
login: "登录",
|
||||
@ -362,6 +363,42 @@ const resources = {
|
||||
generate: "批量生成",
|
||||
"generate-result": "生成结果",
|
||||
error: "请求失败",
|
||||
channels: {
|
||||
id: "渠道 ID",
|
||||
name: "名称",
|
||||
"name-tip": "渠道名称,用于标识渠道",
|
||||
"name-placeholder": "请输入渠道名称",
|
||||
type: "类型",
|
||||
priority: "优先级",
|
||||
"priority-tip": "多渠道时,根据优先级顺序请求,越大优先级越高",
|
||||
weight: "权重",
|
||||
"weight-tip": "同优先级时,根据权重比例进行均衡负载调用",
|
||||
retry: "最大重试次数",
|
||||
"retry-tip": "当渠道请求失败时,最多重试的次数",
|
||||
model: "模型",
|
||||
secret: "密钥",
|
||||
"secret-placeholder":
|
||||
"请输入密钥,格式:{{format}}\n多个密钥时,一行一个,请求时随机选取负载",
|
||||
endpoint: "接入点",
|
||||
"endpoint-placeholder": "请输入接入点(即代理)",
|
||||
mapper: "模型映射",
|
||||
"mapper-tip": "模型名转换,实现非对称的模型请求",
|
||||
"mapper-placeholder":
|
||||
"请输入模型映射,一行一个,格式: model>model\n" +
|
||||
"前者为请求的模型,后者为映射的模型(需要在模型中存在),中间用 > 分隔",
|
||||
state: "状态",
|
||||
action: "操作",
|
||||
edit: "编辑渠道",
|
||||
enable: "启用渠道",
|
||||
disable: "禁用渠道",
|
||||
delete: "删除渠道",
|
||||
create: "创建渠道",
|
||||
"search-model": "搜索模型",
|
||||
"fill-template-models": "填入模板模型 ({{number}} 个)",
|
||||
"add-custom-model": "添加自定义模型",
|
||||
"add-model": "添加模型",
|
||||
"clear-models": "清空全部模型",
|
||||
},
|
||||
},
|
||||
mask: {
|
||||
title: "预设设置",
|
||||
@ -373,6 +410,7 @@ const resources = {
|
||||
en: {
|
||||
translation: {
|
||||
end: ".", // end of sentence
|
||||
add: "Add",
|
||||
"not-found": "Page not found",
|
||||
home: "Home",
|
||||
login: "Login",
|
||||
@ -738,6 +776,46 @@ const resources = {
|
||||
generate: "Generate",
|
||||
"generate-result": "Generate Result",
|
||||
error: "Request Failed",
|
||||
channels: {
|
||||
id: "Channel ID",
|
||||
name: "Name",
|
||||
"name-tip": "Channel name, used to identify the channel",
|
||||
"name-placeholder": "Please enter the channel name",
|
||||
type: "Type",
|
||||
priority: "Priority",
|
||||
"priority-tip":
|
||||
"When there are multiple channels, the request is made according to the priority order, the higher the priority, the higher the priority",
|
||||
weight: "Weight",
|
||||
"weight-tip":
|
||||
"When the priority is the same, the load balancing call is performed according to the weight ratio",
|
||||
retry: "Max Retry",
|
||||
"retry-tip":
|
||||
"When the channel request fails, the maximum number of retries",
|
||||
model: "Model",
|
||||
secret: "Secret",
|
||||
"secret-placeholder":
|
||||
"Please enter the secret, format: {{format}}\nWhen there are multiple secrets, one line is selected randomly when requesting the load",
|
||||
endpoint: "Endpoint",
|
||||
"endpoint-placeholder": "Please enter the endpoint (ie proxy)",
|
||||
mapper: "Model Mapper",
|
||||
"mapper-tip":
|
||||
"Model name conversion to achieve asymmetric model request",
|
||||
"mapper-placeholder":
|
||||
"Please enter the model mapper, one line each, format: model>model\n" +
|
||||
"The former is the requested model, and the latter is the mapped model (which needs to exist in the model), separated by > in the middle",
|
||||
state: "State",
|
||||
action: "Action",
|
||||
edit: "Edit Channel",
|
||||
enable: "Enable Channel",
|
||||
disable: "Disable Channel",
|
||||
delete: "Delete Channel",
|
||||
create: "Create Channel",
|
||||
"search-model": "Search Model",
|
||||
"fill-template-models": "Fill Template Models ({{number}})",
|
||||
"add-custom-model": "Add Custom Model",
|
||||
"add-model": "Add Model",
|
||||
"clear-models": "Clear All Models",
|
||||
},
|
||||
},
|
||||
mask: {
|
||||
title: "Mask Settings",
|
||||
@ -749,6 +827,7 @@ const resources = {
|
||||
ru: {
|
||||
translation: {
|
||||
end: "",
|
||||
add: "Добавить",
|
||||
"not-found": "Страница не найдена",
|
||||
home: "Главная",
|
||||
login: "Войти",
|
||||
@ -1117,6 +1196,46 @@ const resources = {
|
||||
generate: "Генерировать",
|
||||
"generate-result": "Результат",
|
||||
error: "Ошибка запроса",
|
||||
channels: {
|
||||
id: "ID канала",
|
||||
name: "Название",
|
||||
"name-tip": "Название канала, используется для идентификации канала",
|
||||
"name-placeholder": "Введите название канала",
|
||||
type: "Тип",
|
||||
priority: "Приоритет",
|
||||
"priority-tip":
|
||||
"При наличии нескольких каналов запрос выполняется в порядке приоритета, чем выше приоритет, тем выше приоритет",
|
||||
weight: "Вес",
|
||||
"weight-tip":
|
||||
"При равном приоритете вызов балансировки нагрузки выполняется в соответствии с весовым соотношением",
|
||||
retry: "Максимальное количество попыток",
|
||||
"retry-tip":
|
||||
"При сбое запроса канала максимальное количество повторных попыток",
|
||||
model: "Модель",
|
||||
secret: "Секрет",
|
||||
"secret-placeholder":
|
||||
"Введите секрет, формат: {{format}}\nПри наличии нескольких секретов при запросе загрузки выбирается одна строка случайным образом",
|
||||
endpoint: "Конечная точка",
|
||||
"endpoint-placeholder": "Введите конечную точку (т.е. прокси)",
|
||||
mapper: "Модельный маппер",
|
||||
"mapper-tip":
|
||||
"Преобразование имени модели для достижения асимметричного запроса модели",
|
||||
"mapper-placeholder":
|
||||
"Введите модельный маппер, по одной строке, формат: model>model\n" +
|
||||
"Первая модель - запрошенная модель, вторая модель - отображаемая модель (которая должна существовать в модели), разделенная > посередине",
|
||||
state: "Статус",
|
||||
action: "Действие",
|
||||
edit: "Редактировать канал",
|
||||
enable: "Включить канал",
|
||||
disable: "Отключить канал",
|
||||
delete: "Удалить канал",
|
||||
create: "Создать канал",
|
||||
"search-model": "Поиск по имени модели",
|
||||
"fill-template-models": "Заполнить шаблонные модели ({{number}})",
|
||||
"add-custom-model": "Добавить пользовательскую модель",
|
||||
"add-model": "Добавить модель",
|
||||
"clear-models": "Очистить все модели",
|
||||
},
|
||||
},
|
||||
mask: {
|
||||
title: "Настройки маски",
|
||||
|
@ -1,5 +1,27 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ChannelSettings from "@/components/admin/ChannelSettings.tsx";
|
||||
|
||||
function Channel() {
|
||||
return <></>;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={`channel`}>
|
||||
<Card className={`channel-card`}>
|
||||
<CardHeader className={`select-none`}>
|
||||
<CardTitle>{t("admin.channel")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChannelSettings />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Channel;
|
||||
|
116
channel/channel.go
Normal file
116
channel/channel.go
Normal file
@ -0,0 +1,116 @@
|
||||
package channel
|
||||
|
||||
import (
|
||||
"chat/utils"
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var defaultMaxRetries = 1
|
||||
|
||||
func (c *Channel) GetId() int {
|
||||
return c.Id
|
||||
}
|
||||
|
||||
func (c *Channel) GetName() string {
|
||||
return c.Name
|
||||
}
|
||||
|
||||
func (c *Channel) GetType() string {
|
||||
return c.Type
|
||||
}
|
||||
|
||||
func (c *Channel) GetPriority() int {
|
||||
return c.Priority
|
||||
}
|
||||
|
||||
func (c *Channel) GetWeight() int {
|
||||
if c.Weight <= 0 {
|
||||
return 1
|
||||
}
|
||||
return c.Weight
|
||||
}
|
||||
|
||||
func (c *Channel) GetModels() []string {
|
||||
return c.Models
|
||||
}
|
||||
|
||||
func (c *Channel) GetRetry() int {
|
||||
if c.Retry <= 0 {
|
||||
return defaultMaxRetries
|
||||
}
|
||||
return c.Retry
|
||||
}
|
||||
|
||||
func (c *Channel) GetSecret() string {
|
||||
return c.Secret
|
||||
}
|
||||
|
||||
func (c *Channel) GetRandomSecret() string {
|
||||
arr := strings.Split(c.GetSecret(), "\n")
|
||||
idx := rand.Intn(len(arr))
|
||||
return arr[idx]
|
||||
}
|
||||
|
||||
func (c *Channel) GetEndpoint() string {
|
||||
return c.Endpoint
|
||||
}
|
||||
|
||||
func (c *Channel) GetMapper() string {
|
||||
return c.Mapper
|
||||
}
|
||||
|
||||
func (c *Channel) GetReflect() map[string]string {
|
||||
if c.Reflect == nil {
|
||||
var reflect map[string]string
|
||||
arr := strings.Split(c.GetMapper(), "\n")
|
||||
for _, item := range arr {
|
||||
pair := strings.Split(item, ">")
|
||||
if len(pair) == 2 {
|
||||
reflect[pair[0]] = pair[1]
|
||||
}
|
||||
}
|
||||
|
||||
c.Reflect = &reflect
|
||||
}
|
||||
|
||||
return *c.Reflect
|
||||
}
|
||||
|
||||
func (c *Channel) GetModelReflect(model string) string {
|
||||
ref := c.GetReflect()
|
||||
if reflect, ok := ref[model]; ok && len(reflect) > 0 {
|
||||
return reflect
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
func (c *Channel) GetHitModels() []string {
|
||||
if c.HitModels == nil {
|
||||
var res []string
|
||||
|
||||
models := c.GetModels()
|
||||
ref := c.GetReflect()
|
||||
|
||||
for _, model := range models {
|
||||
if !utils.Contains(model, res) {
|
||||
res = append(res, model)
|
||||
}
|
||||
}
|
||||
|
||||
for model := range ref {
|
||||
if !utils.Contains(model, res) {
|
||||
res = append(res, model)
|
||||
}
|
||||
}
|
||||
|
||||
c.HitModels = &res
|
||||
}
|
||||
|
||||
return *c.HitModels
|
||||
}
|
||||
|
||||
func (c *Channel) GetState() bool {
|
||||
return c.State
|
||||
}
|
24
channel/types.go
Normal file
24
channel/types.go
Normal file
@ -0,0 +1,24 @@
|
||||
package channel
|
||||
|
||||
type Channel struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Priority int `json:"priority"`
|
||||
Weight int `json:"weight"`
|
||||
Models []string `json:"models"`
|
||||
Retry int `json:"retry"`
|
||||
Secret string `json:"secret"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Mapper string `json:"mapper"`
|
||||
State bool `json:"state"`
|
||||
|
||||
Reflect *map[string]string `json:"reflect"`
|
||||
HitModels *[]string `json:"hit_models"`
|
||||
}
|
||||
|
||||
type Sequence []*Channel
|
||||
|
||||
type Manager struct {
|
||||
Sequence Sequence `json:"sequence"`
|
||||
}
|
@ -6,3 +6,20 @@ const (
|
||||
Assistant = "assistant"
|
||||
Tool = "tool"
|
||||
)
|
||||
|
||||
const (
|
||||
OpenAIChannelType = iota
|
||||
ClaudeChannelType
|
||||
SlackChannelType
|
||||
SparkdeskChannelType
|
||||
ChatGLMChannelType
|
||||
DashscopeChannelType
|
||||
HunyuanChannelType
|
||||
ZhinaoChannelType
|
||||
BaichuanChannelType
|
||||
SkylarkChannelType
|
||||
BingChannelType
|
||||
PalmChannelType
|
||||
MidjourneyChannelType
|
||||
OneAPIChannelType
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user