diff --git a/app/package.json b/app/package.json index ae83aa4..2e8f433 100644 --- a/app/package.json +++ b/app/package.json @@ -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", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index edf19f8..d9c6ca6 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -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 diff --git a/app/src/admin/channel.ts b/app/src/admin/channel.ts new file mode 100644 index 0000000..e4be86c --- /dev/null +++ b/app/src/admin/channel.ts @@ -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 = { + 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 = { + openai: { + id: 0, + endpoint: "https://api.openai.com", + format: "", + 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: "", + models: ["claude-instant-1", "claude-2"], + }, + slack: { + id: 2, + endpoint: "your-channel", + format: "|", + models: ["claude-slack"], + }, + sparkdesk: { + id: 3, + endpoint: "wss://spark-api.xf-yun.com", + format: "||", + models: ["spark-desk-v1.5", "spark-desk-v2", "spark-desk-v3"], + }, + chatglm: { + id: 4, + endpoint: "https://open.bigmodel.cn", + format: "", + models: [ + "zhipu-chatglm-turbo", + "zhipu-chatglm-pro", + "zhipu-chatglm-std", + "zhipu-chatglm-lite", + ], + }, + qwen: { + id: 5, + endpoint: "https://dashscope.aliyuncs.com", + format: "", + models: ["qwen-turbo", "qwen-plus", "qwen-turbo-net", "qwen-plus-net"], + }, + hunyuan: { + id: 6, + endpoint: "https://hunyuan.cloud.tencent.com", + format: "||", + models: ["hunyuan"], + // endpoint + }, + zhinao: { + id: 7, + endpoint: "https://api.360.cn", + format: "", + models: ["360-gpt-v9"], + }, + baichuan: { + id: 8, + endpoint: "https://api.baichuan-ai.com", + format: "", + models: ["baichuan-53b"], + }, + skylark: { + id: 9, + endpoint: "https://maas-api.ml-platform-cn-beijing.volces.com", + format: "|", + models: [ + "skylark-lite-public", + "skylark-plus-public", + "skylark-pro-public", + "skylark-chat", + ], + }, + bing: { + id: 10, + endpoint: "wss://your.bing.service", + format: "", + 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: "", + models: ["chat-bison-001"], + }, + midjourney: { + id: 12, + endpoint: "https://your.midjourney.proxy", + format: "|", + 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: "", + models: [], + }, +}; + +export const ChannelModels: string[] = Object.values(ChannelInfos).flatMap( + (info) => info.models, +); diff --git a/app/src/assets/admin/all.less b/app/src/assets/admin/all.less index 1dba375..5adf7ba 100644 --- a/app/src/assets/admin/all.less +++ b/app/src/assets/admin/all.less @@ -2,6 +2,7 @@ @import "dashboard"; @import "management"; @import "broadcast"; +@import "channel"; .admin-page { position: relative; diff --git a/app/src/assets/admin/channel.less b/app/src/assets/admin/channel.less new file mode 100644 index 0000000..518d313 --- /dev/null +++ b/app/src/assets/admin/channel.less @@ -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; +} diff --git a/app/src/assets/pages/settings.less b/app/src/assets/pages/settings.less index 1b09819..37e3a27 100644 --- a/app/src/assets/pages/settings.less +++ b/app/src/assets/pages/settings.less @@ -62,7 +62,7 @@ input { text-align: center; - max-width: 3.5rem; + max-width: 4rem; max-height: 1.75rem; } diff --git a/app/src/assets/ui.less b/app/src/assets/ui.less index 2968a62..fc1cb58 100644 --- a/app/src/assets/ui.less +++ b/app/src/assets/ui.less @@ -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); + } } diff --git a/app/src/components/Markdown.tsx b/app/src/components/Markdown.tsx index d2377e2..31254d1 100644 --- a/app/src/components/Markdown.tsx +++ b/app/src/components/Markdown.tsx @@ -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 = { rs: "rust", }; +function getSocialIcon(url: string) { + const { hostname } = new URL(url); + + if (hostname.includes("github.com")) + return ; + if (hostname.includes("twitter.com")) + return ; + if (hostname.includes("youtube.com")) + return ; + if (hostname.includes("codepen.io")) + return ; + if (hostname.includes("codesandbox.io")) + return ; +} + 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} ); diff --git a/app/src/components/OperationAction.tsx b/app/src/components/OperationAction.tsx new file mode 100644 index 0000000..26f93eb --- /dev/null +++ b/app/src/components/OperationAction.tsx @@ -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 ( + + + + {variant === "destructive" ? ( + + + + + + + + + ) : ( + + )} + + {tooltip} + + + ); +} + +export default OperationAction; diff --git a/app/src/components/Require.tsx b/app/src/components/Require.tsx new file mode 100644 index 0000000..2ea0156 --- /dev/null +++ b/app/src/components/Require.tsx @@ -0,0 +1,5 @@ +function Required() { + return *; +} + +export default Required; diff --git a/app/src/components/SelectGroup.tsx b/app/src/components/SelectGroup.tsx index 3d45f95..778aebe 100644 --- a/app/src/components/SelectGroup.tsx +++ b/app/src/components/SelectGroup.tsx @@ -114,8 +114,6 @@ function SelectGroupMobile(props: SelectGroupProps) { {props.list.map((select: SelectItemProps, idx: number) => ( diff --git a/app/src/components/admin/ChannelSettings.tsx b/app/src/components/admin/ChannelSettings.tsx new file mode 100644 index 0000000..35ffb26 --- /dev/null +++ b/app/src/components/admin/ChannelSettings.tsx @@ -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(false); + + return !enabled ? ( + + ) : ( + + ); +} + +export default ChannelSettings; diff --git a/app/src/components/admin/MenuBar.tsx b/app/src/components/admin/MenuBar.tsx index 25943f5..427adb3 100644 --- a/app/src/components/admin/MenuBar.tsx +++ b/app/src/components/admin/MenuBar.tsx @@ -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"; diff --git a/app/src/components/admin/assemblies/ChannelEditor.tsx b/app/src/components/admin/assemblies/ChannelEditor.tsx new file mode 100644 index 0000000..55797d6 --- /dev/null +++ b/app/src/components/admin/assemblies/ChannelEditor.tsx @@ -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 ( +
+ setModel(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") post(); + }} + /> + +
+ ); +} + +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 ( +
+
+
+
+ + {t("admin.channels.name")} + +
+ dispatch({ type: "name", value: e.target.value })} + /> +
+
+
+ + {t("admin.channels.type")} +
+ + {info.description && ( + + {info.description} + + )} +
+
+
+ + {t("admin.channels.model")} +
+
+ {edit.models.map((model: string, idx: number) => ( +
+ {model} + + dispatch({ type: "remove-model", value: model }) + } + /> +
+ ))} +
+
+ + + + + + + + + {unusedModels.map((model, idx) => ( + + dispatch({ type: "add-model", value: model }) + } + className={`px-2`} + > + {model} + + ))} + + + + + { + dispatch({ type: "add-model", value: model }); + }} + /> + + +
+
+
+
+ + {t("admin.channels.secret")} +
+