diff --git a/.claude/settings.json b/.claude/settings.json index 509dbe8447..72dcb5ec73 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -5,5 +5,18 @@ "typescript-lsp@claude-plugins-official": true, "pyright-lsp@claude-plugins-official": true, "ralph-loop@claude-plugins-official": true + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "npx -y block-no-verify@1.1.1" + } + ] + } + ] } } diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 7af24b7ddb..0000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22.11.0 diff --git a/api/pyproject.toml b/api/pyproject.toml index dbc6a2eb83..7d2d68bc8d 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -189,7 +189,7 @@ storage = [ "opendal~=0.46.0", "oss2==2.18.5", "supabase~=2.18.1", - "tos~=2.7.1", + "tos~=2.9.0", ] ############################################################ diff --git a/api/uv.lock b/api/uv.lock index 8e60fad3a7..a999c4ee18 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1731,7 +1731,7 @@ storage = [ { name = "opendal", specifier = "~=0.46.0" }, { name = "oss2", specifier = "==2.18.5" }, { name = "supabase", specifier = "~=2.18.1" }, - { name = "tos", specifier = "~=2.7.1" }, + { name = "tos", specifier = "~=2.9.0" }, ] tools = [ { name = "cloudscraper", specifier = "~=1.2.71" }, @@ -6148,7 +6148,7 @@ wheels = [ [[package]] name = "tos" -version = "2.7.2" +version = "2.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "crcmod" }, @@ -6156,8 +6156,9 @@ dependencies = [ { name = "pytz" }, { name = "requests" }, { name = "six" }, + { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/01/f811af86f1f80d5f289be075c3b281e74bf3fe081cfbe5cfce44954d2c3a/tos-2.7.2.tar.gz", hash = "sha256:3c31257716785bca7b2cac51474ff32543cda94075a7b7aff70d769c15c7b7ed", size = 123407, upload-time = "2024-10-16T15:59:08.634Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/b3/13451226f564f88d9db2323e9b7eabcced792a0ad5ee1e333751a7634257/tos-2.9.0.tar.gz", hash = "sha256:861cfc348e770f099f911cb96b2c41774ada6c9c51b7a89d97e0c426074dd99e", size = 157071, upload-time = "2026-01-06T04:13:08.921Z" } [[package]] name = "tqdm" @@ -7146,31 +7147,31 @@ wheels = [ [[package]] name = "wrapt" -version = "1.17.3" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/4c/063a912e20bcef7124e0df97282a8af3ff3e4b603ce84c481d6d7346be0a/wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", size = 53972, upload-time = "2023-11-09T06:33:30.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, - { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, - { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, - { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, - { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, - { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, - { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, - { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, - { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, - { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, - { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/fd/03/c188ac517f402775b90d6f312955a5e53b866c964b32119f2ed76315697e/wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", size = 37313, upload-time = "2023-11-09T06:31:52.168Z" }, + { url = "https://files.pythonhosted.org/packages/0f/16/ea627d7817394db04518f62934a5de59874b587b792300991b3c347ff5e0/wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", size = 38164, upload-time = "2023-11-09T06:31:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a7/f1212ba098f3de0fd244e2de0f8791ad2539c03bef6c05a9fcb03e45b089/wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", size = 80890, upload-time = "2023-11-09T06:31:55.247Z" }, + { url = "https://files.pythonhosted.org/packages/b7/96/bb5e08b3d6db003c9ab219c487714c13a237ee7dcc572a555eaf1ce7dc82/wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", size = 73118, upload-time = "2023-11-09T06:31:57.023Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/2da48b35193e39ac53cfb141467d9f259851522d0e8c87153f0ba4205fb1/wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", size = 80746, upload-time = "2023-11-09T06:31:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/11/fb/18ec40265ab81c0e82a934de04596b6ce972c27ba2592c8b53d5585e6bcd/wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", size = 85668, upload-time = "2023-11-09T06:31:59.992Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ef/0ecb1fa23145560431b970418dce575cfaec555ab08617d82eb92afc7ccf/wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", size = 78556, upload-time = "2023-11-09T06:32:01.942Z" }, + { url = "https://files.pythonhosted.org/packages/25/62/cd284b2b747f175b5a96cbd8092b32e7369edab0644c45784871528eb852/wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", size = 85712, upload-time = "2023-11-09T06:32:03.686Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a7/47b7ff74fbadf81b696872d5ba504966591a3468f1bc86bca2f407baef68/wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", size = 35327, upload-time = "2023-11-09T06:32:05.284Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c3/0084351951d9579ae83a3d9e38c140371e4c6b038136909235079f2e6e78/wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", size = 37523, upload-time = "2023-11-09T06:32:07.17Z" }, + { url = "https://files.pythonhosted.org/packages/92/17/224132494c1e23521868cdd57cd1e903f3b6a7ba6996b7b8f077ff8ac7fe/wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", size = 37614, upload-time = "2023-11-09T06:32:08.859Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d7/cfcd73e8f4858079ac59d9db1ec5a1349bc486ae8e9ba55698cc1f4a1dff/wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", size = 38316, upload-time = "2023-11-09T06:32:10.719Z" }, + { url = "https://files.pythonhosted.org/packages/7e/79/5ff0a5c54bda5aec75b36453d06be4f83d5cd4932cc84b7cb2b52cee23e2/wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", size = 86322, upload-time = "2023-11-09T06:32:12.592Z" }, + { url = "https://files.pythonhosted.org/packages/c4/81/e799bf5d419f422d8712108837c1d9bf6ebe3cb2a81ad94413449543a923/wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", size = 79055, upload-time = "2023-11-09T06:32:14.394Z" }, + { url = "https://files.pythonhosted.org/packages/62/62/30ca2405de6a20448ee557ab2cd61ab9c5900be7cbd18a2639db595f0b98/wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", size = 87291, upload-time = "2023-11-09T06:32:16.201Z" }, + { url = "https://files.pythonhosted.org/packages/49/4e/5d2f6d7b57fc9956bf06e944eb00463551f7d52fc73ca35cfc4c2cdb7aed/wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", size = 90374, upload-time = "2023-11-09T06:32:18.052Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/c2c21b44ff5b9bf14a83252a8b973fb84923764ff63db3e6dfc3895cf2e0/wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", size = 83896, upload-time = "2023-11-09T06:32:19.533Z" }, + { url = "https://files.pythonhosted.org/packages/14/26/93a9fa02c6f257df54d7570dfe8011995138118d11939a4ecd82cb849613/wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", size = 91738, upload-time = "2023-11-09T06:32:20.989Z" }, + { url = "https://files.pythonhosted.org/packages/a2/5b/4660897233eb2c8c4de3dc7cefed114c61bacb3c28327e64150dc44ee2f6/wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", size = 35568, upload-time = "2023-11-09T06:32:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cc/8297f9658506b224aa4bd71906447dea6bb0ba629861a758c28f67428b91/wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", size = 37653, upload-time = "2023-11-09T06:32:24.533Z" }, + { url = "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", size = 23362, upload-time = "2023-11-09T06:33:28.271Z" }, ] [[package]] diff --git a/web/.nvmrc b/web/.nvmrc new file mode 100644 index 0000000000..5767036af0 --- /dev/null +++ b/web/.nvmrc @@ -0,0 +1 @@ +22.21.1 diff --git a/web/README.md b/web/README.md index 7f5740a471..ae4338d7be 100644 --- a/web/README.md +++ b/web/README.md @@ -11,6 +11,16 @@ Before starting the web frontend service, please make sure the following environ - [Node.js](https://nodejs.org) >= v22.11.x - [pnpm](https://pnpm.io) v10.x +> [!TIP] +> It is recommended to install and enable Corepack to manage package manager versions automatically: +> +> ```bash +> npm install -g corepack +> corepack enable +> ``` +> +> Learn more: [Corepack](https://github.com/nodejs/corepack#readme) + First, install the dependencies: ```bash diff --git a/web/app/components/app/configuration/config-var/index.tsx b/web/app/components/app/configuration/config-var/index.tsx index 4a38fc92a6..6142008ae2 100644 --- a/web/app/components/app/configuration/config-var/index.tsx +++ b/web/app/components/app/configuration/config-var/index.tsx @@ -274,9 +274,9 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar )} {hasVar && ( -
+
{ onPromptVariablesChange?.(list.map(item => item.variable)) }} handle=".handle" diff --git a/web/app/components/app/configuration/config-var/var-item.tsx b/web/app/components/app/configuration/config-var/var-item.tsx index 1fc21e3d33..b26249dac8 100644 --- a/web/app/components/app/configuration/config-var/var-item.tsx +++ b/web/app/components/app/configuration/config-var/var-item.tsx @@ -39,7 +39,7 @@ const VarItem: FC = ({ const [isDeleting, setIsDeleting] = useState(false) return ( -
+
{canDrag && ( diff --git a/web/app/components/app/configuration/config-vision/index.tsx b/web/app/components/app/configuration/config-vision/index.tsx index bc313b9ac1..481e6b5ab6 100644 --- a/web/app/components/app/configuration/config-vision/index.tsx +++ b/web/app/components/app/configuration/config-vision/index.tsx @@ -1,5 +1,6 @@ 'use client' import type { FC } from 'react' +import { noop } from 'es-toolkit/function' import { produce } from 'immer' import * as React from 'react' import { useCallback } from 'react' @@ -10,14 +11,17 @@ import { useFeatures, useFeaturesStore } from '@/app/components/base/features/ho import { Vision } from '@/app/components/base/icons/src/vender/features' import Switch from '@/app/components/base/switch' import Tooltip from '@/app/components/base/tooltip' +import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card' import { SupportUploadFileTypes } from '@/app/components/workflow/types' // import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card' import ConfigContext from '@/context/debug-configuration' +import { Resolution } from '@/types/app' +import { cn } from '@/utils/classnames' import ParamConfig from './param-config' const ConfigVision: FC = () => { const { t } = useTranslation() - const { isShowVisionConfig, isAllowVideoUpload } = useContext(ConfigContext) + const { isShowVisionConfig, isAllowVideoUpload, readonly } = useContext(ConfigContext) const file = useFeatures(s => s.features.file) const featuresStore = useFeaturesStore() @@ -54,7 +58,7 @@ const ConfigVision: FC = () => { setFeatures(newFeatures) }, [featuresStore, isAllowVideoUpload]) - if (!isShowVisionConfig) + if (!isShowVisionConfig || (readonly && !isImageEnabled)) return null return ( @@ -75,37 +79,55 @@ const ConfigVision: FC = () => { />
- {/*
-
{t('appDebug.vision.visionSettings.resolution')}
- - {t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => ( -
{item}
- ))} -
- } - /> -
*/} - {/*
- handleChange(Resolution.high)} - /> - handleChange(Resolution.low)} - /> -
*/} - -
- + {readonly + ? ( + <> +
+
{t('vision.visionSettings.resolution', { ns: 'appDebug' })}
+ + {t('vision.visionSettings.resolutionTooltip', { ns: 'appDebug' }).split('\n').map(item => ( +
{item}
+ ))} +
+ )} + /> +
+
+ + +
+ + ) + : ( + <> + +
+ + + )} +
) diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index 02179822c9..3378afb944 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -40,7 +40,7 @@ type AgentToolWithMoreInfo = AgentTool & { icon: any, collection?: Collection } const AgentTools: FC = () => { const { t } = useTranslation() const [isShowChooseTool, setIsShowChooseTool] = useState(false) - const { modelConfig, setModelConfig } = useContext(ConfigContext) + const { readonly, modelConfig, setModelConfig } = useContext(ConfigContext) const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() @@ -168,10 +168,10 @@ const AgentTools: FC = () => { {tools.filter(item => !!item.enabled).length} / {tools.length} -  +   {t('agent.tools.enabled', { ns: 'appDebug' })} - {tools.length < MAX_TOOLS_NUM && ( + {tools.length < MAX_TOOLS_NUM && !readonly && ( <>
{ )} > -
+
{tools.map((item: AgentTool & { icon: any, collection?: Collection }, index) => (
{ > {getProviderShowName(item)} {item.tool_label} - {!item.isDeleted && ( + {!item.isDeleted && !readonly && ( @@ -260,7 +260,7 @@ const AgentTools: FC = () => {
)} - {!item.isDeleted && ( + {!item.isDeleted && !readonly && (
{!item.notAuthor && ( { {!item.notAuthor && ( { const newModelConfig = produce(modelConfig, (draft) => { @@ -313,6 +313,7 @@ const AgentTools: FC = () => { {item.notAuthor && (
-
-
- -
+ {!readonly && ( +
+
+ +
+ )}
) } diff --git a/web/app/components/app/configuration/config/config-document.tsx b/web/app/components/app/configuration/config/config-document.tsx index 3f192fd401..7d48c1582a 100644 --- a/web/app/components/app/configuration/config/config-document.tsx +++ b/web/app/components/app/configuration/config/config-document.tsx @@ -17,7 +17,7 @@ const ConfigDocument: FC = () => { const { t } = useTranslation() const file = useFeatures(s => s.features.file) const featuresStore = useFeaturesStore() - const { isShowDocumentConfig } = useContext(ConfigContext) + const { isShowDocumentConfig, readonly } = useContext(ConfigContext) const isDocumentEnabled = file?.allowed_file_types?.includes(SupportUploadFileTypes.document) ?? false @@ -45,7 +45,7 @@ const ConfigDocument: FC = () => { setFeatures(newFeatures) }, [featuresStore]) - if (!isShowDocumentConfig) + if (!isShowDocumentConfig || (readonly && !isDocumentEnabled)) return null return ( @@ -65,14 +65,16 @@ const ConfigDocument: FC = () => { )} /> -
-
- -
+ {!readonly && ( +
+
+ +
+ )} ) } diff --git a/web/app/components/app/configuration/config/index.tsx b/web/app/components/app/configuration/config/index.tsx index f208b99e59..3e2b201172 100644 --- a/web/app/components/app/configuration/config/index.tsx +++ b/web/app/components/app/configuration/config/index.tsx @@ -18,6 +18,7 @@ import ConfigDocument from './config-document' const Config: FC = () => { const { + readonly, mode, isAdvancedMode, modelModeType, @@ -27,6 +28,7 @@ const Config: FC = () => { modelConfig, setModelConfig, setPrevPromptConfig, + dataSets, } = useContext(ConfigContext) const isChatApp = [AppModeEnum.ADVANCED_CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.CHAT].includes(mode) const formattingChangedDispatcher = useFormattingChangedDispatcher() @@ -65,19 +67,27 @@ const Config: FC = () => { promptTemplate={promptTemplate} promptVariables={promptVariables} onChange={handlePromptChange} + readonly={readonly} /> {/* Variables */} - + {!(readonly && promptVariables.length === 0) && ( + + )} {/* Dataset */} - - + {!(readonly && dataSets.length === 0) && ( + + )} {/* Tools */} - {isAgent && ( + {isAgent && !(readonly && modelConfig.agentConfig.tools.length === 0) && ( )} @@ -88,7 +98,7 @@ const Config: FC = () => { {/* Chat History */} - {isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && ( + {!readonly && isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && ( = ({ config, onSave, onRemove, + readonly = false, editable = true, }) => { const media = useBreakpoints() @@ -56,6 +57,7 @@ const Item: FC = ({
@@ -70,7 +72,7 @@ const Item: FC = ({
{ - editable && ( + editable && !readonly && ( { e.stopPropagation() @@ -81,14 +83,18 @@ const Item: FC = ({ ) } - onRemove(config.id)} - state={isDeleting ? ActionButtonState.Destructive : ActionButtonState.Default} - onMouseEnter={() => setIsDeleting(true)} - onMouseLeave={() => setIsDeleting(false)} - > - - + { + !readonly && ( + onRemove(config.id)} + state={isDeleting ? ActionButtonState.Destructive : ActionButtonState.Default} + onMouseEnter={() => setIsDeleting(true)} + onMouseLeave={() => setIsDeleting(false)} + > + + + ) + }
{ config.indexing_technique && ( @@ -107,11 +113,13 @@ const Item: FC = ({ ) } setShowSettingsModal(false)} footer={null} mask={isMobile} panelClassName="mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl"> - setShowSettingsModal(false)} - onSave={handleSave} - /> + {showSettingsModal && ( + setShowSettingsModal(false)} + onSave={handleSave} + /> + )}
) diff --git a/web/app/components/app/configuration/dataset-config/index.tsx b/web/app/components/app/configuration/dataset-config/index.tsx index 309c6e7ddb..6de77cad9e 100644 --- a/web/app/components/app/configuration/dataset-config/index.tsx +++ b/web/app/components/app/configuration/dataset-config/index.tsx @@ -30,6 +30,7 @@ import { import { useSelector as useAppContextSelector } from '@/context/app-context' import ConfigContext from '@/context/debug-configuration' import { AppModeEnum } from '@/types/app' +import { cn } from '@/utils/classnames' import { hasEditPermissionForDataset } from '@/utils/permission' import FeaturePanel from '../base/feature-panel' import OperationBtn from '../base/operation-btn' @@ -38,7 +39,11 @@ import CardItem from './card-item' import ContextVar from './context-var' import ParamsConfig from './params-config' -const DatasetConfig: FC = () => { +type Props = { + readonly?: boolean + hideMetadataFilter?: boolean +} +const DatasetConfig: FC = ({ readonly, hideMetadataFilter }) => { const { t } = useTranslation() const userProfile = useAppContextSelector(s => s.userProfile) const { @@ -259,17 +264,19 @@ const DatasetConfig: FC = () => { className="mt-2" title={t('feature.dataSet.title', { ns: 'appDebug' })} headerRight={( -
- {!isAgent && } - -
+ !readonly && ( +
+ {!isAgent && } + +
+ ) )} hasHeaderBottomBorder={!hasData} noBodySpacing > {hasData ? ( -
+
{formattedDataset.map(item => ( { onRemove={onRemove} onSave={handleSave} editable={item.editable} + readonly={readonly} /> ))}
@@ -287,27 +295,29 @@ const DatasetConfig: FC = () => {
)} -
- item.type === MetadataFilteringVariableType.string || item.type === MetadataFilteringVariableType.select)} - availableCommonNumberVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.number)} - /> -
+ {!hideMetadataFilter && ( +
+ item.type === MetadataFilteringVariableType.string || item.type === MetadataFilteringVariableType.select)} + availableCommonNumberVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.number)} + /> +
+ )} - {mode === AppModeEnum.COMPLETION && dataSet.length > 0 && ( + {!readonly && mode === AppModeEnum.COMPLETION && dataSet.length > 0 && ( { const { t } = useTranslation() - const { modelConfig, setInputs } = useContext(ConfigContext) + const { modelConfig, setInputs, readonly } = useContext(ConfigContext) const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => { return key && key?.trim() && name && name?.trim() @@ -89,6 +89,7 @@ const ChatUserInput = ({ placeholder={name} autoFocus={index === 0} maxLength={max_length || DEFAULT_VALUE_MAX_LEN} + readOnly={readonly} /> )} {type === 'paragraph' && ( @@ -97,6 +98,7 @@ const ChatUserInput = ({ placeholder={name} value={inputs[key] ? `${inputs[key]}` : ''} onChange={(e) => { handleInputValueChange(key, e.target.value) }} + readOnly={readonly} /> )} {type === 'select' && ( @@ -106,6 +108,7 @@ const ChatUserInput = ({ onSelect={(i) => { handleInputValueChange(key, i.value as string) }} items={(options || []).map(i => ({ name: i, value: i }))} allowSearch={false} + disabled={readonly} /> )} {type === 'number' && ( @@ -116,6 +119,7 @@ const ChatUserInput = ({ placeholder={name} autoFocus={index === 0} maxLength={max_length || DEFAULT_VALUE_MAX_LEN} + readOnly={readonly} /> )} {type === 'checkbox' && ( @@ -124,6 +128,7 @@ const ChatUserInput = ({ value={!!inputs[key]} required={required} onChange={(value) => { handleInputValueChange(key, value) }} + readonly={readonly} /> )} diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx index d7918e7ad6..eb18ca45b1 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx @@ -15,6 +15,7 @@ import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/ import { useDebugConfigurationContext } from '@/context/debug-configuration' import { useEventEmitterContextContext } from '@/context/event-emitter' import { useProviderContext } from '@/context/provider-context' +import { AppSourceType } from '@/service/share' import { promptVariablesToUserInputsForm } from '@/utils/model-config' import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../types' @@ -130,11 +131,11 @@ const TextGenerationItem: FC = ({ return ( { const { userProfile } = useAppContext() const { + readonly, modelConfig, appId, inputs, @@ -150,6 +151,7 @@ const DebugWithSingleModel = ( return ( = ({ }) => { const { t } = useTranslation() const { + readonly, appId, mode, modelModeType, @@ -416,25 +418,33 @@ const Debug: FC = ({ } {mode !== AppModeEnum.COMPLETION && ( <> - - - - - - {varList.length > 0 && ( -
+ { + !readonly && ( - setExpanded(!expanded)}> - + + + - {expanded &&
} -
- )} + ) + } + + { + varList.length > 0 && ( +
+ + !readonly && setExpanded(!expanded)}> + + + + {expanded &&
} +
+ ) + } )}
@@ -444,19 +454,21 @@ const Debug: FC = ({
)} - {mode === AppModeEnum.COMPLETION && ( - - )} + { + mode === AppModeEnum.COMPLETION && ( + + ) + } { debugWithMultipleModel && ( @@ -510,12 +522,12 @@ const Debug: FC = ({
= ({
) } - {isShowFormattingChangeConfirm && ( - - )} - {!isAPIKeySet && ()} + { + isShowFormattingChangeConfirm && ( + + ) + } + {!isAPIKeySet && !readonly && ()} ) } diff --git a/web/app/components/app/configuration/prompt-value-panel/index.tsx b/web/app/components/app/configuration/prompt-value-panel/index.tsx index 9b61b3c7aa..8aade70458 100644 --- a/web/app/components/app/configuration/prompt-value-panel/index.tsx +++ b/web/app/components/app/configuration/prompt-value-panel/index.tsx @@ -41,7 +41,7 @@ const PromptValuePanel: FC = ({ onVisionFilesChange, }) => { const { t } = useTranslation() - const { modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext) + const { readonly, modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext) const [userInputFieldCollapse, setUserInputFieldCollapse] = useState(false) const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => { return key && key?.trim() && name && name?.trim() @@ -79,12 +79,12 @@ const PromptValuePanel: FC = ({ if (isAdvancedMode) { if (modelModeType === ModelModeType.chat) - return chatPromptConfig.prompt.every(({ text }) => !text) + return chatPromptConfig?.prompt.every(({ text }) => !text) return !completionPromptConfig.prompt?.text } else { return !modelConfig.configs.prompt_template } - }, [chatPromptConfig.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType]) + }, [chatPromptConfig?.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType]) const handleInputValueChange = (key: string, value: string | boolean) => { if (!(key in promptVariableObj)) @@ -143,6 +143,7 @@ const PromptValuePanel: FC = ({ placeholder={name} autoFocus={index === 0} maxLength={max_length || DEFAULT_VALUE_MAX_LEN} + readOnly={readonly} /> )} {type === 'paragraph' && ( @@ -151,6 +152,7 @@ const PromptValuePanel: FC = ({ placeholder={name} value={inputs[key] ? `${inputs[key]}` : ''} onChange={(e) => { handleInputValueChange(key, e.target.value) }} + readOnly={readonly} /> )} {type === 'select' && ( @@ -161,6 +163,7 @@ const PromptValuePanel: FC = ({ items={(options || []).map(i => ({ name: i, value: i }))} allowSearch={false} bgClassName="bg-gray-50" + disabled={readonly} /> )} {type === 'number' && ( @@ -171,6 +174,7 @@ const PromptValuePanel: FC = ({ placeholder={name} autoFocus={index === 0} maxLength={max_length || DEFAULT_VALUE_MAX_LEN} + readOnly={readonly} /> )} {type === 'checkbox' && ( @@ -179,6 +183,7 @@ const PromptValuePanel: FC = ({ value={!!inputs[key]} required={required} onChange={(value) => { handleInputValueChange(key, value) }} + readonly={readonly} /> )} @@ -197,6 +202,7 @@ const PromptValuePanel: FC = ({ url: fileItem.url, upload_file_id: fileItem.fileId, })))} + disabled={readonly} /> @@ -205,12 +211,12 @@ const PromptValuePanel: FC = ({ )} {!userInputFieldCollapse && (
- + {canNotRun && (
diff --git a/web/app/components/app/create-app-dialog/app-card/index.spec.tsx b/web/app/components/app/create-app-dialog/app-card/index.spec.tsx index e1f9773ac3..82e4fb8f94 100644 --- a/web/app/components/app/create-app-dialog/app-card/index.spec.tsx +++ b/web/app/components/app/create-app-dialog/app-card/index.spec.tsx @@ -10,6 +10,7 @@ vi.mock('@heroicons/react/20/solid', () => ({ })) const mockApp: App = { + can_trial: true, app: { id: 'test-app-id', mode: AppModeEnum.CHAT, diff --git a/web/app/components/app/create-app-dialog/app-card/index.tsx b/web/app/components/app/create-app-dialog/app-card/index.tsx index 695faed5e0..15cfbd5411 100644 --- a/web/app/components/app/create-app-dialog/app-card/index.tsx +++ b/web/app/components/app/create-app-dialog/app-card/index.tsx @@ -1,9 +1,14 @@ 'use client' import type { App } from '@/models/explore' import { PlusIcon } from '@heroicons/react/20/solid' +import { RiInformation2Line } from '@remixicon/react' +import { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import { useContextSelector } from 'use-context-selector' import AppIcon from '@/app/components/base/app-icon' import Button from '@/app/components/base/button' +import AppListContext from '@/context/app-list-context' +import { useGlobalPublicStore } from '@/context/global-public-context' import { cn } from '@/utils/classnames' import { AppTypeIcon, AppTypeLabel } from '../../type-selector' @@ -20,6 +25,14 @@ const AppCard = ({ }: AppCardProps) => { const { t } = useTranslation() const { app: appBasicInfo } = app + const { systemFeatures } = useGlobalPublicStore() + const isTrialApp = app.can_trial && systemFeatures.enable_trial_app + const setShowTryAppPanel = useContextSelector(AppListContext, ctx => ctx.setShowTryAppPanel) + const showTryAPPPanel = useCallback((appId: string) => { + return () => { + setShowTryAppPanel?.(true, { appId, app }) + } + }, [setShowTryAppPanel, app.category]) return (
@@ -51,11 +64,17 @@ const AppCard = ({
{canCreate && ( )} diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 410953ccf7..5197a02bb3 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -39,6 +39,7 @@ import { useAppContext } from '@/context/app-context' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useTimestamp from '@/hooks/use-timestamp' import { fetchChatMessages, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log' +import { AppSourceType } from '@/service/share' import { useChatConversationDetail, useCompletionConversationDetail } from '@/service/use-log' import { AppModeEnum } from '@/types/app' import { cn } from '@/utils/classnames' @@ -638,12 +639,12 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
item.from_source === 'admin')} onFeedback={feedback => onFeedback(detail.message.id, feedback)} diff --git a/web/app/components/app/text-generate/item/index.tsx b/web/app/components/app/text-generate/item/index.tsx index 78f4f426f5..c39282a022 100644 --- a/web/app/components/app/text-generate/item/index.tsx +++ b/web/app/components/app/text-generate/item/index.tsx @@ -29,7 +29,7 @@ import { Markdown } from '@/app/components/base/markdown' import NewAudioButton from '@/app/components/base/new-audio-button' import Toast from '@/app/components/base/toast' import { fetchTextGenerationMessage } from '@/service/debug' -import { fetchMoreLikeThis, updateFeedback } from '@/service/share' +import { AppSourceType, fetchMoreLikeThis, updateFeedback } from '@/service/share' import { cn } from '@/utils/classnames' import ResultTab from './result-tab' @@ -53,7 +53,7 @@ export type IGenerationItemProps = { onFeedback?: (feedback: FeedbackType) => void onSave?: (messageId: string) => void isMobile?: boolean - isInstalledApp: boolean + appSourceType: AppSourceType installedAppId?: string taskId?: string controlClearMoreLikeThis?: number @@ -87,7 +87,7 @@ const GenerationItem: FC = ({ onSave, depth = 1, isMobile, - isInstalledApp, + appSourceType, installedAppId, taskId, controlClearMoreLikeThis, @@ -100,6 +100,7 @@ const GenerationItem: FC = ({ const { t } = useTranslation() const params = useParams() const isTop = depth === 1 + const isTryApp = appSourceType === AppSourceType.tryApp const [completionRes, setCompletionRes] = useState('') const [childMessageId, setChildMessageId] = useState(null) const [childFeedback, setChildFeedback] = useState({ @@ -113,7 +114,7 @@ const GenerationItem: FC = ({ const setShowPromptLogModal = useAppStore(s => s.setShowPromptLogModal) const handleFeedback = async (childFeedback: FeedbackType) => { - await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId) + await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, appSourceType, installedAppId) setChildFeedback(childFeedback) } @@ -131,7 +132,7 @@ const GenerationItem: FC = ({ onSave, isShowTextToSpeech, isMobile, - isInstalledApp, + appSourceType, installedAppId, controlClearMoreLikeThis, isWorkflow, @@ -145,7 +146,7 @@ const GenerationItem: FC = ({ return } startQuerying() - const res: any = await fetchMoreLikeThis(messageId as string, isInstalledApp, installedAppId) + const res: any = await fetchMoreLikeThis(messageId as string, appSourceType, installedAppId) setCompletionRes(res.answer) setChildFeedback({ rating: null, @@ -310,7 +311,7 @@ const GenerationItem: FC = ({ )} {/* action buttons */}
- {!isInWebApp && !isInstalledApp && !isResponding && ( + {!isInWebApp && (appSourceType !== AppSourceType.installedApp) && !isResponding && (
@@ -319,12 +320,12 @@ const GenerationItem: FC = ({
)}
- {moreLikeThis && ( + {moreLikeThis && !isTryApp && ( )} - {isShowTextToSpeech && ( + {isShowTextToSpeech && !isTryApp && ( = ({ )} - {isInWebApp && !isWorkflow && ( + {isInWebApp && !isWorkflow && !isTryApp && ( { onSave?.(messageId as string) }}> )}
- {(supportFeedback || isInWebApp) && !isWorkflow && !isError && messageId && ( + {(supportFeedback || isInWebApp) && !isWorkflow && !isTryApp && !isError && messageId && (
{!feedback?.rating && ( <> diff --git a/web/app/components/apps/index.tsx b/web/app/components/apps/index.tsx index b151df1e1f..255bfbf9c5 100644 --- a/web/app/components/apps/index.tsx +++ b/web/app/components/apps/index.tsx @@ -1,7 +1,17 @@ 'use client' +import type { CreateAppModalProps } from '../explore/create-app-modal' +import type { CurrentTryAppParams } from '@/context/explore-context' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useEducationInit } from '@/app/education-apply/hooks' +import AppListContext from '@/context/app-list-context' import useDocumentTitle from '@/hooks/use-document-title' +import { useImportDSL } from '@/hooks/use-import-dsl' +import { DSLImportMode } from '@/models/app' +import { fetchAppDetail } from '@/service/explore' +import DSLConfirmModal from '../app/create-from-dsl-modal/dsl-confirm-modal' +import CreateAppModal from '../explore/create-app-modal' +import TryApp from '../explore/try-app' import List from './list' const Apps = () => { @@ -10,10 +20,124 @@ const Apps = () => { useDocumentTitle(t('menus.apps', { ns: 'common' })) useEducationInit() + const [currentTryAppParams, setCurrentTryAppParams] = useState(undefined) + const currApp = currentTryAppParams?.app + const [isShowTryAppPanel, setIsShowTryAppPanel] = useState(false) + const hideTryAppPanel = useCallback(() => { + setIsShowTryAppPanel(false) + }, []) + const setShowTryAppPanel = (showTryAppPanel: boolean, params?: CurrentTryAppParams) => { + if (showTryAppPanel) + setCurrentTryAppParams(params) + else + setCurrentTryAppParams(undefined) + setIsShowTryAppPanel(showTryAppPanel) + } + const [isShowCreateModal, setIsShowCreateModal] = useState(false) + + const handleShowFromTryApp = useCallback(() => { + setIsShowCreateModal(true) + }, []) + + const [controlRefreshList, setControlRefreshList] = useState(0) + const [controlHideCreateFromTemplatePanel, setControlHideCreateFromTemplatePanel] = useState(0) + const onSuccess = useCallback(() => { + setControlRefreshList(prev => prev + 1) + setControlHideCreateFromTemplatePanel(prev => prev + 1) + }, []) + + const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false) + + const { + handleImportDSL, + handleImportDSLConfirm, + versions, + isFetching, + } = useImportDSL() + + const onConfirmDSL = useCallback(async () => { + await handleImportDSLConfirm({ + onSuccess, + }) + }, [handleImportDSLConfirm, onSuccess]) + + const onCreate: CreateAppModalProps['onConfirm'] = async ({ + name, + icon_type, + icon, + icon_background, + description, + }) => { + hideTryAppPanel() + + const { export_data } = await fetchAppDetail( + currApp?.app.id as string, + ) + const payload = { + mode: DSLImportMode.YAML_CONTENT, + yaml_content: export_data, + name, + icon_type, + icon, + icon_background, + description, + } + await handleImportDSL(payload, { + onSuccess: () => { + setIsShowCreateModal(false) + }, + onPending: () => { + setShowDSLConfirmModal(true) + }, + }) + } + return ( -
- -
+ +
+ + {isShowTryAppPanel && ( + + )} + + { + showDSLConfirmModal && ( + setShowDSLConfirmModal(false)} + onConfirm={onConfirmDSL} + confirmDisabled={isFetching} + /> + ) + } + + {isShowCreateModal && ( + setIsShowCreateModal(false)} + /> + )} +
+
) } diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index e5c9954626..ef814c9e36 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -1,5 +1,6 @@ 'use client' +import type { FC } from 'react' import { RiApps2Line, RiDragDropLine, @@ -29,6 +30,7 @@ import { CheckModal } from '@/hooks/use-pay' import { useInfiniteAppList } from '@/service/use-apps' import { AppModeEnum } from '@/types/app' import { cn } from '@/utils/classnames' +import { isServer } from '@/utils/client' import AppCard from './app-card' import { AppCardSkeleton } from './app-card-skeleton' import Empty from './empty' @@ -54,7 +56,12 @@ const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-fro ssr: false, }) -const List = () => { +type Props = { + controlRefreshList?: number +} +const List: FC = ({ + controlRefreshList = 0, +}) => { const { t } = useTranslation() const { systemFeatures } = useGlobalPublicStore() const router = useRouter() @@ -71,7 +78,7 @@ const List = () => { // 1) Normalize legacy/incorrect query params like ?mode=discover -> ?category=all useEffect(() => { // avoid running on server - if (typeof window === 'undefined') + if (isServer) return const mode = searchParams.get('mode') if (!mode) @@ -139,6 +146,13 @@ const List = () => { refetch, } = useInfiniteAppList(appListQueryParams, { enabled: !isCurrentWorkspaceDatasetOperator }) + useEffect(() => { + if (controlRefreshList > 0) { + refetch() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [controlRefreshList]) + const anchorRef = useRef(null) const options = [ { value: 'all', text: t('types.all', { ns: 'app' }), icon: }, diff --git a/web/app/components/apps/new-app-card.tsx b/web/app/components/apps/new-app-card.tsx index bfa7af3892..a83abd3d37 100644 --- a/web/app/components/apps/new-app-card.tsx +++ b/web/app/components/apps/new-app-card.tsx @@ -6,10 +6,12 @@ import { useSearchParams, } from 'next/navigation' import * as React from 'react' -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useContextSelector } from 'use-context-selector' import { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal' import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files' +import AppListContext from '@/context/app-list-context' import { useProviderContext } from '@/context/provider-context' import { cn } from '@/utils/classnames' @@ -55,6 +57,12 @@ const CreateAppCard = ({ return undefined }, [dslUrl]) + const controlHideCreateFromTemplatePanel = useContextSelector(AppListContext, ctx => ctx.controlHideCreateFromTemplatePanel) + useEffect(() => { + if (controlHideCreateFromTemplatePanel > 0) + setShowNewAppTemplateDialog(false) + }, [controlHideCreateFromTemplatePanel]) + return (
{ +const ActionButton = ({ className, size, state = ActionButtonState.Default, styleCss, children, ref, disabled, ...props }: ActionButtonProps) => { return ( + ) + }, +) +CarouselPrevious.displayName = 'CarouselPrevious' + +const CarouselNext = React.forwardRef( + ({ children, ...props }, ref) => { + const { scrollNext, canScrollNext } = useCarousel() + + return ( + + ) + }, +) +CarouselNext.displayName = 'CarouselNext' + +const CarouselDot = React.forwardRef( + ({ children, ...props }, ref) => { + const { api, selectedIndex } = useCarousel() + + return api?.slideNodes().map((_, index) => { + return ( + + ) + }) + }, +) +CarouselDot.displayName = 'CarouselDot' + +const CarouselPlugins = { + Autoplay, +} + +Carousel.Content = CarouselContent +Carousel.Item = CarouselItem +Carousel.Previous = CarouselPrevious +Carousel.Next = CarouselNext +Carousel.Dot = CarouselDot +Carousel.Plugin = CarouselPlugins + +export { Carousel, useCarousel } diff --git a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx index 25ff39370f..38a3f6c6b2 100644 --- a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx +++ b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx @@ -12,6 +12,7 @@ import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested import { Markdown } from '@/app/components/base/markdown' import { InputVarType } from '@/app/components/workflow/types' import { + AppSourceType, fetchSuggestedQuestions, getUrl, stopChatMessageResponding, @@ -52,6 +53,11 @@ const ChatWrapper = () => { initUserVariables, } = useChatWithHistoryContext() + const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp + + // Semantic variable for better code readability + const isHistoryConversation = !!currentConversationId + const appConfig = useMemo(() => { const config = appParams || {} @@ -79,7 +85,7 @@ const ChatWrapper = () => { inputsForm: inputsForms, }, appPrevChatTree, - taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId), + taskId => stopChatMessageResponding('', taskId, appSourceType, appId), clearChatList, setClearChatList, ) @@ -138,11 +144,11 @@ const ChatWrapper = () => { } handleSend( - getUrl('chat-messages', isInstalledApp, appId || ''), + getUrl('chat-messages', appSourceType, appId || ''), data, { - onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId), - onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted, + onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, appSourceType, appId), + onConversationComplete: isHistoryConversation ? undefined : handleNewConversationCompleted, isPublicAPI: !isInstalledApp, }, ) diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index ed1981b530..ad1de38d07 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -27,6 +27,7 @@ import { useWebAppStore } from '@/context/web-app-context' import { useAppFavicon } from '@/hooks/use-app-favicon' import { changeLanguage } from '@/i18n-config/client' import { + AppSourceType, delConversation, pinConversation, renameConversation, @@ -72,6 +73,7 @@ function getFormattedChatList(messages: any[]) { export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) + const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp const appInfo = useWebAppStore(s => s.appInfo) const appParams = useWebAppStore(s => s.appParams) const appMeta = useWebAppStore(s => s.appMeta) @@ -177,7 +179,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { }, [currentConversationId, newConversationId]) const { data: appPinnedConversationData } = useShareConversations({ - isInstalledApp, + appSourceType, appId, pinned: true, limit: 100, @@ -190,7 +192,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { data: appConversationData, isLoading: appConversationDataLoading, } = useShareConversations({ - isInstalledApp, + appSourceType, appId, pinned: false, limit: 100, @@ -204,7 +206,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { isLoading: appChatListDataLoading, } = useShareChatList({ conversationId: chatShouldReloadKey, - isInstalledApp, + appSourceType, appId, }, { enabled: !!chatShouldReloadKey, @@ -334,10 +336,11 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const { data: newConversation } = useShareConversationName({ conversationId: newConversationId, - isInstalledApp, + appSourceType, appId, }, { refetchOnWindowFocus: false, + enabled: !!newConversationId, }) const [originConversationList, setOriginConversationList] = useState([]) useEffect(() => { @@ -462,16 +465,16 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { }, [invalidateShareConversations]) const handlePinConversation = useCallback(async (conversationId: string) => { - await pinConversation(isInstalledApp, appId, conversationId) + await pinConversation(appSourceType, appId, conversationId) notify({ type: 'success', message: t('api.success', { ns: 'common' }) }) handleUpdateConversationList() - }, [isInstalledApp, appId, notify, t, handleUpdateConversationList]) + }, [appSourceType, appId, notify, t, handleUpdateConversationList]) const handleUnpinConversation = useCallback(async (conversationId: string) => { - await unpinConversation(isInstalledApp, appId, conversationId) + await unpinConversation(appSourceType, appId, conversationId) notify({ type: 'success', message: t('api.success', { ns: 'common' }) }) handleUpdateConversationList() - }, [isInstalledApp, appId, notify, t, handleUpdateConversationList]) + }, [appSourceType, appId, notify, t, handleUpdateConversationList]) const [conversationDeleting, setConversationDeleting] = useState(false) const handleDeleteConversation = useCallback(async ( @@ -485,7 +488,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { try { setConversationDeleting(true) - await delConversation(isInstalledApp, appId, conversationId) + await delConversation(appSourceType, appId, conversationId) notify({ type: 'success', message: t('api.success', { ns: 'common' }) }) onSuccess() } @@ -520,7 +523,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { setConversationRenaming(true) try { - await renameConversation(isInstalledApp, appId, conversationId, newName) + await renameConversation(appSourceType, appId, conversationId, newName) notify({ type: 'success', @@ -550,9 +553,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { }, [handleConversationIdInfoChange, invalidateShareConversations]) const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => { - await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, appId) + await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId) notify({ type: 'success', message: t('api.success', { ns: 'common' }) }) - }, [isInstalledApp, appId, t, notify]) + }, [appSourceType, appId, t, notify]) return { isInstalledApp, diff --git a/web/app/components/base/chat/chat/answer/index.tsx b/web/app/components/base/chat/chat/answer/index.tsx index d680a14477..0eea0b5a18 100644 --- a/web/app/components/base/chat/chat/answer/index.tsx +++ b/web/app/components/base/chat/chat/answer/index.tsx @@ -152,7 +152,7 @@ const Answer: FC = ({ data={workflowProcess} item={item} hideProcessDetail={hideProcessDetail} - readonly={hideProcessDetail && appData ? !appData.site.show_workflow_steps : undefined} + readonly={hideProcessDetail && appData ? !appData.site?.show_workflow_steps : undefined} /> ) } diff --git a/web/app/components/base/chat/chat/answer/suggested-questions.tsx b/web/app/components/base/chat/chat/answer/suggested-questions.tsx index 019ed78348..ce997a49b6 100644 --- a/web/app/components/base/chat/chat/answer/suggested-questions.tsx +++ b/web/app/components/base/chat/chat/answer/suggested-questions.tsx @@ -1,6 +1,7 @@ import type { FC } from 'react' import type { ChatItem } from '../../types' import { memo } from 'react' +import { cn } from '@/utils/classnames' import { useChatContext } from '../context' type SuggestedQuestionsProps = { @@ -9,7 +10,7 @@ type SuggestedQuestionsProps = { const SuggestedQuestions: FC = ({ item, }) => { - const { onSend } = useChatContext() + const { onSend, readonly } = useChatContext() const { isOpeningStatement, @@ -24,8 +25,11 @@ const SuggestedQuestions: FC = ({ {suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => (
onSend?.(question)} + className={cn( + 'system-sm-medium mr-1 mt-1 inline-flex max-w-full shrink-0 cursor-pointer flex-wrap rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3.5 py-2 text-components-button-secondary-accent-text shadow-xs last:mr-0 hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover', + readonly && 'pointer-events-none opacity-50', + )} + onClick={() => !readonly && onSend?.(question)} > {question}
diff --git a/web/app/components/base/chat/chat/chat-input-area/index.tsx b/web/app/components/base/chat/chat/chat-input-area/index.tsx index 192f46fb23..9de52cb18c 100644 --- a/web/app/components/base/chat/chat/chat-input-area/index.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/index.tsx @@ -5,6 +5,7 @@ import type { } from '../../types' import type { InputForm } from '../type' import type { FileUpload } from '@/app/components/base/features/types' +import { noop } from 'es-toolkit/function' import { decode } from 'html-entities' import Recorder from 'js-audio-recorder' import { @@ -30,6 +31,7 @@ import { useTextAreaHeight } from './hooks' import Operation from './operation' type ChatInputAreaProps = { + readonly?: boolean botName?: string showFeatureBar?: boolean showFileUpload?: boolean @@ -45,6 +47,7 @@ type ChatInputAreaProps = { disabled?: boolean } const ChatInputArea = ({ + readonly, botName, showFeatureBar, showFileUpload, @@ -170,6 +173,7 @@ const ChatInputArea = ({ const operation = (
{ @@ -239,7 +244,14 @@ const ChatInputArea = ({ ) }
- {showFeatureBar && } + {showFeatureBar && ( + + )} ) } diff --git a/web/app/components/base/chat/chat/chat-input-area/operation.tsx b/web/app/components/base/chat/chat/chat-input-area/operation.tsx index 27e5bf6cad..5bce827754 100644 --- a/web/app/components/base/chat/chat/chat-input-area/operation.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/operation.tsx @@ -8,6 +8,7 @@ import { RiMicLine, RiSendPlane2Fill, } from '@remixicon/react' +import { noop } from 'es-toolkit/function' import { memo } from 'react' import ActionButton from '@/app/components/base/action-button' import Button from '@/app/components/base/button' @@ -15,6 +16,7 @@ import { FileUploaderInChatInput } from '@/app/components/base/file-uploader' import { cn } from '@/utils/classnames' type OperationProps = { + readonly?: boolean fileConfig?: FileUpload speechToTextConfig?: EnableType onShowVoiceInput?: () => void @@ -23,6 +25,7 @@ type OperationProps = { ref?: Ref } const Operation: FC = ({ + readonly, ref, fileConfig, speechToTextConfig, @@ -41,11 +44,12 @@ const Operation: FC = ({ ref={ref} >
- {fileConfig?.enabled && } + {fileConfig?.enabled && } { speechToTextConfig?.enabled && ( @@ -56,7 +60,7 @@ const Operation: FC = ({ + { + !hideEditEntrance && ( + + ) + }
)}
diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.tsx index 1ae328d67a..08bb8b45d1 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/index.tsx @@ -13,21 +13,27 @@ import FileFromLinkOrLocal from '../file-from-link-or-local' type FileUploaderInChatInputProps = { fileConfig: FileUpload + readonly?: boolean } const FileUploaderInChatInput = ({ fileConfig, + readonly, }: FileUploaderInChatInputProps) => { const renderTrigger = useCallback((open: boolean) => { return ( ) }, []) + if (readonly) + return renderTrigger(false) + return ( = ({ type TextGenerationImageUploaderProps = { settings: VisionSettings onFilesChange: (files: ImageFile[]) => void + disabled?: boolean } const TextGenerationImageUploader: FC = ({ settings, onFilesChange, + disabled, }) => { const { t } = useTranslation() @@ -93,7 +95,7 @@ const TextGenerationImageUploader: FC = ({ const localUpload = ( = settings.number_limits} + disabled={files.length >= settings.number_limits || disabled} limit={+settings.image_file_size_limit!} > { @@ -115,7 +117,7 @@ const TextGenerationImageUploader: FC = ({ const urlUpload = ( = settings.number_limits} + disabled={files.length >= settings.number_limits || disabled} /> ) diff --git a/web/app/components/base/tab-header/index.tsx b/web/app/components/base/tab-header/index.tsx index e762e23232..6ba6a354a3 100644 --- a/web/app/components/base/tab-header/index.tsx +++ b/web/app/components/base/tab-header/index.tsx @@ -16,6 +16,8 @@ export type ITabHeaderProps = { items: Item[] value: string itemClassName?: string + itemWrapClassName?: string + activeItemClassName?: string onChange: (value: string) => void } @@ -23,6 +25,8 @@ const TabHeader: FC = ({ items, value, itemClassName, + itemWrapClassName, + activeItemClassName, onChange, }) => { const renderItem = ({ id, name, icon, extra, disabled }: Item) => ( @@ -30,8 +34,9 @@ const TabHeader: FC = ({ key={id} className={cn( 'system-md-semibold relative flex cursor-pointer items-center border-b-2 border-transparent pb-2 pt-2.5', - id === value ? 'border-components-tab-active text-text-primary' : 'text-text-tertiary', + id === value ? cn('border-components-tab-active text-text-primary', activeItemClassName) : 'text-text-tertiary', disabled && 'cursor-not-allowed opacity-30', + itemWrapClassName, )} onClick={() => !disabled && onChange(id)} > diff --git a/web/app/components/base/voice-input/index.tsx b/web/app/components/base/voice-input/index.tsx index 4fa2c774f4..52e3c754f8 100644 --- a/web/app/components/base/voice-input/index.tsx +++ b/web/app/components/base/voice-input/index.tsx @@ -8,7 +8,7 @@ import { useParams, usePathname } from 'next/navigation' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' -import { audioToText } from '@/service/share' +import { AppSourceType, audioToText } from '@/service/share' import { cn } from '@/utils/classnames' import s from './index.module.css' import { convertToMp3 } from './utils' @@ -108,7 +108,7 @@ const VoiceInput = ({ } try { - const audioResponse = await audioToText(url, isPublic, formData) + const audioResponse = await audioToText(url, isPublic ? AppSourceType.webApp : AppSourceType.installedApp, formData) onConverted(audioResponse.text) onCancel() } diff --git a/web/app/components/explore/app-card/index.spec.tsx b/web/app/components/explore/app-card/index.spec.tsx index 769b317929..152eab92a9 100644 --- a/web/app/components/explore/app-card/index.spec.tsx +++ b/web/app/components/explore/app-card/index.spec.tsx @@ -10,6 +10,7 @@ vi.mock('../../app/type-selector', () => ({ })) const createApp = (overrides?: Partial): App => ({ + can_trial: true, app_id: 'app-id', description: 'App description', copyright: '2024', diff --git a/web/app/components/explore/app-card/index.tsx b/web/app/components/explore/app-card/index.tsx index 0b6cd9920d..5d82ab65cc 100644 --- a/web/app/components/explore/app-card/index.tsx +++ b/web/app/components/explore/app-card/index.tsx @@ -1,8 +1,13 @@ 'use client' import type { App } from '@/models/explore' import { PlusIcon } from '@heroicons/react/20/solid' +import { RiInformation2Line } from '@remixicon/react' +import { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import { useContextSelector } from 'use-context-selector' import AppIcon from '@/app/components/base/app-icon' +import ExploreContext from '@/context/explore-context' +import { useGlobalPublicStore } from '@/context/global-public-context' import { AppModeEnum } from '@/types/app' import { cn } from '@/utils/classnames' import { AppTypeIcon } from '../../app/type-selector' @@ -23,8 +28,17 @@ const AppCard = ({ }: AppCardProps) => { const { t } = useTranslation() const { app: appBasicInfo } = app + const { systemFeatures } = useGlobalPublicStore() + const isTrialApp = app.can_trial && systemFeatures.enable_trial_app + const setShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.setShowTryAppPanel) + const showTryAPPPanel = useCallback((appId: string) => { + return () => { + setShowTryAppPanel?.(true, { appId, app }) + } + }, [setShowTryAppPanel, app]) + return ( -
+
- {isExplore && canCreate && ( + {isExplore && (canCreate || isTrialApp) && ( )} diff --git a/web/app/components/explore/app-list/index.spec.tsx b/web/app/components/explore/app-list/index.spec.tsx index a9e4feeba8..e15f37245f 100644 --- a/web/app/components/explore/app-list/index.spec.tsx +++ b/web/app/components/explore/app-list/index.spec.tsx @@ -102,6 +102,7 @@ const createApp = (overrides: Partial = {}): App => ({ description: overrides.app?.description ?? 'Alpha description', use_icon_as_answer_icon: overrides.app?.use_icon_as_answer_icon ?? false, }, + can_trial: true, app_id: overrides.app_id ?? 'app-1', description: overrides.description ?? 'Alpha description', copyright: overrides.copyright ?? '', @@ -127,6 +128,8 @@ const renderWithContext = (hasEditPermission = false, onSuccess?: () => void) => setInstalledApps: vi.fn(), isFetchingInstalledApps: false, setIsFetchingInstalledApps: vi.fn(), + isShowTryAppPanel: false, + setShowTryAppPanel: vi.fn(), }} > diff --git a/web/app/components/explore/app-list/index.tsx b/web/app/components/explore/app-list/index.tsx index 5b318b780b..1749bde76a 100644 --- a/web/app/components/explore/app-list/index.tsx +++ b/web/app/components/explore/app-list/index.tsx @@ -7,14 +7,17 @@ import { useQueryState } from 'nuqs' import * as React from 'react' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' +import { useContext, useContextSelector } from 'use-context-selector' import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal' +import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' import Loading from '@/app/components/base/loading' import AppCard from '@/app/components/explore/app-card' +import Banner from '@/app/components/explore/banner/banner' import Category from '@/app/components/explore/category' import CreateAppModal from '@/app/components/explore/create-app-modal' import ExploreContext from '@/context/explore-context' +import { useGlobalPublicStore } from '@/context/global-public-context' import { useImportDSL } from '@/hooks/use-import-dsl' import { DSLImportMode, @@ -22,6 +25,7 @@ import { import { fetchAppDetail } from '@/service/explore' import { useExploreAppList } from '@/service/use-explore' import { cn } from '@/utils/classnames' +import TryApp from '../try-app' import s from './style.module.css' type AppsProps = { @@ -32,12 +36,19 @@ const Apps = ({ onSuccess, }: AppsProps) => { const { t } = useTranslation() + const { systemFeatures } = useGlobalPublicStore() const { hasEditPermission } = useContext(ExploreContext) const allCategoriesEn = t('apps.allCategories', { ns: 'explore', lng: 'en' }) const [keywords, setKeywords] = useState('') const [searchKeywords, setSearchKeywords] = useState('') + const hasFilterCondition = !!keywords + const handleResetFilter = useCallback(() => { + setKeywords('') + setSearchKeywords('') + }, []) + const { run: handleSearch } = useDebounceFn(() => { setSearchKeywords(keywords) }, { wait: 500 }) @@ -84,6 +95,18 @@ const Apps = ({ isFetching, } = useImportDSL() const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false) + + const isShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.isShowTryAppPanel) + const setShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.setShowTryAppPanel) + const hideTryAppPanel = useCallback(() => { + setShowTryAppPanel(false) + }, [setShowTryAppPanel]) + const appParams = useContextSelector(ExploreContext, ctx => ctx.currentApp) + const handleShowFromTryApp = useCallback(() => { + setCurrApp(appParams?.app || null) + setIsShowCreateModal(true) + }, [appParams?.app]) + const onCreate: CreateAppModalProps['onConfirm'] = async ({ name, icon_type, @@ -91,6 +114,8 @@ const Apps = ({ icon_background, description, }) => { + hideTryAppPanel() + const { export_data } = await fetchAppDetail( currApp?.app.id as string, ) @@ -137,22 +162,24 @@ const Apps = ({ 'flex h-full flex-col border-l-[0.5px] border-divider-regular', )} > - -
-
{t('apps.title', { ns: 'explore' })}
-
{t('apps.description', { ns: 'explore' })}
-
- + {systemFeatures.enable_explore_banner && ( +
+ +
+ )}
- +
+
{!hasFilterCondition ? t('apps.title', { ns: 'explore' }) : t('apps.resultNum', { num: searchFilteredList.length, ns: 'explore' })}
+ {hasFilterCondition && ( + <> +
+ + + )} +
+
+ +
+
) } + + {isShowTryAppPanel && ( + + )}
) } diff --git a/web/app/components/explore/banner/banner-item.tsx b/web/app/components/explore/banner/banner-item.tsx new file mode 100644 index 0000000000..5c6df39ebc --- /dev/null +++ b/web/app/components/explore/banner/banner-item.tsx @@ -0,0 +1,198 @@ +import type { FC } from 'react' +import { RiArrowRightLine } from '@remixicon/react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useCarousel } from '@/app/components/base/carousel' +import { cn } from '@/utils/classnames' +import { IndicatorButton } from './indicator-button' + +export type BannerData = { + id: string + content: { + 'category': string + 'title': string + 'description': string + 'img-src': string + } + status: 'enabled' | 'disabled' + link: string + created_at: number +} + +type BannerItemProps = { + banner: BannerData + autoplayDelay: number + isPaused?: boolean +} + +const RESPONSIVE_BREAKPOINT = 1200 +const MAX_RESPONSIVE_WIDTH = 600 +const INDICATOR_WIDTH = 20 +const INDICATOR_GAP = 8 +const MIN_VIEW_MORE_WIDTH = 480 + +export const BannerItem: FC = ({ banner, autoplayDelay, isPaused = false }) => { + const { t } = useTranslation() + const { api, selectedIndex } = useCarousel() + const { category, title, description, 'img-src': imgSrc } = banner.content + + const [resetKey, setResetKey] = useState(0) + const textAreaRef = useRef(null) + const [maxWidth, setMaxWidth] = useState(undefined) + + const slideInfo = useMemo(() => { + const slides = api?.slideNodes() ?? [] + const totalSlides = slides.length + const nextIndex = totalSlides > 0 ? (selectedIndex + 1) % totalSlides : 0 + return { slides, totalSlides, nextIndex } + }, [api, selectedIndex]) + + const indicatorsWidth = useMemo(() => { + const count = slideInfo.totalSlides + if (count === 0) + return 0 + // Calculate: indicator buttons + gaps + extra spacing (3 * 20px for divider and padding) + return (count + 2) * INDICATOR_WIDTH + (count - 1) * INDICATOR_GAP + }, [slideInfo.totalSlides]) + + const viewMoreStyle = useMemo(() => { + if (!maxWidth) + return undefined + return { + maxWidth: `${maxWidth}px`, + minWidth: indicatorsWidth ? `${Math.min(maxWidth - indicatorsWidth, MIN_VIEW_MORE_WIDTH)}px` : undefined, + } + }, [maxWidth, indicatorsWidth]) + + const responsiveStyle = useMemo( + () => (maxWidth !== undefined ? { maxWidth: `${maxWidth}px` } : undefined), + [maxWidth], + ) + + const incrementResetKey = useCallback(() => setResetKey(prev => prev + 1), []) + + useEffect(() => { + const updateMaxWidth = () => { + if (window.innerWidth < RESPONSIVE_BREAKPOINT && textAreaRef.current) { + const textAreaWidth = textAreaRef.current.offsetWidth + setMaxWidth(Math.min(textAreaWidth, MAX_RESPONSIVE_WIDTH)) + } + else { + setMaxWidth(undefined) + } + } + + updateMaxWidth() + + const resizeObserver = new ResizeObserver(updateMaxWidth) + if (textAreaRef.current) + resizeObserver.observe(textAreaRef.current) + + window.addEventListener('resize', updateMaxWidth) + + return () => { + resizeObserver.disconnect() + window.removeEventListener('resize', updateMaxWidth) + } + }, []) + + useEffect(() => { + incrementResetKey() + }, [selectedIndex, incrementResetKey]) + + const handleBannerClick = useCallback(() => { + incrementResetKey() + if (banner.link) + window.open(banner.link, '_blank', 'noopener,noreferrer') + }, [banner.link, incrementResetKey]) + + const handleIndicatorClick = useCallback((index: number) => { + incrementResetKey() + api?.scrollTo(index) + }, [api, incrementResetKey]) + + return ( +
+ {/* Left content area */} +
+
+ {/* Text section */} +
+ {/* Title area */} +
+

+ {category} +

+

+ {title} +

+
+ {/* Description area */} +
+

+ {description} +

+
+
+ + {/* Actions section */} +
+ {/* View more button */} +
+
+ +
+ + {t('banner.viewMore', { ns: 'explore' })} + +
+ +
+ {/* Slide navigation indicators */} +
+ {slideInfo.slides.map((_: unknown, index: number) => ( + handleIndicatorClick(index)} + /> + ))} +
+
+
+
+
+
+ + {/* Right image area */} +
+ {title} +
+
+ ) +} diff --git a/web/app/components/explore/banner/banner.tsx b/web/app/components/explore/banner/banner.tsx new file mode 100644 index 0000000000..c94f4bc316 --- /dev/null +++ b/web/app/components/explore/banner/banner.tsx @@ -0,0 +1,95 @@ +import type { FC } from 'react' +import type { BannerData } from './banner-item' +import * as React from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' +import { Carousel } from '@/app/components/base/carousel' +import { useLocale } from '@/context/i18n' +import { useGetBanners } from '@/service/use-explore' +import Loading from '../../base/loading' +import { BannerItem } from './banner-item' + +const AUTOPLAY_DELAY = 5000 +const MIN_LOADING_HEIGHT = 168 +const RESIZE_DEBOUNCE_DELAY = 50 + +const LoadingState: FC = () => ( +
+ +
+) + +const Banner: FC = () => { + const locale = useLocale() + const { data: banners, isLoading, isError } = useGetBanners(locale) + const [isHovered, setIsHovered] = useState(false) + const [isResizing, setIsResizing] = useState(false) + const resizeTimerRef = useRef(null) + + const enabledBanners = useMemo( + () => banners?.filter((banner: BannerData) => banner.status === 'enabled') ?? [], + [banners], + ) + + const isPaused = isHovered || isResizing + + // Handle window resize to pause animation + useEffect(() => { + const handleResize = () => { + setIsResizing(true) + + if (resizeTimerRef.current) + clearTimeout(resizeTimerRef.current) + + resizeTimerRef.current = setTimeout(() => { + setIsResizing(false) + }, RESIZE_DEBOUNCE_DELAY) + } + + window.addEventListener('resize', handleResize) + + return () => { + window.removeEventListener('resize', handleResize) + if (resizeTimerRef.current) + clearTimeout(resizeTimerRef.current) + } + }, []) + + if (isLoading) + return + + if (isError || enabledBanners.length === 0) + return null + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {enabledBanners.map((banner: BannerData) => ( + + + + ))} + + + ) +} + +export default React.memo(Banner) diff --git a/web/app/components/explore/banner/indicator-button.tsx b/web/app/components/explore/banner/indicator-button.tsx new file mode 100644 index 0000000000..a674c74e6d --- /dev/null +++ b/web/app/components/explore/banner/indicator-button.tsx @@ -0,0 +1,111 @@ +import type { FC } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' +import { cn } from '@/utils/classnames' + +type IndicatorButtonProps = { + index: number + selectedIndex: number + isNextSlide: boolean + autoplayDelay: number + resetKey: number + isPaused?: boolean + onClick: () => void +} + +const PROGRESS_MAX = 100 +const DEGREES_PER_PERCENT = 3.6 + +export const IndicatorButton: FC = ({ + index, + selectedIndex, + isNextSlide, + autoplayDelay, + resetKey, + isPaused = false, + onClick, +}) => { + const [progress, setProgress] = useState(0) + const frameIdRef = useRef(undefined) + const startTimeRef = useRef(0) + + const isActive = index === selectedIndex + const shouldAnimate = !document.hidden && !isPaused + + useEffect(() => { + if (!isNextSlide) { + setProgress(0) + if (frameIdRef.current) + cancelAnimationFrame(frameIdRef.current) + return + } + + setProgress(0) + startTimeRef.current = Date.now() + + const animate = () => { + if (!document.hidden && !isPaused) { + const elapsed = Date.now() - startTimeRef.current + const newProgress = Math.min((elapsed / autoplayDelay) * PROGRESS_MAX, PROGRESS_MAX) + setProgress(newProgress) + + if (newProgress < PROGRESS_MAX) + frameIdRef.current = requestAnimationFrame(animate) + } + else { + frameIdRef.current = requestAnimationFrame(animate) + } + } + + if (shouldAnimate) + frameIdRef.current = requestAnimationFrame(animate) + + return () => { + if (frameIdRef.current) + cancelAnimationFrame(frameIdRef.current) + } + }, [isNextSlide, autoplayDelay, resetKey, isPaused]) + + const handleClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation() + onClick() + }, [onClick]) + + const progressDegrees = progress * DEGREES_PER_PERCENT + + return ( + + ) +} diff --git a/web/app/components/explore/category.tsx b/web/app/components/explore/category.tsx index 97a9ca92b3..47c2a4e3a7 100644 --- a/web/app/components/explore/category.tsx +++ b/web/app/components/explore/category.tsx @@ -29,7 +29,7 @@ const Category: FC = ({ const isAllCategories = !list.includes(value as AppCategory) || value === allCategoriesEn const itemClassName = (isSelected: boolean) => cn( - 'flex h-[32px] cursor-pointer items-center rounded-lg border-[0.5px] border-transparent px-3 py-[7px] font-medium leading-[18px] text-text-tertiary hover:bg-components-main-nav-nav-button-bg-active', + 'system-sm-medium flex h-7 cursor-pointer items-center rounded-lg border border-transparent px-3 text-text-tertiary hover:bg-components-main-nav-nav-button-bg-active', isSelected && 'border-components-main-nav-nav-button-border bg-components-main-nav-nav-button-bg-active text-components-main-nav-nav-button-text-active shadow-xs', ) diff --git a/web/app/components/explore/index.tsx b/web/app/components/explore/index.tsx index 30132eea66..0b5e18a1de 100644 --- a/web/app/components/explore/index.tsx +++ b/web/app/components/explore/index.tsx @@ -1,5 +1,6 @@ 'use client' import type { FC } from 'react' +import type { CurrentTryAppParams } from '@/context/explore-context' import type { InstalledApp } from '@/models/explore' import { useRouter } from 'next/navigation' import * as React from 'react' @@ -41,6 +42,16 @@ const Explore: FC = ({ return router.replace('/datasets') }, [isCurrentWorkspaceDatasetOperator]) + const [currentTryAppParams, setCurrentTryAppParams] = useState(undefined) + const [isShowTryAppPanel, setIsShowTryAppPanel] = useState(false) + const setShowTryAppPanel = (showTryAppPanel: boolean, params?: CurrentTryAppParams) => { + if (showTryAppPanel) + setCurrentTryAppParams(params) + else + setCurrentTryAppParams(undefined) + setIsShowTryAppPanel(showTryAppPanel) + } + return (
= ({ setInstalledApps, isFetchingInstalledApps, setIsFetchingInstalledApps, + currentApp: currentTryAppParams, + isShowTryAppPanel, + setShowTryAppPanel, } } > diff --git a/web/app/components/explore/installed-app/index.tsx b/web/app/components/explore/installed-app/index.tsx index def66c0260..7366057445 100644 --- a/web/app/components/explore/installed-app/index.tsx +++ b/web/app/components/explore/installed-app/index.tsx @@ -1,5 +1,6 @@ 'use client' import type { FC } from 'react' +import type { AccessMode } from '@/models/access-control' import type { AppData } from '@/models/share' import * as React from 'react' import { useEffect } from 'react' @@ -62,8 +63,8 @@ const InstalledApp: FC = ({ if (appMeta) updateWebAppMeta(appMeta) if (webAppAccessMode) - updateWebAppAccessMode(webAppAccessMode.accessMode) - updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result)) + updateWebAppAccessMode((webAppAccessMode as { accessMode: AccessMode }).accessMode) + updateUserCanAccessApp(Boolean(userCanAccessApp && (userCanAccessApp as { result: boolean })?.result)) }, [installedApp, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp, webAppAccessMode, updateWebAppAccessMode]) if (appParamsError) { diff --git a/web/app/components/explore/sidebar/app-nav-item/index.tsx b/web/app/components/explore/sidebar/app-nav-item/index.tsx index 3347efeb3f..08558578f6 100644 --- a/web/app/components/explore/sidebar/app-nav-item/index.tsx +++ b/web/app/components/explore/sidebar/app-nav-item/index.tsx @@ -56,7 +56,7 @@ export default function AppNavItem({ <>
-
{name}
+
{name}
e.stopPropagation()}> { setInstalledApps: vi.fn(), isFetchingInstalledApps: false, setIsFetchingInstalledApps: vi.fn(), - }} + } as any} > , diff --git a/web/app/components/explore/sidebar/index.tsx b/web/app/components/explore/sidebar/index.tsx index 1257886165..225b58199b 100644 --- a/web/app/components/explore/sidebar/index.tsx +++ b/web/app/components/explore/sidebar/index.tsx @@ -1,5 +1,7 @@ 'use client' import type { FC } from 'react' +import { RiAppsFill, RiExpandRightLine, RiLayoutLeft2Line } from '@remixicon/react' +import { useBoolean } from 'ahooks' import Link from 'next/link' import { useSelectedLayoutSegments } from 'next/navigation' import * as React from 'react' @@ -14,6 +16,7 @@ import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/s import { cn } from '@/utils/classnames' import Toast from '../../base/toast' import Item from './app-nav-item' +import NoApps from './no-apps' const SelectedDiscoveryIcon = () => ( @@ -45,6 +48,9 @@ const SideBar: FC = ({ const media = useBreakpoints() const isMobile = media === MediaType.mobile + const [isFold, { + toggle: toggleIsFold, + }] = useBoolean(false) const [showConfirm, setShowConfirm] = useState(false) const [currId, setCurrId] = useState('') @@ -84,22 +90,31 @@ const SideBar: FC = ({ const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length return ( -
+
- {isDiscoverySelected ? : } - {!isMobile &&
{t('sidebar.discovery', { ns: 'explore' })}
} +
+ +
+ {!isMobile && !isFold &&
{t('sidebar.title', { ns: 'explore' })}
}
+ + {installedApps.length === 0 && !isMobile && !isFold + && ( +
+ +
+ )} + {installedApps.length > 0 && ( -
-

{t('sidebar.workspace', { ns: 'explore' })}

+
+ {!isMobile && !isFold &&

{t('sidebar.webApps', { ns: 'explore' })}

}
= ({ {installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon_type, icon, icon_url, icon_background } }, index) => ( = ({
)} + + {!isMobile && ( +
+ {isFold + ? + : ( + + )} +
+ )} + {showConfirm && ( { + const { t } = useTranslation() + const { theme } = useTheme() + return ( +
+
+
{t(`${i18nPrefix}.title`, { ns: 'explore' })}
+
{t(`${i18nPrefix}.description`, { ns: 'explore' })}
+ {t(`${i18nPrefix}.learnMore`, { ns: 'explore' })} +
+ ) +} +export default React.memo(NoApps) diff --git a/web/app/components/explore/sidebar/no-apps/no-web-apps-dark.png b/web/app/components/explore/sidebar/no-apps/no-web-apps-dark.png new file mode 100644 index 0000000000..e153686fcd Binary files /dev/null and b/web/app/components/explore/sidebar/no-apps/no-web-apps-dark.png differ diff --git a/web/app/components/explore/sidebar/no-apps/no-web-apps-light.png b/web/app/components/explore/sidebar/no-apps/no-web-apps-light.png new file mode 100644 index 0000000000..2416b957d2 Binary files /dev/null and b/web/app/components/explore/sidebar/no-apps/no-web-apps-light.png differ diff --git a/web/app/components/explore/sidebar/no-apps/style.module.css b/web/app/components/explore/sidebar/no-apps/style.module.css new file mode 100644 index 0000000000..ad3787ce2b --- /dev/null +++ b/web/app/components/explore/sidebar/no-apps/style.module.css @@ -0,0 +1,7 @@ +.light { + background-image: url('./no-web-apps-light.png'); +} + +.dark { + background-image: url('./no-web-apps-dark.png'); +} diff --git a/web/app/components/explore/try-app/app-info/index.tsx b/web/app/components/explore/try-app/app-info/index.tsx new file mode 100644 index 0000000000..eab265bd04 --- /dev/null +++ b/web/app/components/explore/try-app/app-info/index.tsx @@ -0,0 +1,95 @@ +'use client' +import type { FC } from 'react' +import type { TryAppInfo } from '@/service/try-app' +import { RiAddLine } from '@remixicon/react' +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { AppTypeIcon } from '@/app/components/app/type-selector' +import AppIcon from '@/app/components/base/app-icon' +import Button from '@/app/components/base/button' +import { cn } from '@/utils/classnames' +import useGetRequirements from './use-get-requirements' + +type Props = { + appId: string + appDetail: TryAppInfo + category?: string + className?: string + onCreate: () => void +} + +const headerClassName = 'system-sm-semibold-uppercase text-text-secondary mb-3' + +const AppInfo: FC = ({ + appId, + className, + category, + appDetail, + onCreate, +}) => { + const { t } = useTranslation() + const mode = appDetail?.mode + const { requirements } = useGetRequirements({ appDetail, appId }) + return ( +
+ {/* name and icon */} +
+
+ + +
+
+
+
{appDetail.name}
+
+
+ {mode === 'advanced-chat' &&
{t('types.advanced', { ns: 'app' }).toUpperCase()}
} + {mode === 'chat' &&
{t('types.chatbot', { ns: 'app' }).toUpperCase()}
} + {mode === 'agent-chat' &&
{t('types.agent', { ns: 'app' }).toUpperCase()}
} + {mode === 'workflow' &&
{t('types.workflow', { ns: 'app' }).toUpperCase()}
} + {mode === 'completion' &&
{t('types.completion', { ns: 'app' }).toUpperCase()}
} +
+
+
+ {appDetail.description && ( +
{appDetail.description}
+ )} + + + {category && ( +
+
{t('tryApp.category', { ns: 'explore' })}
+
{category}
+
+ )} + {requirements.length > 0 && ( +
+
{t('tryApp.requirements', { ns: 'explore' })}
+
+ {requirements.map(item => ( +
+
+
{item.name}
+
+ ))} +
+
+ )} + +
+ ) +} +export default React.memo(AppInfo) diff --git a/web/app/components/explore/try-app/app-info/use-get-requirements.ts b/web/app/components/explore/try-app/app-info/use-get-requirements.ts new file mode 100644 index 0000000000..976989be73 --- /dev/null +++ b/web/app/components/explore/try-app/app-info/use-get-requirements.ts @@ -0,0 +1,78 @@ +import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types' +import type { ToolNodeType } from '@/app/components/workflow/nodes/tool/types' +import type { TryAppInfo } from '@/service/try-app' +import type { AgentTool } from '@/types/app' +import { uniqBy } from 'es-toolkit/compat' +import { BlockEnum } from '@/app/components/workflow/types' +import { MARKETPLACE_API_PREFIX } from '@/config' +import { useGetTryAppFlowPreview } from '@/service/use-try-app' + +type Params = { + appDetail: TryAppInfo + appId: string +} + +type RequirementItem = { + name: string + iconUrl: string +} +const getIconUrl = (provider: string, tool: string) => { + return `${MARKETPLACE_API_PREFIX}/plugins/${provider}/${tool}/icon` +} + +const useGetRequirements = ({ appDetail, appId }: Params) => { + const isBasic = ['chat', 'completion', 'agent-chat'].includes(appDetail.mode) + const isAgent = appDetail.mode === 'agent-chat' + const isAdvanced = !isBasic + const { data: flowData } = useGetTryAppFlowPreview(appId, isBasic) + + const requirements: RequirementItem[] = [] + if (isBasic) { + const modelProviderAndName = appDetail.model_config.model.provider.split('/') + const name = appDetail.model_config.model.provider.split('/').pop() || '' + requirements.push({ + name, + iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]), + }) + } + if (isAgent) { + requirements.push(...appDetail.model_config.agent_mode.tools.filter(data => (data as AgentTool).enabled).map((data) => { + const tool = data as AgentTool + const modelProviderAndName = tool.provider_id.split('/') + return { + name: tool.tool_label, + iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]), + } + })) + } + if (isAdvanced && flowData && flowData?.graph?.nodes?.length > 0) { + const nodes = flowData.graph.nodes + const llmNodes = nodes.filter(node => node.data.type === BlockEnum.LLM) + requirements.push(...llmNodes.map((node) => { + const data = node.data as LLMNodeType + const modelProviderAndName = data.model.provider.split('/') + return { + name: data.model.name, + iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]), + } + })) + + const toolNodes = nodes.filter(node => node.data.type === BlockEnum.Tool) + requirements.push(...toolNodes.map((node) => { + const data = node.data as ToolNodeType + const toolProviderAndName = data.provider_id.split('/') + return { + name: data.tool_label, + iconUrl: getIconUrl(toolProviderAndName[0], toolProviderAndName[1]), + } + })) + } + + const uniqueRequirements = uniqBy(requirements, 'name') + + return { + requirements: uniqueRequirements, + } +} + +export default useGetRequirements diff --git a/web/app/components/explore/try-app/app/chat.tsx b/web/app/components/explore/try-app/app/chat.tsx new file mode 100644 index 0000000000..25a9093f75 --- /dev/null +++ b/web/app/components/explore/try-app/app/chat.tsx @@ -0,0 +1,101 @@ +'use client' +import type { FC } from 'react' +import type { TryAppInfo } from '@/service/try-app' +import { RiResetLeftLine } from '@remixicon/react' +import { useBoolean } from 'ahooks' +import * as React from 'react' +import { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import ActionButton from '@/app/components/base/action-button' +import Alert from '@/app/components/base/alert' +import AppIcon from '@/app/components/base/app-icon' +import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper' +import { + EmbeddedChatbotContext, +} from '@/app/components/base/chat/embedded-chatbot/context' +import { + useEmbeddedChatbot, +} from '@/app/components/base/chat/embedded-chatbot/hooks' +import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown' +import Tooltip from '@/app/components/base/tooltip' +import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' +import { AppSourceType } from '@/service/share' +import { cn } from '@/utils/classnames' +import { useThemeContext } from '../../../base/chat/embedded-chatbot/theme/theme-context' + +type Props = { + appId: string + appDetail: TryAppInfo + className: string +} + +const TryApp: FC = ({ + appId, + appDetail, + className, +}) => { + const { t } = useTranslation() + const media = useBreakpoints() + const isMobile = media === MediaType.mobile + const themeBuilder = useThemeContext() + const { removeConversationIdInfo, ...chatData } = useEmbeddedChatbot(AppSourceType.tryApp, appId) + const currentConversationId = chatData.currentConversationId + const inputsForms = chatData.inputsForms + useEffect(() => { + if (appId) + removeConversationIdInfo(appId) + }, [appId]) + const [isHideTryNotice, { + setTrue: hideTryNotice, + }] = useBoolean(false) + + const handleNewConversation = () => { + removeConversationIdInfo(appId) + chatData.handleNewConversation() + } + return ( + +
+
+
+ +
{appDetail.name}
+
+
+ {currentConversationId && ( + + + + + + )} + {currentConversationId && inputsForms.length > 0 && ( + + )} +
+
+
+ {!isHideTryNotice && ( + + )} + +
+
+
+ ) +} +export default React.memo(TryApp) diff --git a/web/app/components/explore/try-app/app/index.tsx b/web/app/components/explore/try-app/app/index.tsx new file mode 100644 index 0000000000..f5dc14510d --- /dev/null +++ b/web/app/components/explore/try-app/app/index.tsx @@ -0,0 +1,44 @@ +'use client' +import type { FC } from 'react' +import type { AppData } from '@/models/share' +import type { TryAppInfo } from '@/service/try-app' +import * as React from 'react' +import useDocumentTitle from '@/hooks/use-document-title' +import Chat from './chat' +import TextGeneration from './text-generation' + +type Props = { + appId: string + appDetail: TryAppInfo +} + +const TryApp: FC = ({ + appId, + appDetail, +}) => { + const mode = appDetail?.mode + const isChat = ['chat', 'advanced-chat', 'agent-chat'].includes(mode!) + const isCompletion = !isChat + + useDocumentTitle(appDetail?.site?.title || '') + return ( +
+ {isChat && ( + + )} + {isCompletion && ( + + )} +
+ ) +} +export default React.memo(TryApp) diff --git a/web/app/components/explore/try-app/app/text-generation.tsx b/web/app/components/explore/try-app/app/text-generation.tsx new file mode 100644 index 0000000000..350166329d --- /dev/null +++ b/web/app/components/explore/try-app/app/text-generation.tsx @@ -0,0 +1,261 @@ +'use client' +import type { FC } from 'react' +import type { Task } from '../../../share/text-generation/types' +import type { MoreLikeThisConfig, PromptConfig, TextToSpeechConfig } from '@/models/debug' +import type { AppData, SiteInfo } from '@/models/share' +import type { VisionFile, VisionSettings } from '@/types/app' +import { useBoolean } from 'ahooks' +import { noop } from 'es-toolkit/function' +import * as React from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import Alert from '@/app/components/base/alert' +import AppIcon from '@/app/components/base/app-icon' +import Loading from '@/app/components/base/loading' +import Res from '@/app/components/share/text-generation/result' +import { TaskStatus } from '@/app/components/share/text-generation/types' +import { appDefaultIconBackground } from '@/config' +import { useWebAppStore } from '@/context/web-app-context' +import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' +import { AppSourceType } from '@/service/share' +import { useGetTryAppParams } from '@/service/use-try-app' +import { Resolution, TransferMethod } from '@/types/app' +import { cn } from '@/utils/classnames' +import { userInputsFormToPromptVariables } from '@/utils/model-config' +import RunOnce from '../../../share/text-generation/run-once' + +type Props = { + appId: string + className?: string + isWorkflow?: boolean + appData: AppData | null +} + +const TextGeneration: FC = ({ + appId, + className, + isWorkflow, + appData, +}) => { + const { t } = useTranslation() + const media = useBreakpoints() + const isPC = media === MediaType.pc + + const [inputs, doSetInputs] = useState>({}) + const inputsRef = useRef>(inputs) + const setInputs = useCallback((newInputs: Record) => { + doSetInputs(newInputs) + inputsRef.current = newInputs + }, []) + + const updateAppInfo = useWebAppStore(s => s.updateAppInfo) + const { data: tryAppParams } = useGetTryAppParams(appId) + + const updateAppParams = useWebAppStore(s => s.updateAppParams) + const appParams = useWebAppStore(s => s.appParams) + const [siteInfo, setSiteInfo] = useState(null) + const [promptConfig, setPromptConfig] = useState(null) + const [customConfig, setCustomConfig] = useState | null>(null) + const [moreLikeThisConfig, setMoreLikeThisConfig] = useState(null) + const [textToSpeechConfig, setTextToSpeechConfig] = useState(null) + const [controlSend, setControlSend] = useState(0) + const [visionConfig, setVisionConfig] = useState({ + enabled: false, + number_limits: 2, + detail: Resolution.low, + transfer_methods: [TransferMethod.local_file], + }) + const [completionFiles, setCompletionFiles] = useState([]) + const [isShowResultPanel, { setTrue: doShowResultPanel, setFalse: hideResultPanel }] = useBoolean(false) + const showResultPanel = () => { + // fix: useClickAway hideResSidebar will close sidebar + setTimeout(() => { + doShowResultPanel() + }, 0) + } + + const handleSend = () => { + setControlSend(Date.now()) + showResultPanel() + } + + const [resultExisted, setResultExisted] = useState(false) + + useEffect(() => { + if (!appData) + return + updateAppInfo(appData) + }, [appData, updateAppInfo]) + + useEffect(() => { + if (!tryAppParams) + return + updateAppParams(tryAppParams) + }, [tryAppParams, updateAppParams]) + + useEffect(() => { + (async () => { + if (!appData || !appParams) + return + const { site: siteInfo, custom_config } = appData + setSiteInfo(siteInfo as SiteInfo) + setCustomConfig(custom_config) + + const { user_input_form, more_like_this, file_upload, text_to_speech }: any = appParams + setVisionConfig({ + // legacy of image upload compatible + ...file_upload, + transfer_methods: file_upload?.allowed_file_upload_methods || file_upload?.allowed_upload_methods, + // legacy of image upload compatible + image_file_size_limit: appParams?.system_parameters.image_file_size_limit, + fileUploadConfig: appParams?.system_parameters, + } as any) + const prompt_variables = userInputsFormToPromptVariables(user_input_form) + setPromptConfig({ + prompt_template: '', // placeholder for future + prompt_variables, + } as PromptConfig) + setMoreLikeThisConfig(more_like_this) + setTextToSpeechConfig(text_to_speech) + })() + }, [appData, appParams]) + + const [isCompleted, setIsCompleted] = useState(false) + const handleCompleted = useCallback(() => { + setIsCompleted(true) + }, []) + const [isHideTryNotice, { + setTrue: hideTryNotice, + }] = useBoolean(false) + + const renderRes = (task?: Task) => ( + setResultExisted(true)} + /> + ) + + const renderResWrap = ( +
+
+ {isCompleted && !isHideTryNotice && ( + + )} + {renderRes()} +
+
+ ) + + if (!siteInfo || !promptConfig) { + return ( +
+ +
+ ) + } + + return ( +
+ {/* Left */} +
+ {/* Header */} +
+
+ +
{siteInfo.title}
+
+ {siteInfo.description && ( +
{siteInfo.description}
+ )} +
+ {/* form */} +
+ +
+
+ + {/* Result */} +
+ {!isPC && ( +
{ + if (isShowResultPanel) + hideResultPanel() + else + showResultPanel() + }} + > +
+
+ )} + {renderResWrap} +
+
+ ) +} + +export default React.memo(TextGeneration) diff --git a/web/app/components/explore/try-app/index.tsx b/web/app/components/explore/try-app/index.tsx new file mode 100644 index 0000000000..edf3ae04fa --- /dev/null +++ b/web/app/components/explore/try-app/index.tsx @@ -0,0 +1,73 @@ +'use client' +import type { FC } from 'react' +import { RiCloseLine } from '@remixicon/react' +import * as React from 'react' +import { useState } from 'react' +import Loading from '@/app/components/base/loading' +import Modal from '@/app/components/base/modal/index' +import { useGetTryAppInfo } from '@/service/use-try-app' +import Button from '../../base/button' +import App from './app' +import AppInfo from './app-info' +import Preview from './preview' +import Tab, { TypeEnum } from './tab' + +type Props = { + appId: string + category?: string + onClose: () => void + onCreate: () => void +} + +const TryApp: FC = ({ + appId, + category, + onClose, + onCreate, +}) => { + const [type, setType] = useState(TypeEnum.TRY) + const { data: appDetail, isLoading } = useGetTryAppInfo(appId) + + return ( + + {isLoading ? ( +
+ +
+ ) : ( +
+
+ + +
+ {/* Main content */} +
+ {type === TypeEnum.TRY ? : } + +
+
+ )} +
+ ) +} +export default React.memo(TryApp) diff --git a/web/app/components/explore/try-app/preview/basic-app-preview.tsx b/web/app/components/explore/try-app/preview/basic-app-preview.tsx new file mode 100644 index 0000000000..e1e7465e46 --- /dev/null +++ b/web/app/components/explore/try-app/preview/basic-app-preview.tsx @@ -0,0 +1,364 @@ +'use client' +import type { FC } from 'react' +import type { Features as FeaturesData, FileUpload } from '@/app/components/base/features/types' +import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' +import type { ModelConfig } from '@/models/debug' +import type { ModelConfig as BackendModelConfig, PromptVariable } from '@/types/app' +import { noop } from 'es-toolkit/function' +import { clone } from 'es-toolkit/object' +import * as React from 'react' +import { useMemo, useState } from 'react' +import Config from '@/app/components/app/configuration/config' +import Debug from '@/app/components/app/configuration/debug' +import { FeaturesProvider } from '@/app/components/base/features' +import Loading from '@/app/components/base/loading' +import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' +import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { SupportUploadFileTypes } from '@/app/components/workflow/types' +import { ANNOTATION_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config' +import ConfigContext from '@/context/debug-configuration' +import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' +import { PromptMode } from '@/models/debug' +import { useAllToolProviders } from '@/service/use-tools' +import { useGetTryAppDataSets, useGetTryAppInfo } from '@/service/use-try-app' +import { ModelModeType, Resolution, TransferMethod } from '@/types/app' +import { correctModelProvider, correctToolProvider } from '@/utils' +import { userInputsFormToPromptVariables } from '@/utils/model-config' +import { basePath } from '@/utils/var' +import { useTextGenerationCurrentProviderAndModelAndModelList } from '../../../header/account-setting/model-provider-page/hooks' + +type Props = { + appId: string +} + +const defaultModelConfig = { + provider: 'langgenius/openai/openai', + model_id: 'gpt-3.5-turbo', + mode: ModelModeType.unset, + configs: { + prompt_template: '', + prompt_variables: [] as PromptVariable[], + }, + more_like_this: null, + opening_statement: '', + suggested_questions: [], + sensitive_word_avoidance: null, + speech_to_text: null, + text_to_speech: null, + file_upload: null, + suggested_questions_after_answer: null, + retriever_resource: null, + annotation_reply: null, + dataSets: [], + agentConfig: DEFAULT_AGENT_SETTING, +} +const BasicAppPreview: FC = ({ + appId, +}) => { + const media = useBreakpoints() + const isMobile = media === MediaType.mobile + + const { data: appDetail, isLoading: isLoadingAppDetail } = useGetTryAppInfo(appId) + const { data: collectionListFromServer, isLoading: isLoadingToolProviders } = useAllToolProviders() + const collectionList = collectionListFromServer?.map((item) => { + return { + ...item, + icon: basePath && typeof item.icon == 'string' && !item.icon.includes(basePath) ? `${basePath}${item.icon}` : item.icon, + } + }) + const datasetIds = (() => { + if (isLoadingAppDetail) + return [] + const modelConfig = appDetail?.model_config + if (!modelConfig) + return [] + let datasets: any = null + + if (modelConfig.agent_mode?.tools?.find(({ dataset }: any) => dataset?.enabled)) + datasets = modelConfig.agent_mode?.tools.filter(({ dataset }: any) => dataset?.enabled) + // new dataset struct + else if (modelConfig.dataset_configs.datasets?.datasets?.length > 0) + datasets = modelConfig.dataset_configs?.datasets?.datasets + + if (datasets?.length && datasets?.length > 0) + return datasets.map(({ dataset }: any) => dataset.id) + + return [] + })() + const { data: dataSetData, isLoading: isLoadingDatasets } = useGetTryAppDataSets(appId, datasetIds) + const dataSets = dataSetData?.data || [] + const isLoading = isLoadingAppDetail || isLoadingDatasets || isLoadingToolProviders + + const modelConfig: ModelConfig = ((modelConfig?: BackendModelConfig) => { + if (isLoading || !modelConfig) + return defaultModelConfig + + const model = modelConfig.model + + const newModelConfig = { + provider: correctModelProvider(model.provider), + model_id: model.name, + mode: model.mode, + configs: { + prompt_template: modelConfig.pre_prompt || '', + prompt_variables: userInputsFormToPromptVariables( + [ + ...(modelConfig.user_input_form as any), + ...( + modelConfig.external_data_tools?.length + ? modelConfig.external_data_tools.map((item: any) => { + return { + external_data_tool: { + variable: item.variable as string, + label: item.label as string, + enabled: item.enabled, + type: item.type as string, + config: item.config, + required: true, + icon: item.icon, + icon_background: item.icon_background, + }, + } + }) + : [] + ), + ], + modelConfig.dataset_query_variable, + ), + }, + more_like_this: modelConfig.more_like_this, + opening_statement: modelConfig.opening_statement, + suggested_questions: modelConfig.suggested_questions, + sensitive_word_avoidance: modelConfig.sensitive_word_avoidance, + speech_to_text: modelConfig.speech_to_text, + text_to_speech: modelConfig.text_to_speech, + file_upload: modelConfig.file_upload, + suggested_questions_after_answer: modelConfig.suggested_questions_after_answer, + retriever_resource: modelConfig.retriever_resource, + annotation_reply: modelConfig.annotation_reply, + external_data_tools: modelConfig.external_data_tools, + dataSets, + agentConfig: appDetail?.mode === 'agent-chat' ? { + max_iteration: DEFAULT_AGENT_SETTING.max_iteration, + ...modelConfig.agent_mode, + // remove dataset + enabled: true, // modelConfig.agent_mode?.enabled is not correct. old app: the value of app with dataset's is always true + tools: modelConfig.agent_mode?.tools.filter((tool: any) => { + return !tool.dataset + }).map((tool: any) => { + const toolInCollectionList = collectionList?.find(c => tool.provider_id === c.id) + return { + ...tool, + isDeleted: appDetail?.deleted_tools?.some((deletedTool: any) => deletedTool.id === tool.id && deletedTool.tool_name === tool.tool_name), + notAuthor: toolInCollectionList?.is_team_authorization === false, + ...(tool.provider_type === 'builtin' + ? { + provider_id: correctToolProvider(tool.provider_name, !!toolInCollectionList), + provider_name: correctToolProvider(tool.provider_name, !!toolInCollectionList), + } + : {}), + } + }), + } : DEFAULT_AGENT_SETTING, + } + return (newModelConfig as any) + })(appDetail?.model_config) + const mode = appDetail?.mode + // const isChatApp = ['chat', 'advanced-chat', 'agent-chat'].includes(mode!) + + // chat configuration + const promptMode = modelConfig?.prompt_type === PromptMode.advanced ? PromptMode.advanced : PromptMode.simple + const isAdvancedMode = promptMode === PromptMode.advanced + const isAgent = mode === 'agent-chat' + const chatPromptConfig = isAdvancedMode ? (modelConfig?.chat_prompt_config || clone(DEFAULT_CHAT_PROMPT_CONFIG)) : undefined + const suggestedQuestions = modelConfig?.suggested_questions || [] + const moreLikeThisConfig = modelConfig?.more_like_this || { enabled: false } + const suggestedQuestionsAfterAnswerConfig = modelConfig?.suggested_questions_after_answer || { enabled: false } + const speechToTextConfig = modelConfig?.speech_to_text || { enabled: false } + const textToSpeechConfig = modelConfig?.text_to_speech || { enabled: false, voice: '', language: '' } + const citationConfig = modelConfig?.retriever_resource || { enabled: false } + const annotationConfig = modelConfig?.annotation_reply || { + id: '', + enabled: false, + score_threshold: ANNOTATION_DEFAULT.score_threshold, + embedding_model: { + embedding_provider_name: '', + embedding_model_name: '', + }, + } + const moderationConfig = modelConfig?.sensitive_word_avoidance || { enabled: false } + // completion configuration + const completionPromptConfig = modelConfig?.completion_prompt_config || clone(DEFAULT_COMPLETION_PROMPT_CONFIG) as any + + // prompt & model config + const inputs = {} + const query = '' + const completionParams = useState({}) + + const { + currentModel: currModel, + } = useTextGenerationCurrentProviderAndModelAndModelList( + { + provider: modelConfig.provider, + model: modelConfig.model_id, + }, + ) + + const isShowVisionConfig = !!currModel?.features?.includes(ModelFeatureEnum.vision) + const isShowDocumentConfig = !!currModel?.features?.includes(ModelFeatureEnum.document) + const isShowAudioConfig = !!currModel?.features?.includes(ModelFeatureEnum.audio) + const isAllowVideoUpload = !!currModel?.features?.includes(ModelFeatureEnum.video) + const visionConfig = { + enabled: false, + number_limits: 2, + detail: Resolution.low, + transfer_methods: [TransferMethod.local_file], + } + + const featuresData: FeaturesData = useMemo(() => { + return { + moreLikeThis: modelConfig.more_like_this || { enabled: false }, + opening: { + enabled: !!modelConfig.opening_statement, + opening_statement: modelConfig.opening_statement || '', + suggested_questions: modelConfig.suggested_questions || [], + }, + moderation: modelConfig.sensitive_word_avoidance || { enabled: false }, + speech2text: modelConfig.speech_to_text || { enabled: false }, + text2speech: modelConfig.text_to_speech || { enabled: false }, + file: { + image: { + detail: modelConfig.file_upload?.image?.detail || Resolution.high, + enabled: !!modelConfig.file_upload?.image?.enabled, + number_limits: modelConfig.file_upload?.image?.number_limits || 3, + transfer_methods: modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'], + }, + enabled: !!(modelConfig.file_upload?.enabled || modelConfig.file_upload?.image?.enabled), + allowed_file_types: modelConfig.file_upload?.allowed_file_types || [], + allowed_file_extensions: modelConfig.file_upload?.allowed_file_extensions || [...FILE_EXTS[SupportUploadFileTypes.image], ...FILE_EXTS[SupportUploadFileTypes.video]].map(ext => `.${ext}`), + allowed_file_upload_methods: modelConfig.file_upload?.allowed_file_upload_methods || modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'], + number_limits: modelConfig.file_upload?.number_limits || modelConfig.file_upload?.image?.number_limits || 3, + fileUploadConfig: {}, + } as FileUpload, + suggested: modelConfig.suggested_questions_after_answer || { enabled: false }, + citation: modelConfig.retriever_resource || { enabled: false }, + annotationReply: modelConfig.annotation_reply || { enabled: false }, + } + }, [modelConfig]) + + if (isLoading) { + return ( +
+ +
+ ) + } + const value = { + readonly: true, + appId, + isAPIKeySet: true, + isTrailFinished: false, + mode, + modelModeType: '', + promptMode, + isAdvancedMode, + isAgent, + isOpenAI: false, + isFunctionCall: false, + collectionList: [], + setPromptMode: noop, + canReturnToSimpleMode: false, + setCanReturnToSimpleMode: noop, + chatPromptConfig, + completionPromptConfig, + currentAdvancedPrompt: '', + setCurrentAdvancedPrompt: noop, + conversationHistoriesRole: completionPromptConfig.conversation_histories_role, + showHistoryModal: false, + setConversationHistoriesRole: noop, + hasSetBlockStatus: true, + conversationId: '', + introduction: '', + setIntroduction: noop, + suggestedQuestions, + setSuggestedQuestions: noop, + setConversationId: noop, + controlClearChatMessage: false, + setControlClearChatMessage: noop, + prevPromptConfig: {}, + setPrevPromptConfig: noop, + moreLikeThisConfig, + setMoreLikeThisConfig: noop, + suggestedQuestionsAfterAnswerConfig, + setSuggestedQuestionsAfterAnswerConfig: noop, + speechToTextConfig, + setSpeechToTextConfig: noop, + textToSpeechConfig, + setTextToSpeechConfig: noop, + citationConfig, + setCitationConfig: noop, + annotationConfig, + setAnnotationConfig: noop, + moderationConfig, + setModerationConfig: noop, + externalDataToolsConfig: {}, + setExternalDataToolsConfig: noop, + formattingChanged: false, + setFormattingChanged: noop, + inputs, + setInputs: noop, + query, + setQuery: noop, + completionParams, + setCompletionParams: noop, + modelConfig, + setModelConfig: noop, + showSelectDataSet: noop, + dataSets, + setDataSets: noop, + datasetConfigs: [], + datasetConfigsRef: {}, + setDatasetConfigs: noop, + hasSetContextVar: true, + isShowVisionConfig, + visionConfig, + setVisionConfig: noop, + isAllowVideoUpload, + isShowDocumentConfig, + isShowAudioConfig, + rerankSettingModalOpen: false, + setRerankSettingModalOpen: noop, + } + return ( + + +
+
+
+ +
+ {!isMobile && ( +
+
+ +
+
+ )} +
+
+
+
+ ) +} +export default React.memo(BasicAppPreview) diff --git a/web/app/components/explore/try-app/preview/flow-app-preview.tsx b/web/app/components/explore/try-app/preview/flow-app-preview.tsx new file mode 100644 index 0000000000..ba64aecfba --- /dev/null +++ b/web/app/components/explore/try-app/preview/flow-app-preview.tsx @@ -0,0 +1,39 @@ +'use client' +import type { FC } from 'react' +import * as React from 'react' +import Loading from '@/app/components/base/loading' +import WorkflowPreview from '@/app/components/workflow/workflow-preview' +import { useGetTryAppFlowPreview } from '@/service/use-try-app' +import { cn } from '@/utils/classnames' + +type Props = { + appId: string + className?: string +} + +const FlowAppPreview: FC = ({ + appId, + className, +}) => { + const { data, isLoading } = useGetTryAppFlowPreview(appId) + + if (isLoading) { + return ( +
+ +
+ ) + } + if (!data) + return null + return ( +
+ +
+ ) +} +export default React.memo(FlowAppPreview) diff --git a/web/app/components/explore/try-app/preview/index.tsx b/web/app/components/explore/try-app/preview/index.tsx new file mode 100644 index 0000000000..a0c5fdc594 --- /dev/null +++ b/web/app/components/explore/try-app/preview/index.tsx @@ -0,0 +1,25 @@ +'use client' +import type { FC } from 'react' +import type { TryAppInfo } from '@/service/try-app' +import * as React from 'react' +import BasicAppPreview from './basic-app-preview' +import FlowAppPreview from './flow-app-preview' + +type Props = { + appId: string + appDetail: TryAppInfo +} + +const Preview: FC = ({ + appId, + appDetail, +}) => { + const isBasicApp = ['agent-chat', 'chat', 'completion'].includes(appDetail.mode) + + return ( +
+ {isBasicApp ? : } +
+ ) +} +export default React.memo(Preview) diff --git a/web/app/components/explore/try-app/tab.tsx b/web/app/components/explore/try-app/tab.tsx new file mode 100644 index 0000000000..75ba402204 --- /dev/null +++ b/web/app/components/explore/try-app/tab.tsx @@ -0,0 +1,37 @@ +'use client' +import type { FC } from 'react' +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import TabHeader from '../../base/tab-header' + +export enum TypeEnum { + TRY = 'try', + DETAIL = 'detail', +} + +type Props = { + value: TypeEnum + onChange: (value: TypeEnum) => void +} + +const Tab: FC = ({ + value, + onChange, +}) => { + const { t } = useTranslation() + const tabs = [ + { id: TypeEnum.TRY, name: t('tryApp.tabHeader.try', { ns: 'explore' }) }, + { id: TypeEnum.DETAIL, name: t('tryApp.tabHeader.detail', { ns: 'explore' }) }, + ] + return ( + void} + itemClassName="ml-0 system-md-semibold-uppercase" + itemWrapClassName="pt-2" + activeItemClassName="border-util-colors-blue-brand-blue-brand-500" + /> + ) +} +export default React.memo(Tab) diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index b793a03ce7..80d99f2f81 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -34,7 +34,7 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useDocumentTitle from '@/hooks/use-document-title' import { changeLanguage } from '@/i18n-config/client' import { AccessMode } from '@/models/access-control' -import { fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share' +import { AppSourceType, fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share' import { Resolution, TransferMethod } from '@/types/app' import { cn } from '@/utils/classnames' import { userInputsFormToPromptVariables } from '@/utils/model-config' @@ -73,6 +73,7 @@ const TextGeneration: FC = ({ isWorkflow = false, }) => { const { notify } = Toast + const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp const { t } = useTranslation() const media = useBreakpoints() @@ -102,16 +103,18 @@ const TextGeneration: FC = ({ // save message const [savedMessages, setSavedMessages] = useState([]) const fetchSavedMessage = useCallback(async () => { - const res: any = await doFetchSavedMessage(isInstalledApp, appId) + if (!appId) + return + const res: any = await doFetchSavedMessage(appSourceType, appId) setSavedMessages(res.data) - }, [isInstalledApp, appId]) + }, [appSourceType, appId]) const handleSaveMessage = async (messageId: string) => { - await saveMessage(messageId, isInstalledApp, appId) + await saveMessage(messageId, appSourceType, appId) notify({ type: 'success', message: t('api.saved', { ns: 'common' }) }) fetchSavedMessage() } const handleRemoveSavedMessage = async (messageId: string) => { - await removeMessage(messageId, isInstalledApp, appId) + await removeMessage(messageId, appSourceType, appId) notify({ type: 'success', message: t('api.remove', { ns: 'common' }) }) fetchSavedMessage() } @@ -424,9 +427,8 @@ const TextGeneration: FC = ({ isCallBatchAPI={isCallBatchAPI} isPC={isPC} isMobile={!isPC} - isInstalledApp={isInstalledApp} + appSourceType={isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp} appId={appId} - installedAppInfo={installedAppInfo} isError={task?.status === TaskStatus.failed} promptConfig={promptConfig} moreLikeThisEnabled={!!moreLikeThisConfig?.enabled} diff --git a/web/app/components/share/text-generation/result/index.tsx b/web/app/components/share/text-generation/result/index.tsx index a0ffb31b06..fe518c6d25 100644 --- a/web/app/components/share/text-generation/result/index.tsx +++ b/web/app/components/share/text-generation/result/index.tsx @@ -4,8 +4,8 @@ import type { FeedbackType } from '@/app/components/base/chat/chat/type' import type { WorkflowProcess } from '@/app/components/base/chat/types' import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { PromptConfig } from '@/models/debug' -import type { InstalledApp } from '@/models/explore' import type { SiteInfo } from '@/models/share' +import type { AppSourceType } from '@/service/share' import type { VisionFile, VisionSettings } from '@/types/app' import { RiLoader2Line } from '@remixicon/react' import { useBoolean } from 'ahooks' @@ -35,9 +35,8 @@ export type IResultProps = { isCallBatchAPI: boolean isPC: boolean isMobile: boolean - isInstalledApp: boolean - appId: string - installedAppInfo?: InstalledApp + appSourceType: AppSourceType + appId?: string isError: boolean isShowTextToSpeech: boolean promptConfig: PromptConfig | null @@ -63,9 +62,8 @@ const Result: FC = ({ isCallBatchAPI, isPC, isMobile, - isInstalledApp, + appSourceType, appId, - installedAppInfo, isError, isShowTextToSpeech, promptConfig, @@ -133,7 +131,7 @@ const Result: FC = ({ }) const handleFeedback = async (feedback: FeedbackType) => { - await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, installedAppInfo?.id) + await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId) setFeedback(feedback) } @@ -147,9 +145,9 @@ const Result: FC = ({ setIsStopping(true) try { if (isWorkflow) - await stopWorkflowMessage(appId, currentTaskId, isInstalledApp, installedAppInfo?.id || '') + await stopWorkflowMessage(appId!, currentTaskId, appSourceType, appId || '') else - await stopChatMessageResponding(appId, currentTaskId, isInstalledApp, installedAppInfo?.id || '') + await stopChatMessageResponding(appId!, currentTaskId, appSourceType, appId || '') abortControllerRef.current?.abort() } catch (error) { @@ -159,7 +157,7 @@ const Result: FC = ({ finally { setIsStopping(false) } - }, [appId, currentTaskId, installedAppInfo?.id, isInstalledApp, isStopping, isWorkflow, notify]) + }, [appId, currentTaskId, appSourceType, appId, isStopping, isWorkflow, notify]) useEffect(() => { if (!onRunControlChange) @@ -468,8 +466,8 @@ const Result: FC = ({ })) }, }, - isInstalledApp, - installedAppInfo?.id, + appSourceType, + appId, ).catch((error) => { setRespondingFalse() resetRunState() @@ -514,7 +512,7 @@ const Result: FC = ({ getAbortController: (abortController) => { abortControllerRef.current = abortController }, - }, isInstalledApp, installedAppInfo?.id) + }, appSourceType, appId) } } @@ -562,8 +560,8 @@ const Result: FC = ({ feedback={feedback} onSave={handleSaveMessage} isMobile={isMobile} - isInstalledApp={isInstalledApp} - installedAppId={installedAppInfo?.id} + appSourceType={appSourceType} + installedAppId={appId} isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false} taskId={isCallBatchAPI ? ((taskId as number) < 10 ? `0${taskId}` : `${taskId}`) : undefined} controlClearMoreLikeThis={controlClearMoreLikeThis} diff --git a/web/app/components/share/text-generation/types.ts b/web/app/components/share/text-generation/types.ts new file mode 100644 index 0000000000..dba8eb2ca9 --- /dev/null +++ b/web/app/components/share/text-generation/types.ts @@ -0,0 +1,16 @@ +type TaskParam = { + inputs: Record +} + +export type Task = { + id: number + status: TaskStatus + params: TaskParam +} + +export enum TaskStatus { + pending = 'pending', + running = 'running', + completed = 'completed', + failed = 'failed', +} diff --git a/web/app/components/workflow/block-selector/featured-tools.tsx b/web/app/components/workflow/block-selector/featured-tools.tsx index f2795e7ec0..12a1c59e0e 100644 --- a/web/app/components/workflow/block-selector/featured-tools.tsx +++ b/web/app/components/workflow/block-selector/featured-tools.tsx @@ -13,6 +13,7 @@ import Tooltip from '@/app/components/base/tooltip' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import Action from '@/app/components/workflow/block-selector/market-place-plugin/action' import { useGetLanguage } from '@/context/i18n' +import { isServer } from '@/utils/client' import { formatNumber } from '@/utils/format' import { getMarketplaceUrl } from '@/utils/var' import BlockIcon from '../block-icon' @@ -49,14 +50,14 @@ const FeaturedTools = ({ const language = useGetLanguage() const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) const [isCollapsed, setIsCollapsed] = useState(() => { - if (typeof window === 'undefined') + if (isServer) return false const stored = window.localStorage.getItem(STORAGE_KEY) return stored === 'true' }) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return const stored = window.localStorage.getItem(STORAGE_KEY) if (stored !== null) @@ -64,7 +65,7 @@ const FeaturedTools = ({ }, []) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) }, [isCollapsed]) diff --git a/web/app/components/workflow/block-selector/featured-triggers.tsx b/web/app/components/workflow/block-selector/featured-triggers.tsx index 9ae6181a4f..01cb5d100f 100644 --- a/web/app/components/workflow/block-selector/featured-triggers.tsx +++ b/web/app/components/workflow/block-selector/featured-triggers.tsx @@ -12,6 +12,7 @@ import Tooltip from '@/app/components/base/tooltip' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import Action from '@/app/components/workflow/block-selector/market-place-plugin/action' import { useGetLanguage } from '@/context/i18n' +import { isServer } from '@/utils/client' import { formatNumber } from '@/utils/format' import { getMarketplaceUrl } from '@/utils/var' import BlockIcon from '../block-icon' @@ -42,14 +43,14 @@ const FeaturedTriggers = ({ const language = useGetLanguage() const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) const [isCollapsed, setIsCollapsed] = useState(() => { - if (typeof window === 'undefined') + if (isServer) return false const stored = window.localStorage.getItem(STORAGE_KEY) return stored === 'true' }) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return const stored = window.localStorage.getItem(STORAGE_KEY) if (stored !== null) @@ -57,7 +58,7 @@ const FeaturedTriggers = ({ }, []) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) }, [isCollapsed]) diff --git a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx index 3c62f488dc..e934f27fd1 100644 --- a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx +++ b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx @@ -11,6 +11,7 @@ import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid import Loading from '@/app/components/base/loading' import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils' import { useRAGRecommendedPlugins } from '@/service/use-tools' +import { isServer } from '@/utils/client' import { getMarketplaceUrl } from '@/utils/var' import List from './list' @@ -29,14 +30,14 @@ const RAGToolRecommendations = ({ }: RAGToolRecommendationsProps) => { const { t } = useTranslation() const [isCollapsed, setIsCollapsed] = useState(() => { - if (typeof window === 'undefined') + if (isServer) return false const stored = window.localStorage.getItem(STORAGE_KEY) return stored === 'true' }) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return const stored = window.localStorage.getItem(STORAGE_KEY) if (stored !== null) @@ -44,7 +45,7 @@ const RAGToolRecommendations = ({ }, []) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) }, [isCollapsed]) diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/bool-input.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/bool-input.tsx index 6a384d2cbc..bee42bb488 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/bool-input.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/bool-input.tsx @@ -10,6 +10,7 @@ type Props = { value: boolean required?: boolean onChange: (value: boolean) => void + readonly?: boolean } const BoolInput: FC = ({ @@ -17,6 +18,7 @@ const BoolInput: FC = ({ onChange, name, required, + readonly, }) => { const { t } = useTranslation() const handleChange = useCallback(() => { @@ -28,6 +30,7 @@ const BoolInput: FC = ({ className="!h-4 !w-4" checked={!!value} onCheck={handleChange} + disabled={readonly} />
{name} diff --git a/web/app/components/workflow/workflow-preview/index.tsx b/web/app/components/workflow/workflow-preview/index.tsx index 8f61c2cfb6..bb85e00b6b 100644 --- a/web/app/components/workflow/workflow-preview/index.tsx +++ b/web/app/components/workflow/workflow-preview/index.tsx @@ -61,12 +61,14 @@ type WorkflowPreviewProps = { edges: Edge[] viewport: Viewport className?: string + miniMapToRight?: boolean } const WorkflowPreview = ({ nodes, edges, viewport, className, + miniMapToRight, }: WorkflowPreviewProps) => { const [nodesData, setNodesData] = useState(() => initialNodes(nodes, edges)) const [edgesData, setEdgesData] = useState(() => initialEdges(edges, nodes)) @@ -97,8 +99,7 @@ const WorkflowPreview = ({ height: 72, }} maskColor="var(--color-workflow-minimap-bg)" - className="!absolute !bottom-14 !left-4 z-[9] !m-0 !h-[72px] !w-[102px] !rounded-lg !border-[0.5px] - !border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5" + className={cn('!absolute !bottom-14 z-[9] !m-0 !h-[72px] !w-[102px] !rounded-lg !border-[0.5px] !border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5', miniMapToRight ? '!right-4' : '!left-4')} />
diff --git a/web/context/app-list-context.ts b/web/context/app-list-context.ts new file mode 100644 index 0000000000..130f85966a --- /dev/null +++ b/web/context/app-list-context.ts @@ -0,0 +1,19 @@ +import type { CurrentTryAppParams } from './explore-context' +import { noop } from 'es-toolkit/function' +import { createContext } from 'use-context-selector' + +type Props = { + currentApp?: CurrentTryAppParams + isShowTryAppPanel: boolean + setShowTryAppPanel: (showTryAppPanel: boolean, params?: CurrentTryAppParams) => void + controlHideCreateFromTemplatePanel: number +} + +const AppListContext = createContext({ + isShowTryAppPanel: false, + setShowTryAppPanel: noop, + currentApp: undefined, + controlHideCreateFromTemplatePanel: 0, +}) + +export default AppListContext diff --git a/web/context/debug-configuration.ts b/web/context/debug-configuration.ts index ba157e1bf7..48e34fbe5e 100644 --- a/web/context/debug-configuration.ts +++ b/web/context/debug-configuration.ts @@ -29,6 +29,7 @@ import { PromptMode } from '@/models/debug' import { AppModeEnum, ModelModeType, Resolution, RETRIEVE_TYPE, TransferMethod } from '@/types/app' type IDebugConfiguration = { + readonly?: boolean appId: string isAPIKeySet: boolean isTrailFinished: boolean @@ -108,6 +109,7 @@ type IDebugConfiguration = { } const DebugConfigurationContext = createContext({ + readonly: false, appId: '', isAPIKeySet: false, isTrailFinished: false, diff --git a/web/context/explore-context.ts b/web/context/explore-context.ts index fc446c0453..2e85b8356b 100644 --- a/web/context/explore-context.ts +++ b/web/context/explore-context.ts @@ -1,7 +1,12 @@ -import type { InstalledApp } from '@/models/explore' +import type { App, InstalledApp } from '@/models/explore' import { noop } from 'es-toolkit/function' import { createContext } from 'use-context-selector' +export type CurrentTryAppParams = { + appId: string + app: App +} + type IExplore = { controlUpdateInstalledApps: number setControlUpdateInstalledApps: (controlUpdateInstalledApps: number) => void @@ -10,6 +15,9 @@ type IExplore = { setInstalledApps: (installedApps: InstalledApp[]) => void isFetchingInstalledApps: boolean setIsFetchingInstalledApps: (isFetchingInstalledApps: boolean) => void + currentApp?: CurrentTryAppParams + isShowTryAppPanel: boolean + setShowTryAppPanel: (showTryAppPanel: boolean, params?: CurrentTryAppParams) => void } const ExploreContext = createContext({ @@ -20,6 +28,9 @@ const ExploreContext = createContext({ setInstalledApps: noop, isFetchingInstalledApps: false, setIsFetchingInstalledApps: noop, + isShowTryAppPanel: false, + setShowTryAppPanel: noop, + currentApp: undefined, }) export default ExploreContext diff --git a/web/context/hooks/use-trigger-events-limit-modal.ts b/web/context/hooks/use-trigger-events-limit-modal.ts index 403df58378..72342cd0d3 100644 --- a/web/context/hooks/use-trigger-events-limit-modal.ts +++ b/web/context/hooks/use-trigger-events-limit-modal.ts @@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { NUM_INFINITE } from '@/app/components/billing/config' import { Plan } from '@/app/components/billing/type' import { IS_CLOUD_EDITION } from '@/config' +import { isServer } from '@/utils/client' export type TriggerEventsLimitModalPayload = { usage: number @@ -46,7 +47,7 @@ export const useTriggerEventsLimitModal = ({ useEffect(() => { if (!IS_CLOUD_EDITION) return - if (typeof window === 'undefined') + if (isServer) return if (!currentWorkspaceId) return diff --git a/web/context/query-client.tsx b/web/context/query-client.tsx index a72393490c..1cd64b168b 100644 --- a/web/context/query-client.tsx +++ b/web/context/query-client.tsx @@ -5,12 +5,13 @@ import type { FC, PropsWithChildren } from 'react' import { QueryClientProvider } from '@tanstack/react-query' import { useState } from 'react' import { TanStackDevtoolsLoader } from '@/app/components/devtools/tanstack/loader' +import { isServer } from '@/utils/client' import { makeQueryClient } from './query-client-server' let browserQueryClient: QueryClient | undefined function getQueryClient() { - if (typeof window === 'undefined') { + if (isServer) { return makeQueryClient() } if (!browserQueryClient) diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index b8191a5eea..2b3240eb3e 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -177,7 +177,7 @@ export default antfu( }, rules: { // 'dify-i18n/no-as-any-in-t': ['error', { mode: 'all' }], - 'dify-i18n/no-as-any-in-t': 'error', + // 'dify-i18n/no-as-any-in-t': 'error', // 'dify-i18n/no-legacy-namespace-prefix': 'error', // 'dify-i18n/require-ns-option': 'error', }, diff --git a/web/hooks/use-query-params.spec.tsx b/web/hooks/use-query-params.spec.tsx index b187471809..35e234881d 100644 --- a/web/hooks/use-query-params.spec.tsx +++ b/web/hooks/use-query-params.spec.tsx @@ -12,6 +12,13 @@ import { usePricingModal, } from './use-query-params' +// Mock isServer to allow runtime control in tests +const mockIsServer = vi.hoisted(() => ({ value: false })) +vi.mock('@/utils/client', () => ({ + get isServer() { return mockIsServer.value }, + get isClient() { return !mockIsServer.value }, +})) + const renderWithAdapter = (hook: () => T, searchParams = '') => { const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>() const wrapper = ({ children }: { children: ReactNode }) => ( @@ -428,6 +435,7 @@ describe('clearQueryParams', () => { afterEach(() => { vi.unstubAllGlobals() + mockIsServer.value = false }) it('should remove a single key when provided one key', () => { @@ -463,13 +471,13 @@ describe('clearQueryParams', () => { replaceSpy.mockRestore() }) - it('should no-op when window is undefined', () => { + it('should no-op when running on server', () => { // Arrange const replaceSpy = vi.spyOn(window.history, 'replaceState') - vi.stubGlobal('window', undefined) + mockIsServer.value = true // Act - expect(() => clearQueryParams('foo')).not.toThrow() + clearQueryParams('foo') // Assert expect(replaceSpy).not.toHaveBeenCalled() diff --git a/web/hooks/use-query-params.ts b/web/hooks/use-query-params.ts index 73798a4a4f..0749a1ffa5 100644 --- a/web/hooks/use-query-params.ts +++ b/web/hooks/use-query-params.ts @@ -21,6 +21,7 @@ import { } from 'nuqs' import { useCallback } from 'react' import { ACCOUNT_SETTING_MODAL_ACTION } from '@/app/components/header/account-setting/constants' +import { isServer } from '@/utils/client' /** * Modal State Query Parameters @@ -176,7 +177,7 @@ export function usePluginInstallation() { * clearQueryParams(['param1', 'param2']) */ export function clearQueryParams(keys: string | string[]) { - if (typeof window === 'undefined') + if (isServer) return const url = new URL(window.location.href) diff --git a/web/i18n/ar-TN/explore.json b/web/i18n/ar-TN/explore.json index 80c036e50c..290fa68873 100644 --- a/web/i18n/ar-TN/explore.json +++ b/web/i18n/ar-TN/explore.json @@ -1,11 +1,9 @@ { "appCard.addToWorkspace": "إضافة إلى مساحة العمل", - "appCard.customize": "تخصيص", "appCustomize.nameRequired": "اسم التطبيق مطلوب", "appCustomize.subTitle": "أيقونة التطبيق واسمه", "appCustomize.title": "إنشاء تطبيق من {{name}}", "apps.allCategories": "موصى به", - "apps.description": "استخدم تطبيقات القوالب هذه فورًا أو خصص تطبيقاتك الخاصة بناءً على القوالب.", "apps.title": "استكشاف التطبيقات", "category.Agent": "وكيل", "category.Assistant": "مساعد", @@ -23,7 +21,5 @@ "sidebar.chat": "دردشة", "sidebar.delete.content": "هل أنت متأكد أنك تريد حذف هذا التطبيق؟", "sidebar.delete.title": "حذف التطبيق", - "sidebar.discovery": "اكتشاف", - "sidebar.workspace": "مساحة العمل", "title": "استكشاف" } diff --git a/web/i18n/de-DE/explore.json b/web/i18n/de-DE/explore.json index 6461fbc76d..a06dea7d3a 100644 --- a/web/i18n/de-DE/explore.json +++ b/web/i18n/de-DE/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "Zum Arbeitsbereich hinzufügen", - "appCard.customize": "Anpassen", "appCustomize.nameRequired": "App-Name ist erforderlich", "appCustomize.subTitle": "App-Symbol & Name", "appCustomize.title": "App aus {{name}} erstellen", - "apps.allCategories": "Alle Kategorien", - "apps.description": "Nutzen Sie diese Vorlagen-Apps sofort oder passen Sie Ihre eigenen Apps basierend auf den Vorlagen an.", - "apps.title": "Apps von Dify erkunden", "category.Agent": "Agent", "category.Assistant": "Assistent", "category.Entertainment": "Unterhaltung", @@ -23,7 +18,5 @@ "sidebar.chat": "Chat", "sidebar.delete.content": "Sind Sie sicher, dass Sie diese App löschen möchten?", "sidebar.delete.title": "App löschen", - "sidebar.discovery": "Entdeckung", - "sidebar.workspace": "Arbeitsbereich", "title": "Entdecken" } diff --git a/web/i18n/de-DE/explore.ts b/web/i18n/de-DE/explore.ts new file mode 100644 index 0000000000..abada1081f --- /dev/null +++ b/web/i18n/de-DE/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'Entdecken', + sidebar: { + chat: 'Chat', + action: { + pin: 'Anheften', + unpin: 'Lösen', + rename: 'Umbenennen', + delete: 'Löschen', + }, + delete: { + title: 'App löschen', + content: 'Sind Sie sicher, dass Sie diese App löschen möchten?', + }, + }, + apps: { + }, + appCard: { + customize: 'Anpassen', + }, + appCustomize: { + title: 'App aus {{name}} erstellen', + subTitle: 'App-Symbol & Name', + nameRequired: 'App-Name ist erforderlich', + }, + category: { + Assistant: 'Assistent', + Writing: 'Schreiben', + Translate: 'Übersetzen', + Programming: 'Programmieren', + HR: 'Personalwesen', + Agent: 'Agent', + Workflow: 'Arbeitsablauf', + Entertainment: 'Unterhaltung', + }, +} + +export default translation diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json index 64ac47d804..0c94163260 100644 --- a/web/i18n/en-US/common.json +++ b/web/i18n/en-US/common.json @@ -108,6 +108,7 @@ "chat.conversationName": "Conversation name", "chat.conversationNameCanNotEmpty": "Conversation name required", "chat.conversationNamePlaceholder": "Please input conversation name", + "chat.inputDisabledPlaceholder": "Preview Only", "chat.inputPlaceholder": "Talk to {{botName}}", "chat.renameConversation": "Rename Conversation", "chat.resend": "Resend", diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts new file mode 100644 index 0000000000..e0af44cc89 --- /dev/null +++ b/web/i18n/en-US/common.ts @@ -0,0 +1,789 @@ +const translation = { + theme: { + theme: 'Theme', + light: 'light', + dark: 'dark', + auto: 'system', + }, + api: { + success: 'Success', + actionSuccess: 'Action succeeded', + saved: 'Saved', + create: 'Created', + remove: 'Removed', + }, + operation: { + create: 'Create', + confirm: 'Confirm', + cancel: 'Cancel', + clear: 'Clear', + save: 'Save', + yes: 'Yes', + no: 'No', + deleteConfirmTitle: 'Delete?', + confirmAction: 'Please confirm your action.', + saveAndEnable: 'Save & Enable', + edit: 'Edit', + add: 'Add', + added: 'Added', + refresh: 'Restart', + reset: 'Reset', + search: 'Search', + noSearchResults: 'No {{content}} were found', + resetKeywords: 'Reset keywords', + selectCount: '{{count}} Selected', + searchCount: 'Find {{count}} {{content}}', + noSearchCount: '0 {{content}}', + change: 'Change', + remove: 'Remove', + send: 'Send', + copy: 'Copy', + copied: 'Copied', + lineBreak: 'Line break', + sure: 'I\'m sure', + download: 'Download', + downloadSuccess: 'Download Completed.', + downloadFailed: 'Download failed. Please try again later.', + viewDetails: 'View Details', + delete: 'Delete', + now: 'Now', + deleteApp: 'Delete App', + settings: 'Settings', + setup: 'Setup', + config: 'Config', + getForFree: 'Get for free', + reload: 'Reload', + ok: 'OK', + log: 'Log', + learnMore: 'Learn More', + params: 'Params', + duplicate: 'Duplicate', + rename: 'Rename', + audioSourceUnavailable: 'AudioSource is unavailable', + close: 'Close', + copyImage: 'Copy Image', + imageCopied: 'Image copied', + zoomOut: 'Zoom Out', + zoomIn: 'Zoom In', + openInNewTab: 'Open in new tab', + in: 'in', + saveAndRegenerate: 'Save & Regenerate Child Chunks', + view: 'View', + viewMore: 'VIEW MORE', + regenerate: 'Regenerate', + submit: 'Submit', + skip: 'Skip', + format: 'Format', + more: 'More', + selectAll: 'Select All', + deSelectAll: 'Deselect All', + }, + errorMsg: { + fieldRequired: '{{field}} is required', + urlError: 'url should start with http:// or https://', + }, + placeholder: { + input: 'Please enter', + select: 'Please select', + search: 'Search...', + }, + noData: 'No data', + label: { + optional: '(optional)', + }, + voice: { + language: { + zhHans: 'Chinese', + zhHant: 'Traditional Chinese', + enUS: 'English', + deDE: 'German', + frFR: 'French', + esES: 'Spanish', + itIT: 'Italian', + thTH: 'Thai', + idID: 'Indonesian', + jaJP: 'Japanese', + koKR: 'Korean', + ptBR: 'Portuguese', + ruRU: 'Russian', + ukUA: 'Ukrainian', + viVN: 'Vietnamese', + plPL: 'Polish', + roRO: 'Romanian', + hiIN: 'Hindi', + trTR: 'Türkçe', + faIR: 'Farsi', + }, + }, + unit: { + char: 'chars', + }, + actionMsg: { + noModification: 'No modifications at the moment.', + modifiedSuccessfully: 'Modified successfully', + modifiedUnsuccessfully: 'Modified unsuccessfully', + copySuccessfully: 'Copied successfully', + paySucceeded: 'Payment succeeded', + payCancelled: 'Payment cancelled', + generatedSuccessfully: 'Generated successfully', + generatedUnsuccessfully: 'Generated unsuccessfully', + }, + model: { + params: { + temperature: 'Temperature', + temperatureTip: + 'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.', + top_p: 'Top P', + top_pTip: + 'Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered.', + presence_penalty: 'Presence penalty', + presence_penaltyTip: + 'How much to penalize new tokens based on whether they appear in the text so far.\nIncreases the model\'s likelihood to talk about new topics.', + frequency_penalty: 'Frequency penalty', + frequency_penaltyTip: + 'How much to penalize new tokens based on their existing frequency in the text so far.\nDecreases the model\'s likelihood to repeat the same line verbatim.', + max_tokens: 'Max token', + max_tokensTip: + 'Used to limit the maximum length of the reply, in tokens. \nLarger values may limit the space left for prompt words, chat logs, and Knowledge. \nIt is recommended to set it below two-thirds\ngpt-4-1106-preview, gpt-4-vision-preview max token (input 128k output 4k)', + maxTokenSettingTip: 'Your max token setting is high, potentially limiting space for prompts, queries, and data. Consider setting it below 2/3.', + setToCurrentModelMaxTokenTip: 'Max token is updated to the 80% maximum token of the current model {{maxToken}}.', + stop_sequences: 'Stop sequences', + stop_sequencesTip: 'Up to four sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence.', + stop_sequencesPlaceholder: 'Enter sequence and press Tab', + }, + tone: { + Creative: 'Creative', + Balanced: 'Balanced', + Precise: 'Precise', + Custom: 'Custom', + }, + addMoreModel: 'Go to settings to add more models', + settingsLink: 'Model Provider Settings', + capabilities: 'MultiModal Capabilities', + }, + menus: { + status: 'beta', + explore: 'Explore', + apps: 'Studio', + appDetail: 'App Detail', + account: 'Account', + plugins: 'Plugins', + exploreMarketplace: 'Explore Marketplace', + pluginsTips: 'Integrate third-party plugins or create ChatGPT-compatible AI-Plugins.', + datasets: 'Knowledge', + datasetsTips: 'COMING SOON: Import your own text data or write data in real-time via Webhook for LLM context enhancement.', + newApp: 'New App', + newDataset: 'Create Knowledge', + tools: 'Tools', + }, + userProfile: { + settings: 'Settings', + contactUs: 'Contact Us', + emailSupport: 'Email Support', + workspace: 'Workspace', + createWorkspace: 'Create Workspace', + helpCenter: 'View Docs', + support: 'Support', + compliance: 'Compliance', + forum: 'Forum', + roadmap: 'Roadmap', + github: 'GitHub', + community: 'Community', + about: 'About', + logout: 'Log out', + }, + compliance: { + soc2Type1: 'SOC 2 Type I Report', + soc2Type2: 'SOC 2 Type II Report', + iso27001: 'ISO 27001:2022 Certification', + gdpr: 'GDPR DPA', + sandboxUpgradeTooltip: 'Only available with a Professional or Team plan.', + professionalUpgradeTooltip: 'Only available with a Team plan or above.', + }, + settings: { + accountGroup: 'GENERAL', + workplaceGroup: 'WORKSPACE', + generalGroup: 'GENERAL', + account: 'My account', + members: 'Members', + billing: 'Billing', + integrations: 'Integrations', + language: 'Language', + provider: 'Model Provider', + dataSource: 'Data Source', + plugin: 'Plugins', + apiBasedExtension: 'API Extension', + }, + account: { + account: 'Account', + myAccount: 'My Account', + studio: 'Studio', + avatar: 'Avatar', + name: 'Name', + email: 'Email', + password: 'Password', + passwordTip: 'You can set a permanent password if you don’t want to use temporary login codes', + setPassword: 'Set a password', + resetPassword: 'Reset password', + currentPassword: 'Current password', + newPassword: 'New password', + confirmPassword: 'Confirm password', + notEqual: 'Two passwords are different.', + langGeniusAccount: 'Account\'s data', + langGeniusAccountTip: 'The user data of your account.', + editName: 'Edit Name', + showAppLength: 'Show {{length}} apps', + delete: 'Delete Account', + deleteTip: 'Please note, once confirmed, as the Owner of any Workspaces, your workspaces will be scheduled in a queue for permanent deletion, and all your user data will be queued for permanent deletion.', + deletePrivacyLinkTip: 'For more information about how we handle your data, please see our ', + deletePrivacyLink: 'Privacy Policy.', + deleteSuccessTip: 'Your account needs time to finish deleting. We\'ll email you when it\'s all done.', + deleteLabel: 'To confirm, please type in your email below', + deletePlaceholder: 'Please enter your email', + sendVerificationButton: 'Send Verification Code', + verificationLabel: 'Verification Code', + verificationPlaceholder: 'Paste the 6-digit code', + permanentlyDeleteButton: 'Permanently Delete Account', + feedbackTitle: 'Feedback', + feedbackLabel: 'Tell us why you deleted your account?', + feedbackPlaceholder: 'Optional', + editWorkspaceInfo: 'Edit Workspace Info', + workspaceName: 'Workspace Name', + workspaceIcon: 'Workspace Icon', + changeEmail: { + title: 'Change Email', + verifyEmail: 'Verify your current email', + newEmail: 'Set up a new email address', + verifyNew: 'Verify your new email', + authTip: 'Once your email is changed, Google or GitHub accounts linked to your old email will no longer be able to log in to this account.', + content1: 'If you continue, we\'ll send a verification code to {{email}} for re-authentication.', + content2: 'Your current email is {{email}}. Verification code has been sent to this email address.', + content3: 'Enter a new email and we will send you a verification code.', + content4: 'We just sent you a temporary verification code to {{email}}.', + codeLabel: 'Verification code', + codePlaceholder: 'Paste the 6-digit code', + emailLabel: 'New email', + emailPlaceholder: 'Enter a new email', + existingEmail: 'A user with this email already exists.', + unAvailableEmail: 'This email is temporarily unavailable.', + sendVerifyCode: 'Send verification code', + continue: 'Continue', + changeTo: 'Change to {{email}}', + resendTip: 'Didn\'t receive a code?', + resendCount: 'Resend in {{count}}s', + resend: 'Resend', + }, + }, + members: { + team: 'Team', + invite: 'Add', + name: 'NAME', + lastActive: 'LAST ACTIVE', + role: 'ROLES', + pending: 'Pending...', + owner: 'Owner', + admin: 'Admin', + adminTip: 'Can build apps & manage team settings', + normal: 'Normal', + normalTip: 'Only can use apps, can not build apps', + builder: 'Builder', + builderTip: 'Can build & edit own apps', + editor: 'Editor', + editorTip: 'Can build & edit apps', + datasetOperator: 'Knowledge Admin', + datasetOperatorTip: 'Only can manage the knowledge base', + inviteTeamMember: 'Add team member', + inviteTeamMemberTip: 'They can access your team data directly after signing in.', + emailNotSetup: 'Email server is not set up, so invitation emails cannot be sent. Please notify users of the invitation link that will be issued after invitation instead.', + email: 'Email', + emailInvalid: 'Invalid Email Format', + emailPlaceholder: 'Please input emails', + sendInvite: 'Send Invite', + invitedAsRole: 'Invited as {{role}} user', + invitationSent: 'Invitation sent', + invitationSentTip: 'Invitation sent, and they can sign in to Dify to access your team data.', + invitationLink: 'Invitation Link', + failedInvitationEmails: 'Below users were not invited successfully', + ok: 'OK', + removeFromTeam: 'Remove from team', + removeFromTeamTip: 'Will remove team access', + setAdmin: 'Set as administrator', + setMember: 'Set to ordinary member', + setBuilder: 'Set as builder', + setEditor: 'Set as editor', + disInvite: 'Cancel the invitation', + deleteMember: 'Delete Member', + you: '(You)', + transferOwnership: 'Transfer Ownership', + transferModal: { + title: 'Transfer workspace ownership', + warning: 'You\'re about to transfer ownership of “{{workspace}}”. This takes effect immediately and can\'t be undone.', + warningTip: 'You\'ll become an admin member, and the new owner will have full control.', + sendTip: 'If you continue, we\'ll send a verification code to {{email}} for re-authentication.', + verifyEmail: 'Verify your current email', + verifyContent: 'Your current email is {{email}}.', + verifyContent2: 'We\'ll send a temporary verification code to this email for re-authentication.', + codeLabel: 'Verification code', + codePlaceholder: 'Paste the 6-digit code', + resendTip: 'Didn\'t receive a code?', + resendCount: 'Resend in {{count}}s', + resend: 'Resend', + transferLabel: 'Transfer workspace ownership to', + transferPlaceholder: 'Select a workspace member…', + sendVerifyCode: 'Send verification code', + continue: 'Continue', + transfer: 'Transfer workspace ownership', + }, + }, + feedback: { + title: 'Provide Feedback', + subtitle: 'Please tell us what went wrong with this response', + content: 'Feedback Content', + placeholder: 'Please describe what went wrong or how we can improve...', + }, + integrations: { + connected: 'Connected', + google: 'Google', + googleAccount: 'Login with Google account', + github: 'GitHub', + githubAccount: 'Login with GitHub account', + connect: 'Connect', + }, + language: { + displayLanguage: 'Display Language', + timezone: 'Time Zone', + }, + provider: { + apiKey: 'API Key', + enterYourKey: 'Enter your API key here', + invalidKey: 'Invalid OpenAI API key', + validatedError: 'Validation failed: ', + validating: 'Validating key...', + saveFailed: 'Save api key failed', + apiKeyExceedBill: 'This API KEY has no quota available, please read', + addKey: 'Add Key', + comingSoon: 'Coming Soon', + editKey: 'Edit', + invalidApiKey: 'Invalid API key', + azure: { + apiBase: 'API Base', + apiBasePlaceholder: 'The API Base URL of your Azure OpenAI Endpoint.', + apiKey: 'API Key', + apiKeyPlaceholder: 'Enter your API key here', + helpTip: 'Learn Azure OpenAI Service', + }, + openaiHosted: { + openaiHosted: 'Hosted OpenAI', + onTrial: 'ON TRIAL', + exhausted: 'QUOTA EXHAUSTED', + desc: 'The OpenAI hosting service provided by Dify allows you to use models such as GPT-3.5. Before your trial quota is used up, you need to set up other model providers.', + callTimes: 'Call times', + usedUp: 'Trial quota used up. Add own Model Provider.', + useYourModel: 'Currently using own Model Provider.', + close: 'Close', + }, + anthropicHosted: { + anthropicHosted: 'Anthropic Claude', + onTrial: 'ON TRIAL', + exhausted: 'QUOTA EXHAUSTED', + desc: 'Powerful model, which excels at a wide range of tasks from sophisticated dialogue and creative content generation to detailed instruction.', + callTimes: 'Call times', + usedUp: 'Trial quota used up. Add own Model Provider.', + useYourModel: 'Currently using own Model Provider.', + close: 'Close', + trialQuotaTip: 'Your Anthropic trial quota will expire on 2025/03/17 and will no longer be available thereafter. Please make use of it in time.', + }, + anthropic: { + using: 'The embedding capability is using', + enableTip: 'To enable the Anthropic model, you need to bind to OpenAI or Azure OpenAI Service first.', + notEnabled: 'Not enabled', + keyFrom: 'Get your API key from Anthropic', + }, + encrypted: { + front: 'Your API KEY will be encrypted and stored using', + back: ' technology.', + }, + }, + modelProvider: { + notConfigured: 'The system model has not yet been fully configured', + systemModelSettings: 'System Model Settings', + systemModelSettingsLink: 'Why is it necessary to set up a system model?', + selectModel: 'Select your model', + setupModelFirst: 'Please set up your model first', + systemReasoningModel: { + key: 'System Reasoning Model', + tip: 'Set the default inference model to be used for creating applications, as well as features such as dialogue name generation and next question suggestion will also use the default inference model.', + }, + embeddingModel: { + key: 'Embedding Model', + tip: 'Set the default model for document embedding processing of the Knowledge, both retrieval and import of the Knowledge use this Embedding model for vectorization processing. Switching will cause the vector dimension between the imported Knowledge and the question to be inconsistent, resulting in retrieval failure. To avoid retrieval failure, please do not switch this model at will.', + required: 'Embedding Model is required', + }, + speechToTextModel: { + key: 'Speech-to-Text Model', + tip: 'Set the default model for speech-to-text input in conversation.', + }, + ttsModel: { + key: 'Text-to-Speech Model', + tip: 'Set the default model for text-to-speech input in conversation.', + }, + rerankModel: { + key: 'Rerank Model', + tip: 'Rerank model will reorder the candidate document list based on the semantic match with user query, improving the results of semantic ranking', + }, + apiKey: 'API-KEY', + quota: 'Quota', + searchModel: 'Search model', + noModelFound: 'No model found for {{model}}', + models: 'Models', + showMoreModelProvider: 'Show more model provider', + selector: { + tip: 'This model has been removed. Please add a model or select another model.', + emptyTip: 'No available models', + emptySetting: 'Please go to settings to configure', + rerankTip: 'Please set up the Rerank model', + }, + card: { + quota: 'QUOTA', + onTrial: 'On Trial', + paid: 'Paid', + quotaExhausted: 'Quota exhausted', + callTimes: 'Call times', + tokens: 'Tokens', + buyQuota: 'Buy Quota', + priorityUse: 'Priority use', + removeKey: 'Remove API Key', + tip: 'Priority will be given to the paid quota. The Trial quota will be used after the paid quota is exhausted.', + }, + item: { + deleteDesc: '{{modelName}} are being used as system reasoning models. Some functions will not be available after removal. Please confirm.', + freeQuota: 'FREE QUOTA', + }, + addApiKey: 'Add your API key', + invalidApiKey: 'Invalid API key', + encrypted: { + front: 'Your API KEY will be encrypted and stored using', + back: ' technology.', + }, + freeQuota: { + howToEarn: 'How to earn', + }, + addMoreModelProvider: 'ADD MORE MODEL PROVIDER', + addModel: 'Add Model', + modelsNum: '{{num}} Models', + showModels: 'Show Models', + showModelsNum: 'Show {{num}} Models', + collapse: 'Collapse', + config: 'Config', + modelAndParameters: 'Model and Parameters', + model: 'Model', + featureSupported: '{{feature}} supported', + callTimes: 'Call times', + credits: 'Message Credits', + buyQuota: 'Buy Quota', + getFreeTokens: 'Get free Tokens', + priorityUsing: 'Prioritize using', + deprecated: 'Deprecated', + confirmDelete: 'Confirm deletion?', + quotaTip: 'Remaining available free tokens', + loadPresets: 'Load Presets', + parameters: 'PARAMETERS', + loadBalancing: 'Load balancing', + loadBalancingDescription: 'Configure multiple credentials for the model and invoke them automatically. ', + loadBalancingHeadline: 'Load Balancing', + configLoadBalancing: 'Config Load Balancing', + modelHasBeenDeprecated: 'This model has been deprecated', + providerManaged: 'Provider managed', + providerManagedDescription: 'Use the single set of credentials provided by the model provider.', + defaultConfig: 'Default Config', + apiKeyStatusNormal: 'APIKey status is normal', + apiKeyRateLimit: 'Rate limit was reached, available after {{seconds}}s', + addConfig: 'Add Config', + editConfig: 'Edit Config', + loadBalancingLeastKeyWarning: 'To enable load balancing at least 2 keys must be enabled.', + loadBalancingInfo: 'By default, load balancing uses the Round-robin strategy. If rate limiting is triggered, a 1-minute cooldown period will be applied.', + upgradeForLoadBalancing: 'Upgrade your plan to enable Load Balancing.', + toBeConfigured: 'To be configured', + configureTip: 'Set up api-key or add model to use', + installProvider: 'Install model providers', + installDataSourceProvider: 'Install data source providers', + discoverMore: 'Discover more in ', + emptyProviderTitle: 'Model provider not set up', + emptyProviderTip: 'Please install a model provider first.', + auth: { + unAuthorized: 'Unauthorized', + authRemoved: 'Auth removed', + apiKeys: 'API Keys', + addApiKey: 'Add API Key', + addModel: 'Add model', + addNewModel: 'Add new model', + addCredential: 'Add credential', + addModelCredential: 'Add model credential', + editModelCredential: 'Edit model credential', + modelCredentials: 'Model credentials', + modelCredential: 'Model credential', + configModel: 'Config model', + configLoadBalancing: 'Config Load Balancing', + authorizationError: 'Authorization error', + specifyModelCredential: 'Specify model credential', + specifyModelCredentialTip: 'Use a configured model credential.', + providerManaged: 'Provider managed', + providerManagedTip: 'The current configuration is hosted by the provider.', + apiKeyModal: { + title: 'API Key Authorization Configuration', + desc: 'After configuring credentials, all members within the workspace can use this model when orchestrating applications.', + addModel: 'Add model', + }, + manageCredentials: 'Manage Credentials', + customModelCredentials: 'Custom Model Credentials', + addNewModelCredential: 'Add new model credential', + removeModel: 'Remove Model', + selectModelCredential: 'Select a model credential', + customModelCredentialsDeleteTip: 'Credential is in use and cannot be deleted', + }, + parametersInvalidRemoved: 'Some parameters are invalid and have been removed', + }, + dataSource: { + add: 'Add a data source', + connect: 'Connect', + configure: 'Configure', + notion: { + title: 'Notion', + description: 'Using Notion as a data source for the Knowledge.', + connectedWorkspace: 'Connected workspace', + addWorkspace: 'Add workspace', + connected: 'Connected', + disconnected: 'Disconnected', + changeAuthorizedPages: 'Change authorized pages', + integratedAlert: 'Notion is integrated via internal credential, no need to re-authorize.', + pagesAuthorized: 'Pages authorized', + sync: 'Sync', + remove: 'Remove', + selector: { + pageSelected: 'Pages Selected', + searchPages: 'Search pages...', + noSearchResult: 'No search results', + addPages: 'Add pages', + preview: 'PREVIEW', + }, + }, + website: { + title: 'Website', + description: 'Import content from websites using web crawler.', + with: 'With', + configuredCrawlers: 'Configured crawlers', + active: 'Active', + inactive: 'Inactive', + }, + }, + plugin: { + serpapi: { + apiKey: 'API Key', + apiKeyPlaceholder: 'Enter your API key', + keyFrom: 'Get your SerpAPI key from SerpAPI Account Page', + }, + }, + apiBasedExtension: { + title: 'API extensions provide centralized API management, simplifying configuration for easy use across Dify\'s applications.', + link: 'Learn how to develop your own API Extension.', + add: 'Add API Extension', + selector: { + title: 'API Extension', + placeholder: 'Please select API extension', + manage: 'Manage API Extension', + }, + modal: { + title: 'Add API Extension', + editTitle: 'Edit API Extension', + name: { + title: 'Name', + placeholder: 'Please enter the name', + }, + apiEndpoint: { + title: 'API Endpoint', + placeholder: 'Please enter the API endpoint', + }, + apiKey: { + title: 'API-key', + placeholder: 'Please enter the API-key', + lengthError: 'API-key length cannot be less than 5 characters', + }, + }, + type: 'Type', + }, + about: { + changeLog: 'Changelog', + updateNow: 'Update now', + nowAvailable: 'Dify {{version}} is now available.', + latestAvailable: 'Dify {{version}} is the latest version available.', + }, + appMenus: { + overview: 'Monitoring', + promptEng: 'Orchestrate', + apiAccess: 'API Access', + logAndAnn: 'Logs & Annotations', + logs: 'Logs', + }, + environment: { + testing: 'TESTING', + development: 'DEVELOPMENT', + }, + appModes: { + completionApp: 'Text Generator', + chatApp: 'Chat App', + }, + datasetMenus: { + documents: 'Documents', + hitTesting: 'Retrieval Testing', + settings: 'Settings', + emptyTip: 'This Knowledge has not been integrated within any application. Please refer to the document for guidance.', + viewDoc: 'View documentation', + relatedApp: 'linked apps', + noRelatedApp: 'No linked apps', + pipeline: 'Pipeline', + }, + voiceInput: { + speaking: 'Speak now...', + converting: 'Converting to text...', + notAllow: 'microphone not authorized', + }, + modelName: { + 'gpt-3.5-turbo': 'GPT-3.5-Turbo', + 'gpt-3.5-turbo-16k': 'GPT-3.5-Turbo-16K', + 'gpt-4': 'GPT-4', + 'gpt-4-32k': 'GPT-4-32K', + 'text-davinci-003': 'Text-Davinci-003', + 'text-embedding-ada-002': 'Text-Embedding-Ada-002', + 'whisper-1': 'Whisper-1', + 'claude-instant-1': 'Claude-Instant', + 'claude-2': 'Claude-2', + }, + chat: { + renameConversation: 'Rename Conversation', + conversationName: 'Conversation name', + conversationNamePlaceholder: 'Please input conversation name', + conversationNameCanNotEmpty: 'Conversation name required', + citation: { + title: 'CITATIONS', + linkToDataset: 'Link to Knowledge', + characters: 'Characters:', + hitCount: 'Retrieval count:', + vectorHash: 'Vector hash:', + hitScore: 'Retrieval Score:', + }, + inputPlaceholder: 'Talk to {{botName}}', + inputDisabledPlaceholder: 'Preview Only', + thinking: 'Thinking...', + thought: 'Thought', + resend: 'Resend', + }, + promptEditor: { + placeholder: 'Write your prompt word here, enter \'{\' to insert a variable, enter \'/\' to insert a prompt content block', + context: { + item: { + title: 'Context', + desc: 'Insert context template', + }, + modal: { + title: '{{num}} Knowledge in Context', + add: 'Add Context ', + footer: 'You can manage contexts in the Context section below.', + }, + }, + history: { + item: { + title: 'Conversation History', + desc: 'Insert historical message template', + }, + modal: { + title: 'EXAMPLE', + user: 'Hello', + assistant: 'Hello! How can I assist you today?', + edit: 'Edit Conversation Role Names', + }, + }, + variable: { + item: { + title: 'Variables & External Tools', + desc: 'Insert Variables & External Tools', + }, + outputToolDisabledItem: { + title: 'Variables', + desc: 'Insert Variables', + }, + modal: { + add: 'New variable', + addTool: 'New tool', + }, + }, + query: { + item: { + title: 'Query', + desc: 'Insert user query template', + }, + }, + existed: 'Already exists in the prompt', + }, + imageUploader: { + uploadFromComputer: 'Upload from Computer', + uploadFromComputerReadError: 'Image reading failed, please try again.', + uploadFromComputerUploadError: 'Image upload failed, please upload again.', + uploadFromComputerLimit: 'Upload images cannot exceed {{size}} MB', + pasteImageLink: 'Paste image link', + pasteImageLinkInputPlaceholder: 'Paste image link here', + pasteImageLinkInvalid: 'Invalid image link', + imageUpload: 'Image Upload', + }, + fileUploader: { + uploadFromComputer: 'Local upload', + pasteFileLink: 'Paste file link', + pasteFileLinkInputPlaceholder: 'Enter URL...', + uploadFromComputerReadError: 'File reading failed, please try again.', + uploadFromComputerUploadError: 'File upload failed, please upload again.', + uploadFromComputerLimit: 'Upload {{type}} cannot exceed {{size}}', + pasteFileLinkInvalid: 'Invalid file link', + fileExtensionNotSupport: 'File extension not supported', + fileExtensionBlocked: 'This file type is blocked for security reasons', + }, + tag: { + placeholder: 'All Tags', + addNew: 'Add new tag', + noTag: 'No tags', + noTagYet: 'No tags yet', + addTag: 'Add tags', + editTag: 'Edit tags', + manageTags: 'Manage Tags', + selectorPlaceholder: 'Type to search or create', + create: 'Create', + delete: 'Delete tag', + deleteTip: 'The tag is being used, delete it?', + created: 'Tag created successfully', + failed: 'Tag creation failed', + }, + license: { + expiring: 'Expiring in one day', + expiring_plural: 'Expiring in {{count}} days', + unlimited: 'Unlimited', + }, + pagination: { + perPage: 'Items per page', + }, + avatar: { + deleteTitle: 'Remove Avatar', + deleteDescription: 'Are you sure you want to remove your profile picture? Your account will use the default initial avatar.', + }, + imageInput: { + dropImageHere: 'Drop your image here, or', + browse: 'browse', + supportedFormats: 'Supports PNG, JPG, JPEG, WEBP and GIF', + }, + you: 'You', + dynamicSelect: { + error: 'Loading options failed', + noData: 'No options available', + loading: 'Loading options...', + selected: '{{count}} selected', + }, +} + +export default translation diff --git a/web/i18n/en-US/explore.json b/web/i18n/en-US/explore.json index ba8fd9d448..89bbea81e5 100644 --- a/web/i18n/en-US/explore.json +++ b/web/i18n/en-US/explore.json @@ -1,12 +1,14 @@ { - "appCard.addToWorkspace": "Add to Workspace", - "appCard.customize": "Customize", + "appCard.addToWorkspace": "Use template", + "appCard.try": "Details", "appCustomize.nameRequired": "App name is required", "appCustomize.subTitle": "App icon & name", "appCustomize.title": "Create app from {{name}}", - "apps.allCategories": "Recommended", - "apps.description": "Use these template apps instantly or customize your own apps based on the templates.", - "apps.title": "Explore Apps", + "apps.allCategories": "All", + "apps.resetFilter": "Clear filter", + "apps.resultNum": "{{num}} results", + "apps.title": "Try Dify's curated apps to find AI solutions for your business", + "banner.viewMore": "VIEW MORE", "category.Agent": "Agent", "category.Assistant": "Assistant", "category.Entertainment": "Entertainment", @@ -23,7 +25,16 @@ "sidebar.chat": "Chat", "sidebar.delete.content": "Are you sure you want to delete this app?", "sidebar.delete.title": "Delete app", - "sidebar.discovery": "Discovery", - "sidebar.workspace": "Workspace", - "title": "Explore" + "sidebar.noApps.description": "Published web apps will appear here", + "sidebar.noApps.learnMore": "Learn more", + "sidebar.noApps.title": "No web apps", + "sidebar.title": "App gallery", + "sidebar.webApps": "Web apps", + "title": "Explore", + "tryApp.category": "Category", + "tryApp.createFromSampleApp": "Create from this sample app", + "tryApp.requirements": "Requirements", + "tryApp.tabHeader.detail": "Orchestration Details", + "tryApp.tabHeader.try": "Try it", + "tryApp.tryInfo": "This is a sample app. You can try up to 5 messages. To keep using it, click \"Create form this sample app\" and set it up!" } diff --git a/web/i18n/en-US/explore.ts b/web/i18n/en-US/explore.ts new file mode 100644 index 0000000000..b1891f6d3e --- /dev/null +++ b/web/i18n/en-US/explore.ts @@ -0,0 +1,63 @@ +const translation = { + title: 'Explore', + sidebar: { + title: 'App gallery', + chat: 'Chat', + webApps: 'Web apps', + action: { + pin: 'Pin', + unpin: 'Unpin', + rename: 'Rename', + delete: 'Delete', + }, + delete: { + title: 'Delete app', + content: 'Are you sure you want to delete this app?', + }, + noApps: { + title: 'No web apps', + description: 'Published web apps will appear here', + learnMore: 'Learn more', + }, + }, + apps: { + title: 'Try Dify\'s curated apps to find AI solutions for your business', + allCategories: 'All', + resultNum: '{{num}} results', + resetFilter: 'Clear filter', + }, + appCard: { + addToWorkspace: 'Use template', + try: 'Details', + }, + tryApp: { + tabHeader: { + try: 'Try it', + detail: 'Orchestration Details', + }, + createFromSampleApp: 'Create from this sample app', + category: 'Category', + requirements: 'Requirements', + tryInfo: 'This is a sample app. You can try up to 5 messages. To keep using it, click "Create form this sample app" and set it up!', + }, + appCustomize: { + title: 'Create app from {{name}}', + subTitle: 'App icon & name', + nameRequired: 'App name is required', + }, + category: { + Agent: 'Agent', + Assistant: 'Assistant', + Writing: 'Writing', + Translate: 'Translate', + Programming: 'Programming', + HR: 'HR', + Workflow: 'Workflow', + Entertainment: 'Entertainment', + }, + banner: { + viewMore: 'VIEW MORE', + }, +} + +export default translation diff --git a/web/i18n/es-ES/explore.json b/web/i18n/es-ES/explore.json index 51308de42d..e5d249741d 100644 --- a/web/i18n/es-ES/explore.json +++ b/web/i18n/es-ES/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "Agregar al espacio de trabajo", - "appCard.customize": "Personalizar", "appCustomize.nameRequired": "El nombre de la aplicación es obligatorio", "appCustomize.subTitle": "Icono y nombre de la aplicación", "appCustomize.title": "Crear aplicación a partir de {{name}}", - "apps.allCategories": "Recomendado", - "apps.description": "Utiliza estas aplicaciones de plantilla al instante o personaliza tus propias aplicaciones basadas en las plantillas.", - "apps.title": "Explorar aplicaciones de Dify", "category.Agent": "Agente", "category.Assistant": "Asistente", "category.Entertainment": "Entretenimiento", @@ -23,7 +18,5 @@ "sidebar.chat": "Chat", "sidebar.delete.content": "¿Estás seguro de que quieres eliminar esta aplicación?", "sidebar.delete.title": "Eliminar aplicación", - "sidebar.discovery": "Descubrimiento", - "sidebar.workspace": "Espacio de trabajo", "title": "Explorar" } diff --git a/web/i18n/es-ES/explore.ts b/web/i18n/es-ES/explore.ts new file mode 100644 index 0000000000..dcd7e7ab91 --- /dev/null +++ b/web/i18n/es-ES/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'Explorar', + sidebar: { + chat: 'Chat', + action: { + pin: 'Anclar', + unpin: 'Desanclar', + rename: 'Renombrar', + delete: 'Eliminar', + }, + delete: { + title: 'Eliminar aplicación', + content: '¿Estás seguro de que quieres eliminar esta aplicación?', + }, + }, + apps: { + }, + appCard: { + customize: 'Personalizar', + }, + appCustomize: { + title: 'Crear aplicación a partir de {{name}}', + subTitle: 'Icono y nombre de la aplicación', + nameRequired: 'El nombre de la aplicación es obligatorio', + }, + category: { + Assistant: 'Asistente', + Writing: 'Escritura', + Translate: 'Traducción', + Programming: 'Programación', + HR: 'Recursos Humanos', + Agent: 'Agente', + Workflow: 'Flujo de trabajo', + Entertainment: 'Entretenimiento', + }, +} + +export default translation diff --git a/web/i18n/fa-IR/explore.json b/web/i18n/fa-IR/explore.json index 206a24ea5b..11a7c76465 100644 --- a/web/i18n/fa-IR/explore.json +++ b/web/i18n/fa-IR/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "افزودن به فضای کاری", - "appCard.customize": "سفارشی کردن", "appCustomize.nameRequired": "نام برنامه الزامی است", "appCustomize.subTitle": "آیکون و نام برنامه", "appCustomize.title": "ایجاد برنامه از {{name}}", - "apps.allCategories": "پیشنهاد شده", - "apps.description": "از این برنامه‌های قالبی بلافاصله استفاده کنید یا برنامه‌های خود را بر اساس این قالب‌ها سفارشی کنید.", - "apps.title": "کاوش برنامه‌ها توسط دیفی", "category.Agent": "عامل", "category.Assistant": "دستیار", "category.Entertainment": "سرگرمی", @@ -23,7 +18,5 @@ "sidebar.chat": "چت", "sidebar.delete.content": "آیا مطمئن هستید که می‌خواهید این برنامه را حذف کنید؟", "sidebar.delete.title": "حذف برنامه", - "sidebar.discovery": "کشف", - "sidebar.workspace": "فضای کاری", "title": "کاوش" } diff --git a/web/i18n/fa-IR/explore.ts b/web/i18n/fa-IR/explore.ts new file mode 100644 index 0000000000..0c28102380 --- /dev/null +++ b/web/i18n/fa-IR/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'کاوش', + sidebar: { + chat: 'چت', + action: { + pin: 'سنجاق کردن', + unpin: 'برداشتن سنجاق', + rename: 'تغییر نام', + delete: 'حذف', + }, + delete: { + title: 'حذف برنامه', + content: 'آیا مطمئن هستید که می‌خواهید این برنامه را حذف کنید؟', + }, + }, + apps: { + }, + appCard: { + customize: 'سفارشی کردن', + }, + appCustomize: { + title: 'ایجاد برنامه از {{name}}', + subTitle: 'آیکون و نام برنامه', + nameRequired: 'نام برنامه الزامی است', + }, + category: { + Assistant: 'دستیار', + Writing: 'نوشتن', + Translate: 'ترجمه', + Programming: 'برنامه‌نویسی', + HR: 'منابع انسانی', + Agent: 'عامل', + Workflow: 'گردش', + Entertainment: 'سرگرمی', + }, +} + +export default translation diff --git a/web/i18n/fr-FR/explore.json b/web/i18n/fr-FR/explore.json index 34b8fbfc58..5b18a9d4d9 100644 --- a/web/i18n/fr-FR/explore.json +++ b/web/i18n/fr-FR/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "Ajouter à l'espace de travail", - "appCard.customize": "Personnaliser", "appCustomize.nameRequired": "Le nom de l'application est requis", "appCustomize.subTitle": "Icône de l'application & nom", "appCustomize.title": "Créer une application à partir de {{name}}", - "apps.allCategories": "Recommandé", - "apps.description": "Utilisez ces applications modèles instantanément ou personnalisez vos propres applications basées sur les modèles.", - "apps.title": "Explorez les applications par Dify", "category.Agent": "Agent", "category.Assistant": "Assistant", "category.Entertainment": "Divertissement", @@ -23,7 +18,5 @@ "sidebar.chat": "Discussion", "sidebar.delete.content": "Êtes-vous sûr de vouloir supprimer cette application ?", "sidebar.delete.title": "Supprimer l'application", - "sidebar.discovery": "Découverte", - "sidebar.workspace": "Espace de travail", "title": "Explorer" } diff --git a/web/i18n/fr-FR/explore.ts b/web/i18n/fr-FR/explore.ts new file mode 100644 index 0000000000..1ab22c160b --- /dev/null +++ b/web/i18n/fr-FR/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'Explorer', + sidebar: { + chat: 'Discussion', + action: { + pin: 'Épingle', + unpin: 'Détacher', + rename: 'Renommer', + delete: 'Supprimer', + }, + delete: { + title: 'Supprimer l\'application', + content: 'Êtes-vous sûr de vouloir supprimer cette application ?', + }, + }, + apps: { + }, + appCard: { + customize: 'Personnaliser', + }, + appCustomize: { + title: 'Créer une application à partir de {{name}}', + subTitle: 'Icône de l\'application & nom', + nameRequired: 'Le nom de l\'application est requis', + }, + category: { + Assistant: 'Assistant', + Writing: 'Écriture', + Translate: 'Traduire', + Programming: 'Programmation', + HR: 'RH', + Agent: 'Agent', + Workflow: 'Flux de travail', + Entertainment: 'Divertissement', + }, +} + +export default translation diff --git a/web/i18n/hi-IN/explore.json b/web/i18n/hi-IN/explore.json index 737868a4e5..955ca7a56e 100644 --- a/web/i18n/hi-IN/explore.json +++ b/web/i18n/hi-IN/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "कार्यक्षेत्र में जोड़ें", - "appCard.customize": "अनुकूलित करें", "appCustomize.nameRequired": "ऐप का नाम आवश्यक है", "appCustomize.subTitle": "ऐप आइकन और नाम", "appCustomize.title": "{{name}} से ऐप बनाएँ", - "apps.allCategories": "अनुशंसित", - "apps.description": "इन टेम्प्लेट ऐप्स का तुरंत उपयोग करें या टेम्प्लेट्स के आधार पर अपने स्वयं के ऐप्स को कस्टमाइज़ करें।", - "apps.title": "डिफ़ी द्वारा ऐप्स का अन्वेषण करें", "category.Agent": "आढ़तिया", "category.Assistant": "सहायक", "category.Entertainment": "मनोरंजन", @@ -23,7 +18,5 @@ "sidebar.chat": "चैट", "sidebar.delete.content": "क्या आप वाकई इस ऐप को हटाना चाहते हैं?", "sidebar.delete.title": "ऐप हटाएं", - "sidebar.discovery": "खोज", - "sidebar.workspace": "कार्यक्षेत्र", "title": "अन्वेषण करें" } diff --git a/web/i18n/hi-IN/explore.ts b/web/i18n/hi-IN/explore.ts new file mode 100644 index 0000000000..879b5c4a5d --- /dev/null +++ b/web/i18n/hi-IN/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'अन्वेषण करें', + sidebar: { + chat: 'चैट', + action: { + pin: 'पिन करें', + unpin: 'पिन हटाएँ', + rename: 'नाम बदलें', + delete: 'हटाएं', + }, + delete: { + title: 'ऐप हटाएं', + content: 'क्या आप वाकई इस ऐप को हटाना चाहते हैं?', + }, + }, + apps: { + }, + appCard: { + customize: 'अनुकूलित करें', + }, + appCustomize: { + title: '{{name}} से ऐप बनाएँ', + subTitle: 'ऐप आइकन और नाम', + nameRequired: 'ऐप का नाम आवश्यक है', + }, + category: { + Assistant: 'सहायक', + Writing: 'लेखन', + Translate: 'अनुवाद', + Programming: 'प्रोग्रामिंग', + HR: 'मानव संसाधन', + Workflow: 'कार्यप्रवाह', + Agent: 'आढ़तिया', + Entertainment: 'मनोरंजन', + }, +} + +export default translation diff --git a/web/i18n/id-ID/explore.json b/web/i18n/id-ID/explore.json index 3ba35de9eb..c74576c2d7 100644 --- a/web/i18n/id-ID/explore.json +++ b/web/i18n/id-ID/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "Tambahkan ke Ruang Kerja", - "appCard.customize": "Menyesuaikan", "appCustomize.nameRequired": "Nama aplikasi diperlukan", "appCustomize.subTitle": "Ikon & nama aplikasi", "appCustomize.title": "Buat aplikasi dari {{name}}", - "apps.allCategories": "Direkomendasikan", - "apps.description": "Gunakan aplikasi templat ini secara instan atau sesuaikan aplikasi Anda sendiri berdasarkan templat.", - "apps.title": "Jelajahi Aplikasi", "category.Agent": "Agen", "category.Assistant": "Asisten", "category.Entertainment": "Hiburan", @@ -23,7 +18,5 @@ "sidebar.chat": "Mengobrol", "sidebar.delete.content": "Apakah Anda yakin ingin menghapus aplikasi ini?", "sidebar.delete.title": "Hapus aplikasi", - "sidebar.discovery": "Penemuan", - "sidebar.workspace": "Workspace", "title": "Menjelajahi" } diff --git a/web/i18n/id-ID/explore.ts b/web/i18n/id-ID/explore.ts new file mode 100644 index 0000000000..aa6fa46d16 --- /dev/null +++ b/web/i18n/id-ID/explore.ts @@ -0,0 +1,37 @@ +const translation = { + sidebar: { + action: { + unpin: 'Lepaskan sematan', + pin: 'Sematkan', + delete: 'Hapus', + rename: 'Ganti nama', + }, + delete: { + content: 'Apakah Anda yakin ingin menghapus aplikasi ini?', + title: 'Hapus aplikasi', + }, + chat: 'Mengobrol', + }, + apps: { + }, + appCard: { + customize: 'Menyesuaikan', + }, + appCustomize: { + subTitle: 'Ikon & nama aplikasi', + nameRequired: 'Nama aplikasi diperlukan', + }, + category: { + Entertainment: 'Hiburan', + Agent: 'Agen', + Writing: 'Tulisan', + Assistant: 'Asisten', + Programming: 'Pemrograman', + Translate: 'Terjemah', + Workflow: 'Alur Kerja', + HR: 'HR', + }, + title: 'Menjelajahi', +} + +export default translation diff --git a/web/i18n/it-IT/explore.json b/web/i18n/it-IT/explore.json index 80dc79df02..6e3e400a2b 100644 --- a/web/i18n/it-IT/explore.json +++ b/web/i18n/it-IT/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "Aggiungi a Workspace", - "appCard.customize": "Personalizza", "appCustomize.nameRequired": "Il nome dell'app è obbligatorio", "appCustomize.subTitle": "Icona & nome dell'app", "appCustomize.title": "Crea app da {{name}}", - "apps.allCategories": "Consigliato", - "apps.description": "Usa queste app modello istantaneamente o personalizza le tue app basate sui modelli.", - "apps.title": "Esplora App di Dify", "category.Agent": "Agente", "category.Assistant": "Assistente", "category.Entertainment": "Intrattenimento", @@ -23,7 +18,5 @@ "sidebar.chat": "Chat", "sidebar.delete.content": "Sei sicuro di voler eliminare questa app?", "sidebar.delete.title": "Elimina app", - "sidebar.discovery": "Scoperta", - "sidebar.workspace": "Workspace", "title": "Esplora" } diff --git a/web/i18n/it-IT/explore.ts b/web/i18n/it-IT/explore.ts new file mode 100644 index 0000000000..1150f609af --- /dev/null +++ b/web/i18n/it-IT/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'Esplora', + sidebar: { + chat: 'Chat', + action: { + pin: 'Fissa', + unpin: 'Sblocca', + rename: 'Rinomina', + delete: 'Elimina', + }, + delete: { + title: 'Elimina app', + content: 'Sei sicuro di voler eliminare questa app?', + }, + }, + apps: { + }, + appCard: { + customize: 'Personalizza', + }, + appCustomize: { + title: 'Crea app da {{name}}', + subTitle: 'Icona & nome dell\'app', + nameRequired: 'Il nome dell\'app è obbligatorio', + }, + category: { + Assistant: 'Assistente', + Writing: 'Scrittura', + Translate: 'Traduzione', + Programming: 'Programmazione', + HR: 'Risorse Umane', + Workflow: 'Flusso di lavoro', + Agent: 'Agente', + Entertainment: 'Intrattenimento', + }, +} + +export default translation diff --git a/web/i18n/ja-JP/common.json b/web/i18n/ja-JP/common.json index 11f543e7e5..ff6f15cad0 100644 --- a/web/i18n/ja-JP/common.json +++ b/web/i18n/ja-JP/common.json @@ -108,6 +108,7 @@ "chat.conversationName": "会話名", "chat.conversationNameCanNotEmpty": "会話名は必須です", "chat.conversationNamePlaceholder": "会話名を入力してください", + "chat.inputDisabledPlaceholder": "プレビューのみ", "chat.inputPlaceholder": "{{botName}} と話す", "chat.renameConversation": "会話名を変更", "chat.resend": "再送信してください", diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts new file mode 100644 index 0000000000..a4f858290a --- /dev/null +++ b/web/i18n/ja-JP/common.ts @@ -0,0 +1,776 @@ +const translation = { + theme: { + theme: 'テーマ', + light: '明るい', + dark: '暗い', + auto: 'システム', + }, + api: { + success: '成功', + actionSuccess: 'アクションが成功しました', + saved: '保存済み', + create: '作成済み', + remove: '削除済み', + }, + operation: { + create: '作成', + confirm: '確認', + cancel: 'キャンセル', + clear: 'クリア', + save: '保存', + saveAndEnable: '保存 & 有効に', + edit: '編集', + add: '追加', + added: '追加済み', + refresh: 'リフレッシュ', + reset: 'リセット', + search: '検索', + change: '変更', + remove: '削除', + send: '送信', + copy: 'コピー', + lineBreak: '改行', + sure: '確認済み', + download: 'ダウンロード', + downloadSuccess: 'ダウンロード完了', + downloadFailed: 'ダウンロードに失敗しました、後で再試行してください。', + delete: '削除', + settings: '設定', + setup: 'セットアップ', + getForFree: '無料で入手', + reload: '再読み込み', + ok: 'OK', + log: 'ログ', + learnMore: '詳細はこちら', + params: 'パラメータ', + duplicate: '複製', + rename: '名前の変更', + audioSourceUnavailable: 'AudioSource が利用できません', + zoomIn: 'ズームインする', + openInNewTab: '新しいタブで開く', + zoomOut: 'ズームアウト', + copyImage: '画像をコピー', + saveAndRegenerate: '保存して子チャンクを再生成', + close: '閉じる', + view: '表示', + viewMore: 'さらに表示', + regenerate: '再生成', + submit: '送信', + skip: 'スキップ', + imageCopied: 'コピーした画像', + deleteApp: 'アプリを削除', + viewDetails: '詳細を見る', + copied: 'コピーしました', + in: '中', + format: 'フォーマット', + more: 'もっと', + selectAll: 'すべて選択', + deSelectAll: 'すべて選択解除', + now: '今', + config: 'コンフィグ', + yes: 'はい', + no: 'いいえ', + deleteConfirmTitle: '削除しますか?', + confirmAction: '操作を確認してください。', + }, + errorMsg: { + fieldRequired: '{{field}}は必要です', + urlError: 'URL は http:// または https:// で始まる必要があります', + }, + placeholder: { + input: '入力してください', + select: '選択してください', + }, + voice: { + language: { + zhHans: '中国語', + zhHant: '繁体字中国語', + enUS: '英語', + deDE: 'ドイツ語', + frFR: 'フランス語', + esES: 'スペイン語', + itIT: 'イタリア語', + thTH: 'タイ語', + idID: 'インドネシア語', + jaJP: '日本語', + koKR: '韓国語', + ptBR: 'ポルトガル語', + ruRU: 'ロシア語', + ukUA: 'ウクライナ語', + viVN: 'ベトナム語', + plPL: 'ポーランド語', + roRO: 'ルーマニア語', + hiIN: 'ヒンディー語', + trTR: 'トルコ語', + faIR: 'ペルシア語', + }, + }, + unit: { + char: '文字', + }, + actionMsg: { + noModification: '現在は変更されていません。', + modifiedSuccessfully: '変更が正常に行われました', + modifiedUnsuccessfully: '変更が失敗しました', + copySuccessfully: 'コピーが正常に行われました', + paySucceeded: '支払いが成功しました', + payCancelled: '支払いがキャンセルされました', + generatedSuccessfully: '生成が成功しました', + generatedUnsuccessfully: '生成が失敗しました', + }, + model: { + params: { + temperature: '温度', + temperatureTip: + 'ランダム性を制御します:温度を下げると、よりランダムな完成品が得られます。温度がゼロに近づくにつれて、モデルは決定的で反復的になります。', + top_p: '上位 P', + top_pTip: + 'ニュークリアスサンプリングによる多様性の制御:0.5 は、すべての尤度加重オプションの半分が考慮されることを意味します。', + presence_penalty: '存在ペナルティ', + presence_penaltyTip: + 'これまでのテキストにトークンが表示されるかどうかに基づいて、新しいトークンにいくらペナルティを科すかを制御します。\nモデルが新しいトピックについて話す可能性が高まります。', + frequency_penalty: '頻度ペナルティ', + frequency_penaltyTip: + 'これまでのテキスト内のトークンの既存の頻度に基づいて、新しいトークンにどれだけペナルティを科すかを制御します。\nモデルが同じ行を文字通りに繰り返す可能性が低くなります。', + max_tokens: '最大トークン', + max_tokensTip: + '返信の最大長をトークン単位で制限するために使用されます。\n大きな値はプロンプトの単語、チャットログ、およびナレッジのために残されたスペースを制限する可能性があります。\nそれを 2/3 以下に設定することをお勧めします。\ngpt-4-1106-preview、gpt-4-vision-preview の最大トークン(入力 128k 出力 4k)以下に設定することをお勧めします。', + maxTokenSettingTip: '最大トークン設定が高いため、プロンプト、クエリ、およびデータのスペースが制限される可能性があります。現在のモデルの最大トークンの 80% 以下に設定してください。', + setToCurrentModelMaxTokenTip: '最大トークンが現在のモデルの最大トークンの 80% に更新されました {{maxToken}}.', + stop_sequences: '停止シーケンス', + stop_sequencesTip: 'API が進行中のトークンの生成を停止する最大 4 つのシーケンス。返されたテキストには停止シーケンスは含まれません。', + stop_sequencesPlaceholder: 'シーケンスを入力してタブキーを押してください', + }, + tone: { + Creative: 'クリエイティブ', + Balanced: 'バランス', + Precise: '正確', + Custom: 'カスタム', + }, + addMoreModel: '設定画面から他のモデルを追加してください', + capabilities: 'マルチモーダル機能', + settingsLink: 'モデルプロバイダー設定', + }, + menus: { + status: 'ベータ版', + explore: '探索', + apps: 'スタジオ', + appDetail: 'アプリの詳細', + account: 'アカウント', + plugins: 'プラグイン', + pluginsTips: 'サードパーティのプラグインを統合するか、ChatGPT 互換の AI プラグインを作成します。', + datasets: 'ナレッジ', + datasetsTips: '近日公開:独自のテキストデータをインポートするか、Webhook を介してリアルタイムにデータを記述して LLM コンテキストを強化します。', + newApp: '新しいアプリ', + newDataset: 'ナレッジの作成', + tools: 'ツール', + exploreMarketplace: 'マーケットプレイスを探索する', + }, + userProfile: { + settings: '設定', + emailSupport: 'サポート', + workspace: 'ワークスペース', + createWorkspace: 'ワークスペースを作成', + helpCenter: 'ドキュメントを見る', + support: 'サポート', + compliance: 'コンプライアンス', + roadmap: 'ロードマップ', + community: 'コミュニティ', + about: 'Dify について', + logout: 'ログアウト', + github: 'GitHub', + contactUs: 'お問い合わせ', + forum: 'フォーラム', + }, + compliance: { + soc2Type1: 'SOC 2 Type I 報告書', + soc2Type2: 'SOC 2 Type II 報告書', + iso27001: 'ISO 27001:2022 認証', + gdpr: 'GDPR データ処理契約(DPA)', + sandboxUpgradeTooltip: 'プロフェッショナルプランまたはチームプランでのみ利用可能', + professionalUpgradeTooltip: 'チームプラン以上の契約が必要です', + }, + settings: { + accountGroup: 'アカウント', + workplaceGroup: 'ワークスペース', + account: 'マイアカウント', + members: 'メンバー', + billing: '請求', + integrations: '統合', + language: '言語', + provider: 'モデルプロバイダー', + dataSource: 'データソース', + plugin: 'プラグイン', + apiBasedExtension: 'API 拡張', + generalGroup: '一般', + }, + account: { + avatar: 'アバター', + name: '名前', + email: 'メール', + password: 'パスワード', + passwordTip: '一時的なログインコードを使用したくない場合は、永続的なパスワードを設定できます。', + setPassword: 'パスワードを設定', + resetPassword: 'パスワードをリセット', + currentPassword: '現在のパスワード', + newPassword: '新しいパスワード', + confirmPassword: 'パスワードを確認', + notEqual: '2 つのパスワードが異なります。', + langGeniusAccount: 'アカウント関連データ', + langGeniusAccountTip: 'アカウントに関連するユーザーデータ。', + editName: '名前を編集', + showAppLength: '{{length}}アプリを表示', + delete: 'アカウントを削除', + deleteTip: 'アカウントを削除すると、すべてのデータが完全に消去され、復元できなくなります。', + account: 'アカウント', + myAccount: 'マイアカウント', + studio: 'スタジオ', + deletePrivacyLinkTip: 'お客様のデータの取り扱い方法の詳細については、当社の', + deletePrivacyLink: 'プライバシーポリシー。', + deleteSuccessTip: 'アカウントの削除が完了するまでに時間が必要です。すべて完了しましたら、メールでお知らせします。', + deleteLabel: '確認するには、以下にメールアドレスを入力してください', + deletePlaceholder: 'メールアドレスを入力してください', + verificationLabel: '認証コード', + verificationPlaceholder: '6 桁のコードを貼り付けます', + permanentlyDeleteButton: 'アカウントを完全に削除', + feedbackTitle: 'フィードバック', + feedbackLabel: 'アカウントを削除した理由を教えてください。', + feedbackPlaceholder: '任意', + sendVerificationButton: '確認コードの送信', + editWorkspaceInfo: 'ワークスペース情報を編集', + workspaceName: 'ワークスペース名', + workspaceIcon: 'ワークスペースアイコン', + changeEmail: { + title: 'メールアドレスを変更', + verifyEmail: '現在のメールアドレスを確認してください', + newEmail: '新しいメールアドレスを設定する', + verifyNew: '新しいメールアドレスを確認してください', + authTip: 'メールアドレスが変更されると、旧メールアドレスにリンクされている Google または GitHub アカウントは、このアカウントにログインできなくなります。', + content1: '変更を続ける場合、{{email}} に認証用の確認コードをお送りします。', + content2: '現在のメールアドレスは {{email}} です。認証コードはこのメールアドレスに送信されました。', + content3: '新しいメールアドレスを入力すると、確認コードが送信されます。', + content4: '一時確認コードを {{email}} に送信しました。', + codeLabel: 'コード', + codePlaceholder: 'コードを入力してください', + emailLabel: '新しいメール', + emailPlaceholder: '新しいメールを入力してください', + existingEmail: 'このメールアドレスのユーザーは既に存在します', + unAvailableEmail: 'このメールアドレスは現在使用できません。', + sendVerifyCode: '確認コードを送信', + continue: '続行', + changeTo: '{{email}} に変更', + resendTip: 'コードが届きませんか?', + resendCount: '{{count}} 秒後に再送信', + resend: '再送信', + }, + }, + members: { + team: 'チーム', + invite: '招待', + name: '名前', + lastActive: '最終アクティブ', + role: 'ロール', + pending: '保留中...', + owner: 'オーナー', + admin: '管理者', + adminTip: 'アプリの構築およびチーム設定の管理ができます', + normal: '通常', + normalTip: 'アプリの使用のみが可能で、アプリの構築はできません', + builder: 'ビルダー', + builderTip: '独自のアプリを作成・編集できる', + editor: 'エディター', + editorTip: 'アプリの構築ができますが、チーム設定の管理はできません', + datasetOperator: 'ナレッジ管理員', + datasetOperatorTip: 'ナレッジベースのみを管理できる', + inviteTeamMember: 'チームメンバーを招待する', + inviteTeamMemberTip: '彼らはサインイン後、直接あなたのチームデータにアクセスできます。', + emailNotSetup: 'メールサーバーがセットアップされていないので、招待メールを送信することはできません。代わりに招待後に発行される招待リンクをユーザーに通知してください。', + email: 'メール', + emailInvalid: '無効なメール形式', + emailPlaceholder: 'メールを入力してください', + sendInvite: '招待を送る', + invitedAsRole: '{{role}}ユーザーとして招待されました', + invitationSent: '招待が送信されました', + invitationSentTip: '招待が送信され、彼らは Dify にサインインしてあなたのチームデータにアクセスできます。', + invitationLink: '招待リンク', + failedInvitationEmails: '以下のユーザーは正常に招待されませんでした', + ok: 'OK', + removeFromTeam: 'チームから削除', + removeFromTeamTip: 'チームへのアクセスが削除されます', + setAdmin: '管理者に設定', + setMember: '通常のメンバーに設定', + setBuilder: 'ビルダーに設定', + setEditor: 'エディターに設定', + disInvite: '招待をキャンセル', + deleteMember: 'メンバーを削除', + you: '(あなた)', + transferOwnership: '所有権の移転', + transferModal: { + title: 'ワークスペースの所有権を移する', + warning: '「{{workspace}}」の所有権を移しようとしています。この操作は即時に有効となり、元に戻すことはできません。', + warningTip: 'あなたは管理者メンバーになり、新しいオーナーがすべての権限を持つことになります。', + sendTip: '続行する場合は、本人確認のため {{email}} に認証コードを送信します。', + verifyEmail: '現在のメールアドレスを確認', + verifyContent: '現在のメールアドレスは {{email}}。', + verifyContent2: 'このメールアドレスに一時的な認証コードを送信し、再認証を行います。', + codeLabel: '認証コード', + codePlaceholder: '6 桁のコードを入力してください', + resendTip: '認証コードを受け取れない場合は、', + resendCount: '{{count}} 秒後に再送信', + resend: '認証コードを再送信', + transferLabel: 'ワークスペースの所有権を転移する相手は', + transferPlaceholder: 'メールアドレスを入力してください', + sendVerifyCode: '認証コードを送信', + continue: '続行する', + transfer: 'ワークスペースの所有権を移する', + }, + }, + integrations: { + connected: '接続済み', + google: 'Google', + googleAccount: 'Google アカウントでログイン', + github: 'GitHub', + githubAccount: 'GitHub アカウントでログイン', + connect: '接続', + }, + language: { + displayLanguage: '表示言語', + timezone: 'タイムゾーン', + }, + provider: { + apiKey: 'API キー', + enterYourKey: 'ここに API キーを入力してください', + invalidKey: '無効な OpenAI API キー', + validatedError: '検証に失敗しました:', + validating: 'キーの検証中...', + saveFailed: 'API キーの保存に失敗しました', + apiKeyExceedBill: 'この API KEY には使用可能なクォータがありません。詳細は', + addKey: 'キーを追加', + comingSoon: '近日公開', + editKey: '編集', + invalidApiKey: '無効な API キー', + azure: { + apiBase: 'API ベース', + apiBasePlaceholder: 'Azure OpenAI エンドポイントの API ベース URL。', + apiKey: 'API キー', + apiKeyPlaceholder: 'ここに API キーを入力してください', + helpTip: 'Azure OpenAI サービスを学ぶ', + }, + openaiHosted: { + openaiHosted: 'ホステッド OpenAI', + onTrial: 'トライアル中', + exhausted: 'クォータが使い果たされました', + desc: 'Dify が提供する OpenAI ホスティングサービスを使用すると、GPT-3.5 などのモデルを使用できます。トライアルクォータが使い果たされる前に、他のモデルプロバイダを設定する必要があります。', + callTimes: '通話回数', + usedUp: 'トライアルクォータが使い果たされました。独自のモデルプロバイダを追加してください。', + useYourModel: '現在、独自のモデルプロバイダを使用しています。', + close: '閉じる', + }, + anthropicHosted: { + anthropicHosted: 'アンソピッククロード', + onTrial: 'トライアル中', + exhausted: 'クォータが使い果たされました', + desc: '高度なダイアログやクリエイティブなコンテンツ生成から詳細な指示まで、幅広いタスクに優れたパワフルなモデルです。', + callTimes: '通話回数', + usedUp: 'トライアルクォータが使い果たされました。独自のモデルプロバイダを追加してください。', + useYourModel: '現在、独自のモデルプロバイダを使用しています。', + close: '閉じる', + trialQuotaTip: 'お客様の Anthropic 試用枠は 2025/03/17 に失効し、その後は利用できなくなります。お早めにご利用ください。', + }, + anthropic: { + using: '埋め込み機能は使用中です', + enableTip: 'Anthropic モデルを有効にするには、まず OpenAI または Azure OpenAI サービスにバインドする必要があります。', + notEnabled: '有効にされていません', + keyFrom: 'Anthropic から API キーを取得してください', + }, + encrypted: { + front: 'API KEY は', + back: '技術を使用して暗号化および保存されます。', + }, + }, + modelProvider: { + notConfigured: 'システムモデルがまだ完全に設定されておらず、一部の機能が利用できない場合があります。', + systemModelSettings: 'システムモデル設定', + systemModelSettingsLink: 'システムモデルの設定が必要な理由は何ですか?', + selectModel: 'モデルを選択', + setupModelFirst: 'まずモデルをセットアップしてください', + systemReasoningModel: { + key: 'システム推論モデル', + tip: 'アプリの作成に使用されるデフォルトの推論モデルを設定します。また、対話名の生成や次の質問の提案などの機能もデフォルトの推論モデルを使用します。', + }, + embeddingModel: { + key: '埋め込みモデル', + tip: 'ナレッジのドキュメント埋め込み処理のデフォルトモデルを設定します。ナレッジの取得とインポートの両方に、この埋め込みモデルをベクトル化処理に使用します。切り替えると、インポートされたナレッジと質問の間のベクトル次元が一致せず、取得に失敗します。取得の失敗を避けるためには、このモデルを任意に切り替えないでください。', + required: '埋め込みモデルが必要です', + }, + speechToTextModel: { + key: '音声-to-テキストモデル', + tip: '会話での音声-to-テキスト入力に使用するデフォルトモデルを設定します。', + }, + ttsModel: { + key: 'テキスト-to-音声モデル', + tip: '会話でのテキスト-to-音声入力に使用するデフォルトモデルを設定します。', + }, + rerankModel: { + key: 'Rerank モデル', + tip: 'Rerank モデルは、ユーザークエリとの意味的一致に基づいて候補文書リストを再配置し、意味的ランキングの結果を向上させます。', + }, + apiKey: 'API-キー', + quota: 'クォータ', + searchModel: '検索モデル', + noModelFound: '{{model}}に対するモデルが見つかりません', + models: 'モデル', + showMoreModelProvider: 'より多くのモデルプロバイダを表示', + selector: { + tip: 'このモデルは削除されました。別のモデルを追加するか、別のモデルを選択してください。', + emptyTip: '利用可能なモデルはありません', + emptySetting: '設定に移動して構成してください', + rerankTip: 'Rerank モデルを設定してください', + }, + card: { + quota: 'クォータ', + onTrial: 'トライアル中', + paid: '有料', + quotaExhausted: 'クォータが使い果たされました', + callTimes: '通話回数', + tokens: 'トークン', + buyQuota: 'クォータを購入', + priorityUse: '優先利用', + removeKey: 'API キーを削除', + tip: '有料クォータは優先して使用されます。有料クォータを使用し終えた後、トライアルクォータが利用されます。', + }, + item: { + deleteDesc: '{{modelName}}はシステムが推測するモデルとして利用されています。削除すると、一部の機能が使用不可能になる可能性があります。ご確認ください。', + freeQuota: '無料のクォータ', + }, + addApiKey: 'API キーを追加', + invalidApiKey: '無効な API キー', + encrypted: { + front: 'API キーは', + back: ' の技術で暗号化されて保存されます。', + }, + freeQuota: { + howToEarn: '獲得方法', + }, + addMoreModelProvider: 'モデルプロバイダを追加', + addModel: 'モデルを追加', + modelsNum: '{{num}}のモデル', + showModels: 'モデルの表示', + showModelsNum: '{{num}}のモデルを表示', + collapse: '折り畳み', + config: '設定', + modelAndParameters: 'モデルとパラメータ', + model: 'モデル', + featureSupported: '{{feature}}に対応', + callTimes: '呼び出し回数', + credits: 'クレジット', + buyQuota: 'クォータ購入', + getFreeTokens: '無料トークンを獲得', + priorityUsing: '優先利用', + deprecated: '廃止予定', + confirmDelete: '削除を確認', + quotaTip: '残りの無料トークン', + loadPresets: 'プリセットの読み込み', + parameters: 'パラメータ', + loadBalancing: '負荷分散', + loadBalancingDescription: '複数の認証情報を使って負荷を分散させます。', + loadBalancingHeadline: '負荷分散', + configLoadBalancing: '負荷分散の設定', + modelHasBeenDeprecated: 'このモデルは廃止予定です', + providerManaged: 'プロバイダ管理', + providerManagedDescription: 'モデルプロバイダによって提供される認証情報を使用します。', + defaultConfig: 'デフォルトの設定', + apiKeyStatusNormal: 'API キーの状態は正常', + apiKeyRateLimit: 'レート制限に到達しました。{{seconds}}秒後に再度利用可能です', + addConfig: '設定を追加', + editConfig: '設定を編集', + loadBalancingLeastKeyWarning: '負荷分散を利用するには、最低 2 つのキーを有効化する必要があります。', + loadBalancingInfo: 'デフォルトでは、負荷分散はラウンドロビン方式を採用しています。レート制限が発生した場合、1 分間のクールダウン期間が適用されます。', + upgradeForLoadBalancing: '負荷分散を利用するには、プランのアップグレードが必要です。', + emptyProviderTitle: 'モデルプロバイダーが設定されていません', + discoverMore: 'もっと発見する', + installProvider: 'モデルプロバイダーをインストールする', + installDataSourceProvider: 'データソースプロバイダーをインストールする', + configureTip: 'API キーを設定するか、使用するモデルを追加してください', + toBeConfigured: '設定中', + emptyProviderTip: '最初にモデルプロバイダーをインストールしてください。', + auth: { + apiKeyModal: { + title: 'APIキー認証設定', + addModel: 'モデルを追加する', + desc: '認証情報を設定した後、ワークスペース内のすべてのメンバーは、アプリケーションを調整する際にこのモデルを使用できます。', + }, + authorizationError: '認証エラー', + apiKeys: 'APIキー', + unAuthorized: '無許可', + configModel: 'モデルを構成する', + addApiKey: 'APIキーを追加してください', + addCredential: '認証情報を追加する', + authRemoved: '認証が削除されました', + modelCredentials: 'モデルの資格情報', + providerManaged: 'プロバイダーが管理しました', + addNewModel: '新しいモデルを追加する', + configLoadBalancing: '構成ロードバランシング', + addModelCredential: 'モデルの資格情報を追加', + providerManagedTip: '現在の設定はプロバイダーによってホストされています。', + specifyModelCredential: 'モデルの資格情報を指定してください', + specifyModelCredentialTip: '構成されたモデルの認証情報を使用してください。', + addModel: 'モデルを追加する', + addNewModelCredential: '新しいモデルの認証情報を追加する', + editModelCredential: 'モデルの資格情報を編集する', + removeModel: 'モデルを削除する', + customModelCredentialsDeleteTip: '認証情報は使用中で、削除できません。', + modelCredential: 'モデルの資格情報', + manageCredentials: '認証情報を管理する', + customModelCredentials: 'カスタムモデルの認証情報', + selectModelCredential: 'モデルの資格情報を選択する', + }, + parametersInvalidRemoved: 'いくつかのパラメータが無効であり、削除されました。', + }, + dataSource: { + add: 'データソースの追加', + connect: '接続', + configure: '設定', + notion: { + title: 'Notion', + description: 'ナレッジデータソースとして Notion を使用します。', + connectedWorkspace: '接続済みワークスペース', + addWorkspace: 'ワークスペースの追加', + connected: '接続済み', + disconnected: '接続解除', + changeAuthorizedPages: '認証済みページの変更', + pagesAuthorized: '認証済みページ', + sync: '同期', + remove: '削除', + selector: { + pageSelected: '選択済みページ', + searchPages: 'ページ検索...', + noSearchResult: '検索結果なし', + addPages: 'ページの追加', + preview: 'プレビュー', + }, + integratedAlert: 'Notionは内部資格情報を通じて統合されており、再認証する必要はありません。', + }, + website: { + title: 'ウェブサイト', + description: 'ウェブクローラーを使ってウェブサイトからコンテンツを取り込みます。', + with: 'による', + configuredCrawlers: '設定済みクローラー', + active: 'アクティブ', + inactive: '非アクティブ', + }, + }, + plugin: { + serpapi: { + apiKey: 'API キー', + apiKeyPlaceholder: 'API キーを入力してください', + keyFrom: 'SerpAPI アカウントページから SerpAPI キーを取得してください', + }, + }, + apiBasedExtension: { + title: 'API 拡張機能は、Dify のアプリケーション全体での簡単な使用のための設定を簡素化し、集中的な API 管理を提供します。', + link: '独自の API 拡張機能を開発する方法について学ぶ。', + add: 'API 拡張機能を追加', + selector: { + title: 'API 拡張機能', + placeholder: 'API 拡張機能を選択してください', + manage: 'API 拡張機能を管理', + }, + modal: { + title: 'API 拡張機能を追加', + editTitle: 'API 拡張機能を編集', + name: { + title: '名前', + placeholder: '名前を入力してください', + }, + apiEndpoint: { + title: 'API エンドポイント', + placeholder: 'API エンドポイントを入力してください', + }, + apiKey: { + title: 'API キー', + placeholder: 'API キーを入力してください', + lengthError: 'API キーの長さは 5 文字未満にできません', + }, + }, + type: 'タイプ', + }, + about: { + changeLog: '変更ログ', + updateNow: '今すぐ更新', + nowAvailable: 'Dify {{version}} が利用可能です。', + latestAvailable: 'Dify {{version}} が最新バージョンです。', + }, + appMenus: { + overview: '監視', + promptEng: 'オーケストレート', + apiAccess: 'API アクセス', + logAndAnn: 'ログ&注釈', + logs: 'ログ', + }, + environment: { + testing: 'テスト', + development: '開発', + }, + appModes: { + completionApp: 'テキスト生成', + chatApp: 'チャットアプリ', + }, + datasetMenus: { + documents: 'ドキュメント', + hitTesting: '検索テスト', + settings: '設定', + emptyTip: 'このナレッジはどのアプリケーションにも統合されていません。ドキュメントを参照してガイダンスを確認してください。', + viewDoc: 'ドキュメントを表示', + relatedApp: '関連アプリ', + noRelatedApp: '関連付けられたアプリはありません', + pipeline: 'パイプライン', + }, + voiceInput: { + speaking: '今話しています...', + converting: 'テキストに変換中...', + notAllow: 'マイクが許可されていません', + }, + modelName: { + 'gpt-3.5-turbo': 'GPT-3.5-Turbo', + 'gpt-3.5-turbo-16k': 'GPT-3.5-Turbo-16K', + 'gpt-4': 'GPT-4', + 'gpt-4-32k': 'GPT-4-32K', + 'text-davinci-003': 'Text-Davinci-003', + 'text-embedding-ada-002': 'Text-Embedding-Ada-002', + 'whisper-1': 'Whisper-1', + 'claude-instant-1': 'Claude-Instant', + 'claude-2': 'Claude-2', + }, + chat: { + renameConversation: '会話名を変更', + conversationName: '会話名', + conversationNamePlaceholder: '会話名を入力してください', + conversationNameCanNotEmpty: '会話名は必須です', + citation: { + title: '引用', + linkToDataset: 'ナレッジへのリンク', + characters: '文字数:', + hitCount: '検索回数:', + vectorHash: 'ベクトルハッシュ:', + hitScore: '検索スコア:', + }, + inputPlaceholder: '{{botName}} と話す', + inputDisabledPlaceholder: 'プレビューのみ', + thought: '思考', + thinking: '考え中...', + resend: '再送信してください', + }, + promptEditor: { + placeholder: 'ここにプロンプトワードを入力してください。変数を挿入するには「{」を、プロンプトコンテンツブロックを挿入するには「/」を入力します。', + context: { + item: { + title: 'コンテキスト', + desc: 'コンテキストテンプレートを挿入', + }, + modal: { + title: '{{num}} 番目のコンテキスト', + add: 'コンテキストを追加', + footer: '以下のコンテキストセクションでコンテキストを管理できます。', + }, + }, + history: { + item: { + title: '会話履歴', + desc: '過去のメッセージテンプレートを挿入', + }, + modal: { + title: '例', + user: 'こんにちは', + assistant: 'こんにちは!今日はどのようにお手伝いできますか?', + edit: '会話の役割名を編集', + }, + }, + variable: { + item: { + title: '変数&外部ツール', + desc: '変数&外部ツールを挿入', + }, + outputToolDisabledItem: { + title: '変数', + desc: '変数を挿入', + }, + modal: { + add: '新しい変数', + addTool: '新しいツール', + }, + }, + query: { + item: { + title: 'クエリ', + desc: 'ユーザークエリテンプレートを挿入', + }, + }, + existed: 'プロンプトにすでに存在します', + }, + imageUploader: { + uploadFromComputer: 'コンピューターからアップロード', + uploadFromComputerReadError: '画像の読み込みに失敗しました。もう一度お試しください。', + uploadFromComputerUploadError: '画像のアップロードに失敗しました。もう一度アップロードしてください。', + uploadFromComputerLimit: 'アップロード画像のサイズは {{size}} MB を超えることはできません', + pasteImageLink: '画像リンクを貼り付ける', + pasteImageLinkInputPlaceholder: 'ここに画像リンクを貼り付けてください', + pasteImageLinkInvalid: '無効な画像リンク', + imageUpload: '画像アップロード', + }, + tag: { + placeholder: 'すべてのタグ', + addNew: '新しいタグを追加', + noTag: 'タグなし', + noTagYet: 'まだタグがありません', + addTag: 'タグを追加', + editTag: 'タグを編集', + manageTags: 'タグの管理', + selectorPlaceholder: '検索または作成する文字を入力', + create: '作成', + delete: 'タグを削除', + deleteTip: 'タグは使用されています、削除しますか', + created: 'タグは正常に作成されました', + failed: 'タグの作成に失敗しました', + }, + fileUploader: { + uploadFromComputer: 'ローカルアップロード', + pasteFileLink: 'ファイルリンクの貼り付け', + pasteFileLinkInputPlaceholder: 'URL を入力...', + uploadFromComputerLimit: 'アップロードファイルは{{size}}を超えてはなりません', + uploadFromComputerUploadError: 'ファイルのアップロードに失敗しました。再度アップロードしてください。', + uploadFromComputerReadError: 'ファイルの読み取りに失敗しました。もう一度やり直してください。', + fileExtensionNotSupport: 'ファイル拡張子はサポートされていません', + pasteFileLinkInvalid: '無効なファイルリンク', + fileExtensionBlocked: 'このファイルタイプは、セキュリティ上の理由でブロックされています', + }, + license: { + expiring_plural: '有効期限 {{count}} 日', + expiring: '1 日で有効期限が切れます', + unlimited: '無制限', + }, + pagination: { + perPage: 'ページあたりのアイテム数', + }, + you: 'あなた', + imageInput: { + browse: 'ブラウズする', + supportedFormats: 'PNG、JPG、JPEG、WEBP、および GIF をサポートしています。', + dropImageHere: 'ここに画像をドロップするか、', + }, + avatar: { + deleteTitle: 'アバターを削除する', + deleteDescription: '本当にプロフィール写真を削除してもよろしいですか?あなたのアカウントはデフォルトの初期アバターを使用します。', + }, + feedback: { + content: 'フィードバックコンテンツ', + title: 'フィードバックを提供する', + subtitle: 'この回答で何が間違っていたのか教えてください。', + placeholder: '何が間違っていたか、またはどのように改善できるかを教えてください...', + }, + label: { + optional: '(オプション)', + }, +} + +export default translation diff --git a/web/i18n/ja-JP/explore.json b/web/i18n/ja-JP/explore.json index 51afbe6133..661f1e87d0 100644 --- a/web/i18n/ja-JP/explore.json +++ b/web/i18n/ja-JP/explore.json @@ -1,12 +1,14 @@ { - "appCard.addToWorkspace": "ワークスペースに追加", - "appCard.customize": "カスタマイズ", + "appCard.addToWorkspace": "テンプレートを使用", + "appCard.try": "詳細", "appCustomize.nameRequired": "アプリ名は必須です", "appCustomize.subTitle": "アプリアイコンと名前", "appCustomize.title": "{{name}}からアプリを作成", - "apps.allCategories": "推奨", - "apps.description": "これらのテンプレートアプリを即座に使用するか、テンプレートに基づいて独自のアプリをカスタマイズしてください。", - "apps.title": "アプリを探索", + "apps.allCategories": "全て", + "apps.resetFilter": "クリア", + "apps.resultNum": "{{num}}件の結果", + "apps.title": "Difyの厳選アプリを試して、ビジネス向けのAIソリューションを見つけましょう", + "banner.viewMore": "もっと見る", "category.Agent": "エージェント", "category.Assistant": "アシスタント", "category.Entertainment": "エンターテイメント", @@ -23,7 +25,16 @@ "sidebar.chat": "チャット", "sidebar.delete.content": "このアプリを削除してもよろしいですか?", "sidebar.delete.title": "アプリを削除", - "sidebar.discovery": "探索", - "sidebar.workspace": "ワークスペース", - "title": "探索" + "sidebar.noApps.description": "公開されたWebアプリがここに表示されます", + "sidebar.noApps.learnMore": "詳細", + "sidebar.noApps.title": "Webアプリなし", + "sidebar.title": "アプリギャラリー", + "sidebar.webApps": "Webアプリ", + "title": "探索", + "tryApp.category": "カテゴリー", + "tryApp.createFromSampleApp": "テンプレートから作成", + "tryApp.requirements": "必要項目", + "tryApp.tabHeader.detail": "オーケストレーション詳細", + "tryApp.tabHeader.try": "お試し", + "tryApp.tryInfo": "これはサンプルアプリです。最大5件のメッセージまでお試しいただけます。引き続き利用するには、「テンプレートから作成」 をクリックして設定を行ってください。" } diff --git a/web/i18n/ja-JP/explore.ts b/web/i18n/ja-JP/explore.ts new file mode 100644 index 0000000000..2639bfc1dd --- /dev/null +++ b/web/i18n/ja-JP/explore.ts @@ -0,0 +1,64 @@ +const translation = { + title: '探索', + sidebar: { + title: 'アプリギャラリー', + chat: 'チャット', + webApps: 'Webアプリ', + action: { + pin: 'ピン留め', + unpin: 'ピン留め解除', + rename: '名前変更', + delete: '削除', + }, + delete: { + title: 'アプリを削除', + content: 'このアプリを削除してもよろしいですか?', + }, + noApps: { + title: 'Webアプリなし', + description: '公開されたWebアプリがここに表示されます', + learnMore: '詳細', + }, + }, + apps: { + title: 'Difyの厳選アプリを試して、ビジネス向けのAIソリューションを見つけましょう', + allCategories: '全て', + resultNum: '{{num}}件の結果', + resetFilter: 'クリア', + }, + appCard: { + addToWorkspace: 'テンプレートを使用', + try: '詳細', + customize: 'カスタマイズ', + }, + tryApp: { + tabHeader: { + try: 'お試し', + detail: 'オーケストレーション詳細', + }, + createFromSampleApp: 'テンプレートから作成', + category: 'カテゴリー', + requirements: '必要項目', + tryInfo: 'これはサンプルアプリです。最大5件のメッセージまでお試しいただけます。引き続き利用するには、「テンプレートから作成」 をクリックして設定を行ってください。', + }, + appCustomize: { + title: '{{name}}からアプリを作成', + subTitle: 'アプリアイコンと名前', + nameRequired: 'アプリ名は必須です', + }, + category: { + Assistant: 'アシスタント', + Writing: '執筆', + Translate: '翻訳', + Programming: 'プログラミング', + HR: '人事', + Workflow: 'ワークフロー', + Agent: 'エージェント', + Entertainment: 'エンターテイメント', + }, + banner: { + viewMore: 'もっと見る', + }, +} + +export default translation diff --git a/web/i18n/ko-KR/explore.json b/web/i18n/ko-KR/explore.json index f7bbc63757..a44e90e3c1 100644 --- a/web/i18n/ko-KR/explore.json +++ b/web/i18n/ko-KR/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "작업 공간에 추가", - "appCard.customize": "사용자 정의", "appCustomize.nameRequired": "앱 이름은 필수입니다", "appCustomize.subTitle": "앱 아이콘 및 이름", "appCustomize.title": "{{name}}으로 앱 만들기", - "apps.allCategories": "모든 카테고리", - "apps.description": "이 템플릿 앱을 즉시 사용하거나 템플릿을 기반으로 고유한 앱을 사용자 정의하세요.", - "apps.title": "Dify 로 앱 탐색", "category.Agent": "에이전트", "category.Assistant": "어시스턴트", "category.Entertainment": "오락", @@ -23,7 +18,5 @@ "sidebar.chat": "채팅", "sidebar.delete.content": "이 앱을 삭제해도 괜찮습니까?", "sidebar.delete.title": "앱 삭제", - "sidebar.discovery": "탐색", - "sidebar.workspace": "작업 공간", "title": "탐색" } diff --git a/web/i18n/ko-KR/explore.ts b/web/i18n/ko-KR/explore.ts new file mode 100644 index 0000000000..756849b374 --- /dev/null +++ b/web/i18n/ko-KR/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: '탐색', + sidebar: { + chat: '채팅', + action: { + pin: '고정', + unpin: '고정 해제', + rename: '이름 변경', + delete: '삭제', + }, + delete: { + title: '앱 삭제', + content: '이 앱을 삭제해도 괜찮습니까?', + }, + }, + apps: { + }, + appCard: { + customize: '사용자 정의', + }, + appCustomize: { + title: '{{name}}으로 앱 만들기', + subTitle: '앱 아이콘 및 이름', + nameRequired: '앱 이름은 필수입니다', + }, + category: { + Assistant: '어시스턴트', + Writing: '작성', + Translate: '번역', + Programming: '프로그래밍', + Agent: '에이전트', + Workflow: '워크플로우', + HR: '인사', + Entertainment: '오락', + }, +} + +export default translation diff --git a/web/i18n/pl-PL/explore.json b/web/i18n/pl-PL/explore.json index 409f0f4236..4d23037f99 100644 --- a/web/i18n/pl-PL/explore.json +++ b/web/i18n/pl-PL/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "Dodaj do przestrzeni roboczej", - "appCard.customize": "Dostosuj", "appCustomize.nameRequired": "Nazwa aplikacji jest wymagana", "appCustomize.subTitle": "Ikona i nazwa aplikacji", "appCustomize.title": "Utwórz aplikację z {{name}}", - "apps.allCategories": "Polecane", - "apps.description": "Wykorzystaj te aplikacje szablonowe natychmiast lub dostosuj własne aplikacje na podstawie szablonów.", - "apps.title": "Odkrywaj aplikacje stworzone przez Dify", "category.Agent": "Agent", "category.Assistant": "Asystent", "category.Entertainment": "Rozrywka", @@ -23,7 +18,5 @@ "sidebar.chat": "Czat", "sidebar.delete.content": "Czy na pewno chcesz usunąć tę aplikację?", "sidebar.delete.title": "Usuń aplikację", - "sidebar.discovery": "Odkrywanie", - "sidebar.workspace": "Przestrzeń robocza", "title": "Odkryj" } diff --git a/web/i18n/pl-PL/explore.ts b/web/i18n/pl-PL/explore.ts new file mode 100644 index 0000000000..864dee6f49 --- /dev/null +++ b/web/i18n/pl-PL/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'Odkryj', + sidebar: { + chat: 'Czat', + action: { + pin: 'Przypnij', + unpin: 'Odepnij', + rename: 'Zmień nazwę', + delete: 'Usuń', + }, + delete: { + title: 'Usuń aplikację', + content: 'Czy na pewno chcesz usunąć tę aplikację?', + }, + }, + apps: { + }, + appCard: { + customize: 'Dostosuj', + }, + appCustomize: { + title: 'Utwórz aplikację z {{name}}', + subTitle: 'Ikona i nazwa aplikacji', + nameRequired: 'Nazwa aplikacji jest wymagana', + }, + category: { + Assistant: 'Asystent', + Writing: 'Pisanie', + Translate: 'Tłumaczenie', + Programming: 'Programowanie', + HR: 'HR', + Agent: 'Agent', + Workflow: 'Przepływ pracy', + Entertainment: 'Rozrywka', + }, +} + +export default translation diff --git a/web/i18n/pt-BR/explore.json b/web/i18n/pt-BR/explore.json index 03692aac06..69df273cf7 100644 --- a/web/i18n/pt-BR/explore.json +++ b/web/i18n/pt-BR/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "Adicionar ao Espaço de Trabalho", - "appCard.customize": "Personalizar", "appCustomize.nameRequired": "O nome do aplicativo é obrigatório", "appCustomize.subTitle": "Ícone e nome do aplicativo", "appCustomize.title": "Criar aplicativo a partir de {{name}}", - "apps.allCategories": "Recomendado", - "apps.description": "Use esses aplicativos modelo instantaneamente ou personalize seus próprios aplicativos com base nos modelos.", - "apps.title": "Explorar Aplicações por Dify", "category.Agent": "Agente", "category.Assistant": "Assistente", "category.Entertainment": "Entretenimento", @@ -23,7 +18,5 @@ "sidebar.chat": "Chat", "sidebar.delete.content": "Tem certeza de que deseja excluir este aplicativo?", "sidebar.delete.title": "Excluir aplicativo", - "sidebar.discovery": "Descoberta", - "sidebar.workspace": "Espaço de Trabalho", "title": "Badać" } diff --git a/web/i18n/pt-BR/explore.ts b/web/i18n/pt-BR/explore.ts new file mode 100644 index 0000000000..5bd24bb581 --- /dev/null +++ b/web/i18n/pt-BR/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'Badać', + sidebar: { + chat: 'Chat', + action: { + pin: 'Fixar', + unpin: 'Desafixar', + rename: 'Renomear', + delete: 'Excluir', + }, + delete: { + title: 'Excluir aplicativo', + content: 'Tem certeza de que deseja excluir este aplicativo?', + }, + }, + apps: { + }, + appCard: { + customize: 'Personalizar', + }, + appCustomize: { + title: 'Criar aplicativo a partir de {{name}}', + subTitle: 'Ícone e nome do aplicativo', + nameRequired: 'O nome do aplicativo é obrigatório', + }, + category: { + Assistant: 'Assistente', + Writing: 'Escrita', + Translate: 'Traduzir', + Programming: 'Programação', + HR: 'RH', + Workflow: 'Fluxo de trabalho', + Agent: 'Agente', + Entertainment: 'Entretenimento', + }, +} + +export default translation diff --git a/web/i18n/ro-RO/explore.json b/web/i18n/ro-RO/explore.json index 28509ab4fc..d2dec572b4 100644 --- a/web/i18n/ro-RO/explore.json +++ b/web/i18n/ro-RO/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "Adăugați la spațiul de lucru", - "appCard.customize": "Personalizați", "appCustomize.nameRequired": "Numele aplicației este obligatoriu", "appCustomize.subTitle": "Pictogramă și nume aplicație", "appCustomize.title": "Creați o aplicație din {{name}}", - "apps.allCategories": "Recomandate", - "apps.description": "Utilizați aceste aplicații model imediat sau personalizați-vă propria aplicație pe baza modelelor.", - "apps.title": "Explorați aplicațiile Dify", "category.Agent": "Agent", "category.Assistant": "Asistent", "category.Entertainment": "Divertisment", @@ -23,7 +18,5 @@ "sidebar.chat": "Chat", "sidebar.delete.content": "Sunteți sigur că doriți să ștergeți această aplicație?", "sidebar.delete.title": "Ștergeți aplicația", - "sidebar.discovery": "Descoperire", - "sidebar.workspace": "Spațiu de lucru", "title": "Explorați" } diff --git a/web/i18n/ro-RO/explore.ts b/web/i18n/ro-RO/explore.ts new file mode 100644 index 0000000000..918713bc90 --- /dev/null +++ b/web/i18n/ro-RO/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'Explorați', + sidebar: { + chat: 'Chat', + action: { + pin: 'Fixați', + unpin: 'Deblocați', + rename: 'Redenumire', + delete: 'Ștergeți', + }, + delete: { + title: 'Ștergeți aplicația', + content: 'Sunteți sigur că doriți să ștergeți această aplicație?', + }, + }, + apps: { + }, + appCard: { + customize: 'Personalizați', + }, + appCustomize: { + title: 'Creați o aplicație din {{name}}', + subTitle: 'Pictogramă și nume aplicație', + nameRequired: 'Numele aplicației este obligatoriu', + }, + category: { + Assistant: 'Asistent', + Writing: 'Scriere', + Translate: 'Traducere', + Programming: 'Programare', + HR: 'Resurse Umane', + Agent: 'Agent', + Workflow: 'Flux de lucru', + Entertainment: 'Divertisment', + }, +} + +export default translation diff --git a/web/i18n/ru-RU/explore.json b/web/i18n/ru-RU/explore.json index a061c35c0a..e92573f04d 100644 --- a/web/i18n/ru-RU/explore.json +++ b/web/i18n/ru-RU/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "Добавить в рабочее пространство", - "appCard.customize": "Настроить", "appCustomize.nameRequired": "Название приложения обязательно", "appCustomize.subTitle": "Значок и название приложения", "appCustomize.title": "Создать приложение из {{name}}", - "apps.allCategories": "Рекомендуемые", - "apps.description": "Используйте эти шаблонные приложения мгновенно или настройте свои собственные приложения на основе шаблонов.", - "apps.title": "Обзор приложений от Dify", "category.Agent": "Агент", "category.Assistant": "Ассистент", "category.Entertainment": "Развлечение", @@ -23,7 +18,5 @@ "sidebar.chat": "Чат", "sidebar.delete.content": "Вы уверены, что хотите удалить это приложение?", "sidebar.delete.title": "Удалить приложение", - "sidebar.discovery": "Открытия", - "sidebar.workspace": "Рабочее пространство", "title": "Обзор" } diff --git a/web/i18n/ru-RU/explore.ts b/web/i18n/ru-RU/explore.ts new file mode 100644 index 0000000000..fd23926d7b --- /dev/null +++ b/web/i18n/ru-RU/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'Обзор', + sidebar: { + chat: 'Чат', + action: { + pin: 'Закрепить', + unpin: 'Открепить', + rename: 'Переименовать', + delete: 'Удалить', + }, + delete: { + title: 'Удалить приложение', + content: 'Вы уверены, что хотите удалить это приложение?', + }, + }, + apps: { + }, + appCard: { + customize: 'Настроить', + }, + appCustomize: { + title: 'Создать приложение из {{name}}', + subTitle: 'Значок и название приложения', + nameRequired: 'Название приложения обязательно', + }, + category: { + Assistant: 'Ассистент', + Writing: 'Написание', + Translate: 'Перевод', + Programming: 'Программирование', + HR: 'HR', + Agent: 'Агент', + Workflow: 'Рабочий процесс', + Entertainment: 'Развлечение', + }, +} + +export default translation diff --git a/web/i18n/sl-SI/explore.json b/web/i18n/sl-SI/explore.json index ad8de813f9..f37c9bdea3 100644 --- a/web/i18n/sl-SI/explore.json +++ b/web/i18n/sl-SI/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "Dodaj v delovni prostor", - "appCard.customize": "Prilagodi", "appCustomize.nameRequired": "Ime aplikacije je obvezno", "appCustomize.subTitle": "Ikona aplikacije & ime", "appCustomize.title": "Ustvari aplikacijo iz {{name}}", - "apps.allCategories": "Priporočeno", - "apps.description": "Uporabite te predloge aplikacij takoj ali prilagodite svoje aplikacije na podlagi predlog.", - "apps.title": "Razišči aplikacije Dify", "category.Agent": "Agent", "category.Assistant": "Pomočnik", "category.Entertainment": "Zabava", @@ -23,7 +18,5 @@ "sidebar.chat": "Klepet", "sidebar.delete.content": "Ali ste prepričani, da želite izbrisati to aplikacijo?", "sidebar.delete.title": "Izbriši aplikacijo", - "sidebar.discovery": "Odkritja", - "sidebar.workspace": "Delovni prostor", "title": "Razišči" } diff --git a/web/i18n/sl-SI/explore.ts b/web/i18n/sl-SI/explore.ts new file mode 100644 index 0000000000..ae25382b46 --- /dev/null +++ b/web/i18n/sl-SI/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'Razišči', + sidebar: { + chat: 'Klepet', + action: { + pin: 'Pripni', + unpin: 'Odpni', + rename: 'Preimenuj', + delete: 'Izbriši', + }, + delete: { + title: 'Izbriši aplikacijo', + content: 'Ali ste prepričani, da želite izbrisati to aplikacijo?', + }, + }, + apps: { + }, + appCard: { + customize: 'Prilagodi', + }, + appCustomize: { + title: 'Ustvari aplikacijo iz {{name}}', + subTitle: 'Ikona aplikacije & ime', + nameRequired: 'Ime aplikacije je obvezno', + }, + category: { + Assistant: 'Pomočnik', + Writing: 'Pisanje', + Translate: 'Prevajanje', + Programming: 'Programiranje', + HR: 'Kadri', + Workflow: 'Potek dela', + Agent: 'Agent', + Entertainment: 'Zabava', + }, +} + +export default translation diff --git a/web/i18n/th-TH/explore.json b/web/i18n/th-TH/explore.json index 17d998d177..4bdeb041c0 100644 --- a/web/i18n/th-TH/explore.json +++ b/web/i18n/th-TH/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "เพิ่มไปยังพื้นที่ทํางาน", - "appCard.customize": "ปรับแต่ง", "appCustomize.nameRequired": "ต้องใช้ชื่อแอป", "appCustomize.subTitle": "ไอคอนและชื่อแอป", "appCustomize.title": "สร้างแอปจาก {{name}}", - "apps.allCategories": "แนะ นำ", - "apps.description": "ใช้แอปเทมเพลตเหล่านี้ทันทีหรือปรับแต่งแอปของคุณเองตามเทมเพลต", - "apps.title": "สํารวจแอพโดย Dify", "category.Agent": "ตัวแทน", "category.Assistant": "ผู้ช่วย", "category.Entertainment": "ความบันเทิง", @@ -23,7 +18,5 @@ "sidebar.chat": "สนทนา", "sidebar.delete.content": "คุณแน่ใจหรือไม่ว่าต้องการลบแอปนี้?", "sidebar.delete.title": "ลบแอป", - "sidebar.discovery": "การค้นพบ", - "sidebar.workspace": "พื้นที่", "title": "สํารวจ" } diff --git a/web/i18n/th-TH/explore.ts b/web/i18n/th-TH/explore.ts new file mode 100644 index 0000000000..239d1e7182 --- /dev/null +++ b/web/i18n/th-TH/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'สํารวจ', + sidebar: { + chat: 'สนทนา', + action: { + pin: 'เข็มกลัด', + unpin: 'ปลดหมุด', + rename: 'ตั้งชื่อใหม่', + delete: 'ลบ', + }, + delete: { + title: 'ลบแอป', + content: 'คุณแน่ใจหรือไม่ว่าต้องการลบแอปนี้?', + }, + }, + apps: { + }, + appCard: { + customize: 'ปรับแต่ง', + }, + appCustomize: { + title: 'สร้างแอปจาก {{name}}', + subTitle: 'ไอคอนและชื่อแอป', + nameRequired: 'ต้องใช้ชื่อแอป', + }, + category: { + Assistant: 'ผู้ช่วย', + Writing: 'การเขียน', + Translate: 'แปล', + Programming: 'โปรแกรม', + HR: 'ชั่วโมง', + Workflow: 'เวิร์กโฟลว์', + Agent: 'ตัวแทน', + Entertainment: 'ความบันเทิง', + }, +} + +export default translation diff --git a/web/i18n/tr-TR/explore.json b/web/i18n/tr-TR/explore.json index c4badf8b6f..76f3a7e321 100644 --- a/web/i18n/tr-TR/explore.json +++ b/web/i18n/tr-TR/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "Çalışma Alanına Ekle", - "appCard.customize": "Özelleştir", "appCustomize.nameRequired": "Uygulama ismi gereklidir", "appCustomize.subTitle": "Uygulama simgesi ve ismi", "appCustomize.title": "{{name}} uygulamasından uygulama oluştur", - "apps.allCategories": "Önerilen", - "apps.description": "Bu şablon uygulamalarını anında kullanın veya şablonlara dayalı kendi uygulamalarınızı özelleştirin.", - "apps.title": "Dify Tarafından Keşfet Uygulamaları", "category.Agent": "Aracı", "category.Assistant": "Asistan", "category.Entertainment": "Eğlence", @@ -23,7 +18,5 @@ "sidebar.chat": "Sohbet", "sidebar.delete.content": "Bu uygulamayı silmek istediğinizden emin misiniz?", "sidebar.delete.title": "Uygulamayı sil", - "sidebar.discovery": "Keşif", - "sidebar.workspace": "Çalışma Alanı", "title": "Keşfet" } diff --git a/web/i18n/tr-TR/explore.ts b/web/i18n/tr-TR/explore.ts new file mode 100644 index 0000000000..b2e3a48e7b --- /dev/null +++ b/web/i18n/tr-TR/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'Keşfet', + sidebar: { + chat: 'Sohbet', + action: { + pin: 'Sabitle', + unpin: 'Sabitlemeyi Kaldır', + rename: 'Yeniden Adlandır', + delete: 'Sil', + }, + delete: { + title: 'Uygulamayı sil', + content: 'Bu uygulamayı silmek istediğinizden emin misiniz?', + }, + }, + apps: { + }, + appCard: { + customize: 'Özelleştir', + }, + appCustomize: { + title: '{{name}} uygulamasından uygulama oluştur', + subTitle: 'Uygulama simgesi ve ismi', + nameRequired: 'Uygulama ismi gereklidir', + }, + category: { + Assistant: 'Asistan', + Writing: 'Yazma', + Translate: 'Çeviri', + Programming: 'Programlama', + HR: 'İK', + Agent: 'Aracı', + Workflow: 'İş Akışı', + Entertainment: 'Eğlence', + }, +} + +export default translation diff --git a/web/i18n/uk-UA/explore.json b/web/i18n/uk-UA/explore.json index 28672b723a..29e4995535 100644 --- a/web/i18n/uk-UA/explore.json +++ b/web/i18n/uk-UA/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "Додати до робочого простору", - "appCard.customize": "Налаштувати", "appCustomize.nameRequired": "Назва програми обов’язкова", "appCustomize.subTitle": "Значок програми та назва", "appCustomize.title": "Створити додаток з {{name}}", - "apps.allCategories": "Рекомендовані", - "apps.description": "Використовуйте ці шаблони миттєво або налаштуйте власні програми на основі шаблонів.", - "apps.title": "Вивчайте програми від Dify", "category.Agent": "Агент", "category.Assistant": "Помічник", "category.Entertainment": "Розваги", @@ -23,7 +18,5 @@ "sidebar.chat": "Чат", "sidebar.delete.content": "Ви впевнені, що хочете видалити цю програму?", "sidebar.delete.title": "Видалити додаток", - "sidebar.discovery": "Відкриття", - "sidebar.workspace": "Робочий простір", "title": "Досліджувати" } diff --git a/web/i18n/uk-UA/explore.ts b/web/i18n/uk-UA/explore.ts new file mode 100644 index 0000000000..eb6adae23a --- /dev/null +++ b/web/i18n/uk-UA/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'Досліджувати', + sidebar: { + chat: 'Чат', + action: { + pin: 'Закріпити', + unpin: 'Відкріпити', + rename: 'Перейменувати', + delete: 'Видалити', + }, + delete: { + title: 'Видалити додаток', + content: 'Ви впевнені, що хочете видалити цю програму?', + }, + }, + apps: { + }, + appCard: { + customize: 'Налаштувати', + }, + appCustomize: { + title: 'Створити додаток з {{name}}', + subTitle: 'Значок програми та назва', + nameRequired: 'Назва програми обов’язкова', + }, + category: { + Assistant: 'Помічник', + Writing: 'Написання', + Translate: 'Переклад', + Programming: 'Програмування', + HR: 'HR', + Workflow: 'Робочий процес', + Agent: 'Агент', + Entertainment: 'Розваги', + }, +} + +export default translation diff --git a/web/i18n/vi-VN/explore.json b/web/i18n/vi-VN/explore.json index a7bcf64ffa..605c0661c9 100644 --- a/web/i18n/vi-VN/explore.json +++ b/web/i18n/vi-VN/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "Thêm vào không gian làm việc", - "appCard.customize": "Tùy chỉnh", "appCustomize.nameRequired": "Vui lòng nhập tên ứng dụng", "appCustomize.subTitle": "Biểu tượng và tên ứng dụng", "appCustomize.title": "Tạo ứng dụng từ {{name}}", - "apps.allCategories": "Tất cả danh mục", - "apps.description": "Sử dụng ngay các ứng dụng mẫu này hoặc tùy chỉnh ứng dụng của bạn dựa trên các mẫu có sẵn.", - "apps.title": "Khám phá ứng dụng bởi Dify", "category.Agent": "Người đại lý", "category.Assistant": "Trợ lý", "category.Entertainment": "Giải trí", @@ -23,7 +18,5 @@ "sidebar.chat": "Trò chuyện", "sidebar.delete.content": "Bạn có chắc chắn muốn xóa ứng dụng này không?", "sidebar.delete.title": "Xóa ứng dụng", - "sidebar.discovery": "Khám phá", - "sidebar.workspace": "Không gian làm việc", "title": "Khám phá" } diff --git a/web/i18n/vi-VN/explore.ts b/web/i18n/vi-VN/explore.ts new file mode 100644 index 0000000000..ed27f42b32 --- /dev/null +++ b/web/i18n/vi-VN/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: 'Khám phá', + sidebar: { + chat: 'Trò chuyện', + action: { + pin: 'Ghim', + unpin: 'Bỏ ghim', + rename: 'Đổi tên', + delete: 'Xóa', + }, + delete: { + title: 'Xóa ứng dụng', + content: 'Bạn có chắc chắn muốn xóa ứng dụng này không?', + }, + }, + apps: { + }, + appCard: { + customize: 'Tùy chỉnh', + }, + appCustomize: { + title: 'Tạo ứng dụng từ {{name}}', + subTitle: 'Biểu tượng và tên ứng dụng', + nameRequired: 'Vui lòng nhập tên ứng dụng', + }, + category: { + Assistant: 'Trợ lý', + Writing: 'Viết lách', + Translate: 'Dịch thuật', + Programming: 'Lập trình', + HR: 'Nhân sự', + Agent: 'Người đại lý', + Workflow: 'Quy trình làm việc', + Entertainment: 'Giải trí', + }, +} + +export default translation diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json index be7d4690af..d6b9c75511 100644 --- a/web/i18n/zh-Hans/common.json +++ b/web/i18n/zh-Hans/common.json @@ -108,6 +108,7 @@ "chat.conversationName": "会话名称", "chat.conversationNameCanNotEmpty": "会话名称必填", "chat.conversationNamePlaceholder": "请输入会话名称", + "chat.inputDisabledPlaceholder": "仅供试用", "chat.inputPlaceholder": "和 {{botName}} 聊天", "chat.renameConversation": "重命名会话", "chat.resend": "重新发送", diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts new file mode 100644 index 0000000000..8ad5f67d1b --- /dev/null +++ b/web/i18n/zh-Hans/common.ts @@ -0,0 +1,789 @@ +const translation = { + theme: { + theme: '主题', + light: '浅色', + dark: '深色', + auto: '自动', + }, + api: { + success: '成功', + actionSuccess: '操作成功', + saved: '已保存', + create: '已创建', + remove: '已移除', + }, + operation: { + create: '创建', + confirm: '确认', + cancel: '取消', + clear: '清空', + save: '保存', + yes: '是', + no: '否', + deleteConfirmTitle: '删除?', + confirmAction: '请确认您的操作。', + saveAndEnable: '保存并启用', + edit: '编辑', + add: '添加', + added: '已添加', + refresh: '重新开始', + reset: '重置', + search: '搜索', + noSearchResults: '没有找到{{content}}', + resetKeywords: '重置关键词', + selectCount: '已选择 {{count}} 项', + searchCount: '找到 {{count}} 个 {{content}}', + noSearchCount: '0 个 {{content}}', + change: '更改', + remove: '移除', + send: '发送', + copy: '复制', + copied: ' 已复制', + lineBreak: '换行', + sure: '我确定', + download: '下载', + downloadSuccess: '下载完毕', + downloadFailed: '下载失败,请稍后重试。', + viewDetails: '查看详情', + delete: '删除', + deleteApp: '删除应用', + settings: '设置', + setup: '设置', + config: '配置', + getForFree: '免费获取', + reload: '刷新', + ok: '好的', + log: '日志', + learnMore: '了解更多', + params: '参数设置', + duplicate: '复制', + rename: '重命名', + audioSourceUnavailable: '音源不可用', + copyImage: '复制图片', + imageCopied: '图片已复制', + zoomOut: '缩小', + zoomIn: '放大', + openInNewTab: '在新标签页打开', + in: '在', + saveAndRegenerate: '保存并重新生成子分段', + close: '关闭', + view: '查看', + viewMore: '查看更多', + regenerate: '重新生成', + submit: '提交', + skip: '跳过', + format: '格式化', + more: '更多', + selectAll: '全选', + deSelectAll: '取消全选', + now: '现在', + }, + errorMsg: { + fieldRequired: '{{field}} 为必填项', + urlError: 'url 应该以 http:// 或 https:// 开头', + }, + placeholder: { + input: '请输入', + select: '请选择', + search: '搜索...', + }, + noData: '暂无数据', + label: { + optional: '(可选)', + }, + voice: { + language: { + zhHans: '中文', + zhHant: '繁体中文', + enUS: '英语', + deDE: '德语', + frFR: '法语', + esES: '西班牙语', + itIT: '意大利语', + thTH: '泰语', + idID: '印尼语', + jaJP: '日语', + koKR: '韩语', + ptBR: '葡萄牙语', + ruRU: '俄语', + ukUA: '乌克兰语', + viVN: '越南语', + plPL: '波兰语', + roRO: '罗马尼亚语', + hiIN: '印地语', + trTR: '土耳其语', + faIR: '波斯语', + }, + }, + unit: { + char: '个字符', + }, + actionMsg: { + noModification: '暂无修改', + modifiedSuccessfully: '修改成功', + modifiedUnsuccessfully: '修改失败', + copySuccessfully: '复制成功', + generatedSuccessfully: '已重新生成', + generatedUnsuccessfully: '生成失败', + paySucceeded: '已支付成功', + payCancelled: '已取消支付', + }, + model: { + params: { + temperature: '随机性 temperature', + temperatureTip: + '控制回复的随机性。\n值越大,回复越随机。\n值越小,回复越确定或一致。', + top_p: '核采样 top_p', + top_pTip: + '控制生成多样性。\n值越大,输出会包括更多的单词选项。\n值越小,模型会更集中在高概率的单词上,输出更确定但可能缺乏多样性。\n核采样和随机性不建议同时修改。', + presence_penalty: '话题新鲜度 presence_penalty', + presence_penaltyTip: + '控制生成时对上文已存在的话题的偏好程度。\n值越大,越可能使用到新的话题。', + frequency_penalty: '频率惩罚度 frequency_penalty', + frequency_penaltyTip: + '影响常见与罕见词汇使用。\n值较大时,倾向于生成不常见的词汇和表达方式。\n值越小,更倾向于使用常见和普遍接受的词汇或短语。', + max_tokens: '单次回复限制 max_tokens', + max_tokensTip: + '用于限制回复的最大长度,以 token 为单位。\n较大的值可能会限制给提示词、聊天记录和知识库留出的空间。\n建议将其设置在三分之二以下。\ngpt-4-1106-preview、gpt-4-vision-preview 最大长度 (输入 128k,输出 4k)', + maxTokenSettingTip: '您设置的最大 tokens 数较大,可能会导致 prompt、用户问题、知识库内容没有 token 空间进行处理,建议设置到 2/3 以下。', + setToCurrentModelMaxTokenTip: '最大令牌数更新为当前模型最大的令牌数 {{maxToken}} 的 80%。', + stop_sequences: '停止序列 stop_sequences', + stop_sequencesTip: '最多四个序列,API 将停止生成更多的 token。返回的文本将不包含停止序列。', + stop_sequencesPlaceholder: '输入序列并按 Tab 键', + }, + tone: { + Creative: '创意', + Balanced: '平衡', + Precise: '精确', + Custom: '自定义', + }, + addMoreModel: '添加更多模型', + settingsLink: '模型设置', + capabilities: '多模态能力', + }, + menus: { + status: 'beta', + explore: '探索', + apps: '工作室', + appDetail: '应用详情', + account: '账户', + plugins: '插件', + exploreMarketplace: '探索 Marketplace', + pluginsTips: '集成第三方插件或创建与 ChatGPT 兼容的 AI 插件。', + datasets: '知识库', + datasetsTips: '即将到来:上传自己的长文本数据,或通过 Webhook 集成自己的数据源', + newApp: '创建应用', + newDataset: '创建知识库', + tools: '工具', + }, + userProfile: { + settings: '设置', + emailSupport: '邮件支持', + workspace: '工作空间', + createWorkspace: '创建工作空间', + helpCenter: '查看帮助文档', + support: '支持', + compliance: '合规', + forum: '论坛', + roadmap: '路线图', + github: 'GitHub', + community: '社区', + about: '关于', + logout: '登出', + contactUs: '联系我们', + }, + compliance: { + soc2Type1: 'SOC 2 Type I Report', + soc2Type2: 'SOC 2 Type II Report', + iso27001: 'ISO 27001:2022 Certification', + gdpr: 'GDPR DPA', + sandboxUpgradeTooltip: '仅适用于 Professional 或 Team 版计划。', + professionalUpgradeTooltip: '仅适用于 Team 版计划或以上。', + }, + settings: { + accountGroup: '通用', + workplaceGroup: '工作空间', + generalGroup: '通用', + account: '我的账户', + members: '成员', + billing: '账单', + integrations: '集成', + language: '语言', + provider: '模型供应商', + dataSource: '数据来源', + plugin: '插件', + apiBasedExtension: 'API 扩展', + }, + account: { + account: '账户', + myAccount: '我的账户', + studio: '工作室', + avatar: '头像', + name: '用户名', + email: '邮箱', + password: '密码', + passwordTip: '如果您不想使用验证码登录,可以设置永久密码', + setPassword: '设置密码', + resetPassword: '重置密码', + currentPassword: '原密码', + newPassword: '新密码', + notEqual: '两个密码不相同', + confirmPassword: '确认密码', + langGeniusAccount: '账号关联数据', + langGeniusAccountTip: '您的账号相关的用户数据。', + editName: '编辑名字', + showAppLength: '显示 {{length}} 个应用', + delete: '删除账户', + deleteTip: '请注意,一旦确认,作为任何空间的所有者,您的空间将被安排进入永久删除队列,您的所有用户数据也将被排入永久删除队列。', + deletePrivacyLinkTip: '有关我们如何处理您的数据的更多信息,请参阅我们的', + deletePrivacyLink: '隐私政策', + deleteSuccessTip: '删除账户需要一些时间。完成后,我们会通过邮件通知您。', + deleteLabel: '请输入您的邮箱以确认', + deletePlaceholder: '输入您的邮箱...', + sendVerificationButton: '发送验证码', + verificationLabel: '验证码', + verificationPlaceholder: '输入 6 位数字验证码', + permanentlyDeleteButton: '永久删除', + feedbackTitle: '反馈', + feedbackLabel: '请告诉我们您为什么删除账户?', + feedbackPlaceholder: '选填', + editWorkspaceInfo: '编辑工作空间信息', + workspaceName: '工作空间名称', + workspaceIcon: '工作空间图标', + changeEmail: { + title: '更改邮箱', + verifyEmail: '验证当前邮箱', + newEmail: '设置新邮箱', + verifyNew: '验证新邮箱', + authTip: '一旦您的电子邮件地址更改,链接到您旧电子邮件地址的 Google 或 GitHub 帐户将无法再登录该帐户。', + content1: '如果您继续,我们将向 {{email}} 发送验证码以进行重新验证。', + content2: '你的当前邮箱是 {{email}} 。验证码已发送至该邮箱。', + content3: '输入新的邮箱,我们将向您发送验证码。', + content4: '我们已将验证码发送至 {{email}}。', + codeLabel: '验证码', + codePlaceholder: '输入 6 位数字验证码', + emailLabel: '新邮箱', + emailPlaceholder: '输入新邮箱', + existingEmail: '该邮箱已存在', + unAvailableEmail: '该邮箱暂时无法使用。', + sendVerifyCode: '发送验证码', + continue: '继续', + changeTo: '更改为 {{email}}', + resendTip: '没有收到验证码?', + resendCount: '请在 {{count}} 秒后重新发送', + resend: '重新发送', + }, + }, + members: { + team: '团队', + invite: '添加', + name: '姓名', + lastActive: '上次活动时间', + role: '角色', + pending: '待定...', + owner: '所有者', + admin: '管理员', + adminTip: '能够建立应用程序和管理团队设置', + normal: '成员', + normalTip: '只能使用应用程序,不能建立应用程序', + editor: '编辑', + editorTip: '能够建立并编辑应用程序,不能管理团队设置', + datasetOperator: '知识库管理员', + datasetOperatorTip: '只能管理知识库', + inviteTeamMember: '添加团队成员', + inviteTeamMemberTip: '对方在登录后可以访问你的团队数据。', + emailNotSetup: '由于邮件服务器未设置,无法发送邀请邮件。请将邀请后生成的邀请链接通知用户。', + email: '邮箱', + emailInvalid: '邮箱格式无效', + emailPlaceholder: '输入邮箱', + sendInvite: '发送邀请', + invitedAsRole: '邀请为{{role}}用户', + invitationSent: '邀请已发送', + invitationSentTip: '邀请已发送,对方登录 Dify 后即可访问你的团队数据。', + invitationLink: '邀请链接', + failedInvitationEmails: '邀请以下邮箱失败', + ok: '好的', + removeFromTeam: '移出团队', + removeFromTeamTip: '将取消团队访问', + setAdmin: '设为管理员', + setMember: '设为普通成员', + setEditor: '设为编辑', + disInvite: '取消邀请', + deleteMember: '删除成员', + you: '(你)', + builderTip: '可以构建和编辑自己的应用程序', + setBuilder: 'Set as builder(设置为构建器)', + builder: '构建器', + transferOwnership: '转移所有权', + transferModal: { + title: '转移工作空间所有权', + warning: '您即将转移 “{{workspace}}”的所有权。该操作将立即生效,且无法撤销。', + warningTip: '您将成为管理员成员,新所有者将拥有完全控制权。', + sendTip: '如果您继续,我们将向 {{email}} 发送验证码以进行身份认证。', + verifyEmail: '验证您当前的邮箱', + verifyContent: '您当前的邮箱是 {{email}}。', + verifyContent2: '我们将向该邮箱发送临时验证码以完成身份验证。', + codeLabel: '验证码', + codePlaceholder: '输入 6 位数字验证码', + resendTip: '没有收到验证码?', + resendCount: '请在 {{count}} 秒后重新发送', + resend: '重新发送', + transferLabel: '新所有者', + transferPlaceholder: '选择一个成员', + sendVerifyCode: '发送验证码', + continue: '继续', + transfer: '转移工作空间所有权', + }, + }, + integrations: { + connected: '登录方式', + google: 'Google', + googleAccount: 'Google 账号登录', + github: 'GitHub', + githubAccount: 'GitHub 账号登录', + connect: '绑定', + }, + language: { + displayLanguage: '界面语言', + timezone: '时区', + }, + provider: { + apiKey: 'API 密钥', + enterYourKey: '输入你的 API 密钥', + invalidKey: '无效的 OpenAI API 密钥', + validatedError: '校验失败:', + validating: '验证密钥中...', + saveFailed: 'API 密钥保存失败', + apiKeyExceedBill: '此 API KEY 已没有可用配额,请阅读', + addKey: '添加 密钥', + comingSoon: '即将推出', + editKey: '编辑', + invalidApiKey: '无效的 API 密钥', + azure: { + apiBase: 'API Base', + apiBasePlaceholder: '输入您的 Azure OpenAI API Base 地址', + apiKey: 'API Key', + apiKeyPlaceholder: '输入你的 API 密钥', + helpTip: '了解 Azure OpenAI Service', + }, + openaiHosted: { + openaiHosted: '托管 OpenAI', + onTrial: '体验', + exhausted: '超出限额', + desc: '托管 OpenAI 由 Dify 提供的托管 OpenAI 服务,你可以使用 GPT-3.5 等模型,在体验额度消耗完毕前你需要设置其它模型供应商。', + callTimes: '调用次数', + usedUp: '试用额度已用完,请在下方添加自己的模型供应商', + useYourModel: '当前正在使用你自己的模型供应商。', + close: '关闭', + }, + anthropicHosted: { + anthropicHosted: 'Anthropic Claude', + onTrial: '体验', + exhausted: '超出限额', + desc: '功能强大的模型,擅长执行从复杂对话和创意内容生成到详细指导的各种任务。', + callTimes: '调用次数', + usedUp: '试用额度已用完,请在下方添加自己的模型供应商', + useYourModel: '当前正在使用你自己的模型供应商。', + close: '关闭', + trialQuotaTip: '您的 Anthropic 体验额度将于 2025/03/17 过期,过期后将无法使用,请尽快体验。', + }, + anthropic: { + using: '嵌入能力正在使用', + enableTip: '要启用 Anthropic 模型,您需要先绑定 OpenAI 或 Azure OpenAI 服务。', + notEnabled: '未启用', + keyFrom: '从 Anthropic 获取您的 API 密钥', + }, + encrypted: { + front: '密钥将使用 ', + back: ' 技术进行加密和存储。', + }, + }, + modelProvider: { + notConfigured: '系统模型尚未完全配置', + systemModelSettings: '系统模型设置', + systemModelSettingsLink: '为什么需要设置系统模型?', + selectModel: '选择您的模型', + setupModelFirst: '请先设置您的模型', + systemReasoningModel: { + key: '系统推理模型', + tip: '设置创建应用使用的默认推理模型,以及对话名称生成、下一步问题建议等功能也会使用该默认推理模型。', + }, + embeddingModel: { + key: 'Embedding 模型', + tip: '设置知识库文档嵌入处理的默认模型,检索和导入知识库均使用该 Embedding 模型进行向量化处理,切换后将导致已导入的知识库与问题之间的向量维度不一致,从而导致检索失败。为避免检索失败,请勿随意切换该模型。', + required: '请选择 Embedding 模型', + }, + speechToTextModel: { + key: '语音转文本模型', + tip: '设置对话中语音转文字输入的默认使用模型。', + }, + ttsModel: { + key: '文本转语音模型', + tip: '设置对话中文字转语音输出的默认使用模型。', + }, + rerankModel: { + key: 'Rerank 模型', + tip: '重排序模型将根据候选文档列表与用户问题语义匹配度进行重新排序,从而改进语义排序的结果', + }, + quota: '额度', + searchModel: '搜索模型', + noModelFound: '找不到模型 {{model}}', + models: '模型列表', + showMoreModelProvider: '显示更多模型提供商', + selector: { + tip: '该模型已被删除。请添模型或选择其他模型。', + emptyTip: '无可用模型', + emptySetting: '请前往设置进行配置', + rerankTip: '请设置 Rerank 模型', + }, + card: { + quota: '额度', + onTrial: '试用中', + paid: '已购买', + quotaExhausted: '配额已用完', + callTimes: '调用次数', + tokens: 'Tokens', + buyQuota: '购买额度', + priorityUse: '优先使用', + removeKey: '删除 API 密钥', + tip: '已付费额度将优先考虑。试用额度将在付费额度用完后使用。', + }, + item: { + deleteDesc: '{{modelName}} 被用作系统推理模型。删除后部分功能将无法使用。请确认。', + freeQuota: '免费额度', + }, + addApiKey: '添加您的 API 密钥', + invalidApiKey: 'Invalid API key', + encrypted: { + front: '您的密钥将使用', + back: '技术进行加密和存储。', + }, + freeQuota: { + howToEarn: '如何获取', + }, + addMoreModelProvider: '添加更多模型提供商', + addModel: '添加模型', + modelsNum: '{{num}} 个模型', + showModels: '显示模型', + showModelsNum: '显示 {{num}} 个模型', + collapse: '收起', + config: '配置', + modelAndParameters: '模型及参数', + model: '模型', + featureSupported: '支持 {{feature}} 功能', + callTimes: '调用次数', + credits: '消息额度', + buyQuota: '购买额度', + getFreeTokens: '获得免费 Tokens', + priorityUsing: '优先使用', + deprecated: '已弃用', + confirmDelete: '确认删除?', + quotaTip: '剩余免费额度', + loadPresets: '加载预设', + parameters: '参数', + loadBalancing: '负载均衡', + loadBalancingDescription: '为模型配置多组凭据,并自动调用。', + loadBalancingHeadline: '负载均衡', + configLoadBalancing: '设置负载均衡', + modelHasBeenDeprecated: '该模型已废弃', + providerManaged: '由模型供应商管理', + providerManagedDescription: '使用模型供应商提供的单组凭据', + defaultConfig: '默认配置', + apiKeyStatusNormal: 'API Key 正常', + apiKeyRateLimit: '已达频率上限,{{seconds}}秒后恢复', + addConfig: '增加配置', + editConfig: '修改配置', + loadBalancingLeastKeyWarning: '至少启用 2 个 Key 以使用负载均衡', + loadBalancingInfo: '默认情况下,负载均衡使用 Round-robin 策略。如果触发速率限制,将应用 1 分钟的冷却时间', + upgradeForLoadBalancing: '升级以解锁负载均衡功能', + apiKey: 'API 密钥', + toBeConfigured: '待配置', + configureTip: '请配置 API 密钥,添加模型。', + installProvider: '安装模型供应商', + installDataSourceProvider: '安装数据源供应商', + discoverMore: '发现更多就在', + emptyProviderTitle: '尚未安装模型供应商', + emptyProviderTip: '请安装模型供应商。', + auth: { + unAuthorized: '未授权', + authRemoved: '授权已移除', + apiKeys: 'API 密钥', + addApiKey: '添加 API 密钥', + addModel: '添加模型', + addNewModel: '添加新模型', + addCredential: '添加凭据', + addModelCredential: '添加模型凭据', + editModelCredential: '编辑模型凭据', + modelCredentials: '模型凭据', + modelCredential: '模型凭据', + configModel: '配置模型', + configLoadBalancing: '配置负载均衡', + authorizationError: '授权错误', + specifyModelCredential: '指定模型凭据', + specifyModelCredentialTip: '使用已配置的模型凭据。', + providerManaged: '由模型供应商管理', + providerManagedTip: '使用模型供应商提供的单组凭据。', + apiKeyModal: { + title: 'API 密钥授权配置', + desc: '配置凭据后,工作空间中的所有成员都可以在编排应用时使用此模型。', + addModel: '添加模型', + }, + manageCredentials: '管理凭据', + customModelCredentials: '自定义模型凭据', + addNewModelCredential: '添加模型新凭据', + removeModel: '移除模型', + selectModelCredential: '选择模型凭据', + customModelCredentialsDeleteTip: '模型凭据正在使用中,无法删除', + }, + parametersInvalidRemoved: '部分参数无效,已移除', + }, + dataSource: { + add: '添加数据源', + connect: '绑定', + configure: '配置', + notion: { + title: 'Notion', + description: '使用 Notion 作为知识库的数据源。', + connectedWorkspace: '已绑定工作空间', + addWorkspace: '添加工作空间', + connected: '已绑定', + disconnected: '未绑定', + changeAuthorizedPages: '更改授权页面', + pagesAuthorized: '已授权页面', + sync: '同步', + remove: '删除', + selector: { + pageSelected: '已选页面', + searchPages: '搜索页面...', + noSearchResult: '无搜索结果', + addPages: '添加页面', + preview: '预览', + }, + integratedAlert: 'Notion通过内部凭证集成,无需重新授权。', + }, + website: { + title: '网站', + description: '使用网络爬虫从网站导入内容。', + with: '使用', + configuredCrawlers: '已配置的爬虫', + active: '可用', + inactive: '不可用', + }, + }, + plugin: { + serpapi: { + apiKey: 'API Key', + apiKeyPlaceholder: '输入你的 API 密钥', + keyFrom: '从 SerpAPI 帐户页面获取您的 SerpAPI 密钥', + }, + }, + apiBasedExtension: { + title: 'API 扩展提供了一个集中式的 API 管理,在此统一添加 API 配置后,方便在 Dify 上的各类应用中直接使用。', + link: '了解如何开发您自己的 API 扩展。', + add: '新增 API 扩展', + selector: { + title: 'API 扩展', + placeholder: '请选择 API 扩展', + manage: '管理 API 扩展', + }, + modal: { + title: '新增 API 扩展', + editTitle: '编辑 API 扩展', + name: { + title: '名称', + placeholder: '请输入名称', + }, + apiEndpoint: { + title: 'API Endpoint', + placeholder: '请输入 API endpoint', + }, + apiKey: { + title: 'API-key', + placeholder: '请输入 API-key', + lengthError: 'API-key 不能少于 5 位', + }, + }, + type: '类型', + }, + about: { + changeLog: '更新日志', + updateNow: '现在更新', + nowAvailable: 'Dify {{version}} 现已可用。', + latestAvailable: 'Dify {{version}} 已是最新版本。', + }, + appMenus: { + overview: '监测', + promptEng: '编排', + apiAccess: '访问 API', + logAndAnn: '日志与标注', + logs: '日志', + }, + environment: { + testing: '测试环境', + development: '开发环境', + }, + appModes: { + completionApp: '文本生成型应用', + chatApp: '对话型应用', + }, + datasetMenus: { + documents: '文档', + hitTesting: '召回测试', + settings: '设置', + emptyTip: '此知识尚未集成到任何应用程序中。请参阅文档以获取指导。', + viewDoc: '查看文档', + relatedApp: '个关联应用', + noRelatedApp: '无关联应用', + pipeline: '流水线', + }, + voiceInput: { + speaking: '现在讲...', + converting: '正在转换为文本...', + notAllow: '麦克风未授权', + }, + modelName: { + 'gpt-3.5-turbo': 'GPT-3.5-Turbo', + 'gpt-3.5-turbo-16k': 'GPT-3.5-Turbo-16K', + 'gpt-4': 'GPT-4', + 'gpt-4-32k': 'GPT-4-32K', + 'text-davinci-003': 'Text-Davinci-003', + 'text-embedding-ada-002': 'Text-Embedding-Ada-002', + 'whisper-1': 'Whisper-1', + 'claude-instant-1': 'Claude-Instant', + 'claude-2': 'Claude-2', + }, + chat: { + renameConversation: '重命名会话', + conversationName: '会话名称', + conversationNamePlaceholder: '请输入会话名称', + conversationNameCanNotEmpty: '会话名称必填', + citation: { + title: '引用', + linkToDataset: '跳转至知识库', + characters: '字符:', + hitCount: '召回次数:', + vectorHash: '向量哈希:', + hitScore: '召回得分:', + }, + inputPlaceholder: '和 {{botName}} 聊天', + inputDisabledPlaceholder: '仅供试用', + thinking: '深度思考中...', + thought: '已深度思考', + resend: '重新发送', + }, + promptEditor: { + placeholder: '在这里写你的提示词,输入\'{\' 插入变量、输入\'/\' 插入提示内容块', + context: { + item: { + title: '上下文', + desc: '插入上下文模板', + }, + modal: { + title: '有 {{num}} 个知识库在上下文中', + add: '添加上下文', + footer: '您可以在下面的“上下文”部分中管理上下文。', + }, + }, + history: { + item: { + title: '会话历史', + desc: '插入历史消息模板', + }, + modal: { + title: '示例', + user: '你好', + assistant: '你好!今天我能为您提供什么帮助?', + edit: '编辑对话角色名称', + }, + }, + variable: { + item: { + title: '变量 & 外部工具', + desc: '插入变量和外部工具', + }, + outputToolDisabledItem: { + title: '变量', + desc: '插入变量', + }, + modal: { + add: '添加新变量', + addTool: '添加工具', + }, + }, + query: { + item: { + title: '查询内容', + desc: '插入用户查询模板', + }, + }, + existed: 'Prompt 中已存在', + }, + imageUploader: { + uploadFromComputer: '从本地上传', + uploadFromComputerReadError: '图片读取失败,请重新选择。', + uploadFromComputerUploadError: '图片上传失败,请重新上传。', + uploadFromComputerLimit: '上传图片不能超过 {{size}} MB', + pasteImageLink: '粘贴图片链接', + pasteImageLinkInputPlaceholder: '将图像链接粘贴到此处', + pasteImageLinkInvalid: '图片链接无效', + imageUpload: '图片上传', + }, + fileUploader: { + uploadFromComputer: '从本地上传', + pasteFileLink: '粘贴文件链接', + pasteFileLinkInputPlaceholder: '输入文件链接', + uploadFromComputerReadError: '文件读取失败,请重新选择。', + uploadFromComputerUploadError: '文件上传失败,请重新上传。', + uploadFromComputerLimit: '上传 {{type}} 不能超过 {{size}}', + pasteFileLinkInvalid: '文件链接无效', + fileExtensionNotSupport: '文件类型不支持', + fileExtensionBlocked: '出于安全考虑,该文件类型已被禁止上传', + }, + tag: { + placeholder: '全部标签', + addNew: '创建新标签', + noTag: '没有标签', + noTagYet: '还没有标签', + addTag: '添加标签', + editTag: '修改标签', + manageTags: '管理标签', + selectorPlaceholder: '搜索或者创建', + create: '创建', + delete: '删除标签', + deleteTip: '标签正在使用中,是否删除?', + created: '标签创建成功', + failed: '标签创建失败', + }, + license: { + expiring: '许可证还有 1 天到期', + expiring_plural: '许可证还有 {{count}} 天到期', + unlimited: '无限制', + }, + pagination: { + perPage: '每页显示', + }, + avatar: { + deleteTitle: '删除头像', + deleteDescription: '确定要删除你的个人头像吗?你的账号将使用默认的首字母头像。', + }, + imageInput: { + dropImageHere: '将图片拖放到此处,或', + browse: '浏览', + supportedFormats: '支持 PNG、JPG、JPEG、WEBP 和 GIF 格式', + }, + you: '你', + feedback: { + content: '反馈内容', + subtitle: '请告诉我们这次回应出错的原因。', + title: '提供反馈', + placeholder: '请描述发生了什么问题或我们可以如何改进...', + }, + dynamicSelect: { + error: '加载选项失败', + noData: '没有可用的选项', + loading: '加载选项...', + selected: '已选择 {{count}} 项', + }, +} + +export default translation diff --git a/web/i18n/zh-Hans/explore.json b/web/i18n/zh-Hans/explore.json index fb4c4ace80..b1005aec4d 100644 --- a/web/i18n/zh-Hans/explore.json +++ b/web/i18n/zh-Hans/explore.json @@ -1,12 +1,14 @@ { - "appCard.addToWorkspace": "添加到工作区", - "appCard.customize": "自定义", + "appCard.addToWorkspace": "使用模板", + "appCard.try": "详情", "appCustomize.nameRequired": "应用程序名称不能为空", "appCustomize.subTitle": "应用程序图标和名称", "appCustomize.title": "从 {{name}} 创建应用程序", - "apps.allCategories": "推荐", - "apps.description": "使用这些模板应用程序,或根据模板自定义您自己的应用程序。", - "apps.title": "探索应用", + "apps.allCategories": "所有", + "apps.resetFilter": "清除筛选", + "apps.resultNum": "{{num}} 个结果", + "apps.title": "试用 Dify 精选示例应用,为您的业务寻找 AI 解决方案", + "banner.viewMore": "查看更多", "category.Agent": "Agent", "category.Assistant": "助手", "category.Entertainment": "娱乐", @@ -23,7 +25,16 @@ "sidebar.chat": "智聊", "sidebar.delete.content": "您确定要删除此程序吗?", "sidebar.delete.title": "删除程序", - "sidebar.discovery": "发现", - "sidebar.workspace": "工作区", - "title": "探索" + "sidebar.noApps.description": "已发布的 web apps 将出现在此处", + "sidebar.noApps.learnMore": "了解更多", + "sidebar.noApps.title": "没有 web apps", + "sidebar.title": "应用库", + "sidebar.webApps": "WEB APPS", + "title": "探索", + "tryApp.category": "分类", + "tryApp.createFromSampleApp": "从此模板创建应用", + "tryApp.requirements": "必须配置项", + "tryApp.tabHeader.detail": "编排详情", + "tryApp.tabHeader.try": "试用", + "tryApp.tryInfo": "这是一个示例应用,您可以试用最多 5 条消息。如需继续使用,请点击 “从此模板创建应用” 并完成配置!" } diff --git a/web/i18n/zh-Hans/explore.ts b/web/i18n/zh-Hans/explore.ts new file mode 100644 index 0000000000..2080033904 --- /dev/null +++ b/web/i18n/zh-Hans/explore.ts @@ -0,0 +1,64 @@ +const translation = { + title: '探索', + sidebar: { + title: '应用库', + chat: '智聊', + webApps: 'WEB APPS', + action: { + pin: '置顶', + unpin: '取消置顶', + rename: '重命名', + delete: '删除', + }, + delete: { + title: '删除程序', + content: '您确定要删除此程序吗?', + }, + noApps: { + title: '没有 web apps', + description: '已发布的 web apps 将出现在此处', + learnMore: '了解更多', + }, + }, + apps: { + title: '试用 Dify 精选示例应用,为您的业务寻找 AI 解决方案', + allCategories: '所有', + resultNum: '{{num}} 个结果', + resetFilter: '清除筛选', + }, + appCard: { + addToWorkspace: '使用模板', + try: '详情', + customize: '自定义', + }, + tryApp: { + tabHeader: { + try: '试用', + detail: '编排详情', + }, + createFromSampleApp: '从此模板创建应用', + category: '分类', + requirements: '必须配置项', + tryInfo: '这是一个示例应用,您可以试用最多 5 条消息。如需继续使用,请点击 “从此模板创建应用” 并完成配置!', + }, + appCustomize: { + title: '从 {{name}} 创建应用程序', + subTitle: '应用程序图标和名称', + nameRequired: '应用程序名称不能为空', + }, + category: { + Agent: 'Agent', + Assistant: '助手', + Writing: '写作', + Translate: '翻译', + Programming: '编程', + HR: '人力资源', + Workflow: '工作流', + Entertainment: '娱乐', + }, + banner: { + viewMore: '查看更多', + }, +} + +export default translation diff --git a/web/i18n/zh-Hant/explore.json b/web/i18n/zh-Hant/explore.json index 5a19e649ff..edfab80fcc 100644 --- a/web/i18n/zh-Hant/explore.json +++ b/web/i18n/zh-Hant/explore.json @@ -1,12 +1,7 @@ { - "appCard.addToWorkspace": "新增到工作區", - "appCard.customize": "自定義", "appCustomize.nameRequired": "應用程式名稱不能為空", "appCustomize.subTitle": "應用程式圖示和名稱", "appCustomize.title": "從 {{name}} 建立應用程式", - "apps.allCategories": "推薦", - "apps.description": "使用這些模板應用程式,或根據模板自定義您自己的應用程式。", - "apps.title": "探索應用", "category.Agent": "代理", "category.Assistant": "助手", "category.Entertainment": "娛樂", @@ -23,7 +18,5 @@ "sidebar.chat": "智聊", "sidebar.delete.content": "您確定要刪除此程式嗎?", "sidebar.delete.title": "刪除程式", - "sidebar.discovery": "發現", - "sidebar.workspace": "工作區", "title": "探索" } diff --git a/web/i18n/zh-Hant/explore.ts b/web/i18n/zh-Hant/explore.ts new file mode 100644 index 0000000000..cbb20f0e77 --- /dev/null +++ b/web/i18n/zh-Hant/explore.ts @@ -0,0 +1,38 @@ +const translation = { + title: '探索', + sidebar: { + chat: '智聊', + action: { + pin: '置頂', + unpin: '取消置頂', + rename: '重新命名', + delete: '刪除', + }, + delete: { + title: '刪除程式', + content: '您確定要刪除此程式嗎?', + }, + }, + apps: { + }, + appCard: { + customize: '自定義', + }, + appCustomize: { + title: '從 {{name}} 建立應用程式', + subTitle: '應用程式圖示和名稱', + nameRequired: '應用程式名稱不能為空', + }, + category: { + Assistant: '助手', + Writing: '寫作', + Translate: '翻譯', + Programming: '程式設計', + HR: '人力資源', + Agent: '代理', + Workflow: '工作流', + Entertainment: '娛樂', + }, +} + +export default translation diff --git a/web/models/debug.ts b/web/models/debug.ts index 73d0910e82..fb75ae7946 100644 --- a/web/models/debug.ts +++ b/web/models/debug.ts @@ -134,6 +134,7 @@ export type ModelConfig = { provider: string // LLM Provider: for example "OPENAI" model_id: string mode: ModelModeType + prompt_type?: PromptMode configs: PromptConfig chat_prompt_config?: ChatPromptConfig | null completion_prompt_config?: CompletionPromptConfig | null diff --git a/web/models/explore.ts b/web/models/explore.ts index 1d513e9b70..bca92abee5 100644 --- a/web/models/explore.ts +++ b/web/models/explore.ts @@ -28,6 +28,7 @@ export type App = { installed: boolean editable: boolean is_agent: boolean + can_trial: boolean } export type InstalledApp = { diff --git a/web/package.json b/web/package.json index 4a14ab8601..99eb63f6a8 100644 --- a/web/package.json +++ b/web/package.json @@ -11,7 +11,7 @@ } }, "engines": { - "node": ">=v22.11.0" + "node": ">=22.12.0" }, "browserslist": [ "last 1 Chrome version", @@ -88,6 +88,8 @@ "echarts": "^5.6.0", "echarts-for-react": "^3.0.5", "elkjs": "^0.9.3", + "embla-carousel-autoplay": "^8.6.0", + "embla-carousel-react": "^8.6.0", "emoji-mart": "^5.6.0", "es-toolkit": "^1.43.0", "fast-deep-equal": "^3.1.3", @@ -166,7 +168,7 @@ "@storybook/addon-onboarding": "9.1.13", "@storybook/addon-themes": "9.1.13", "@storybook/nextjs": "9.1.13", - "@storybook/react": "9.1.13", + "@storybook/react": "9.1.17", "@tanstack/eslint-plugin-query": "^5.91.2", "@tanstack/react-devtools": "^0.9.0", "@tanstack/react-form-devtools": "^0.2.9", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 2ce94ae8eb..2f98a6e4b1 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -165,6 +165,12 @@ importers: elkjs: specifier: ^0.9.3 version: 0.9.3 + embla-carousel-autoplay: + specifier: ^8.6.0 + version: 8.6.0(embla-carousel@8.6.0) + embla-carousel-react: + specifier: ^8.6.0 + version: 8.6.0(react@19.2.3) emoji-mart: specifier: ^5.6.0 version: 5.6.0 @@ -182,7 +188,7 @@ importers: version: 1.11.13 i18next: specifier: ^25.7.3 - version: 25.7.3(typescript@5.9.3) + version: 25.7.4(typescript@5.9.3) i18next-resources-to-backend: specifier: ^1.2.1 version: 1.2.1 @@ -266,7 +272,7 @@ importers: version: 4.6.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-i18next: specifier: ^16.5.0 - version: 16.5.0(i18next@25.7.3(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + version: 16.5.1(i18next@25.7.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) react-markdown: specifier: ^9.1.0 version: 9.1.0(@types/react@19.2.7)(react@19.2.3) @@ -395,17 +401,17 @@ importers: specifier: 9.1.13 version: 9.1.13(esbuild@0.25.0)(next@15.5.9(@babel/core@7.28.5)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.95.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.95.0)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(type-fest@4.2.0)(typescript@5.9.3)(uglify-js@3.19.3)(webpack-hot-middleware@2.26.1)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) '@storybook/react': - specifier: 9.1.13 - version: 9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3) + specifier: 9.1.17 + version: 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3) '@tanstack/eslint-plugin-query': specifier: ^5.91.2 version: 5.91.2(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@tanstack/react-devtools': specifier: ^0.9.0 - version: 0.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10) + version: 0.9.1(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10) '@tanstack/react-form-devtools': specifier: ^0.2.9 - version: 0.2.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10) + version: 0.2.11(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.0)(react@19.2.3)(solid-js@1.9.10) '@tanstack/react-query-devtools': specifier: ^5.90.2 version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.3))(react@19.2.3) @@ -513,7 +519,7 @@ importers: version: 1.16.0 knip: specifier: ^5.78.0 - version: 5.78.0(@types/node@18.15.0)(typescript@5.9.3) + version: 5.80.0(@types/node@18.15.0)(typescript@5.9.3) lint-staged: specifier: ^15.5.2 version: 15.5.2 @@ -3281,6 +3287,13 @@ packages: react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta storybook: ^9.1.13 + '@storybook/react-dom-shim@9.1.17': + resolution: {integrity: sha512-Ss/lNvAy0Ziynu+KniQIByiNuyPz3dq7tD62hqSC/pHw190X+M7TKU3zcZvXhx2AQx1BYyxtdSHIZapb+P5mxQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.1.17 + '@storybook/react@9.1.13': resolution: {integrity: sha512-B0UpYikKf29t8QGcdmumWojSQQ0phSDy/Ne2HYdrpNIxnUvHHUVOlGpq4lFcIDt52Ip5YG5GuAwJg3+eR4LCRg==} engines: {node: '>=20.0.0'} @@ -3293,6 +3306,18 @@ packages: typescript: optional: true + '@storybook/react@9.1.17': + resolution: {integrity: sha512-TZCplpep5BwjHPIIcUOMHebc/2qKadJHYPisRn5Wppl014qgT3XkFLpYkFgY1BaRXtqw8Mn3gqq4M/49rQ7Iww==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.1.17 + typescript: '>= 4.9.x' + peerDependenciesMeta: + typescript: + optional: true + '@stylistic/eslint-plugin@5.6.1': resolution: {integrity: sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3410,17 +3435,20 @@ packages: peerDependencies: solid-js: '>=1.9.7' - '@tanstack/devtools-utils@0.0.9': - resolution: {integrity: sha512-tCObM6wbEjuHeGNs3JDhrqBhoMxpJpVuVIg5Kc33EmUI1ZO7KLpC1277Qf6AmSWy3aVOreGwn3y5bJzxmAJNXg==} + '@tanstack/devtools-utils@0.2.3': + resolution: {integrity: sha512-Ob7wAGTNs7SfOJWZlV+3fi7JGT8ApgeNKoaKV4trRk3TDjThm0w4EwbNDsfZu5NCvefSOY0sequp2qG8g7zG/g==} engines: {node: '>=18'} peerDependencies: '@types/react': ~19.2.7 + preact: '>=10.0.0' react: '>=17.0.0' solid-js: '>=1.9.7' vue: '>=3.2.0' peerDependenciesMeta: '@types/react': optional: true + preact: + optional: true react: optional: true solid-js: @@ -3428,8 +3456,8 @@ packages: vue: optional: true - '@tanstack/devtools@0.10.1': - resolution: {integrity: sha512-1gtPmCDXV4Pl1nVtoqwjV0tc4E9GMuFtlkBX1Lz1KfqI3W9JojT5YsVifOQ/g8BTQ5w5+tyIANwHU7WYgLq/MQ==} + '@tanstack/devtools@0.10.2': + resolution: {integrity: sha512-6TPNl3jTrCFpyV3m9lBeHxum6btmiihbv+A3xkDpt3JScRcWP1a8G5rZzKhlOtikzG1QSiceRrbckKnIAvZ7FQ==} engines: {node: '>=18'} peerDependencies: solid-js: '>=1.9.7' @@ -3442,11 +3470,11 @@ packages: '@tanstack/form-core@1.27.1': resolution: {integrity: sha512-hPM+0tUnZ2C2zb2TE1lar1JJ0S0cbnQHlUwFcCnVBpMV3rjtUzkoM766gUpWrlmTGCzNad0GbJ0aTxVsjT6J8g==} - '@tanstack/form-core@1.27.6': - resolution: {integrity: sha512-1C4PUpOcCpivddKxtAeqdeqncxnPKiPpTVDRknDExCba+6zCsAjxgL+p3qYA3hu+EFyUAdW71rU+uqYbEa7qqA==} + '@tanstack/form-core@1.27.7': + resolution: {integrity: sha512-nvogpyE98fhb0NDw1Bf2YaCH+L7ZIUgEpqO9TkHucDn6zg3ni521boUpv0i8HKIrmmFwDYjWZoCnrgY4HYWTkw==} - '@tanstack/form-devtools@0.2.9': - resolution: {integrity: sha512-KOJiwvlFPsHeuWXvHUXRVdciXG1OPhg1c476MsLre0YLdaw1jeMlDYSlqq7sdEULX+2Sg/lhNpX86QbQuxzd2A==} + '@tanstack/form-devtools@0.2.11': + resolution: {integrity: sha512-wCQ5uicGfxs34ytZmVhppKELijrx4OU5zRj4PYs0RbBJH3bJYOj5MATsj+rkdDLi1FeVrwLgK2qX4a6c/hWF4g==} peerDependencies: solid-js: '>=1.9.9' @@ -3464,8 +3492,8 @@ packages: '@tanstack/query-devtools@5.91.1': resolution: {integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==} - '@tanstack/react-devtools@0.9.0': - resolution: {integrity: sha512-Lq0svXOTG5N61SHgx8F0on6zz2GB0kmFjN/yyfNLrJyRgJ+U3jYFRd9ti3uBPABsXzHQMHYYujnTXrOYp/OaUg==} + '@tanstack/react-devtools@0.9.1': + resolution: {integrity: sha512-ONDMs117FrzWxFD1JQ9Z94QnTx+63RuQ+9Z3ieSS9bHAWmty4PWiLddAZPvyHCbV9iljlpUEkCoKCO1HMywR2Q==} engines: {node: '>=18'} peerDependencies: '@types/react': ~19.2.7 @@ -3473,8 +3501,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-form-devtools@0.2.9': - resolution: {integrity: sha512-wg0xrcVY8evIFGVHrnl9s+/9ENzuVbqv5Ru4HyAJjjL4uECtl6KdDJsi0lZdOyoM1UYEQoVdcN8jfBbxkA3q1g==} + '@tanstack/react-form-devtools@0.2.11': + resolution: {integrity: sha512-Hv6aZqH2dfamFPWwonA20xKj61xU3omilOYEGdihldOddl+cFLY6P7q66Zqs0p9a9mOQvBS+/i0zBcZjRzvupg==} peerDependencies: react: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -3728,9 +3756,6 @@ packages: '@types/node@20.19.26': resolution: {integrity: sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==} - '@types/node@20.19.27': - resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} - '@types/papaparse@5.5.1': resolution: {integrity: sha512-esEO+VISsLIyE+JZBmb89NzsYYbpwV8lmv2rPo6oX5y9KhBaIP7hhHgjuTut54qjdKVMufTEcrh5fUl9+58huw==} @@ -3816,12 +3841,6 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.50.1': - resolution: {integrity: sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.49.0': resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3830,10 +3849,6 @@ packages: resolution: {integrity: sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.50.1': - resolution: {integrity: sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.49.0': resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3846,12 +3861,6 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.50.1': - resolution: {integrity: sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.49.0': resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3874,10 +3883,6 @@ packages: resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.50.1': - resolution: {integrity: sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.49.0': resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3890,12 +3895,6 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.50.1': - resolution: {integrity: sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.49.0': resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3910,13 +3909,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.50.1': - resolution: {integrity: sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.49.0': resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3925,10 +3917,6 @@ packages: resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.50.1': - resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251209.1': resolution: {integrity: sha512-F1cnYi+ZeinYQnaTQKKIsbuoq8vip5iepBkSZXlB8PjbG62LW1edUdktd/nVEc+Q+SEysSQ3jRdk9eU766s5iw==} cpu: [arm64] @@ -5078,6 +5066,24 @@ packages: elliptic@6.6.1: resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} + embla-carousel-autoplay@8.6.0: + resolution: {integrity: sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel-react@8.6.0: + resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.6.0: + resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + emoji-mart@5.6.0: resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==} @@ -5886,8 +5892,8 @@ packages: i18next-resources-to-backend@1.2.1: resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==} - i18next@25.7.3: - resolution: {integrity: sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==} + i18next@25.7.4: + resolution: {integrity: sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==} peerDependencies: typescript: ^5 peerDependenciesMeta: @@ -6226,8 +6232,8 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - knip@5.78.0: - resolution: {integrity: sha512-nB7i/fgiJl7WVxdv5lX4ZPfDt9/zrw/lOgZtyioy988xtFhKuFJCRdHWT1Zg9Avc0yaojvnmEuAXU8SeMblKww==} + knip@5.80.0: + resolution: {integrity: sha512-K/Ga2f/SHEUXXriVdaw2GfeIUJ5muwdqusHGkCtaG/1qeMmQJiuwZj9KnPxaDbnYPAu8RWjYYh8Nyb+qlJ3d8A==} engines: {node: '>=18.18.0'} hasBin: true peerDependencies: @@ -6264,8 +6270,8 @@ packages: lexical@0.38.2: resolution: {integrity: sha512-JJmfsG3c4gwBHzUGffbV7ifMNkKAWMCnYE3xJl87gty7hjyV5f3xq7eqTjP5HFYvO4XpjJvvWO2/djHp5S10tw==} - lib0@0.2.117: - resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==} + lib0@0.2.115: + resolution: {integrity: sha512-noaW4yNp6hCjOgDnWWxW0vGXE3kZQI5Kqiwz+jIWXavI9J9WyfJ9zjsbQlQlgjIbHBrvlA/x3TSIXBUJj+0L6g==} engines: {node: '>=16'} hasBin: true @@ -7282,8 +7288,8 @@ packages: react: '>=16.8.1' react-dom: '>=16.8.1' - react-i18next@16.5.0: - resolution: {integrity: sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw==} + react-i18next@16.5.1: + resolution: {integrity: sha512-Hks6UIRZWW4c+qDAnx1csVsCGYeIR4MoBGQgJ+NUoNnO6qLxXuf8zu0xdcinyXUORgGzCdRsexxO1Xzv3sTdnw==} peerDependencies: i18next: '>= 25.6.2' react: '>= 16.8.0' @@ -8558,7 +8564,6 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} - deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} @@ -8882,7 +8887,7 @@ snapshots: '@amplitude/targeting@0.2.0': dependencies: '@amplitude/analytics-client-common': 2.4.16 - '@amplitude/analytics-core': 2.35.0 + '@amplitude/analytics-core': 2.33.0 '@amplitude/analytics-types': 2.11.0 '@amplitude/experiment-core': 0.7.2 idb: 8.0.0 @@ -9865,7 +9870,7 @@ snapshots: '@es-joy/jsdoccomment@0.76.0': dependencies: '@types/estree': 1.0.8 - '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/types': 8.50.0 comment-parser: 1.4.1 esquery: 1.6.0 jsdoc-type-pratt-parser: 6.10.0 @@ -9873,7 +9878,7 @@ snapshots: '@es-joy/jsdoccomment@0.78.0': dependencies: '@types/estree': 1.0.8 - '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/types': 8.50.0 comment-parser: 1.4.1 esquery: 1.6.0 jsdoc-type-pratt-parser: 7.0.0 @@ -10053,7 +10058,7 @@ snapshots: '@eslint-react/eff': 2.3.13 '@typescript-eslint/types': 8.50.0 '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) string-ts: 2.3.1 typescript: 5.9.3 @@ -10068,7 +10073,7 @@ snapshots: '@eslint-react/var': 2.3.13(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.50.0 '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) birecord: 0.1.1 eslint: 9.39.2(jiti@1.21.7) ts-pattern: 5.9.0 @@ -10085,7 +10090,7 @@ snapshots: '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) eslint-plugin-react-dom: 2.3.13(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint-plugin-react-hooks-extra: 2.3.13(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) @@ -10100,7 +10105,7 @@ snapshots: '@eslint-react/shared@2.3.13(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-react/eff': 2.3.13 - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) ts-pattern: 5.9.0 typescript: 5.9.3 @@ -10114,7 +10119,7 @@ snapshots: '@eslint-react/eff': 2.3.13 '@typescript-eslint/scope-manager': 8.50.0 '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) ts-pattern: 5.9.0 typescript: 5.9.3 @@ -11676,6 +11681,12 @@ snapshots: react-dom: 19.2.3(react@19.2.3) storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@storybook/react-dom-shim@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))': + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@storybook/react@9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3)': dependencies: '@storybook/global': 5.0.0 @@ -11686,10 +11697,20 @@ snapshots: optionalDependencies: typescript: 5.9.3 + '@storybook/react@9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 9.1.17(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + optionalDependencies: + typescript: 5.9.3 + '@stylistic/eslint-plugin@5.6.1(eslint@9.39.2(jiti@1.21.7))': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) - '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/types': 8.49.0 eslint: 9.39.2(jiti@1.21.7) eslint-visitor-keys: 4.2.1 espree: 10.4.0 @@ -11787,17 +11808,18 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-utils@0.0.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)': + '@tanstack/devtools-utils@0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.0)(react@19.2.3)(solid-js@1.9.10)': dependencies: '@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.10) optionalDependencies: '@types/react': 19.2.7 + preact: 10.28.0 react: 19.2.3 solid-js: 1.9.10 transitivePeerDependencies: - csstype - '@tanstack/devtools@0.10.1(csstype@3.2.3)(solid-js@1.9.10)': + '@tanstack/devtools@0.10.2(csstype@3.2.3)(solid-js@1.9.10)': dependencies: '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.10) '@solid-primitives/keyboard': 1.3.3(solid-js@1.9.10) @@ -11815,7 +11837,7 @@ snapshots: '@tanstack/eslint-plugin-query@5.91.2(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) transitivePeerDependencies: - supports-color @@ -11827,17 +11849,17 @@ snapshots: '@tanstack/pacer': 0.15.4 '@tanstack/store': 0.7.7 - '@tanstack/form-core@1.27.6': + '@tanstack/form-core@1.27.7': dependencies: '@tanstack/devtools-event-client': 0.4.0 '@tanstack/pacer-lite': 0.1.1 '@tanstack/store': 0.7.7 - '@tanstack/form-devtools@0.2.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)': + '@tanstack/form-devtools@0.2.11(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.0)(react@19.2.3)(solid-js@1.9.10)': dependencies: '@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.10) - '@tanstack/devtools-utils': 0.0.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10) - '@tanstack/form-core': 1.27.6 + '@tanstack/devtools-utils': 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.0)(react@19.2.3)(solid-js@1.9.10) + '@tanstack/form-core': 1.27.7 clsx: 2.1.1 dayjs: 1.11.19 goober: 2.1.18(csstype@3.2.3) @@ -11845,6 +11867,7 @@ snapshots: transitivePeerDependencies: - '@types/react' - csstype + - preact - react - vue @@ -11859,9 +11882,9 @@ snapshots: '@tanstack/query-devtools@5.91.1': {} - '@tanstack/react-devtools@0.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10)': + '@tanstack/react-devtools@0.9.1(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10)': dependencies: - '@tanstack/devtools': 0.10.1(csstype@3.2.3)(solid-js@1.9.10) + '@tanstack/devtools': 0.10.2(csstype@3.2.3)(solid-js@1.9.10) '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) react: 19.2.3 @@ -11872,14 +11895,15 @@ snapshots: - solid-js - utf-8-validate - '@tanstack/react-form-devtools@0.2.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10)': + '@tanstack/react-form-devtools@0.2.11(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.0)(react@19.2.3)(solid-js@1.9.10)': dependencies: - '@tanstack/devtools-utils': 0.0.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10) - '@tanstack/form-devtools': 0.2.9(@types/react@19.2.7)(csstype@3.2.3)(react@19.2.3)(solid-js@1.9.10) + '@tanstack/devtools-utils': 0.2.3(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.0)(react@19.2.3)(solid-js@1.9.10) + '@tanstack/form-devtools': 0.2.11(@types/react@19.2.7)(csstype@3.2.3)(preact@10.28.0)(react@19.2.3)(solid-js@1.9.10) react: 19.2.3 transitivePeerDependencies: - '@types/react' - csstype + - preact - solid-js - vue @@ -12167,11 +12191,6 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@20.19.27': - dependencies: - undici-types: 6.21.0 - optional: true - '@types/papaparse@5.5.1': dependencies: '@types/node': 18.15.0 @@ -12262,17 +12281,8 @@ snapshots: '@typescript-eslint/project-service@8.50.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) - '@typescript-eslint/types': 8.50.1 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.50.1(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) - '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) + '@typescript-eslint/types': 8.50.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -12288,11 +12298,6 @@ snapshots: '@typescript-eslint/types': 8.50.0 '@typescript-eslint/visitor-keys': 8.50.0 - '@typescript-eslint/scope-manager@8.50.1': - dependencies: - '@typescript-eslint/types': 8.50.1 - '@typescript-eslint/visitor-keys': 8.50.1 - '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -12301,10 +12306,6 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.50.1(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - '@typescript-eslint/type-utils@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.49.0 @@ -12333,8 +12334,6 @@ snapshots: '@typescript-eslint/types@8.50.0': {} - '@typescript-eslint/types@8.50.1': {} - '@typescript-eslint/typescript-estree@8.49.0(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.49.0(typescript@5.9.3) @@ -12365,21 +12364,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.50.1(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.50.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) - '@typescript-eslint/types': 8.50.1 - '@typescript-eslint/visitor-keys': 8.50.1 - debug: 4.4.3 - minimatch: 9.0.5 - semver: 7.7.3 - tinyglobby: 0.2.15 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) @@ -12402,17 +12386,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.50.1 - '@typescript-eslint/types': 8.50.1 - '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) - eslint: 9.39.2(jiti@1.21.7) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/visitor-keys@8.49.0': dependencies: '@typescript-eslint/types': 8.49.0 @@ -12423,11 +12396,6 @@ snapshots: '@typescript-eslint/types': 8.50.0 eslint-visitor-keys: 4.2.1 - '@typescript-eslint/visitor-keys@8.50.1': - dependencies: - '@typescript-eslint/types': 8.50.1 - eslint-visitor-keys: 4.2.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251209.1': optional: true @@ -12492,8 +12460,8 @@ snapshots: '@vitest/eslint-plugin@1.6.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)(vitest@4.0.16(@types/node@18.15.0)(happy-dom@20.0.11)(jiti@1.21.7)(jsdom@27.3.0(canvas@3.2.0))(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@typescript-eslint/scope-manager': 8.50.1 - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) optionalDependencies: typescript: 5.9.3 @@ -13683,6 +13651,22 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + embla-carousel-autoplay@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel-react@8.6.0(react@19.2.3): + dependencies: + embla-carousel: 8.6.0 + embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) + react: 19.2.3 + + embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: {} + emoji-mart@5.6.0: {} emoji-regex@8.0.0: {} @@ -13922,8 +13906,8 @@ snapshots: eslint-plugin-perfectionist@4.15.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/types': 8.50.1 - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) natural-orderby: 5.0.0 transitivePeerDependencies: @@ -13950,7 +13934,7 @@ snapshots: '@eslint-react/var': 2.3.13(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) compare-versions: 6.1.1 eslint: 9.39.2(jiti@1.21.7) string-ts: 2.3.1 @@ -13969,7 +13953,7 @@ snapshots: '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) string-ts: 2.3.1 ts-pattern: 5.9.0 @@ -13998,7 +13982,7 @@ snapshots: '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) string-ts: 2.3.1 ts-pattern: 5.9.0 @@ -14019,7 +14003,7 @@ snapshots: '@eslint-react/var': 2.3.13(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) string-ts: 2.3.1 ts-pattern: 5.9.0 @@ -14037,7 +14021,7 @@ snapshots: '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) compare-versions: 6.1.1 eslint: 9.39.2(jiti@1.21.7) is-immutable-type: 5.0.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) @@ -14075,7 +14059,7 @@ snapshots: eslint-plugin-storybook@10.1.10(eslint@9.39.2(jiti@1.21.7))(storybook@9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.2(jiti@1.21.7) storybook: 9.1.17(@testing-library/dom@10.4.1)(vite@7.3.0(@types/node@18.15.0)(jiti@1.21.7)(sass@1.95.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: @@ -14536,7 +14520,7 @@ snapshots: happy-dom@20.0.11: dependencies: - '@types/node': 20.19.27 + '@types/node': 20.19.26 '@types/whatwg-mimetype': 3.0.2 whatwg-mimetype: 3.0.0 optional: true @@ -14792,7 +14776,7 @@ snapshots: dependencies: '@babel/runtime': 7.28.4 - i18next@25.7.3(typescript@5.9.3): + i18next@25.7.4(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 optionalDependencies: @@ -15076,7 +15060,7 @@ snapshots: kleur@4.1.5: {} - knip@5.78.0(@types/node@18.15.0)(typescript@5.9.3): + knip@5.80.0(@types/node@18.15.0)(typescript@5.9.3): dependencies: '@nodelib/fs.walk': 1.2.8 '@types/node': 18.15.0 @@ -15125,7 +15109,7 @@ snapshots: lexical@0.38.2: {} - lib0@0.2.117: + lib0@0.2.115: dependencies: isomorphic.js: 0.2.5 @@ -16483,11 +16467,11 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - react-i18next@16.5.0(i18next@25.7.3(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): + react-i18next@16.5.1(i18next@25.7.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 html-parse-stringify: 3.0.1 - i18next: 25.7.3(typescript@5.9.3) + i18next: 25.7.4(typescript@5.9.3) react: 19.2.3 use-sync-external-store: 1.6.0(react@19.2.3) optionalDependencies: @@ -17985,7 +17969,7 @@ snapshots: yjs@13.6.27: dependencies: - lib0: 0.2.117 + lib0: 0.2.115 yocto-queue@0.1.0: {} diff --git a/web/service/debug.ts b/web/service/debug.ts index 850f3dfc24..9f11643e7f 100644 --- a/web/service/debug.ts +++ b/web/service/debug.ts @@ -1,4 +1,4 @@ -import type { IOnCompleted, IOnData, IOnError, IOnFile, IOnMessageEnd, IOnMessageReplace, IOnThought } from './base' +import type { IOnCompleted, IOnData, IOnError, IOnMessageReplace } from './base' import type { ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { ChatPromptConfig, CompletionPromptConfig } from '@/models/debug' import type { AppModeEnum, ModelModeType } from '@/types/app' @@ -25,24 +25,6 @@ export type CodeGenRes = { error?: string } -export const sendChatMessage = async (appId: string, body: Record, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace }: { - onData: IOnData - onCompleted: IOnCompleted - onFile: IOnFile - onThought: IOnThought - onMessageEnd: IOnMessageEnd - onMessageReplace: IOnMessageReplace - onError: IOnError - getAbortController?: (abortController: AbortController) => void -}) => { - return ssePost(`apps/${appId}/chat-messages`, { - body: { - ...body, - response_mode: 'streaming', - }, - }, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace }) -} - export const stopChatMessageResponding = async (appId: string, taskId: string) => { return post(`apps/${appId}/chat-messages/${taskId}/stop`) } diff --git a/web/service/explore.ts b/web/service/explore.ts index b4056da4ab..5837544e52 100644 --- a/web/service/explore.ts +++ b/web/service/explore.ts @@ -32,3 +32,8 @@ export const updatePinStatus = (id: string, isPinned: boolean) => { export const getAppAccessModeByAppId = (appId: string) => { return get<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`) } + +export const fetchBanners = (language?: string): Promise => { + const url = language ? `/explore/banners?language=${language}` : '/explore/banners' + return get(url) +} diff --git a/web/service/share.ts b/web/service/share.ts index 203dc896db..add1256f30 100644 --- a/web/service/share.ts +++ b/web/service/share.ts @@ -2,22 +2,17 @@ import type { IOnCompleted, IOnData, IOnError, - IOnFile, IOnIterationFinished, IOnIterationNext, IOnIterationStarted, IOnLoopFinished, IOnLoopNext, IOnLoopStarted, - IOnMessageEnd, IOnMessageReplace, IOnNodeFinished, IOnNodeStarted, IOnTextChunk, IOnTextReplace, - IOnThought, - IOnTTSChunk, - IOnTTSEnd, IOnWorkflowFinished, IOnWorkflowStarted, } from './base' @@ -44,45 +39,43 @@ import { } from './base' import { getWebAppAccessToken } from './webapp-auth' -function getAction(action: 'get' | 'post' | 'del' | 'patch', isInstalledApp: boolean) { +export enum AppSourceType { + webApp = 'webApp', + installedApp = 'installedApp', + tryApp = 'tryApp', +} + +const apiPrefix = { + [AppSourceType.webApp]: '', + [AppSourceType.installedApp]: 'installed-apps', + [AppSourceType.tryApp]: 'trial-apps', +} + +function getIsPublicAPI(appSourceType: AppSourceType) { + return appSourceType === AppSourceType.webApp +} + +function getAction(action: 'get' | 'post' | 'del' | 'patch', appSourceType: AppSourceType) { + const isNeedLogin = !getIsPublicAPI(appSourceType) switch (action) { case 'get': - return isInstalledApp ? consoleGet : get + return isNeedLogin ? consoleGet : get case 'post': - return isInstalledApp ? consolePost : post + return isNeedLogin ? consolePost : post case 'patch': - return isInstalledApp ? consolePatch : patch + return isNeedLogin ? consolePatch : patch case 'del': - return isInstalledApp ? consoleDel : del + return isNeedLogin ? consoleDel : del } } -export function getUrl(url: string, isInstalledApp: boolean, installedAppId: string) { - return isInstalledApp ? `installed-apps/${installedAppId}/${url.startsWith('/') ? url.slice(1) : url}` : url +export function getUrl(url: string, appSourceType: AppSourceType, appId: string) { + const hasPrefix = appSourceType !== AppSourceType.webApp + return hasPrefix ? `${apiPrefix[appSourceType]}/${appId}/${url.startsWith('/') ? url.slice(1) : url}` : url } -export const sendChatMessage = async (body: Record, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace, onTTSChunk, onTTSEnd }: { - onData: IOnData - onCompleted: IOnCompleted - onFile: IOnFile - onThought: IOnThought - onError: IOnError - onMessageEnd?: IOnMessageEnd - onMessageReplace?: IOnMessageReplace - getAbortController?: (abortController: AbortController) => void - onTTSChunk?: IOnTTSChunk - onTTSEnd?: IOnTTSEnd -}, isInstalledApp: boolean, installedAppId = '') => { - return ssePost(getUrl('chat-messages', isInstalledApp, installedAppId), { - body: { - ...body, - response_mode: 'streaming', - }, - }, { onData, onCompleted, onThought, onFile, isPublicAPI: !isInstalledApp, onError, getAbortController, onMessageEnd, onMessageReplace, onTTSChunk, onTTSEnd }) -} - -export const stopChatMessageResponding = async (appId: string, taskId: string, isInstalledApp: boolean, installedAppId = '') => { - return getAction('post', isInstalledApp)(getUrl(`chat-messages/${taskId}/stop`, isInstalledApp, installedAppId)) +export const stopChatMessageResponding = async (appId: string, taskId: string, appSourceType: AppSourceType, installedAppId = '') => { + return getAction('post', appSourceType)(getUrl(`chat-messages/${taskId}/stop`, appSourceType, installedAppId)) } export const sendCompletionMessage = async (body: Record, { onData, onCompleted, onError, onMessageReplace, getAbortController }: { @@ -91,13 +84,13 @@ export const sendCompletionMessage = async (body: Record, { onData, onError: IOnError onMessageReplace: IOnMessageReplace getAbortController?: (abortController: AbortController) => void -}, isInstalledApp: boolean, installedAppId = '') => { - return ssePost(getUrl('completion-messages', isInstalledApp, installedAppId), { +}, appSourceType: AppSourceType, installedAppId = '') => { + return ssePost(getUrl('completion-messages', appSourceType, installedAppId), { body: { ...body, response_mode: 'streaming', }, - }, { onData, onCompleted, isPublicAPI: !isInstalledApp, onError, onMessageReplace, getAbortController }) + }, { onData, onCompleted, isPublicAPI: getIsPublicAPI(appSourceType), onError, onMessageReplace, getAbortController }) } export const sendWorkflowMessage = async ( @@ -129,10 +122,10 @@ export const sendWorkflowMessage = async ( onTextChunk: IOnTextChunk onTextReplace: IOnTextReplace }, - isInstalledApp: boolean, - installedAppId = '', + appSourceType: AppSourceType, + appId = '', ) => { - return ssePost(getUrl('workflows/run', isInstalledApp, installedAppId), { + return ssePost(getUrl('workflows/run', appSourceType, appId), { body: { ...body, response_mode: 'streaming', @@ -141,7 +134,7 @@ export const sendWorkflowMessage = async ( onNodeStarted, onWorkflowStarted, onWorkflowFinished, - isPublicAPI: !isInstalledApp, + isPublicAPI: getIsPublicAPI(appSourceType), onNodeFinished, onIterationStart, onIterationNext, @@ -154,42 +147,42 @@ export const sendWorkflowMessage = async ( }) } -export const stopWorkflowMessage = async (_appId: string, taskId: string, isInstalledApp: boolean, installedAppId = '') => { +export const stopWorkflowMessage = async (_appId: string, taskId: string, appSourceType: AppSourceType, installedAppId = '') => { if (!taskId) return - return getAction('post', isInstalledApp)(getUrl(`workflows/tasks/${taskId}/stop`, isInstalledApp, installedAppId)) + return getAction('post', appSourceType)(getUrl(`workflows/tasks/${taskId}/stop`, appSourceType, installedAppId)) } export const fetchAppInfo = async () => { return get('/site') as Promise } -export const fetchConversations = async (isInstalledApp: boolean, installedAppId = '', last_id?: string, pinned?: boolean, limit?: number) => { - return getAction('get', isInstalledApp)(getUrl('conversations', isInstalledApp, installedAppId), { params: { limit: limit || 20, ...(last_id ? { last_id } : {}), ...(pinned !== undefined ? { pinned } : {}) } }) as Promise +export const fetchConversations = async (appSourceType: AppSourceType, installedAppId = '', last_id?: string, pinned?: boolean, limit?: number) => { + return getAction('get', appSourceType)(getUrl('conversations', appSourceType, installedAppId), { params: { limit: limit || 20, ...(last_id ? { last_id } : {}), ...(pinned !== undefined ? { pinned } : {}) } }) as Promise } -export const pinConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => { - return getAction('patch', isInstalledApp)(getUrl(`conversations/${id}/pin`, isInstalledApp, installedAppId)) +export const pinConversation = async (appSourceType: AppSourceType, installedAppId = '', id: string) => { + return getAction('patch', appSourceType)(getUrl(`conversations/${id}/pin`, appSourceType, installedAppId)) } -export const unpinConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => { - return getAction('patch', isInstalledApp)(getUrl(`conversations/${id}/unpin`, isInstalledApp, installedAppId)) +export const unpinConversation = async (appSourceType: AppSourceType, installedAppId = '', id: string) => { + return getAction('patch', appSourceType)(getUrl(`conversations/${id}/unpin`, appSourceType, installedAppId)) } -export const delConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => { - return getAction('del', isInstalledApp)(getUrl(`conversations/${id}`, isInstalledApp, installedAppId)) +export const delConversation = async (appSourceType: AppSourceType, installedAppId = '', id: string) => { + return getAction('del', appSourceType)(getUrl(`conversations/${id}`, appSourceType, installedAppId)) } -export const renameConversation = async (isInstalledApp: boolean, installedAppId = '', id: string, name: string) => { - return getAction('post', isInstalledApp)(getUrl(`conversations/${id}/name`, isInstalledApp, installedAppId), { body: { name } }) +export const renameConversation = async (appSourceType: AppSourceType, installedAppId = '', id: string, name: string) => { + return getAction('post', appSourceType)(getUrl(`conversations/${id}/name`, appSourceType, installedAppId), { body: { name } }) } -export const generationConversationName = async (isInstalledApp: boolean, installedAppId = '', id: string) => { - return getAction('post', isInstalledApp)(getUrl(`conversations/${id}/name`, isInstalledApp, installedAppId), { body: { auto_generate: true } }) as Promise +export const generationConversationName = async (appSourceType: AppSourceType, installedAppId = '', id: string) => { + return getAction('post', appSourceType)(getUrl(`conversations/${id}/name`, appSourceType, installedAppId), { body: { auto_generate: true } }) as Promise } -export const fetchChatList = async (conversationId: string, isInstalledApp: boolean, installedAppId = '') => { - return getAction('get', isInstalledApp)(getUrl('messages', isInstalledApp, installedAppId), { params: { conversation_id: conversationId, limit: 20, last_id: '' } }) as any +export const fetchChatList = async (conversationId: string, appSourceType: AppSourceType, installedAppId = '') => { + return getAction('get', appSourceType)(getUrl('messages', appSourceType, installedAppId), { params: { conversation_id: conversationId, limit: 20, last_id: '' } }) as any } // Abandoned API interface @@ -198,12 +191,12 @@ export const fetchChatList = async (conversationId: string, isInstalledApp: bool // } // init value. wait for server update -export const fetchAppParams = async (isInstalledApp: boolean, installedAppId = '') => { - return (getAction('get', isInstalledApp))(getUrl('parameters', isInstalledApp, installedAppId)) as Promise +export const fetchAppParams = async (appSourceType: AppSourceType, appId = '') => { + return (getAction('get', appSourceType))(getUrl('parameters', appSourceType, appId)) as Promise } export const fetchWebSAMLSSOUrl = async (appCode: string, redirectUrl: string) => { - return (getAction('get', false))(getUrl('/enterprise/sso/saml/login', false, ''), { + return (getAction('get', AppSourceType.webApp))(getUrl('/enterprise/sso/saml/login', AppSourceType.webApp, ''), { params: { app_code: appCode, redirect_url: redirectUrl, @@ -212,7 +205,7 @@ export const fetchWebSAMLSSOUrl = async (appCode: string, redirectUrl: string) = } export const fetchWebOIDCSSOUrl = async (appCode: string, redirectUrl: string) => { - return (getAction('get', false))(getUrl('/enterprise/sso/oidc/login', false, ''), { + return (getAction('get', AppSourceType.webApp))(getUrl('/enterprise/sso/oidc/login', AppSourceType.webApp, ''), { params: { app_code: appCode, redirect_url: redirectUrl, @@ -222,7 +215,7 @@ export const fetchWebOIDCSSOUrl = async (appCode: string, redirectUrl: string) = } export const fetchWebOAuth2SSOUrl = async (appCode: string, redirectUrl: string) => { - return (getAction('get', false))(getUrl('/enterprise/sso/oauth2/login', false, ''), { + return (getAction('get', AppSourceType.webApp))(getUrl('/enterprise/sso/oauth2/login', AppSourceType.webApp, ''), { params: { app_code: appCode, redirect_url: redirectUrl, @@ -231,7 +224,7 @@ export const fetchWebOAuth2SSOUrl = async (appCode: string, redirectUrl: string) } export const fetchMembersSAMLSSOUrl = async (appCode: string, redirectUrl: string) => { - return (getAction('get', false))(getUrl('/enterprise/sso/members/saml/login', false, ''), { + return (getAction('get', AppSourceType.webApp))(getUrl('/enterprise/sso/members/saml/login', AppSourceType.webApp, ''), { params: { app_code: appCode, redirect_url: redirectUrl, @@ -240,7 +233,7 @@ export const fetchMembersSAMLSSOUrl = async (appCode: string, redirectUrl: strin } export const fetchMembersOIDCSSOUrl = async (appCode: string, redirectUrl: string) => { - return (getAction('get', false))(getUrl('/enterprise/sso/members/oidc/login', false, ''), { + return (getAction('get', AppSourceType.webApp))(getUrl('/enterprise/sso/members/oidc/login', AppSourceType.webApp, ''), { params: { app_code: appCode, redirect_url: redirectUrl, @@ -250,7 +243,7 @@ export const fetchMembersOIDCSSOUrl = async (appCode: string, redirectUrl: strin } export const fetchMembersOAuth2SSOUrl = async (appCode: string, redirectUrl: string) => { - return (getAction('get', false))(getUrl('/enterprise/sso/members/oauth2/login', false, ''), { + return (getAction('get', AppSourceType.webApp))(getUrl('/enterprise/sso/members/oauth2/login', AppSourceType.webApp, ''), { params: { app_code: appCode, redirect_url: redirectUrl, @@ -258,48 +251,50 @@ export const fetchMembersOAuth2SSOUrl = async (appCode: string, redirectUrl: str }) as Promise<{ url: string }> } -export const fetchAppMeta = async (isInstalledApp: boolean, installedAppId = '') => { - return (getAction('get', isInstalledApp))(getUrl('meta', isInstalledApp, installedAppId)) as Promise +export const fetchAppMeta = async (appSourceType: AppSourceType, installedAppId = '') => { + return (getAction('get', appSourceType))(getUrl('meta', appSourceType, installedAppId)) as Promise } -export const updateFeedback = async ({ url, body }: { url: string, body: FeedbackType }, isInstalledApp: boolean, installedAppId = '') => { - return (getAction('post', isInstalledApp))(getUrl(url, isInstalledApp, installedAppId), { body }) +export const updateFeedback = async ({ url, body }: { url: string, body: FeedbackType }, appSourceType: AppSourceType, installedAppId = '') => { + return (getAction('post', appSourceType))(getUrl(url, appSourceType, installedAppId), { body }) } -export const fetchMoreLikeThis = async (messageId: string, isInstalledApp: boolean, installedAppId = '') => { - return (getAction('get', isInstalledApp))(getUrl(`/messages/${messageId}/more-like-this`, isInstalledApp, installedAppId), { +export const fetchMoreLikeThis = async (messageId: string, appSourceType: AppSourceType, installedAppId = '') => { + return (getAction('get', appSourceType))(getUrl(`/messages/${messageId}/more-like-this`, appSourceType, installedAppId), { params: { response_mode: 'blocking', }, }) } -export const saveMessage = (messageId: string, isInstalledApp: boolean, installedAppId = '') => { - return (getAction('post', isInstalledApp))(getUrl('/saved-messages', isInstalledApp, installedAppId), { body: { message_id: messageId } }) +export const saveMessage = (messageId: string, appSourceType: AppSourceType, installedAppId = '') => { + return (getAction('post', appSourceType))(getUrl('/saved-messages', appSourceType, installedAppId), { body: { message_id: messageId } }) } -export const fetchSavedMessage = async (isInstalledApp: boolean, installedAppId = '') => { - return (getAction('get', isInstalledApp))(getUrl('/saved-messages', isInstalledApp, installedAppId)) +export const fetchSavedMessage = async (appSourceType: AppSourceType, installedAppId = '') => { + return (getAction('get', appSourceType))(getUrl('/saved-messages', appSourceType, installedAppId), {}, { + silent: true, + }) } -export const removeMessage = (messageId: string, isInstalledApp: boolean, installedAppId = '') => { - return (getAction('del', isInstalledApp))(getUrl(`/saved-messages/${messageId}`, isInstalledApp, installedAppId)) +export const removeMessage = (messageId: string, appSourceType: AppSourceType, installedAppId = '') => { + return (getAction('del', appSourceType))(getUrl(`/saved-messages/${messageId}`, appSourceType, installedAppId)) } -export const fetchSuggestedQuestions = (messageId: string, isInstalledApp: boolean, installedAppId = '') => { - return (getAction('get', isInstalledApp))(getUrl(`/messages/${messageId}/suggested-questions`, isInstalledApp, installedAppId)) +export const fetchSuggestedQuestions = (messageId: string, appSourceType: AppSourceType, installedAppId = '') => { + return (getAction('get', appSourceType))(getUrl(`/messages/${messageId}/suggested-questions`, appSourceType, installedAppId)) } -export const audioToText = (url: string, isPublicAPI: boolean, body: FormData) => { - return (getAction('post', !isPublicAPI))(url, { body }, { bodyStringify: false, deleteContentType: true }) as Promise<{ text: string }> +export const audioToText = (url: string, appSourceType: AppSourceType, body: FormData) => { + return (getAction('post', appSourceType))(url, { body }, { bodyStringify: false, deleteContentType: true }) as Promise<{ text: string }> } -export const textToAudio = (url: string, isPublicAPI: boolean, body: FormData) => { - return (getAction('post', !isPublicAPI))(url, { body }, { bodyStringify: false, deleteContentType: true }) as Promise<{ data: string }> +export const textToAudioStream = (url: string, appSourceType: AppSourceType, header: { content_type: string }, body: { streaming: boolean, voice?: string, message_id?: string, text?: string | null | undefined }) => { + return (getAction('post', appSourceType))(url, { body, header }, { needAllResponseContent: true }) } -export const textToAudioStream = (url: string, isPublicAPI: boolean, header: { content_type: string }, body: { streaming: boolean, voice?: string, message_id?: string, text?: string | null | undefined }) => { - return (getAction('post', !isPublicAPI))(url, { body, header }, { needAllResponseContent: true }) +export const textToAudio = (url: string, appSourceType: AppSourceType, body: FormData) => { + return (getAction('post', appSourceType))(url, { body }, { bodyStringify: false, deleteContentType: true }) as Promise<{ data: string }> } export const fetchAccessToken = async ({ userId, appCode }: { userId?: string, appCode: string }) => { diff --git a/web/service/try-app.ts b/web/service/try-app.ts new file mode 100644 index 0000000000..c1bf79a74a --- /dev/null +++ b/web/service/try-app.ts @@ -0,0 +1,41 @@ +import type { Viewport } from 'reactflow' +import type { Edge, Node } from '@/app/components/workflow/types' +import type { DataSetListResponse } from '@/models/datasets' +import type { + SiteInfo, +} from '@/models/share' +import type { AppModeEnum, ModelConfig } from '@/types/app' +import qs from 'qs' +import { + get, +} from './base' + +export type TryAppInfo = { + name: string + description: string + mode: AppModeEnum + site: SiteInfo + model_config: ModelConfig + deleted_tools: any[] +} + +export const fetchTryAppInfo = async (appId: string) => { + return get(`/trial-apps/${appId}`) as Promise +} + +export const fetchTryAppDatasets = (appId: string, ids: string[]) => { + const urlParams = qs.stringify({ ids }, { indices: false }) + return get(`/trial-apps/${appId}/datasets?${urlParams}`) +} + +type TryAppFlowPreview = { + graph: { + nodes: Node[] + edges: Edge[] + viewport: Viewport + } +} + +export const fetchTryAppFlowPreview = (appId: string) => { + return get(`/trial-apps/${appId}/workflows`) +} diff --git a/web/service/use-explore.ts b/web/service/use-explore.ts index a15b926306..3e3b9ff255 100644 --- a/web/service/use-explore.ts +++ b/web/service/use-explore.ts @@ -2,8 +2,8 @@ import type { App, AppCategory } from '@/models/explore' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useGlobalPublicStore } from '@/context/global-public-context' import { AccessMode } from '@/models/access-control' -import { fetchAppList, fetchInstalledAppList, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore' -import { fetchAppMeta, fetchAppParams } from './share' +import { fetchAppList, fetchBanners, fetchInstalledAppList, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore' +import { AppSourceType, fetchAppMeta, fetchAppParams } from './share' const NAME_SPACE = 'explore' @@ -81,7 +81,7 @@ export const useGetInstalledAppParams = (appId: string | null) => { queryFn: () => { if (!appId || appId.length === 0) return Promise.reject(new Error('App ID is required to get app params')) - return fetchAppParams(true, appId) + return fetchAppParams(AppSourceType.installedApp, appId) }, enabled: !!appId, }) @@ -93,8 +93,17 @@ export const useGetInstalledAppMeta = (appId: string | null) => { queryFn: () => { if (!appId || appId.length === 0) return Promise.reject(new Error('App ID is required to get app meta')) - return fetchAppMeta(true, appId) + return fetchAppMeta(AppSourceType.installedApp, appId) }, enabled: !!appId, }) } + +export const useGetBanners = (locale?: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'banners', locale], + queryFn: () => { + return fetchBanners(locale) + }, + }) +} diff --git a/web/service/use-share.spec.tsx b/web/service/use-share.spec.tsx index db20329767..5b1501bead 100644 --- a/web/service/use-share.spec.tsx +++ b/web/service/use-share.spec.tsx @@ -3,6 +3,7 @@ import type { AppConversationData, ConversationItem } from '@/models/share' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, renderHook, waitFor } from '@testing-library/react' import { + AppSourceType, fetchChatList, fetchConversations, generationConversationName, @@ -80,6 +81,7 @@ describe('useShareConversations', () => { appId: undefined, pinned: true, limit: 50, + appSourceType: AppSourceType.webApp, } const response = createConversationData() mockFetchConversations.mockResolvedValueOnce(response) @@ -102,6 +104,7 @@ describe('useShareConversations', () => { const params = { isInstalledApp: true, appId: undefined, + appSourceType: AppSourceType.installedApp, } // Act @@ -127,6 +130,7 @@ describe('useShareChatList', () => { conversationId: 'conversation-1', isInstalledApp: true, appId: 'app-1', + appSourceType: AppSourceType.installedApp, } const response = { data: [] } mockFetchChatList.mockResolvedValueOnce(response) @@ -149,6 +153,7 @@ describe('useShareChatList', () => { conversationId: '', isInstalledApp: false, appId: undefined, + appSourceType: AppSourceType.webApp, } // Act @@ -171,6 +176,7 @@ describe('useShareChatList', () => { conversationId: 'conversation-1', isInstalledApp: false, appId: undefined, + appSourceType: AppSourceType.webApp, } const initialResponse = { data: [{ id: '1', content: 'initial' }] } const updatedResponse = { data: [{ id: '1', content: 'initial' }, { id: '2', content: 'new message' }] } @@ -219,6 +225,7 @@ describe('useShareConversationName', () => { conversationId: 'conversation-2', isInstalledApp: false, appId: undefined, + appSourceType: AppSourceType.webApp, } const response = createConversationItem({ id: 'conversation-2', name: 'Generated' }) mockGenerationConversationName.mockResolvedValueOnce(response) @@ -241,6 +248,7 @@ describe('useShareConversationName', () => { conversationId: 'conversation-3', isInstalledApp: false, appId: undefined, + appSourceType: AppSourceType.webApp, } // Act diff --git a/web/service/use-share.ts b/web/service/use-share.ts index eef61ccc29..cb99525a78 100644 --- a/web/service/use-share.ts +++ b/web/service/use-share.ts @@ -1,6 +1,7 @@ import type { AppConversationData, ConversationItem } from '@/models/share' import { useQuery } from '@tanstack/react-query' import { + AppSourceType, fetchAppInfo, fetchAppMeta, fetchAppParams, @@ -14,7 +15,7 @@ import { useInvalid } from './use-base' const NAME_SPACE = 'webapp' type ShareConversationsParams = { - isInstalledApp: boolean + appSourceType: AppSourceType appId?: string lastId?: string pinned?: boolean @@ -23,13 +24,13 @@ type ShareConversationsParams = { type ShareChatListParams = { conversationId: string - isInstalledApp: boolean + appSourceType: AppSourceType appId?: string } type ShareConversationNameParams = { conversationId: string - isInstalledApp: boolean + appSourceType: AppSourceType appId?: string } @@ -73,7 +74,7 @@ export const useGetWebAppParams = () => { return useQuery({ queryKey: shareQueryKeys.appParams, queryFn: () => { - return fetchAppParams(false) + return fetchAppParams(AppSourceType.webApp) }, }) } @@ -82,7 +83,7 @@ export const useGetWebAppMeta = () => { return useQuery({ queryKey: shareQueryKeys.appMeta, queryFn: () => { - return fetchAppMeta(false) + return fetchAppMeta(AppSourceType.webApp) }, }) } @@ -93,11 +94,11 @@ export const useShareConversations = (params: ShareConversationsParams, options: refetchOnReconnect, refetchOnWindowFocus, } = options - const isEnabled = enabled && (!params.isInstalledApp || !!params.appId) + const isEnabled = enabled && params.appSourceType !== AppSourceType.tryApp && (params.appSourceType !== AppSourceType.installedApp || !!params.appId) return useQuery({ queryKey: shareQueryKeys.conversationList(params), queryFn: () => fetchConversations( - params.isInstalledApp, + params.appSourceType, params.appId, params.lastId, params.pinned, @@ -115,10 +116,10 @@ export const useShareChatList = (params: ShareChatListParams, options: ShareQuer refetchOnReconnect, refetchOnWindowFocus, } = options - const isEnabled = enabled && (!params.isInstalledApp || !!params.appId) && !!params.conversationId + const isEnabled = enabled && params.appSourceType !== AppSourceType.tryApp && (params.appSourceType !== AppSourceType.installedApp || !!params.appId) && !!params.conversationId return useQuery({ queryKey: shareQueryKeys.chatList(params), - queryFn: () => fetchChatList(params.conversationId, params.isInstalledApp, params.appId), + queryFn: () => fetchChatList(params.conversationId, params.appSourceType, params.appId), enabled: isEnabled, refetchOnReconnect, refetchOnWindowFocus, @@ -135,10 +136,10 @@ export const useShareConversationName = (params: ShareConversationNameParams, op refetchOnReconnect, refetchOnWindowFocus, } = options - const isEnabled = enabled && (!params.isInstalledApp || !!params.appId) && !!params.conversationId + const isEnabled = enabled && (params.appSourceType !== AppSourceType.installedApp || !!params.appId) && !!params.conversationId return useQuery({ queryKey: shareQueryKeys.conversationName(params), - queryFn: () => generationConversationName(params.isInstalledApp, params.appId, params.conversationId), + queryFn: () => generationConversationName(params.appSourceType, params.appId, params.conversationId), enabled: isEnabled, refetchOnReconnect, refetchOnWindowFocus, diff --git a/web/service/use-try-app.ts b/web/service/use-try-app.ts new file mode 100644 index 0000000000..a2f170bff7 --- /dev/null +++ b/web/service/use-try-app.ts @@ -0,0 +1,46 @@ +import type { DataSetListResponse } from '@/models/datasets' +import { useQuery } from '@tanstack/react-query' +import { AppSourceType, fetchAppParams } from './share' +import { fetchTryAppDatasets, fetchTryAppFlowPreview, fetchTryAppInfo } from './try-app' + +const NAME_SPACE = 'try-app' + +export const useGetTryAppInfo = (appId: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'appInfo', appId], + queryFn: () => { + return fetchTryAppInfo(appId) + }, + enabled: !!appId, + }) +} + +export const useGetTryAppParams = (appId: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'appParams', appId], + queryFn: () => { + return fetchAppParams(AppSourceType.tryApp, appId) + }, + enabled: !!appId, + }) +} + +export const useGetTryAppDataSets = (appId: string, ids: string[]) => { + return useQuery({ + queryKey: [NAME_SPACE, 'dataSets', appId, ids], + queryFn: () => { + return fetchTryAppDatasets(appId, ids) + }, + enabled: ids.length > 0, + }) +} + +export const useGetTryAppFlowPreview = (appId: string, disabled?: boolean) => { + return useQuery({ + queryKey: [NAME_SPACE, 'preview', appId], + enabled: !disabled, + queryFn: () => { + return fetchTryAppFlowPreview(appId) + }, + }) +} diff --git a/web/types/feature.ts b/web/types/feature.ts index bd331d4508..9dd2c694d2 100644 --- a/web/types/feature.ts +++ b/web/types/feature.ts @@ -59,6 +59,8 @@ export type SystemFeatures = { allow_email_code_login: boolean allow_email_password_login: boolean } + enable_trial_app: boolean + enable_explore_banner: boolean } export const defaultSystemFeatures: SystemFeatures = { @@ -98,6 +100,8 @@ export const defaultSystemFeatures: SystemFeatures = { allow_email_code_login: false, allow_email_password_login: false, }, + enable_trial_app: false, + enable_explore_banner: false, } export enum DatasetAttr { diff --git a/web/utils/client.ts b/web/utils/client.ts new file mode 100644 index 0000000000..5c4532997b --- /dev/null +++ b/web/utils/client.ts @@ -0,0 +1,3 @@ +export const isServer = typeof window === 'undefined' + +export const isClient = typeof window !== 'undefined' diff --git a/web/utils/gtag.ts b/web/utils/gtag.ts index 5af51a6564..5f199f1fbc 100644 --- a/web/utils/gtag.ts +++ b/web/utils/gtag.ts @@ -1,3 +1,5 @@ +import { isServer } from '@/utils/client' + /** * Send Google Analytics event * @param eventName - event name @@ -7,7 +9,7 @@ export const sendGAEvent = ( eventName: string, eventParams?: GtagEventParams, ): void => { - if (typeof window === 'undefined' || typeof (window as any).gtag !== 'function') { + if (isServer || typeof (window as any).gtag !== 'function') { return } (window as any).gtag('event', eventName, eventParams)