Compare commits
756 Commits
test/cli-e
...
feat/agent
| Author | SHA1 | Date | |
|---|---|---|---|
| c03fc9bb1e | |||
| 2a86ba9882 | |||
| 1427b0b098 | |||
| d55f7e66da | |||
| be664f9b08 | |||
| 2893adf5e4 | |||
| eb2aaf2ac1 | |||
| 84a77fa0f1 | |||
| 24ee393b91 | |||
| 9a6325a972 | |||
| 0e4878b8ee | |||
| cd877ed430 | |||
| cb34ed6bc0 | |||
| 05597a71a2 | |||
| 3392f5187d | |||
| 10c7d62853 | |||
| 48e22f9b2a | |||
| 959b6f23ba | |||
| f5a9de8866 | |||
| ec55804ea2 | |||
| 5dcaa458ce | |||
| 06f2ea7056 | |||
| 56f17ed3f7 | |||
| 0f0b0a017f | |||
| 2d1da4c274 | |||
| 43a8d49c87 | |||
| a987714097 | |||
| 4220c305a1 | |||
| b3ba2b5c43 | |||
| d04dfe44df | |||
| 7b4978be74 | |||
| c4c5b1231f | |||
| c860620e07 | |||
| 88f80980a7 | |||
| 97328278fc | |||
| e909f3b3b9 | |||
| a093e75735 | |||
| f47f4c5316 | |||
| 684441b50a | |||
| 012c9753e1 | |||
| 2483106744 | |||
| 8ab663da86 | |||
| 07ac1619b7 | |||
| 3f35f6e8e2 | |||
| b613cc19a3 | |||
| 0b03e72421 | |||
| 61af7de49d | |||
| ebb1b01bc8 | |||
| 649ef315d1 | |||
| 8076f0e36c | |||
| aee88de232 | |||
| aeb54da4b3 | |||
| edf79cb70e | |||
| 2f7a484464 | |||
| d03ea65e83 | |||
| 07f4f95ffc | |||
| cbd7f904f8 | |||
| fc97a587c5 | |||
| 75321dd8e1 | |||
| 6d8917db0c | |||
| ee87d968c3 | |||
| 62fc2da5d1 | |||
| 1acf17c9ce | |||
| d4656a1770 | |||
| aa2ef0c6b6 | |||
| 3472f2ade6 | |||
| 474d09b7c0 | |||
| d5f28e77a0 | |||
| bc204417b4 | |||
| 0565ed3bb2 | |||
| f571affb5c | |||
| dcfa15049b | |||
| bea319a6a7 | |||
| 723c9902b6 | |||
| f639da357f | |||
| 8a24750117 | |||
| 7feaed7b0b | |||
| ae2b12fef8 | |||
| b46dac0416 | |||
| c69fcb0110 | |||
| 2e1fa86950 | |||
| 4a87e17692 | |||
| c2f43a2720 | |||
| 34826f5748 | |||
| cb903ec0c0 | |||
| 61c61fe9d6 | |||
| 9b7d8a19ae | |||
| 272f11896f | |||
| 5a8485399b | |||
| 1f7badef07 | |||
| 31045cd229 | |||
| f6768a9b0c | |||
| 920c61197d | |||
| bfa7504240 | |||
| 027bdfb092 | |||
| be3dfeba1b | |||
| dabe29fd4f | |||
| 0007595320 | |||
| f4ffb9fc10 | |||
| 8617836e0c | |||
| 4f30d9bd92 | |||
| ae726a0607 | |||
| 6f2bd7c2c6 | |||
| 49336e2dbc | |||
| 9e8a0f0d34 | |||
| 04bde3bc8d | |||
| ca8ac4fed9 | |||
| 7009cd8060 | |||
| f0e22be4e8 | |||
| 5b808e3cb8 | |||
| 65cb1a8505 | |||
| 3c719a96ea | |||
| 32fd48b58f | |||
| 76ad1d58cd | |||
| c47381777d | |||
| e485e02b4f | |||
| 5a56b5158d | |||
| 05e929bf70 | |||
| fd49fa3653 | |||
| c587ca8120 | |||
| 0c849380a0 | |||
| 271c0f7ca5 | |||
| 6158974646 | |||
| 9f8be0a002 | |||
| 7d1eec789b | |||
| d02b478a2f | |||
| 6fe59c4a29 | |||
| b8216523ad | |||
| cf54e72689 | |||
| 62bcc01d35 | |||
| c17452bff4 | |||
| a15df89c2b | |||
| a2187ad4a2 | |||
| ea8a058179 | |||
| b9def65839 | |||
| a0e64a679e | |||
| ca765e0aea | |||
| 6a031d7f58 | |||
| c17d5744b0 | |||
| b662187f6f | |||
| 4eea58b613 | |||
| 79c8491588 | |||
| 68a9bf1d2e | |||
| 7e37100c2c | |||
| a126adf78c | |||
| b79c9d6dd5 | |||
| d282f7a7d4 | |||
| 6ac486b862 | |||
| f75870fe0f | |||
| 586fe488b1 | |||
| 1d9e379269 | |||
| bb63d1ee99 | |||
| e40e79f615 | |||
| 0bbc408607 | |||
| e7d564302d | |||
| 6ecfdf4206 | |||
| 3472475eac | |||
| 71296b983c | |||
| adbcf8774c | |||
| bdf66cd87a | |||
| 2aa244f538 | |||
| f4057f1304 | |||
| e17f7d50a4 | |||
| b4305192c4 | |||
| 54d08be1ea | |||
| b1c5c3d73b | |||
| bef5cea422 | |||
| 8d3f772cd6 | |||
| d8eae55b21 | |||
| 795e95726c | |||
| 7ca985c7b3 | |||
| ed0dbb8c02 | |||
| 2fa705b5a6 | |||
| fb9cc6f4af | |||
| 5564c97408 | |||
| fcb38cf717 | |||
| c3c18ef579 | |||
| 03ab30542b | |||
| 1b518a05b7 | |||
| 77c9ee308b | |||
| ed5ad808b6 | |||
| c537aa393d | |||
| b8b51c7123 | |||
| 7780c91e81 | |||
| e87f714792 | |||
| 02a4e53e81 | |||
| 4cf25f73c4 | |||
| 0b1b3af7b1 | |||
| 4669ef1060 | |||
| e749e3f129 | |||
| c19df018e8 | |||
| da05973852 | |||
| d590246d6b | |||
| 6c698b3969 | |||
| 7fff3d914d | |||
| 9a0ca09a6c | |||
| 404dacae8a | |||
| 487cad9a57 | |||
| 6c30fafc4f | |||
| 2471fcd3d1 | |||
| 1fc8185673 | |||
| e4118eee84 | |||
| eda00f0a19 | |||
| 88f54bfbd4 | |||
| f6277a08c0 | |||
| 2e6cab3b65 | |||
| c386bd3f91 | |||
| 2858cbaf4e | |||
| 2a1d3311c6 | |||
| 8e508ba903 | |||
| fae725017e | |||
| 34010ebed8 | |||
| ea96663246 | |||
| 0ab52daed3 | |||
| 757d83c17f | |||
| a44d85d473 | |||
| 6a50e11461 | |||
| fdffff8c91 | |||
| 4c47c186b8 | |||
| 964377a935 | |||
| cafa12d875 | |||
| 5f078668bd | |||
| d76d5b87cb | |||
| 1f23aed10b | |||
| dda9dacad3 | |||
| 022b0b77bc | |||
| e6e7770cd3 | |||
| bf1b3de6bd | |||
| 8d8755652d | |||
| 19aa23611b | |||
| 7fe46e0628 | |||
| 1a1115df43 | |||
| cc76ad8f89 | |||
| e8140c7ada | |||
| 1b8753f04d | |||
| dd92acf6da | |||
| e7522ee3ff | |||
| 5bab70d030 | |||
| 6612cc5efe | |||
| 9ed97c9c61 | |||
| a42c803445 | |||
| 2f0e302276 | |||
| 43bffeca39 | |||
| 7f348aa7c6 | |||
| a7fe7536f3 | |||
| c147159c43 | |||
| 4fcf969e31 | |||
| a82afc3e5a | |||
| c23ae4bd3b | |||
| 87078de5c5 | |||
| 3ed34298de | |||
| 5774137b98 | |||
| 5849f25fb4 | |||
| 387f11fcb0 | |||
| a054c30d22 | |||
| 79485323cf | |||
| 063f4a40c5 | |||
| 3ba8944d32 | |||
| a064bc6439 | |||
| 22cd164d42 | |||
| 625a459144 | |||
| 432e161c65 | |||
| b6014441eb | |||
| 0aeff4d058 | |||
| 6e9270210e | |||
| 6df51751aa | |||
| c1b3fa3aed | |||
| 860dc57620 | |||
| 91f551d660 | |||
| b03549ef6a | |||
| 7fd9df1cd5 | |||
| 01e6457cb2 | |||
| 700fffc216 | |||
| 6f27c8ecb9 | |||
| 54d3aa8ecb | |||
| eff00899f7 | |||
| c486042ed4 | |||
| dd0e6f75fa | |||
| 3a20cf46af | |||
| c15583bcb2 | |||
| 35b63f466a | |||
| 5b1c90b28e | |||
| 5174ebff77 | |||
| 6c364a573a | |||
| 1c63822a08 | |||
| af4766b9d9 | |||
| 595f341c56 | |||
| 88b2b56e84 | |||
| b598949a2c | |||
| 45cf171ef4 | |||
| 6e9613f5b7 | |||
| ca2a3ac141 | |||
| 96124f6c9f | |||
| 0f1ad29efb | |||
| c2533a1833 | |||
| e17dc405c1 | |||
| 524682873e | |||
| d626048fd6 | |||
| 6eb97610f0 | |||
| 71f94c1d15 | |||
| 329d744942 | |||
| ab956f7870 | |||
| 1d2b603383 | |||
| a2e2c840c3 | |||
| 5446b89054 | |||
| 0fe6e910f8 | |||
| c6a0ce5f4d | |||
| 140c2cc3e0 | |||
| eb2521082c | |||
| deda62dd94 | |||
| 543b9a454e | |||
| b71010beb3 | |||
| 16b4f36635 | |||
| 63a865220c | |||
| 1967624b82 | |||
| cbb8bd29cf | |||
| a7d8386583 | |||
| 47d790d4eb | |||
| 60110a57e8 | |||
| 4e53eb5aaf | |||
| 6cc5514f64 | |||
| 44fd4b1b2f | |||
| 4743cf4e74 | |||
| c74aece481 | |||
| 9b2d06182b | |||
| 560e15a836 | |||
| 8fe9be11e9 | |||
| 8d59634903 | |||
| 16f85ef709 | |||
| 35f53e9fbf | |||
| 83db975374 | |||
| 18efc8ed88 | |||
| 46e1a5a6db | |||
| a2267276ae | |||
| 5619062ad1 | |||
| 38af34a742 | |||
| 1bf2748130 | |||
| a6084ca3c7 | |||
| d8b847dcf0 | |||
| 2509682e07 | |||
| 5582b35d56 | |||
| db83df9f9c | |||
| 0683c0e7a7 | |||
| 42b7bf8152 | |||
| bc33ef1b97 | |||
| 0f493a52a1 | |||
| fc7cd1100e | |||
| fc4f9db79a | |||
| e9b8a1606e | |||
| 453c4c4c5f | |||
| 58e25d0534 | |||
| d7b9f2a86b | |||
| 18d2423ed1 | |||
| 4176e7d146 | |||
| 3556611c2b | |||
| e37b4af0e8 | |||
| a0eb2ba0ff | |||
| 75f1094459 | |||
| f61f9634f8 | |||
| 56c569d6af | |||
| 11f78289d3 | |||
| 85e600e579 | |||
| 062341ab26 | |||
| c56e9813bb | |||
| 6ea9ba5926 | |||
| 7a9054fdea | |||
| 1820e6eab8 | |||
| 508baa782c | |||
| f5ee121d2c | |||
| 2bf60a67ad | |||
| f6433097ad | |||
| 23b1038b99 | |||
| 0dd2d36d68 | |||
| 786c7190f0 | |||
| ed10b82bb1 | |||
| ba19aee2bf | |||
| 41106ef6c9 | |||
| 5ee7bedb56 | |||
| f0bcb77d55 | |||
| 44a89bf870 | |||
| 60ad023553 | |||
| 9c6a7679ac | |||
| 11b2ba29c1 | |||
| c52df73117 | |||
| 9b8d81c852 | |||
| b28b2892f1 | |||
| 7f5349e707 | |||
| 0aaa1df1b8 | |||
| bd41c5c3c0 | |||
| 95a2eea611 | |||
| d959c73884 | |||
| 19193891cf | |||
| 4746dd39d3 | |||
| a5e6f58285 | |||
| 4bf0398873 | |||
| e6db98ef64 | |||
| 6b694ce829 | |||
| 0ab3b5d677 | |||
| 346fabda27 | |||
| 3da0f12815 | |||
| 303cff1353 | |||
| 10c3849887 | |||
| 29295950ea | |||
| c61a7ce442 | |||
| 92fdbd5c51 | |||
| 09053ab760 | |||
| c1afdc030c | |||
| ab383969a8 | |||
| 8a0aab4d81 | |||
| d389284813 | |||
| 530e366440 | |||
| e4510a7d8f | |||
| 5c2f4709a5 | |||
| 19d7a9b5d9 | |||
| 1d063e3fd6 | |||
| 850c7e311f | |||
| de0ccbb960 | |||
| 0493552c73 | |||
| 407d3e28bb | |||
| 8a51b3a296 | |||
| 8fc3882042 | |||
| 0ffb9667d4 | |||
| e7886c1bac | |||
| 41894ad182 | |||
| 95ea709c91 | |||
| 3a01b91a45 | |||
| f50abac3f9 | |||
| 7f44b2f601 | |||
| 9583f17960 | |||
| c71d2ac460 | |||
| 505ad3994c | |||
| 87c6a82df0 | |||
| 0d97d44222 | |||
| 64f1d125a6 | |||
| 090ef21881 | |||
| 56ed953e2a | |||
| f61f15371a | |||
| 1a7542052e | |||
| 009c6adc8f | |||
| 6d26f6ea73 | |||
| 35e21de9f8 | |||
| 8c925b1422 | |||
| b0d3347de9 | |||
| bf2a35bff8 | |||
| 63d50a61d3 | |||
| 101a6fcc53 | |||
| d407c1fbf7 | |||
| fe6ee8aa04 | |||
| 25228e3cde | |||
| 8049da9331 | |||
| e404195c8c | |||
| b3c7110768 | |||
| 7b2b21c348 | |||
| 3d3a1f4f90 | |||
| 274944f05e | |||
| a0d53b9f07 | |||
| a24d1a0bbc | |||
| fffa89a10e | |||
| 422461f360 | |||
| 2ce913aaac | |||
| 30f0a69fea | |||
| 04e8c6127f | |||
| 20e37a0457 | |||
| 36c7209301 | |||
| ccc9122980 | |||
| 9143d44ec6 | |||
| 1f856960f0 | |||
| a8218d5809 | |||
| 70ec17bc34 | |||
| e4ea9d2e07 | |||
| fd342ccac0 | |||
| 929a1da26c | |||
| b72bfe060d | |||
| dfc7f136ef | |||
| 15d66814ee | |||
| 5f109213a5 | |||
| a53828e826 | |||
| 3a2ea826ff | |||
| 4581ce2f45 | |||
| 3925b2bf4f | |||
| 5744148e6d | |||
| 6ba42e6d73 | |||
| aff22cb5ed | |||
| 58c4a174ba | |||
| c98f65cbcb | |||
| add7c75f18 | |||
| 75e74ee8b9 | |||
| 40a5236553 | |||
| f820813e9f | |||
| c4c3a2b265 | |||
| 99be8b34c8 | |||
| 777265d898 | |||
| 7024913866 | |||
| 3c862c3e98 | |||
| fb497c60dd | |||
| 075af9cd44 | |||
| 18e07cac9a | |||
| 284c1027da | |||
| ba06ed5f41 | |||
| 3fd9d5eb14 | |||
| a70e8eb2b5 | |||
| 15d2714e9d | |||
| f36852646f | |||
| 72c3cd0d67 | |||
| fd0cb47a81 | |||
| 87382673f3 | |||
| 3c716a6eee | |||
| 8da34fb60b | |||
| f0efb73fd0 | |||
| e2bd2355e9 | |||
| 73b50f5ede | |||
| 52cfe62d8d | |||
| f293253a7a | |||
| 1ac3ad4c81 | |||
| 2d6fd70733 | |||
| 639c8d5967 | |||
| 74c9b7fddd | |||
| be997384f3 | |||
| acbea6701c | |||
| 86ac4dadd6 | |||
| f3e11ec0ee | |||
| c9c4fca7a7 | |||
| d23eddc924 | |||
| d65c7229f2 | |||
| 66508326f9 | |||
| 310f49229e | |||
| f400be4280 | |||
| 2855ab3a15 | |||
| e2fd5421d2 | |||
| ff37ba83b4 | |||
| 7b97ec57ef | |||
| 791296cc8d | |||
| 19d34b5a93 | |||
| 5c315ea7fe | |||
| e525880773 | |||
| d82e30561f | |||
| d2508db11f | |||
| 7f76fe68ea | |||
| 08bbd3bfdf | |||
| ac09702d08 | |||
| b14d1e60ec | |||
| b072136ca0 | |||
| c8b6ec5fb0 | |||
| 0b5b4271f0 | |||
| fd60339625 | |||
| f56f93f5c2 | |||
| 2921d27929 | |||
| 98e3bff509 | |||
| 6c43fa459e | |||
| d2eae74b5e | |||
| 87a3980c76 | |||
| 27bdee85fe | |||
| cca8295ad8 | |||
| f8cc85ce28 | |||
| 4d1b3605b5 | |||
| d94be05f68 | |||
| fea7590779 | |||
| 71ba903d4c | |||
| 44831839d1 | |||
| 88c3512471 | |||
| 67dee6e07e | |||
| 95d7fa997b | |||
| 2d4e494162 | |||
| 4a0b177eee | |||
| 15b2a8fdb7 | |||
| 65098a6b4f | |||
| 8055f8840c | |||
| f033f91a68 | |||
| 0b98319bd3 | |||
| 386de25e26 | |||
| d30805353a | |||
| 21c5825508 | |||
| 2d324add39 | |||
| 62beaf493e | |||
| b34e5aa915 | |||
| 2ea19c2b1c | |||
| 5712f29e8b | |||
| 93f7404c6b | |||
| 4f631d6f4c | |||
| 2b3e15cc83 | |||
| 5e1fac09bb | |||
| 35956247ab | |||
| 343531b9dc | |||
| 236f389fce | |||
| 5a2604265c | |||
| 6a8aaa5a36 | |||
| 0ad1e8c2d9 | |||
| 8c7540f698 | |||
| bf345136eb | |||
| c9ed50c3ae | |||
| c78c603a38 | |||
| 48b38446a3 | |||
| 8a8bec4bc6 | |||
| 89571bd241 | |||
| afee58cca7 | |||
| 76a55535f2 | |||
| 29cb993042 | |||
| 00581a4daa | |||
| bfc71bb087 | |||
| a95a6ea263 | |||
| 264e97a4c2 | |||
| 95936a8bac | |||
| ac8a1107ca | |||
| 43f67ef2d1 | |||
| c118fe9ad2 | |||
| 0c96426d91 | |||
| 67fee14770 | |||
| d94006162d | |||
| 3d53cee8a9 | |||
| 1acd1b568a | |||
| 68f939f3b3 | |||
| 1f4b76ba7e | |||
| 4d974d8f72 | |||
| 1dc12d1661 | |||
| 82345977cd | |||
| 83c943bc21 | |||
| 7e34e2347a | |||
| 94a376a5a7 | |||
| 33f6b0c9aa | |||
| 2b130d0d2a | |||
| 33d95ab23a | |||
| 7a8a92082b | |||
| 4f9adfb9ae | |||
| f3974d6176 | |||
| ef00f850e4 | |||
| cb2e404eb6 | |||
| 14e7fc87e4 | |||
| 40b4c3476d | |||
| 1c641d2b44 | |||
| c3c9a349cc | |||
| 169293c8da | |||
| 7815228395 | |||
| dcd40b5004 | |||
| bcc4b208c7 | |||
| c252006644 | |||
| 9e5668c233 | |||
| 52ce49b3c6 | |||
| e90aa76ba2 | |||
| de9373e1b8 | |||
| 58923f38e6 | |||
| 8486a5b213 | |||
| 28a8be0d5f | |||
| f2d4d5b267 | |||
| f62a59a18a | |||
| b488812714 | |||
| 755760b97c | |||
| 955c3fb797 | |||
| 0c9aa20047 | |||
| 065246a9a7 | |||
| 0d12b5ab1c | |||
| 514dcae189 | |||
| 228dd84a91 | |||
| 336ddad096 | |||
| 92bb9a17b7 | |||
| b8868dab90 | |||
| 94225682cd | |||
| 18b6568c2a | |||
| a3a9ded29b | |||
| de78a26920 | |||
| c54d029e7c | |||
| ad4b9dc2c3 | |||
| cdec0c69a6 | |||
| 53acc3726c | |||
| b1d393f4d9 | |||
| 62e9bdd70d | |||
| d36c76c20e | |||
| f525e1a5eb | |||
| e2f779b20d | |||
| e198d6305c | |||
| 5e67514265 | |||
| b63896de87 | |||
| e463389f2c | |||
| cda348ca10 | |||
| ca48050666 | |||
| 9c0f592f34 | |||
| b70241ad36 | |||
| 4abe622b2e | |||
| 16c32c82e3 | |||
| 46424513d1 | |||
| 2c4baa20d8 | |||
| b0ae553f2e | |||
| 0266a12ee5 | |||
| 9d7765d5fd | |||
| d4ef983f42 | |||
| 018f36711d | |||
| dacd333e4a | |||
| b079a26314 | |||
| 7e953ebe0b | |||
| b4d28fca54 | |||
| 728c6b8201 | |||
| f56e23b5fd | |||
| 5600cefa53 | |||
| 561eb9cbd2 | |||
| 83766ca694 | |||
| 678be94d22 | |||
| 9e852429be | |||
| d93c5028f1 | |||
| 54f189305e | |||
| a610a24507 | |||
| 05e8a94bb5 | |||
| b2e2e7b60b | |||
| e7d2e66ff5 | |||
| c51069685c | |||
| 28c208f36a | |||
| 53a1386b87 | |||
| 0e366c7300 | |||
| 939bdde373 | |||
| 13dfa3aba4 | |||
| 2705a7c1db | |||
| 258a751b8c | |||
| 5a35d3d9cd | |||
| c3fbafae83 | |||
| f727c8f838 | |||
| 90af4c39b4 | |||
| f7c3a4e4cb | |||
| be7d043edd | |||
| cef8fe3a4b | |||
| afe0e6c393 | |||
| 37309b931e | |||
| 6a83c6705c | |||
| 3e75d5e443 | |||
| 7be8a5b883 | |||
| 80dcb344f4 | |||
| b029c9b1cd | |||
| 6cb97e9201 | |||
| 4ef2e952bd | |||
| cc5545339c | |||
| 0a8c46a3a7 | |||
| 65770903d1 | |||
| 5a6ba2ffb5 | |||
| aa53afe07d | |||
| 4740a89f4a | |||
| 328db3d67a | |||
| 88062fb247 | |||
| 045da59220 | |||
| 948b0f6bc7 | |||
| 14a59f6e44 | |||
| f9f361113e | |||
| eea6f59307 | |||
| 718f69dc43 | |||
| 82a2ba9264 | |||
| 6c8e032fbb | |||
| 28c2c3bfd3 | |||
| 9d463e1024 | |||
| 7f87616625 | |||
| 43a04ed0c2 | |||
| 5083edd0ce | |||
| 8306fa41b9 | |||
| 8f33305e90 | |||
| 7077a43c1c | |||
| 884a43ae0a | |||
| 914f89f478 | |||
| 163153db18 | |||
| 49d890d514 | |||
| 0292bc2728 | |||
| 5c21120977 |
18
AGENTS.md
@ -44,3 +44,21 @@ The codebase is split into:
|
||||
- Backend architecture adheres to DDD and Clean Architecture principles.
|
||||
- Async work runs through Celery with Redis as the broker.
|
||||
- Frontend user-facing strings must use `web/i18n/en-US/`; avoid hardcoded text.
|
||||
|
||||
## Agent V2 Frontend Constraints
|
||||
|
||||
- Treat workflow Agent and Agent v2 as separate product surfaces. The existing workflow `agent` node is the legacy Old Agent and should keep its current strategy/plugin-based behavior. Do not refactor legacy Agent code to support Agent v2 unless explicitly requested.
|
||||
- New Agent work must use the Agent v2 surface and code path: `web/features/agent-v2`, `web/app/components/workflow/nodes/agent-v2`, `BlockEnum.AgentV2`, and the `agentV2` i18n namespace where applicable. Do not add new Agent behavior to legacy `web/app/components/workflow/nodes/agent`.
|
||||
- Do not mix, alias, or compatibility-bridge Old Agent and Agent v2 data shapes. Keep fields such as `agent_strategy_*` on legacy Agent only, and fields such as `agent_roster`, `agent_task`, and Agent v2 backend bindings on Agent v2 only.
|
||||
- Shared workflow utilities may branch on the explicit node type/discriminator when necessary, but they must preserve the boundary: legacy Agent behavior must not depend on Agent v2 data, and Agent v2 behavior must not fall back to legacy Agent strategy/plugin behavior.
|
||||
- For Agent v2 frontend work under `web/features/agent-v2`, use generated contracts and `consoleQuery` from `@/service/client` for all Agent v2 backend APIs. Do not add ad hoc REST helpers, mock data, compatibility shims, or handwritten API types for new Agent v2 interfaces.
|
||||
- Keep Agent v2 composer state split by responsibility: TanStack Query is the server source of truth, Jotai atoms hold only the editable composer draft, and local component state owns transient UI such as menus, dialogs, and expanded panels.
|
||||
- Wrap editable Agent v2 composer surfaces in an instance-level `AgentComposerProvider`. The provider is an editing-session boundary, not a data-fetching layer; use one store per agent/configure page or workflow node composer instance to avoid draft leakage.
|
||||
- Hydrate composer atoms from generated contract data by mapping the response into both `originalDraft` and `draft`. Compute dirty state from `draft` vs `originalDraft`; do not compare editable draft directly against live TanStack Query cache.
|
||||
- Keep mock or transitional composer defaults at the owning surface boundary, such as the configure page. Do not put page-specific mock data into shared `agent-composer` store defaults.
|
||||
- Use existing `@langgenius/dify-ui/*` primitives before adding feature-local UI chrome. Prefer primitive default styles; add call-site CSS only for real design deltas.
|
||||
- Prefer primitive data/CSS selectors for visual states instead of mirroring state in React only to choose classes.
|
||||
- Avoid arbitrary Tailwind values when an existing project utility or token class expresses the same value. Keep arbitrary values only for design-system exceptions without a native utility.
|
||||
- Preserve keyboard accessibility in Agent v2 pages: visible focus rings must not be clipped, and inert layout regions should not become keyboard focus targets.
|
||||
- Keep Agent v2 i18n scoped to the explicitly maintained locales unless the supported-locale scope changes.
|
||||
- Keep Agent v2 module copy in the `agentV2` namespace; use shared namespaces such as `common` only for genuinely shared operation labels.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""add identity mode to mcp tool provider
|
||||
|
||||
Revision ID: 3df4dbcc1e21
|
||||
Revises: 2b3c4d5e6f70
|
||||
Revises: c4d5e6f7a8b9
|
||||
Create Date: 2026-05-29 15:00:00.000000
|
||||
|
||||
Adds the `identity_mode` column to `tool_mcp_providers` to drive the M2 MCP
|
||||
@ -23,7 +23,7 @@ import models as models
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "3df4dbcc1e21"
|
||||
down_revision = "2b3c4d5e6f70"
|
||||
down_revision = "c4d5e6f7a8b9"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
@ -60,6 +60,7 @@ class ComposerSavePayload(BaseModel):
|
||||
|
||||
class RosterAgentCreatePayload(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
mode: Literal["agent"] = "agent"
|
||||
description: str = ""
|
||||
role: str = Field(default="", max_length=255)
|
||||
icon_type: AgentIconType | None = None
|
||||
|
||||
@ -215,169 +215,6 @@ describe('E2E / error message standards (spec 5.3)', () => {
|
||||
expect(result.stderr).not.toContain(sentValue)
|
||||
})
|
||||
|
||||
// ── 5.70d-j ErrorBody contract — error.server structure ─────────────────
|
||||
// The ErrorBody unification spec introduces a canonical error body:
|
||||
// { code, status, message, hint?, details?[{type, loc, msg}] }
|
||||
// On a canonical 422 the CLI attaches the full server object as error.server
|
||||
// in the JSON envelope and uses server?.code for the human-readable header.
|
||||
|
||||
it('[P0] 5.70d JSON envelope contains error.server with code, status, message on validation failure', async () => {
|
||||
// Spec: every canonical 422 from @accepts carries code:"invalid_param",
|
||||
// status:422 and message. The CLI attaches the parsed ErrorBody verbatim as
|
||||
// error.server — zero field copying so the contract is single-source.
|
||||
const result = await fx.r([
|
||||
'run',
|
||||
'app',
|
||||
E.workflowAppId,
|
||||
'--inputs',
|
||||
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
|
||||
'-o',
|
||||
'json',
|
||||
])
|
||||
assertNonZeroExit(result)
|
||||
const envelope = JSON.parse(result.stderr.trim()) as {
|
||||
error: {
|
||||
code: string
|
||||
server?: { code: string, status: number, message: string }
|
||||
}
|
||||
}
|
||||
expect(envelope.error.server, 'error.server must be present for canonical ErrorBody responses').toBeDefined()
|
||||
expect(envelope.error.server?.code).toBe('invalid_param')
|
||||
expect(envelope.error.server?.status).toBe(422)
|
||||
expect(typeof envelope.error.server?.message).toBe('string')
|
||||
expect(envelope.error.server?.message.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('[P1] 5.70e error.server.details array carries field-level error entries', async () => {
|
||||
// Spec: @accepts emits details:[{type, loc, msg}] for each failing field.
|
||||
// CLI forwards the array as-is inside error.server.details — no truncation.
|
||||
const result = await fx.r([
|
||||
'run',
|
||||
'app',
|
||||
E.workflowAppId,
|
||||
'--inputs',
|
||||
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
|
||||
'-o',
|
||||
'json',
|
||||
])
|
||||
assertNonZeroExit(result)
|
||||
const envelope = JSON.parse(result.stderr.trim()) as {
|
||||
error: {
|
||||
server?: {
|
||||
details?: Array<{ type: string, msg: string, loc?: Array<string | number> }>
|
||||
}
|
||||
}
|
||||
}
|
||||
const details = envelope.error.server?.details
|
||||
expect(Array.isArray(details), 'error.server.details must be an array').toBe(true)
|
||||
expect(details!.length, 'details must contain at least one entry').toBeGreaterThan(0)
|
||||
// Each entry must have type and msg; loc is optional but expected for body fields
|
||||
const entry = details![0]!
|
||||
expect(typeof entry.type, 'detail.type must be a string').toBe('string')
|
||||
expect(entry.type.length).toBeGreaterThan(0)
|
||||
expect(typeof entry.msg, 'detail.msg must be a string').toBe('string')
|
||||
expect(entry.msg.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('[P1] 5.70f human-readable text mode renders details as indented lines', async () => {
|
||||
// Spec: format.ts iterates server?.details and renders each entry as
|
||||
// " - <loc>: <msg> (<type>)"
|
||||
// This means field-level errors are visible without -v.
|
||||
const result = await fx.r([
|
||||
'run',
|
||||
'app',
|
||||
E.workflowAppId,
|
||||
'--inputs',
|
||||
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
|
||||
])
|
||||
assertNonZeroExit(result)
|
||||
// Must contain at least one " - ... (...)" detail line
|
||||
expect(result.stderr).toMatch(/\s+-\s[^(]+\([^)]+\)/)
|
||||
})
|
||||
|
||||
it('[P1] 5.70g text mode header uses server code (invalid_param) not CLI classification code', async () => {
|
||||
// Spec: renderHuman computes headerCode = server?.code ?? e.code
|
||||
// For a canonical 422, server.code = "invalid_param" wins over the CLI's
|
||||
// classification code ("server_4xx_other"), so stderr starts with "invalid_param:".
|
||||
const result = await fx.r([
|
||||
'run',
|
||||
'app',
|
||||
E.workflowAppId,
|
||||
'--inputs',
|
||||
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
|
||||
])
|
||||
assertNonZeroExit(result)
|
||||
expect(result.stderr.trimStart()).toMatch(/^invalid_param:/)
|
||||
})
|
||||
|
||||
it('[P1] 5.70h JSON envelope error.code is CLI classification; server code lives in error.server.code', async () => {
|
||||
// Spec: toEnvelope() sets error.code = c.code (CLI classification = "server_4xx_other")
|
||||
// while the server's semantic code sits separately in error.server.code.
|
||||
// Agents and tooling can read error.server.code for semantic branching
|
||||
// without parsing human-readable text.
|
||||
const result = await fx.r([
|
||||
'run',
|
||||
'app',
|
||||
E.workflowAppId,
|
||||
'--inputs',
|
||||
JSON.stringify({ x: 'hello', num: 'not-a-number', enum_var: 'A', paragraph: 'ok' }),
|
||||
'-o',
|
||||
'json',
|
||||
])
|
||||
assertNonZeroExit(result)
|
||||
const envelope = JSON.parse(result.stderr.trim()) as {
|
||||
error: { code: string, server?: { code: string } }
|
||||
}
|
||||
expect(envelope.error.code).toBe('server_4xx_other')
|
||||
expect(envelope.error.server?.code).toBe('invalid_param')
|
||||
})
|
||||
|
||||
// ── 5.70i / 5.70j PR #37285 boundary contract ───────────────────────────
|
||||
|
||||
it('[P1] 5.70i unknown /openapi/v1 route returns canonical 404 ErrorBody without route suggestions', async () => {
|
||||
// PR #37285: ExternalApi._help_on_404 suppresses flask-restx route enumeration.
|
||||
// Previously, an unknown path under /openapi/v1/ returned flask-restx's default
|
||||
// 404 with a "Did you mean /openapi/v1/apps?" suggestion, leaking the route table.
|
||||
// After the fix it must return a canonical ErrorBody and contain no suggestions.
|
||||
const res = await fetch(`${E.host.replace(/\/$/, '')}/openapi/v1/this-path-does-not-exist-e2e`, {
|
||||
headers: { Authorization: `Bearer ${E.token}` },
|
||||
signal: AbortSignal.timeout(8_000),
|
||||
})
|
||||
expect(res.status).toBe(404)
|
||||
const body = await res.json() as Record<string, unknown>
|
||||
// canonical ErrorBody fields must be present
|
||||
expect(typeof body.code, '404 body must have a string code field').toBe('string')
|
||||
expect(body.status, '404 body must have status: 404').toBe(404)
|
||||
// no flask-restx route enumeration in the response
|
||||
const raw = JSON.stringify(body)
|
||||
expect(raw).not.toMatch(/did you mean/i)
|
||||
expect(raw).not.toMatch(/you might want/i)
|
||||
})
|
||||
|
||||
it('[P1] 5.70j device-flow 4xx uses RFC 8628 format, not ErrorBody — zErrorBody parse fails gracefully', async () => {
|
||||
// PR #37285 explicitly excludes RFC 8628 device-flow endpoints from the
|
||||
// ErrorBody contract. This test pins that contract:
|
||||
// - The device/token endpoint returns RFC 8628 {error: string} on failure,
|
||||
// not the canonical {code, status, message} shape.
|
||||
// - When the CLI's classifyResponse encounters this, zErrorBody.safeParse
|
||||
// returns failure → serverError = undefined → generic status-based message,
|
||||
// no error.server field, no crash.
|
||||
const res = await fetch(`${E.host.replace(/\/$/, '')}/openapi/v1/oauth/device/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ device_code: 'fake-invalid-device-code-e2e-test', client_id: 'difyctl' }),
|
||||
signal: AbortSignal.timeout(8_000),
|
||||
})
|
||||
// device flow errors are 4xx (400 bad_request or 401 expired_token etc.)
|
||||
expect(res.status).toBeGreaterThanOrEqual(400)
|
||||
expect(res.status).toBeLessThan(500)
|
||||
const body = await res.json() as Record<string, unknown>
|
||||
// RFC 8628 shape: has 'error' string, must NOT have ErrorBody 'code'/'status' pair
|
||||
expect(typeof body.error, 'RFC 8628 body must have a string error field').toBe('string')
|
||||
expect(body).not.toHaveProperty('status')
|
||||
// zErrorBody.safeParse would fail → CLI sets serverError = undefined → generic message
|
||||
})
|
||||
|
||||
// ── 5.76 Failed command + -o yaml → stderr is still JSON envelope ────────
|
||||
|
||||
it('[P1] 5.76 failed command with -o yaml still outputs a JSON error envelope on stderr', async () => {
|
||||
|
||||
@ -135,7 +135,6 @@ AMPLITUDE_API_KEY=
|
||||
TEXT_GENERATION_TIMEOUT_MS=60000
|
||||
CSP_WHITELIST=
|
||||
ALLOW_EMBED=false
|
||||
ALLOW_INLINE_STYLES=false
|
||||
ALLOW_UNSAFE_DATA_SCHEME=false
|
||||
TOP_K_MAX_VALUE=10
|
||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
|
||||
|
||||
@ -387,7 +387,6 @@ services:
|
||||
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
|
||||
CSP_WHITELIST: ${CSP_WHITELIST:-}
|
||||
ALLOW_EMBED: ${ALLOW_EMBED:-false}
|
||||
ALLOW_INLINE_STYLES: ${ALLOW_INLINE_STYLES:-false}
|
||||
ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false}
|
||||
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai}
|
||||
MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai}
|
||||
|
||||
@ -393,7 +393,6 @@ services:
|
||||
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
|
||||
CSP_WHITELIST: ${CSP_WHITELIST:-}
|
||||
ALLOW_EMBED: ${ALLOW_EMBED:-false}
|
||||
ALLOW_INLINE_STYLES: ${ALLOW_INLINE_STYLES:-false}
|
||||
ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false}
|
||||
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai}
|
||||
MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai}
|
||||
|
||||
@ -192,11 +192,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/(commonLayout)/snippets/[snippetId]/page.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/(shareLayout)/components/authenticated-layout.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
@ -5025,14 +5020,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/blocks.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/block-selector/featured-tools.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
@ -5717,7 +5704,7 @@
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 6
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx": {
|
||||
@ -5898,11 +5885,6 @@
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/components.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/data-source-empty/hooks.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -7073,19 +7055,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/run/agent-log/agent-log-trigger.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/run/agent-log/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/run/hooks.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
@ -7768,11 +7737,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/client.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/common.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
||||
@ -166,8 +166,23 @@ See `[web/docs/overlay.md](../../web/docs/overlay.md)` for the web app overlay b
|
||||
|
||||
- `pnpm -C packages/dify-ui test` — Vitest unit tests for primitives.
|
||||
- `pnpm -C packages/dify-ui storybook` — Storybook on the default port. Each primitive has `index.stories.tsx`.
|
||||
- `pnpm -C packages/dify-ui test:storybook` — Storybook component tests in Vitest browser mode. Stories without `play` are render and a11y smoke tests; stories with `play` should cover public UI contracts such as opening overlays, keyboard navigation, disabled/loading guards, form submission, and controlled state updates.
|
||||
- `pnpm -C packages/dify-ui type-check` — `tsgo --noEmit` for this package only.
|
||||
|
||||
### Test Boundary
|
||||
|
||||
Use Storybook tests for behavior that belongs to the documented component example:
|
||||
visible state changes, user interaction, keyboard paths, overlay open/close flows,
|
||||
and accessibility-facing semantics. Keep regular Vitest unit tests for lower-level
|
||||
wrapper contracts such as class variants, Base UI passthrough props, hidden input
|
||||
serialization, data attribute hooks, store behavior, and edge cases that do not
|
||||
need a full story.
|
||||
|
||||
Storybook accessibility testing stays enabled globally with `a11y.test = 'error'`.
|
||||
If a story is temporarily marked `todo`, keep the exception local to that story
|
||||
and do not treat an interaction `play` test as a replacement for fixing the
|
||||
underlying accessibility issue.
|
||||
|
||||
### Disabling Animations In Tests
|
||||
|
||||
Base UI can wait for `element.getAnimations()` to finish before it unmounts overlays, panels, and transition-driven components. Browser-based test runners can make that timing unstable, especially when tests assert final DOM state rather than animation behavior.
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import * as React from 'react'
|
||||
import { expect, waitFor, within } from 'storybook/test'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
@ -55,6 +56,21 @@ export const Default: Story = {
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
),
|
||||
play: async ({ canvas, canvasElement, userEvent }) => {
|
||||
const body = within(canvasElement.ownerDocument.body)
|
||||
|
||||
await userEvent.click(canvas.getByRole('button', { name: 'Delete project' }))
|
||||
|
||||
const dialog = body.getByRole('alertdialog', { name: 'Delete project?' })
|
||||
await waitFor(async () => {
|
||||
await expect(dialog).toBeVisible()
|
||||
})
|
||||
|
||||
await userEvent.click(body.getByRole('button', { name: 'Cancel' }))
|
||||
await waitFor(async () => {
|
||||
await expect(body.queryByRole('alertdialog', { name: 'Delete project?' })).not.toBeInTheDocument()
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export const NonDestructive: Story = {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import * as React from 'react'
|
||||
import { expect, fn } from 'storybook/test'
|
||||
|
||||
import { Button } from '.'
|
||||
|
||||
@ -90,8 +91,21 @@ export const Loading: Story = {
|
||||
args: {
|
||||
variant: 'primary',
|
||||
loading: true,
|
||||
onClick: fn(),
|
||||
children: 'Loading Button',
|
||||
},
|
||||
play: async ({ args, canvas, userEvent }) => {
|
||||
const button = canvas.getByRole('button', { name: 'Loading Button' })
|
||||
|
||||
await expect(button).toHaveAttribute('aria-disabled', 'true')
|
||||
await expect(button).toHaveAttribute('aria-busy', 'true')
|
||||
|
||||
button.focus()
|
||||
await expect(button).toHaveFocus()
|
||||
|
||||
await userEvent.click(button)
|
||||
await expect(args.onClick).not.toHaveBeenCalled()
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
|
||||
@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import type { Virtualizer } from '@tanstack/react-virtual'
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import * as React from 'react'
|
||||
import { expect } from 'storybook/test'
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxChip,
|
||||
@ -768,6 +769,15 @@ const MultipleChipsDemo = () => {
|
||||
|
||||
export const MultipleChips: Story = {
|
||||
render: () => <MultipleChipsDemo />,
|
||||
play: async ({ canvas, userEvent }) => {
|
||||
await expect(canvas.getByText('Maya Chen')).toBeVisible()
|
||||
await expect(canvas.getByText('Liam Brooks')).toBeVisible()
|
||||
|
||||
await userEvent.click(canvas.getByRole('button', { name: 'Remove Maya Chen' }))
|
||||
|
||||
await expect(canvas.queryByText('Maya Chen')).not.toBeInTheDocument()
|
||||
await expect(canvas.getByText('Liam Brooks')).toBeVisible()
|
||||
},
|
||||
}
|
||||
|
||||
export const VirtualizedLongList: Story = {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import * as React from 'react'
|
||||
import { expect, waitFor, within } from 'storybook/test'
|
||||
import {
|
||||
Dialog,
|
||||
DialogCloseButton,
|
||||
@ -66,6 +67,22 @@ export const Default: Story = {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
),
|
||||
play: async ({ canvas, canvasElement, userEvent }) => {
|
||||
const body = within(canvasElement.ownerDocument.body)
|
||||
|
||||
await userEvent.click(canvas.getByRole('button', { name: 'Open dialog' }))
|
||||
|
||||
const dialog = body.getByRole('dialog', { name: 'Invite collaborators' })
|
||||
await waitFor(async () => {
|
||||
await expect(dialog).toBeVisible()
|
||||
})
|
||||
await expect(body.getByRole('textbox', { name: 'Email address' })).toBeVisible()
|
||||
|
||||
await userEvent.click(body.getByRole('button', { name: 'Close' }))
|
||||
await waitFor(async () => {
|
||||
await expect(body.queryByRole('dialog', { name: 'Invite collaborators' })).not.toBeInTheDocument()
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export const WithoutCloseButton: Story = {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import type { FileTreeIconType } from '.'
|
||||
import * as React from 'react'
|
||||
import { expect } from 'storybook/test'
|
||||
import {
|
||||
FileTreeBadge,
|
||||
FileTreeFile,
|
||||
@ -330,6 +331,19 @@ function VisualStates() {
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <ComposedFileTree />,
|
||||
play: async ({ canvas, userEvent }) => {
|
||||
const srcFolder = canvas.getByRole('button', { name: 'src' })
|
||||
|
||||
await expect(canvas.getByRole('button', { name: 'components' })).toBeVisible()
|
||||
|
||||
await userEvent.click(srcFolder)
|
||||
await expect(srcFolder).toHaveAttribute('aria-expanded', 'false')
|
||||
await expect(canvas.queryByRole('button', { name: 'components' })).not.toBeInTheDocument()
|
||||
|
||||
await userEvent.click(srcFolder)
|
||||
await expect(srcFolder).toHaveAttribute('aria-expanded', 'true')
|
||||
await expect(canvas.getByRole('button', { name: 'components' })).toBeVisible()
|
||||
},
|
||||
}
|
||||
|
||||
export const DataDriven: Story = {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import * as React from 'react'
|
||||
import { expect } from 'storybook/test'
|
||||
import {
|
||||
Pagination,
|
||||
PaginationSkeleton,
|
||||
@ -77,6 +78,15 @@ type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Playground: Story = {
|
||||
render: () => <PaginationDemo />,
|
||||
play: async ({ canvas, userEvent }) => {
|
||||
await expect(canvas.getByRole('button', { name: 'Edit page number, current page 2 of 200' })).toBeVisible()
|
||||
|
||||
await userEvent.click(canvas.getByRole('button', { name: 'Next page' }))
|
||||
await expect(canvas.getByRole('button', { name: 'Edit page number, current page 3 of 200' })).toBeVisible()
|
||||
|
||||
await userEvent.click(canvas.getByRole('button', { name: '50' }))
|
||||
await expect(canvas.getByRole('button', { name: '50' })).toHaveAttribute('aria-pressed', 'true')
|
||||
},
|
||||
parameters: {
|
||||
a11y: {
|
||||
test: 'todo',
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import * as React from 'react'
|
||||
import { expect, waitFor, within } from 'storybook/test'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -19,6 +20,8 @@ const triggerWidth = 'w-64'
|
||||
const cityItems = [
|
||||
{ label: 'Seattle', value: 'seattle' },
|
||||
{ label: 'New York', value: 'new-york' },
|
||||
{ label: 'Tokyo', value: 'tokyo' },
|
||||
{ label: 'Paris', value: 'paris' },
|
||||
]
|
||||
|
||||
const meta = {
|
||||
@ -41,11 +44,11 @@ type Story = StoryObj<typeof meta>
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<div className={triggerWidth}>
|
||||
<Select defaultValue="seattle">
|
||||
<Select items={cityItems} defaultValue="seattle">
|
||||
<SelectTrigger aria-label="City">
|
||||
<SelectValue placeholder="Select a city" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent listProps={{ 'aria-label': 'City options' }}>
|
||||
<SelectItem value="seattle">
|
||||
<SelectItemText>Seattle</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
@ -66,6 +69,27 @@ export const Default: Story = {
|
||||
</Select>
|
||||
</div>
|
||||
),
|
||||
play: async ({ canvas, canvasElement, userEvent }) => {
|
||||
const trigger = canvas.getByRole('combobox', { name: 'City' })
|
||||
const body = within(canvasElement.ownerDocument.body)
|
||||
|
||||
await expect(trigger).toHaveTextContent('Seattle')
|
||||
|
||||
trigger.focus()
|
||||
await userEvent.keyboard('{ArrowDown}')
|
||||
|
||||
await waitFor(async () => {
|
||||
await expect(body.getByRole('option', { name: 'Tokyo' })).toBeVisible()
|
||||
})
|
||||
|
||||
await userEvent.keyboard('{ArrowDown}{ArrowDown}{Enter}')
|
||||
await expect(trigger).toHaveTextContent('Tokyo')
|
||||
|
||||
await userEvent.keyboard('{Escape}')
|
||||
await waitFor(async () => {
|
||||
await expect(body.queryByRole('listbox', { name: 'City options' })).not.toBeInTheDocument()
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export const WithVisibleLabel: Story = {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import * as React from 'react'
|
||||
import { expect } from 'storybook/test'
|
||||
import { Switch, SwitchSkeleton } from '.'
|
||||
import {
|
||||
FieldDescription,
|
||||
@ -77,6 +78,17 @@ export const Default: Story = {
|
||||
checked: false,
|
||||
disabled: false,
|
||||
},
|
||||
play: async ({ canvas, userEvent }) => {
|
||||
const switchControl = canvas.getByRole('switch', { name: 'Enable auto retry' })
|
||||
|
||||
await expect(switchControl).toHaveAttribute('aria-checked', 'false')
|
||||
await expect(canvas.getByText('Failures require manual retry.')).toBeVisible()
|
||||
|
||||
await userEvent.click(switchControl)
|
||||
|
||||
await expect(switchControl).toHaveAttribute('aria-checked', 'true')
|
||||
await expect(canvas.getByText('Failures will retry automatically.')).toBeVisible()
|
||||
},
|
||||
}
|
||||
|
||||
export const DefaultOn: Story = {
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.30314 1.54615C8.48087 1.50085 8.66708 1.49473 8.84742 1.52858C9.07685 1.57176 9.27914 1.6956 9.41968 1.77467L13.1768 3.88795C13.2961 3.955 13.4666 4.04398 13.6098 4.17701L13.6697 4.23691C13.7899 4.36839 13.8809 4.52463 13.9366 4.69394C14.0074 4.90941 13.9998 5.13856 13.9998 5.29485V9.55917C13.9998 9.70492 14.0069 9.91888 13.9444 10.1223C13.9076 10.2422 13.8527 10.3559 13.7823 10.4596L13.7068 10.5598C13.5702 10.7233 13.3869 10.8339 13.2647 10.9133L8.25171 14.1718C8.11671 14.2596 7.92272 14.396 7.69637 14.4537C7.51868 14.499 7.33292 14.5051 7.15275 14.4713C6.92315 14.4281 6.72034 14.3049 6.57984 14.2258L2.82268 12.1119C2.68658 12.0353 2.48283 11.9301 2.32984 11.7629C2.20953 11.6314 2.11852 11.475 2.06291 11.3059C1.99209 11.0903 1.99976 10.8606 1.99976 10.7044V6.44069C1.99976 6.29486 1.99262 6.08092 2.0551 5.87753L2.09807 5.7597C2.14656 5.64426 2.21216 5.53647 2.29273 5.44003L2.34611 5.38144C2.47424 5.24974 2.62775 5.15609 2.73479 5.08652L7.85979 1.75514C7.98194 1.67712 8.13342 1.58951 8.30314 1.54615ZM3.33309 10.7044C3.33309 10.7559 3.33334 10.7962 3.33374 10.8307C3.33392 10.8452 3.33411 10.8578 3.33439 10.8684C3.34356 10.8739 3.3543 10.8806 3.36695 10.8879C3.39682 10.9052 3.43208 10.9245 3.47697 10.9498L6.74064 12.7857V11.649L3.33309 9.73235V10.7044ZM8.07398 11.621V12.6972L12.5382 9.7955C12.5784 9.76933 12.6098 9.74883 12.6365 9.73105C12.6475 9.72368 12.6564 9.71639 12.6645 9.71087C12.6647 9.70125 12.6656 9.69004 12.6658 9.67701C12.6661 9.64494 12.6664 9.60721 12.6664 9.55917V8.636L8.07398 11.621ZM3.33309 8.2024L6.74064 10.1191V8.98235L3.33309 7.06568V8.2024ZM8.07398 8.95436V10.0299L12.6664 7.04485V5.96933L8.07398 8.95436ZM8.58374 2.87493L3.95288 5.8847L7.38192 7.81373L12.046 4.78183L8.76604 2.93613C8.71971 2.91007 8.68338 2.89008 8.6521 2.87298C8.63863 2.86561 8.62684 2.85931 8.61695 2.8541C8.60751 2.85987 8.59651 2.86685 8.58374 2.87493Z" fill="#155AEF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1,8 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.5 11.25C7.91421 11.25 8.25 11.5858 8.25 12V14.25C8.25 14.6642 7.91421 15 7.5 15C7.08579 15 6.75 14.6642 6.75 14.25V12C6.75 11.5858 7.08579 11.25 7.5 11.25Z" fill="currentColor" />
|
||||
<path d="M2.19653 2.19653C2.48937 1.90372 2.96418 1.90382 3.25708 2.19653L8.03027 6.96973C8.09162 7.03108 8.13966 7.10082 8.17529 7.1748C8.19164 7.20869 8.20587 7.24378 8.21704 7.28027C8.24638 7.37633 8.25641 7.477 8.24634 7.57617C8.23743 7.66451 8.21216 7.74788 8.17529 7.82446C8.13963 7.89868 8.09176 7.96874 8.03027 8.03027L3.25708 12.8035C2.96419 13.096 2.48932 13.0962 2.19653 12.8035C1.90394 12.5107 1.90405 12.0358 2.19653 11.7429L5.68945 8.25H0.75C0.335786 8.25 0 7.91421 0 7.5C0 7.08579 0.335786 6.75 0.75 6.75H5.68945L2.19653 3.25708C1.90389 2.96423 1.90388 2.48937 2.19653 2.19653Z" fill="currentColor" />
|
||||
<path d="M10.1521 10.1521C10.445 9.85921 10.9198 9.85921 11.2126 10.1521L12.8035 11.7429C13.096 12.0358 13.0962 12.5107 12.8035 12.8035C12.5107 13.0962 12.0358 13.096 11.7429 12.8035L10.1521 11.2126C9.85921 10.9198 9.85922 10.445 10.1521 10.1521Z" fill="currentColor" />
|
||||
<path d="M14.25 6.75C14.6642 6.75 15 7.08579 15 7.5C15 7.91421 14.6642 8.25 14.25 8.25H12C11.5858 8.25 11.25 7.91421 11.25 7.5C11.25 7.08579 11.5858 6.75 12 6.75H14.25Z" fill="currentColor" />
|
||||
<path d="M11.7422 2.19653C12.035 1.90387 12.5098 1.90406 12.8027 2.19653C13.0956 2.4894 13.0955 2.96419 12.8027 3.25708L11.2119 4.8479C10.919 5.14079 10.4443 5.1408 10.1514 4.8479C9.85883 4.55497 9.85858 4.08013 10.1514 3.78735L11.7422 2.19653Z" fill="currentColor" />
|
||||
<path d="M7.5 0C7.91421 0 8.25 0.335786 8.25 0.75V3C8.25 3.41421 7.91421 3.75 7.5 3.75C7.08579 3.75 6.75 3.41421 6.75 3V0.75C6.75 0.335786 7.08579 0 7.5 0Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 7.33325C13.1046 7.33325 14 8.22865 14 9.33325C14 10.1403 13.5218 10.8356 12.8333 11.1516V11.9999L12.3333 12.4999L12.8333 12.9511V13.6666L12 14.3333L11.1667 13.6666V11.1516C10.4782 10.8356 10 10.1403 10 9.33325C10 8.22865 10.8954 7.33325 12 7.33325ZM12 8.66659C11.6318 8.66659 11.3333 8.96505 11.3333 9.33325C11.3333 9.70145 11.6318 9.99992 12 9.99992C12.3682 9.99992 12.6667 9.70145 12.6667 9.33325C12.6667 8.96505 12.3682 8.66659 12 8.66659Z" fill="currentColor"/>
|
||||
<path d="M8 7.99992C8.2545 7.99992 8.50382 8.01506 8.7474 8.04484L8.58594 9.36841C8.39687 9.34527 8.20127 9.33325 8 9.33325C5.8465 9.33325 4.25915 10.7274 3.78646 12.6666H10V13.9999H2.26758L2.33594 13.2708C2.61081 10.3473 4.82817 7.99992 8 7.99992Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 1.33325C9.65687 1.33325 11 2.6764 11 4.33325C11 5.99011 9.65687 7.33325 8 7.33325C6.34315 7.33325 5 5.99011 5 4.33325C5 2.6764 6.34315 1.33325 8 1.33325ZM8 2.66659C7.07953 2.66659 6.33333 3.41278 6.33333 4.33325C6.33333 5.25373 7.07953 5.99992 8 5.99992C8.92047 5.99992 9.66667 5.25373 9.66667 4.33325C9.66667 3.41278 8.92047 2.66659 8 2.66659Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 0C17.5523 0 18 0.447715 18 1V6C18 6.55228 17.5523 7 17 7H12C11.4477 7 11 6.55228 11 6V4.5H6.94629C5.92438 4.50039 5.56101 5.85276 6.44531 6.36523L12.5576 9.90332C15.2116 11.4402 14.1206 15.4996 11.0537 15.5H7V17C7 17.5523 6.55228 18 6 18H1C0.447715 18 0 17.5523 0 17V12C0 11.4477 0.447715 11 1 11H6C6.55228 11 7 11.4477 7 12V13.5H11.0537C12.0756 13.4996 12.4394 12.1472 11.5557 11.6348L5.44336 8.09668C2.789 6.55983 3.87917 2.50039 6.94629 2.5H11V1C11 0.447715 11.4477 0 12 0H17ZM2 16H5V13H2V16ZM13 5H16V2H13V5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 693 B |
@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.91669 1.16669C1.95019 1.16669 1.16669 1.95019 1.16669 2.91669V11.0834C1.16669 12.0499 1.95019 12.8334 2.91669 12.8334H11.0834C12.0499 12.8334 12.8334 12.0499 12.8334 11.0834V2.91669C12.8334 1.95019 12.0499 1.16669 11.0834 1.16669H2.91669ZM2.33335 2.91669C2.33335 2.59452 2.59452 2.33335 2.91669 2.33335H11.0834C11.4055 2.33335 11.6667 2.59452 11.6667 2.91669V11.0834C11.6667 11.4055 11.4055 11.6667 11.0834 11.6667H2.91669C2.59452 11.6667 2.33335 11.4055 2.33335 11.0834V2.91669ZM5.67188 10.5L9.67186 3.50002H8.32815L4.32817 10.5H5.67188Z" fill="#676F83" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 675 B |
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16" fill="none">
|
||||
<path d="M6.25 6.875C6.82523 6.875 7.29167 7.34128 7.29167 7.91667V9.16667C7.29167 9.74205 6.82523 10.2083 6.25 10.2083C5.67477 10.2083 5.20833 9.74205 5.20833 9.16667V7.91667C5.20833 7.34128 5.67477 6.875 6.25 6.875Z" fill="#676F83"/>
|
||||
<path d="M10.4167 6.875C10.992 6.875 11.4583 7.34135 11.4583 7.91667V9.16667C11.4583 9.74199 10.992 10.2083 10.4167 10.2083C9.84135 10.2083 9.375 9.74199 9.375 9.16667V7.91667C9.375 7.34135 9.84135 6.875 10.4167 6.875Z" fill="#676F83"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.33333 0C9.13875 0 9.79167 0.652918 9.79167 1.45833C9.79167 2.02329 9.46964 2.51173 8.99984 2.75391V3.33822C9.38912 3.34279 9.77995 3.35006 10.175 3.36263C11.6983 3.41112 12.7377 3.42425 13.6401 3.90951C14.375 4.30477 15.0255 4.97655 15.3971 5.72347C15.5468 6.02442 15.6427 6.33532 15.7056 6.66667H15.8333C16.2936 6.66667 16.6667 7.03976 16.6667 7.5V10C16.6667 10.4602 16.2936 10.8333 15.8333 10.8333H15.8285C15.8235 11.2254 15.813 11.5735 15.7869 11.8831C15.7386 12.4571 15.6361 12.9628 15.3971 13.4432C15.0254 14.1901 14.3749 14.8619 13.6401 15.2572C12.7377 15.7424 11.6982 15.7556 10.175 15.804C8.93336 15.8436 7.73328 15.8436 6.4917 15.804C4.96843 15.7556 3.92896 15.7424 3.02653 15.2572C2.29178 14.8619 1.64121 14.1902 1.26953 13.4432C1.03058 12.9628 0.928072 12.4571 0.87972 11.8831C0.853642 11.5735 0.843216 11.2254 0.838216 10.8333H0.833333C0.373096 10.8333 0 10.4602 0 10V7.5C0 7.03976 0.373096 6.66667 0.833333 6.66667H0.9611C1.02392 6.33532 1.11984 6.02442 1.26953 5.72347C1.64119 4.97649 2.29177 4.30475 3.02653 3.90951C3.92895 3.42425 4.96837 3.41112 6.4917 3.36263C6.88671 3.35006 7.27754 3.34279 7.66683 3.33822V2.75391C7.19703 2.51173 6.875 2.02329 6.875 1.45833C6.875 0.652918 7.52792 0 8.33333 0ZM10.1213 5.02848C8.91522 4.9901 7.75142 4.9901 6.54541 5.02848C4.85908 5.08217 4.29323 5.12091 3.81592 5.3776C3.38476 5.60954 2.98015 6.02734 2.76204 6.46566C2.65217 6.68652 2.57959 6.96168 2.54069 7.4235C2.50069 7.89854 2.5 8.50363 2.5 9.37825V9.78841C2.5 10.663 2.50069 11.2681 2.54069 11.7432C2.57959 12.205 2.65215 12.4801 2.76204 12.701C2.98015 13.1393 3.38475 13.5571 3.81592 13.7891C4.29321 14.0458 4.85904 14.0845 6.54541 14.1382C7.75141 14.1766 8.91523 14.1766 10.1213 14.1382C11.8075 14.0845 12.3734 14.0458 12.8507 13.7891C13.2819 13.5572 13.6865 13.1394 13.9046 12.701C14.0145 12.4801 14.0871 12.205 14.126 11.7432C14.166 11.2681 14.1667 10.663 14.1667 9.78841V9.37825C14.1667 8.50363 14.166 7.89854 14.126 7.4235C14.0871 6.96168 14.0145 6.68652 13.9046 6.46566C13.6865 6.02729 13.2819 5.60951 12.8507 5.3776C12.3734 5.12091 11.8075 5.08217 10.1213 5.02848Z" fill="#676F83"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@ -0,0 +1,6 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.9362 1.5C12.4177 1.50006 12.8411 1.81975 12.9727 2.28296L15.4651 11.061C15.5117 11.2251 15.6125 11.3687 15.7515 11.4675L16.1235 11.7319C16.3544 11.8963 16.4728 12.1769 16.4297 12.4571L15.9206 15.7669C15.8394 16.2948 15.2475 16.5708 14.7905 16.2942L12.8006 15.0893C12.6837 15.0186 12.5618 14.9546 12.4307 14.9165C12.2411 14.8615 12.0443 14.833 11.8462 14.833C10.6885 14.833 9.75 13.8945 9.75 12.7368V9.14722C9.75 8.23747 10.4875 7.5 11.3972 7.5C11.5688 7.5 11.7164 7.62113 11.7503 7.78928L12.3824 10.9483L12.4043 11.0215C12.4721 11.182 12.6457 11.2781 12.8233 11.2427C13.0009 11.2072 13.1242 11.0515 13.125 10.8772L13.1177 10.8017L12.4856 7.6428C12.3818 7.12384 11.9265 6.75002 11.3972 6.75C11.08 6.75 10.7771 6.81165 10.5 6.92359V2.93628C10.5 2.14312 11.1431 1.5 11.9362 1.5Z" fill="currentColor"/>
|
||||
<path d="M2.28761 11.1211C2.263 11.2855 2.25026 11.4538 2.25026 11.625C2.25026 13.3862 3.59948 14.8313 5.32057 14.9854L3.0757 16.3674C2.65801 16.6245 2.10961 16.418 1.96534 15.9492L0.926773 12.5742C0.823558 12.2388 0.967018 11.876 1.27174 11.7019L2.28761 11.1211Z" fill="currentColor"/>
|
||||
<path d="M6.42041 1.5C7.01664 1.5 7.49997 1.98337 7.49997 2.57959V8.81835C6.96373 8.4594 6.31878 8.25 5.62501 8.25C4.91854 8.25 4.26271 8.46705 3.7207 8.83815L5.01124 2.6455C5.15039 1.97822 5.73876 1.50007 6.42041 1.5Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.625 9C7.07475 9 8.25 10.1753 8.25 11.625C8.25 13.0747 7.07475 14.25 5.625 14.25C4.17525 14.25 3 13.0747 3 11.625C3 10.1753 4.17525 9 5.625 9ZM5.625 10.875C5.21078 10.875 4.875 11.2108 4.875 11.625C4.875 12.0392 5.21078 12.375 5.625 12.375C6.03921 12.375 6.375 12.0392 6.375 11.625C6.375 11.2108 6.03921 10.875 5.625 10.875Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@ -0,0 +1,9 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.875 11.8125C7.875 13.1587 6.7837 14.25 5.4375 14.25C4.0913 14.25 3 13.1587 3 11.8125C3 10.4663 4.0913 9.375 5.4375 9.375C6.7837 9.375 7.875 10.4663 7.875 11.8125Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.25 15.75L5.625 14.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1.5 11.25L3 10.875" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.41699 12.5625C5.83121 12.5625 6.16699 12.2267 6.16699 11.8125C6.16699 11.3983 5.83121 11.0625 5.41699 11.0625C5.00278 11.0625 4.66699 11.3983 4.66699 11.8125C4.66699 12.2267 5.00278 12.5625 5.41699 12.5625Z" fill="currentColor"/>
|
||||
<path d="M13.125 11.25L12.4956 8.10292C12.4255 7.75237 12.1177 7.5 11.7601 7.5H11.625C10.7966 7.5 10.125 8.17155 10.125 9V12.3939C10.125 13.419 10.956 14.25 11.9811 14.25C12.2408 14.25 12.4976 14.3045 12.7349 14.41L15.75 15.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.875 7.5V3.75C10.875 2.92157 11.5466 2.25 12.375 2.25H12.5394C12.8836 2.25 13.1836 2.48422 13.267 2.81811L15.3332 11.0833L16.5 11.625" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.125 9.75V3C7.125 2.58579 6.78921 2.25 6.375 2.25H5.52089C5.14964 2.25 4.83426 2.5216 4.77919 2.88875L3.75 9.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@ -1,8 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="agent">
|
||||
<g id="Vector">
|
||||
<path d="M14.7401 5.80454C14.5765 4.77996 14.1638 3.79808 13.5306 2.97273C12.8973 2.14738 12.0648 1.48568 11.1185 1.06589C10.1722 0.646098 9.12632 0.461106 8.08751 0.546487C7.05582 0.624753 6.04548 0.966277 5.17744 1.53548C4.3094 2.09758 3.58366 2.88024 3.09272 3.79808C2.59466 4.70881 2.33852 5.7405 2.33852 6.7793V7.22756L1.25703 9.3692C1.04357 9.80322 1.22145 10.3368 1.65547 10.5574L2.3314 10.8989V12.3006C2.3314 12.82 2.53063 13.3038 2.90061 13.6738C3.2706 14.0367 3.75442 14.243 4.27382 14.243H6.01702V14.7624C6.01702 15.1538 6.3372 15.4739 6.72853 15.4739C7.11986 15.4739 7.44004 15.1538 7.44004 14.7624V13.7094C7.44004 13.2185 7.04159 12.82 6.55065 12.82H4.27382C4.13864 12.82 4.00345 12.7631 3.91095 12.6706C3.81846 12.5781 3.76154 12.4429 3.76154 12.3077V10.5716C3.76154 10.2301 3.56943 9.92417 3.2706 9.77476L2.77254 9.52573L3.66904 7.73984C3.72596 7.61889 3.76154 7.4837 3.76154 7.34851V6.77219C3.76154 5.96818 3.96076 5.17129 4.34498 4.4669C4.72919 3.76251 5.28417 3.15772 5.9601 2.7237C6.63603 2.28968 7.41158 2.02643 8.20847 1.96239C9.00536 1.89835 9.81648 2.04066 10.5493 2.36795C11.2822 2.69524 11.9225 3.20042 12.4135 3.84077C12.8973 4.47402 13.2246 5.23533 13.3456 6.02511C13.4665 6.81488 13.3954 7.63312 13.125 8.38731C12.8617 9.12017 12.4206 9.78187 11.8585 10.3084C11.6735 10.4792 11.5668 10.7139 11.5668 10.9701V14.7624C11.5668 15.1538 11.887 15.4739 12.2783 15.4739C12.6696 15.4739 12.9898 15.1538 12.9898 14.7624V11.1978C13.6515 10.5432 14.1567 9.73918 14.4697 8.87114C14.8184 7.89637 14.918 6.83623 14.7615 5.81165L14.7401 5.80454Z" fill="white"/>
|
||||
<path d="M10.8055 7.99599C10.8909 7.83234 10.962 7.66158 11.0189 7.4837H11.6522C12.0435 7.4837 12.3637 7.16352 12.3637 6.77219C12.3637 6.38086 12.0435 6.06068 11.6522 6.06068H11.0189C10.9691 5.8828 10.898 5.71204 10.8055 5.54839L11.2537 5.10014C11.5312 4.82266 11.5312 4.3744 11.2537 4.09692C10.9762 3.81943 10.528 3.81943 10.2505 4.09692L9.80225 4.54517C9.6386 4.45267 9.46784 4.38863 9.28996 4.33171V3.69847C9.28996 3.30714 8.96978 2.98696 8.57845 2.98696C8.18712 2.98696 7.86694 3.30714 7.86694 3.69847V4.33171C7.68907 4.38152 7.5183 4.45267 7.35466 4.54517L6.90641 4.09692C6.62892 3.81943 6.18067 3.81943 5.90318 4.09692C5.62569 4.3744 5.62569 4.82266 5.90318 5.10014L6.35143 5.54839C6.26605 5.71204 6.1949 5.8828 6.13798 6.06068H5.50473C5.1134 6.06068 4.79323 6.38086 4.79323 6.77219C4.79323 7.16352 5.1134 7.4837 5.50473 7.4837H6.13798C6.18778 7.66158 6.25893 7.83234 6.35143 7.99599L5.90318 8.44424C5.62569 8.72172 5.62569 9.16997 5.90318 9.44746C6.04548 9.58976 6.22336 9.6538 6.40835 9.6538C6.59334 9.6538 6.77122 9.58265 6.91352 9.44746L7.36177 8.99921C7.52542 9.08459 7.69618 9.15574 7.87406 9.21267V9.84591C7.87406 10.2372 8.19424 10.5574 8.58557 10.5574C8.9769 10.5574 9.29708 10.2372 9.29708 9.84591V9.21267C9.47496 9.16286 9.64572 9.09171 9.80936 8.99921L10.2576 9.44746C10.3999 9.58976 10.5778 9.6538 10.7628 9.6538C10.9478 9.6538 11.1257 9.58265 11.268 9.44746C11.5454 9.16997 11.5454 8.72172 11.268 8.44424L10.8197 7.99599H10.8055ZM7.44004 6.77219C7.44004 6.14606 7.94521 5.64089 8.57134 5.64089C9.19747 5.64089 9.70264 6.14606 9.70264 6.77219C9.70264 7.39832 9.19747 7.90349 8.57134 7.90349C7.94521 7.90349 7.44004 7.39832 7.44004 6.77219Z" fill="white"/>
|
||||
</g>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="agent" transform="translate(2.5 1)">
|
||||
<path d="M3.3178 20.9524V15.7184C3.31774 15.7136 3.31456 15.6845 3.28404 15.6468L3.24312 15.6069C1.26589 13.9971 0 11.5398 0 8.78813C0.000122972 3.93464 3.93464 0.000122969 8.78813 0C13.5146 0 17.3698 3.73066 17.5691 8.40858C17.5712 8.45846 17.5841 8.48161 17.5865 8.48531L19.3226 11.089C19.7937 11.7956 19.6309 12.7481 18.9513 13.2579L17.5998 14.2707C17.5851 14.2819 17.5763 14.2996 17.5763 14.3178V15.4237C17.576 17.2235 16.1176 18.682 14.3178 18.6822H13.2119C13.1798 18.6822 13.1537 18.7085 13.1536 18.7405V20.9524C13.1536 21.5309 12.6845 21.9999 12.1059 22C11.5274 22 11.0583 21.531 11.0583 20.9524V18.7405C11.0584 17.5513 12.0226 16.587 13.2119 16.587H14.3178C14.9604 16.5868 15.4808 16.0663 15.481 15.4237V14.3178C15.481 13.64 15.8006 13.0016 16.3424 12.595L17.3195 11.8614L15.8432 9.64853C15.603 9.28835 15.4913 8.88291 15.4749 8.49758C15.323 4.93639 12.3872 2.09524 8.78813 2.09524C5.09181 2.09536 2.09536 5.09181 2.09524 8.78813C2.09524 10.883 3.05647 12.7533 4.56594 13.9822C5.0571 14.3822 5.41299 15.0012 5.41304 15.7184V20.9524C5.41304 21.5309 4.94385 21.9998 4.36542 22C3.78684 22 3.3178 21.531 3.3178 20.9524Z" fill="currentColor"/>
|
||||
<path d="M9.79012 6.5163L9.31429 5.27923C9.23058 5.06159 9.02158 4.91799 8.78836 4.91799C8.55514 4.91799 8.34614 5.06159 8.26243 5.27923L7.7866 6.5163C7.56194 7.10037 7.10038 7.56194 6.5163 7.78659L5.27923 8.26239C5.06159 8.34609 4.91799 8.55519 4.91799 8.78836C4.91799 9.02158 5.06159 9.23058 5.27923 9.31429L6.5163 9.79012C7.10037 10.0148 7.56194 10.4764 7.7866 11.0604L8.26243 12.2975C8.34614 12.5151 8.55514 12.6587 8.78836 12.6587C9.02158 12.6587 9.23058 12.5151 9.31429 12.2975L9.79012 11.0604C10.0148 10.4764 10.4764 10.0148 11.0604 9.79012L12.2975 9.31429C12.5151 9.23058 12.6587 9.02158 12.6587 8.78836C12.6587 8.55519 12.5151 8.34609 12.2975 8.26239L11.0604 7.78659C10.4764 7.56194 10.0148 7.10038 9.79012 6.5163Z" fill="currentColor"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.0 KiB |
@ -1,7 +1,10 @@
|
||||
{
|
||||
"prefix": "custom-public",
|
||||
"lastModified": 1781246368,
|
||||
"lastModified": 1781515983,
|
||||
"icons": {
|
||||
"agent-building-blocks": {
|
||||
"body": "<path fill=\"#155AEF\" fill-rule=\"evenodd\" d=\"M8.303 1.546c.178-.045.364-.051.544-.017c.23.043.432.167.573.246l3.757 2.113c.12.067.29.156.433.289l.06.06c.12.131.21.288.267.457c.07.215.063.445.063.6V9.56c0 .146.007.36-.056.563q-.055.181-.162.338l-.075.1c-.137.163-.32.274-.442.353l-5.013 3.259c-.135.088-.33.224-.556.282a1.3 1.3 0 0 1-.543.017c-.23-.043-.433-.166-.573-.245l-3.757-2.114c-.136-.077-.34-.182-.493-.35a1.3 1.3 0 0 1-.267-.456C1.993 11.09 2 10.86 2 10.704V6.441c0-.146-.007-.36.055-.563l.043-.118a1.3 1.3 0 0 1 .195-.32l.053-.059c.128-.131.282-.225.389-.294L7.86 1.755c.122-.078.273-.165.443-.209m-4.97 9.158l.001.164l.033.02l.11.062l3.264 1.836v-1.137L3.333 9.732zm4.741.917v1.076l4.464-2.901l.098-.065l.029-.02v-.034l.001-.118v-.923zm-4.74-3.419L6.74 10.12V8.982L3.333 7.066zm4.74.752v1.076l4.592-2.985V5.969zm.51-6.08l-4.631 3.01l3.429 1.93l4.664-3.032l-3.28-1.846l-.15-.082z\" clip-rule=\"evenodd\"/>"
|
||||
},
|
||||
"avatar-user": {
|
||||
"body": "<g fill=\"none\"><g clip-path=\"url(#svgID0)\"><rect width=\"512\" height=\"512\" fill=\"#B2DDFF\" rx=\"256\"/><circle cx=\"256\" cy=\"196\" r=\"84\" fill=\"#fff\" opacity=\".68\"/><ellipse cx=\"256\" cy=\"583.5\" fill=\"#fff\" opacity=\".68\" rx=\"266\" ry=\"274.5\"/></g><defs><clipPath id=\"svgID0\"><rect width=\"512\" height=\"512\" fill=\"#fff\" rx=\"256\"/></clipPath></defs></g>",
|
||||
"width": 512,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"prefix": "custom-public",
|
||||
"name": "Dify Custom Public",
|
||||
"total": 144,
|
||||
"total": 145,
|
||||
"version": "0.0.0-private",
|
||||
"author": {
|
||||
"name": "LangGenius, Inc.",
|
||||
@ -13,12 +13,12 @@
|
||||
"url": "https://github.com/langgenius/dify/blob/main/LICENSE"
|
||||
},
|
||||
"samples": [
|
||||
"agent-building-blocks",
|
||||
"avatar-user",
|
||||
"billing-ar-cube-1",
|
||||
"billing-asterisk",
|
||||
"billing-aws-marketplace-dark",
|
||||
"billing-aws-marketplace-light",
|
||||
"billing-azure"
|
||||
"billing-aws-marketplace-light"
|
||||
],
|
||||
"palette": false
|
||||
}
|
||||
|
||||
@ -1,7 +1,29 @@
|
||||
{
|
||||
"prefix": "custom-vender",
|
||||
"lastModified": 1781246368,
|
||||
"lastModified": 1781515983,
|
||||
"icons": {
|
||||
"agent-v2-access-point": {
|
||||
"body": "<g fill=\"none\"><path d=\"M7.5 11.25C7.91421 11.25 8.25 11.5858 8.25 12V14.25C8.25 14.6642 7.91421 15 7.5 15C7.08579 15 6.75 14.6642 6.75 14.25V12C6.75 11.5858 7.08579 11.25 7.5 11.25Z\" fill=\"currentColor\"/><path d=\"M2.19653 2.19653C2.48937 1.90372 2.96418 1.90382 3.25708 2.19653L8.03027 6.96973C8.09162 7.03108 8.13966 7.10082 8.17529 7.1748C8.19164 7.20869 8.20587 7.24378 8.21704 7.28027C8.24638 7.37633 8.25641 7.477 8.24634 7.57617C8.23743 7.66451 8.21216 7.74788 8.17529 7.82446C8.13963 7.89868 8.09176 7.96874 8.03027 8.03027L3.25708 12.8035C2.96419 13.096 2.48932 13.0962 2.19653 12.8035C1.90394 12.5107 1.90405 12.0358 2.19653 11.7429L5.68945 8.25H0.75C0.335786 8.25 0 7.91421 0 7.5C0 7.08579 0.335786 6.75 0.75 6.75H5.68945L2.19653 3.25708C1.90389 2.96423 1.90388 2.48937 2.19653 2.19653Z\" fill=\"currentColor\"/><path d=\"M10.1521 10.1521C10.445 9.85921 10.9198 9.85921 11.2126 10.1521L12.8035 11.7429C13.096 12.0358 13.0962 12.5107 12.8035 12.8035C12.5107 13.0962 12.0358 13.096 11.7429 12.8035L10.1521 11.2126C9.85921 10.9198 9.85922 10.445 10.1521 10.1521Z\" fill=\"currentColor\"/><path d=\"M14.25 6.75C14.6642 6.75 15 7.08579 15 7.5C15 7.91421 14.6642 8.25 14.25 8.25H12C11.5858 8.25 11.25 7.91421 11.25 7.5C11.25 7.08579 11.5858 6.75 12 6.75H14.25Z\" fill=\"currentColor\"/><path d=\"M11.7422 2.19653C12.035 1.90387 12.5098 1.90406 12.8027 2.19653C13.0956 2.4894 13.0955 2.96419 12.8027 3.25708L11.2119 4.8479C10.919 5.14079 10.4443 5.1408 10.1514 4.8479C9.85883 4.55497 9.85858 4.08013 10.1514 3.78735L11.7422 2.19653Z\" fill=\"currentColor\"/><path d=\"M7.5 0C7.91421 0 8.25 0.335786 8.25 0.75V3C8.25 3.41421 7.91421 3.75 7.5 3.75C7.08579 3.75 6.75 3.41421 6.75 3V0.75C6.75 0.335786 7.08579 0 7.5 0Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 15,
|
||||
"height": 15
|
||||
},
|
||||
"agent-v2-end-user-auth": {
|
||||
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 7.33325C13.1046 7.33325 14 8.22865 14 9.33325C14 10.1403 13.5218 10.8356 12.8333 11.1516V11.9999L12.3333 12.4999L12.8333 12.9511V13.6666L12 14.3333L11.1667 13.6666V11.1516C10.4782 10.8356 10 10.1403 10 9.33325C10 8.22865 10.8954 7.33325 12 7.33325ZM12 8.66659C11.6318 8.66659 11.3333 8.96505 11.3333 9.33325C11.3333 9.70145 11.6318 9.99992 12 9.99992C12.3682 9.99992 12.6667 9.70145 12.6667 9.33325C12.6667 8.96505 12.3682 8.66659 12 8.66659Z\" fill=\"currentColor\"/><path d=\"M8 7.99992C8.2545 7.99992 8.50382 8.01506 8.7474 8.04484L8.58594 9.36841C8.39687 9.34527 8.20127 9.33325 8 9.33325C5.8465 9.33325 4.25915 10.7274 3.78646 12.6666H10V13.9999H2.26758L2.33594 13.2708C2.61081 10.3473 4.82817 7.99992 8 7.99992Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8 1.33325C9.65687 1.33325 11 2.6764 11 4.33325C11 5.99011 9.65687 7.33325 8 7.33325C6.34315 7.33325 5 5.99011 5 4.33325C5 2.6764 6.34315 1.33325 8 1.33325ZM8 2.66659C7.07953 2.66659 6.33333 3.41278 6.33333 4.33325C6.33333 5.25373 7.07953 5.99992 8 5.99992C8.92047 5.99992 9.66667 5.25373 9.66667 4.33325C9.66667 3.41278 8.92047 2.66659 8 2.66659Z\" fill=\"currentColor\"/></g>"
|
||||
},
|
||||
"agent-v2-plan": {
|
||||
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M17 0C17.5523 0 18 0.447715 18 1V6C18 6.55228 17.5523 7 17 7H12C11.4477 7 11 6.55228 11 6V4.5H6.94629C5.92438 4.50039 5.56101 5.85276 6.44531 6.36523L12.5576 9.90332C15.2116 11.4402 14.1206 15.4996 11.0537 15.5H7V17C7 17.5523 6.55228 18 6 18H1C0.447715 18 0 17.5523 0 17V12C0 11.4477 0.447715 11 1 11H6C6.55228 11 7 11.4477 7 12V13.5H11.0537C12.0756 13.4996 12.4394 12.1472 11.5557 11.6348L5.44336 8.09668C2.789 6.55983 3.87917 2.50039 6.94629 2.5H11V1C11 0.447715 11.4477 0 12 0H17ZM2 16H5V13H2V16ZM13 5H16V2H13V5Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"agent-v2-prompt-insert": {
|
||||
"body": "<g fill=\"none\"><path d=\"M2.91669 1.16669C1.95019 1.16669 1.16669 1.95019 1.16669 2.91669V11.0834C1.16669 12.0499 1.95019 12.8334 2.91669 12.8334H11.0834C12.0499 12.8334 12.8334 12.0499 12.8334 11.0834V2.91669C12.8334 1.95019 12.0499 1.16669 11.0834 1.16669H2.91669ZM2.33335 2.91669C2.33335 2.59452 2.59452 2.33335 2.91669 2.33335H11.0834C11.4055 2.33335 11.6667 2.59452 11.6667 2.91669V11.0834C11.6667 11.4055 11.4055 11.6667 11.0834 11.6667H2.91669C2.59452 11.6667 2.33335 11.4055 2.33335 11.0834V2.91669ZM5.67188 10.5L9.67186 3.50002H8.32815L4.32817 10.5H5.67188Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 14,
|
||||
"height": 14
|
||||
},
|
||||
"agent-v2-robot-3": {
|
||||
"body": "<g fill=\"none\"><path d=\"M6.25 6.875C6.82523 6.875 7.29167 7.34128 7.29167 7.91667V9.16667C7.29167 9.74205 6.82523 10.2083 6.25 10.2083C5.67477 10.2083 5.20833 9.74205 5.20833 9.16667V7.91667C5.20833 7.34128 5.67477 6.875 6.25 6.875Z\" fill=\"currentColor\"/><path d=\"M10.4167 6.875C10.992 6.875 11.4583 7.34135 11.4583 7.91667V9.16667C11.4583 9.74199 10.992 10.2083 10.4167 10.2083C9.84135 10.2083 9.375 9.74199 9.375 9.16667V7.91667C9.375 7.34135 9.84135 6.875 10.4167 6.875Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8.33333 0C9.13875 0 9.79167 0.652918 9.79167 1.45833C9.79167 2.02329 9.46964 2.51173 8.99984 2.75391V3.33822C9.38912 3.34279 9.77995 3.35006 10.175 3.36263C11.6983 3.41112 12.7377 3.42425 13.6401 3.90951C14.375 4.30477 15.0255 4.97655 15.3971 5.72347C15.5468 6.02442 15.6427 6.33532 15.7056 6.66667H15.8333C16.2936 6.66667 16.6667 7.03976 16.6667 7.5V10C16.6667 10.4602 16.2936 10.8333 15.8333 10.8333H15.8285C15.8235 11.2254 15.813 11.5735 15.7869 11.8831C15.7386 12.4571 15.6361 12.9628 15.3971 13.4432C15.0254 14.1901 14.3749 14.8619 13.6401 15.2572C12.7377 15.7424 11.6982 15.7556 10.175 15.804C8.93336 15.8436 7.73328 15.8436 6.4917 15.804C4.96843 15.7556 3.92896 15.7424 3.02653 15.2572C2.29178 14.8619 1.64121 14.1902 1.26953 13.4432C1.03058 12.9628 0.928072 12.4571 0.87972 11.8831C0.853642 11.5735 0.843216 11.2254 0.838216 10.8333H0.833333C0.373096 10.8333 0 10.4602 0 10V7.5C0 7.03976 0.373096 6.66667 0.833333 6.66667H0.9611C1.02392 6.33532 1.11984 6.02442 1.26953 5.72347C1.64119 4.97649 2.29177 4.30475 3.02653 3.90951C3.92895 3.42425 4.96837 3.41112 6.4917 3.36263C6.88671 3.35006 7.27754 3.34279 7.66683 3.33822V2.75391C7.19703 2.51173 6.875 2.02329 6.875 1.45833C6.875 0.652918 7.52792 0 8.33333 0ZM10.1213 5.02848C8.91522 4.9901 7.75142 4.9901 6.54541 5.02848C4.85908 5.08217 4.29323 5.12091 3.81592 5.3776C3.38476 5.60954 2.98015 6.02734 2.76204 6.46566C2.65217 6.68652 2.57959 6.96168 2.54069 7.4235C2.50069 7.89854 2.5 8.50363 2.5 9.37825V9.78841C2.5 10.663 2.50069 11.2681 2.54069 11.7432C2.57959 12.205 2.65215 12.4801 2.76204 12.701C2.98015 13.1393 3.38475 13.5571 3.81592 13.7891C4.29321 14.0458 4.85904 14.0845 6.54541 14.1382C7.75141 14.1766 8.91523 14.1766 10.1213 14.1382C11.8075 14.0845 12.3734 14.0458 12.8507 13.7891C13.2819 13.5572 13.6865 13.1394 13.9046 12.701C14.0145 12.4801 14.0871 12.205 14.126 11.7432C14.166 11.2681 14.1667 10.663 14.1667 9.78841V9.37825C14.1667 8.50363 14.166 7.89854 14.126 7.4235C14.0871 6.96168 14.0145 6.68652 13.9046 6.46566C13.6865 6.02729 13.2819 5.60951 12.8507 5.3776C12.3734 5.12091 11.8075 5.08217 10.1213 5.02848Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 17
|
||||
},
|
||||
"features-citations": {
|
||||
"body": "<g fill=\"none\"><path d=\"M1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12ZM7 11.9702V14.958H11.0356V11.2339H8.8125C8.78418 10.8185 8.85498 10.4173 9.0249 10.0303C9.35531 9.29395 10.002 8.77474 10.9648 8.47266V7C9.67155 7.25488 8.68506 7.79297 8.00537 8.61426C7.33512 9.43555 7 10.5542 7 11.9702ZM15.0391 10.0586C15.3695 9.29395 16.0114 8.7653 16.9648 8.47266V7C15.7093 7.25488 14.7323 7.78825 14.0337 8.6001C13.3446 9.41195 13 10.5353 13 11.9702V14.958H17.0356V11.2339H14.8125C14.7747 10.8563 14.8503 10.4645 15.0391 10.0586Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 24,
|
||||
@ -811,6 +833,16 @@
|
||||
"width": 24,
|
||||
"height": 24
|
||||
},
|
||||
"main-nav-roster": {
|
||||
"body": "<g fill=\"none\"><path d=\"M7.875 11.8125C7.875 13.1587 6.7837 14.25 5.4375 14.25C4.0913 14.25 3 13.1587 3 11.8125C3 10.4663 4.0913 9.375 5.4375 9.375C6.7837 9.375 7.875 10.4663 7.875 11.8125Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M2.25 15.75L5.625 14.25\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M1.5 11.25L3 10.875\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M5.41699 12.5625C5.83121 12.5625 6.16699 12.2267 6.16699 11.8125C6.16699 11.3983 5.83121 11.0625 5.41699 11.0625C5.00278 11.0625 4.66699 11.3983 4.66699 11.8125C4.66699 12.2267 5.00278 12.5625 5.41699 12.5625Z\" fill=\"currentColor\"/><path d=\"M13.125 11.25L12.4956 8.10292C12.4255 7.75237 12.1177 7.5 11.7601 7.5H11.625C10.7966 7.5 10.125 8.17155 10.125 9V12.3939C10.125 13.419 10.956 14.25 11.9811 14.25C12.2408 14.25 12.4976 14.3045 12.7349 14.41L15.75 15.75\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M10.875 7.5V3.75C10.875 2.92157 11.5466 2.25 12.375 2.25H12.5394C12.8836 2.25 13.1836 2.48422 13.267 2.81811L15.3332 11.0833L16.5 11.625\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M7.125 9.75V3C7.125 2.58579 6.78921 2.25 6.375 2.25H5.52089C5.14964 2.25 4.83426 2.5216 4.77919 2.88875L3.75 9.75\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-roster-active": {
|
||||
"body": "<g fill=\"none\"><path d=\"M11.9362 1.5C12.4177 1.50006 12.8411 1.81975 12.9727 2.28296L15.4651 11.061C15.5117 11.2251 15.6125 11.3687 15.7515 11.4675L16.1235 11.7319C16.3544 11.8963 16.4728 12.1769 16.4297 12.4571L15.9206 15.7669C15.8394 16.2948 15.2475 16.5708 14.7905 16.2942L12.8006 15.0893C12.6837 15.0186 12.5618 14.9546 12.4307 14.9165C12.2411 14.8615 12.0443 14.833 11.8462 14.833C10.6885 14.833 9.75 13.8945 9.75 12.7368V9.14722C9.75 8.23747 10.4875 7.5 11.3972 7.5C11.5688 7.5 11.7164 7.62113 11.7503 7.78928L12.3824 10.9483L12.4043 11.0215C12.4721 11.182 12.6457 11.2781 12.8233 11.2427C13.0009 11.2072 13.1242 11.0515 13.125 10.8772L13.1177 10.8017L12.4856 7.6428C12.3818 7.12384 11.9265 6.75002 11.3972 6.75C11.08 6.75 10.7771 6.81165 10.5 6.92359V2.93628C10.5 2.14312 11.1431 1.5 11.9362 1.5Z\" fill=\"currentColor\"/><path d=\"M2.28761 11.1211C2.263 11.2855 2.25026 11.4538 2.25026 11.625C2.25026 13.3862 3.59948 14.8313 5.32057 14.9854L3.0757 16.3674C2.65801 16.6245 2.10961 16.418 1.96534 15.9492L0.926773 12.5742C0.823558 12.2388 0.967018 11.876 1.27174 11.7019L2.28761 11.1211Z\" fill=\"currentColor\"/><path d=\"M6.42041 1.5C7.01664 1.5 7.49997 1.98337 7.49997 2.57959V8.81835C6.96373 8.4594 6.31878 8.25 5.62501 8.25C4.91854 8.25 4.26271 8.46705 3.7207 8.83815L5.01124 2.6455C5.15039 1.97822 5.73876 1.50007 6.42041 1.5Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M5.625 9C7.07475 9 8.25 10.1753 8.25 11.625C8.25 13.0747 7.07475 14.25 5.625 14.25C4.17525 14.25 3 13.0747 3 11.625C3 10.1753 4.17525 9 5.625 9ZM5.625 10.875C5.21078 10.875 4.875 11.2108 4.875 11.625C4.875 12.0392 5.21078 12.375 5.625 12.375C6.03921 12.375 6.375 12.0392 6.375 11.625C6.375 11.2108 6.03921 10.875 5.625 10.875Z\" fill=\"currentColor\"/></g>",
|
||||
"width": 18,
|
||||
"height": 18
|
||||
},
|
||||
"main-nav-studio": {
|
||||
"body": "<g fill=\"none\"><path d=\"M15.8206 2.0275C15.7973 1.82217 15.6238 1.66696 15.4171 1.66675C15.2104 1.66654 15.0365 1.82139 15.0128 2.02667C14.865 3.30836 14.1416 4.03176 12.8599 4.17959C12.6547 4.20326 12.4998 4.37719 12.5 4.58383C12.5003 4.79047 12.6554 4.96408 12.8608 4.98733C14.1243 5.13046 14.8978 5.84689 15.0117 7.12955C15.0304 7.33946 15.2064 7.50032 15.4171 7.50008C15.6278 7.49984 15.8035 7.33859 15.8217 7.12863C15.9311 5.86411 16.6973 5.09787 17.9619 4.98841C18.1718 4.97023 18.3331 4.79461 18.3333 4.58387C18.3336 4.37313 18.1728 4.19715 17.9628 4.17851C16.6802 4.06457 15.9637 3.29101 15.8206 2.0275Z\" fill=\"currentColor\"/><path d=\"M7.29167 9.16659C8.9025 9.16659 10.2083 7.86075 10.2083 6.24992C10.2083 4.63909 8.9025 3.33325 7.29167 3.33325C5.68084 3.33325 4.375 4.63909 4.375 6.24992C4.375 7.86075 5.68084 9.16659 7.29167 9.16659Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path d=\"M1.66699 16.6667C1.66699 13.9053 3.90557 11.6667 6.66699 11.6667H7.08366\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M9.16634 16.6666L10.833 10.8333H18.333L16.6663 16.6666H9.16634ZM9.16634 16.6666H5.83301\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></g>",
|
||||
"width": 20,
|
||||
@ -1295,9 +1327,9 @@
|
||||
"height": 24
|
||||
},
|
||||
"workflow-agent": {
|
||||
"body": "<g fill=\"none\"><g id=\"agent\"><g id=\"Vector\"><path d=\"M14.7401 5.80454C14.5765 4.77996 14.1638 3.79808 13.5306 2.97273C12.8973 2.14738 12.0648 1.48568 11.1185 1.06589C10.1722 0.646098 9.12632 0.461106 8.08751 0.546487C7.05582 0.624753 6.04548 0.966277 5.17744 1.53548C4.3094 2.09758 3.58366 2.88024 3.09272 3.79808C2.59466 4.70881 2.33852 5.7405 2.33852 6.7793V7.22756L1.25703 9.3692C1.04357 9.80322 1.22145 10.3368 1.65547 10.5574L2.3314 10.8989V12.3006C2.3314 12.82 2.53063 13.3038 2.90061 13.6738C3.2706 14.0367 3.75442 14.243 4.27382 14.243H6.01702V14.7624C6.01702 15.1538 6.3372 15.4739 6.72853 15.4739C7.11986 15.4739 7.44004 15.1538 7.44004 14.7624V13.7094C7.44004 13.2185 7.04159 12.82 6.55065 12.82H4.27382C4.13864 12.82 4.00345 12.7631 3.91095 12.6706C3.81846 12.5781 3.76154 12.4429 3.76154 12.3077V10.5716C3.76154 10.2301 3.56943 9.92417 3.2706 9.77476L2.77254 9.52573L3.66904 7.73984C3.72596 7.61889 3.76154 7.4837 3.76154 7.34851V6.77219C3.76154 5.96818 3.96076 5.17129 4.34498 4.4669C4.72919 3.76251 5.28417 3.15772 5.9601 2.7237C6.63603 2.28968 7.41158 2.02643 8.20847 1.96239C9.00536 1.89835 9.81648 2.04066 10.5493 2.36795C11.2822 2.69524 11.9225 3.20042 12.4135 3.84077C12.8973 4.47402 13.2246 5.23533 13.3456 6.02511C13.4665 6.81488 13.3954 7.63312 13.125 8.38731C12.8617 9.12017 12.4206 9.78187 11.8585 10.3084C11.6735 10.4792 11.5668 10.7139 11.5668 10.9701V14.7624C11.5668 15.1538 11.887 15.4739 12.2783 15.4739C12.6696 15.4739 12.9898 15.1538 12.9898 14.7624V11.1978C13.6515 10.5432 14.1567 9.73918 14.4697 8.87114C14.8184 7.89637 14.918 6.83623 14.7615 5.81165L14.7401 5.80454Z\" fill=\"currentColor\"/><path d=\"M10.8055 7.99599C10.8909 7.83234 10.962 7.66158 11.0189 7.4837H11.6522C12.0435 7.4837 12.3637 7.16352 12.3637 6.77219C12.3637 6.38086 12.0435 6.06068 11.6522 6.06068H11.0189C10.9691 5.8828 10.898 5.71204 10.8055 5.54839L11.2537 5.10014C11.5312 4.82266 11.5312 4.3744 11.2537 4.09692C10.9762 3.81943 10.528 3.81943 10.2505 4.09692L9.80225 4.54517C9.6386 4.45267 9.46784 4.38863 9.28996 4.33171V3.69847C9.28996 3.30714 8.96978 2.98696 8.57845 2.98696C8.18712 2.98696 7.86694 3.30714 7.86694 3.69847V4.33171C7.68907 4.38152 7.5183 4.45267 7.35466 4.54517L6.90641 4.09692C6.62892 3.81943 6.18067 3.81943 5.90318 4.09692C5.62569 4.3744 5.62569 4.82266 5.90318 5.10014L6.35143 5.54839C6.26605 5.71204 6.1949 5.8828 6.13798 6.06068H5.50473C5.1134 6.06068 4.79323 6.38086 4.79323 6.77219C4.79323 7.16352 5.1134 7.4837 5.50473 7.4837H6.13798C6.18778 7.66158 6.25893 7.83234 6.35143 7.99599L5.90318 8.44424C5.62569 8.72172 5.62569 9.16997 5.90318 9.44746C6.04548 9.58976 6.22336 9.6538 6.40835 9.6538C6.59334 9.6538 6.77122 9.58265 6.91352 9.44746L7.36177 8.99921C7.52542 9.08459 7.69618 9.15574 7.87406 9.21267V9.84591C7.87406 10.2372 8.19424 10.5574 8.58557 10.5574C8.9769 10.5574 9.29708 10.2372 9.29708 9.84591V9.21267C9.47496 9.16286 9.64572 9.09171 9.80936 8.99921L10.2576 9.44746C10.3999 9.58976 10.5778 9.6538 10.7628 9.6538C10.9478 9.6538 11.1257 9.58265 11.268 9.44746C11.5454 9.16997 11.5454 8.72172 11.268 8.44424L10.8197 7.99599H10.8055ZM7.44004 6.77219C7.44004 6.14606 7.94521 5.64089 8.57134 5.64089C9.19747 5.64089 9.70264 6.14606 9.70264 6.77219C9.70264 7.39832 9.19747 7.90349 8.57134 7.90349C7.94521 7.90349 7.44004 7.39832 7.44004 6.77219Z\" fill=\"currentColor\"/></g></g></g>",
|
||||
"width": 16,
|
||||
"height": 16
|
||||
"body": "<g fill=\"none\"><g id=\"agent\" transform=\"translate(2.5 1)\"><path d=\"M3.3178 20.9524V15.7184C3.31774 15.7136 3.31456 15.6845 3.28404 15.6468L3.24312 15.6069C1.26589 13.9971 0 11.5398 0 8.78813C0.000122972 3.93464 3.93464 0.000122969 8.78813 0C13.5146 0 17.3698 3.73066 17.5691 8.40858C17.5712 8.45846 17.5841 8.48161 17.5865 8.48531L19.3226 11.089C19.7937 11.7956 19.6309 12.7481 18.9513 13.2579L17.5998 14.2707C17.5851 14.2819 17.5763 14.2996 17.5763 14.3178V15.4237C17.576 17.2235 16.1176 18.682 14.3178 18.6822H13.2119C13.1798 18.6822 13.1537 18.7085 13.1536 18.7405V20.9524C13.1536 21.5309 12.6845 21.9999 12.1059 22C11.5274 22 11.0583 21.531 11.0583 20.9524V18.7405C11.0584 17.5513 12.0226 16.587 13.2119 16.587H14.3178C14.9604 16.5868 15.4808 16.0663 15.481 15.4237V14.3178C15.481 13.64 15.8006 13.0016 16.3424 12.595L17.3195 11.8614L15.8432 9.64853C15.603 9.28835 15.4913 8.88291 15.4749 8.49758C15.323 4.93639 12.3872 2.09524 8.78813 2.09524C5.09181 2.09536 2.09536 5.09181 2.09524 8.78813C2.09524 10.883 3.05647 12.7533 4.56594 13.9822C5.0571 14.3822 5.41299 15.0012 5.41304 15.7184V20.9524C5.41304 21.5309 4.94385 21.9998 4.36542 22C3.78684 22 3.3178 21.531 3.3178 20.9524Z\" fill=\"currentColor\"/><path d=\"M9.79012 6.5163L9.31429 5.27923C9.23058 5.06159 9.02158 4.91799 8.78836 4.91799C8.55514 4.91799 8.34614 5.06159 8.26243 5.27923L7.7866 6.5163C7.56194 7.10037 7.10038 7.56194 6.5163 7.78659L5.27923 8.26239C5.06159 8.34609 4.91799 8.55519 4.91799 8.78836C4.91799 9.02158 5.06159 9.23058 5.27923 9.31429L6.5163 9.79012C7.10037 10.0148 7.56194 10.4764 7.7866 11.0604L8.26243 12.2975C8.34614 12.5151 8.55514 12.6587 8.78836 12.6587C9.02158 12.6587 9.23058 12.5151 9.31429 12.2975L9.79012 11.0604C10.0148 10.4764 10.4764 10.0148 11.0604 9.79012L12.2975 9.31429C12.5151 9.23058 12.6587 9.02158 12.6587 8.78836C12.6587 8.55519 12.5151 8.34609 12.2975 8.26239L11.0604 7.78659C10.4764 7.56194 10.0148 7.10038 9.79012 6.5163Z\" fill=\"currentColor\"/></g></g>",
|
||||
"width": 24,
|
||||
"height": 24
|
||||
},
|
||||
"workflow-answer": {
|
||||
"body": "<g fill=\"none\"><g id=\"icons/answer\"><path id=\"Vector (Stroke)\" fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M3.50114 1.67701L10.5011 1.677C11.5079 1.677 12.3241 2.49311 12.3241 3.49992V9.35414C12.3241 10.3609 11.5079 11.177 10.5012 11.1771H8.9954L7.41734 12.4845C7.17339 12.6866 6.81987 12.6856 6.57708 12.4821L5.02026 11.1771H3.50114C2.49436 11.1771 1.67822 10.3608 1.67822 9.35414V3.49993C1.67822 2.49316 2.49437 1.67701 3.50114 1.67701ZM10.5011 2.9895L3.50114 2.98951C3.21924 2.98951 2.99072 3.21803 2.99072 3.49993V9.35414C2.99072 9.63601 3.21926 9.86455 3.50114 9.86455H5.04675C5.33794 9.86455 5.61984 9.96705 5.84302 10.1541L7.00112 11.1249L8.17831 10.1496C8.40069 9.96537 8.68041 9.86455 8.96916 9.86455H10.5011C10.5011 9.86455 10.5011 9.86455 10.5011 9.86455C10.783 9.8645 11.0116 9.63592 11.0116 9.35414V3.49992C11.0116 3.21806 10.7831 2.9895 10.5011 2.9895ZM9.06809 4.93171C9.32437 5.18799 9.32437 5.60351 9.06809 5.85979L7.02642 7.90146C6.77014 8.15774 6.35464 8.15774 6.09835 7.90146L5.22333 7.02646C4.96704 6.77019 4.96704 6.35467 5.22332 6.09839C5.4796 5.8421 5.89511 5.8421 6.15139 6.09837L6.56238 6.50935L8.14001 4.93171C8.3963 4.67543 8.81181 4.67543 9.06809 4.93171Z\" fill=\"currentColor\"/></g></g>",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export type IconifyJSON = {
|
||||
export interface IconifyJSON {
|
||||
prefix: string
|
||||
icons: Record<string, IconifyIcon>
|
||||
aliases?: Record<string, IconifyAlias>
|
||||
@ -7,7 +7,7 @@ export type IconifyJSON = {
|
||||
lastModified?: number
|
||||
}
|
||||
|
||||
export type IconifyIcon = {
|
||||
export interface IconifyIcon {
|
||||
body: string
|
||||
left?: number
|
||||
top?: number
|
||||
@ -18,11 +18,11 @@ export type IconifyIcon = {
|
||||
vFlip?: boolean
|
||||
}
|
||||
|
||||
export type IconifyAlias = {
|
||||
export interface IconifyAlias extends Omit<IconifyIcon, 'body'> {
|
||||
parent: string
|
||||
} & Omit<IconifyIcon, 'body'>
|
||||
}
|
||||
|
||||
export type IconifyInfo = {
|
||||
export interface IconifyInfo {
|
||||
prefix: string
|
||||
name: string
|
||||
total: number
|
||||
@ -40,11 +40,11 @@ export type IconifyInfo = {
|
||||
palette?: boolean
|
||||
}
|
||||
|
||||
export type IconifyMetaData = {
|
||||
export interface IconifyMetaData {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type IconifyChars = {
|
||||
export interface IconifyChars {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
const chars = require('./chars.json')
|
||||
const icons = require('./icons.json')
|
||||
const info = require('./info.json')
|
||||
const metadata = require('./metadata.json')
|
||||
const chars = require('./chars.json')
|
||||
|
||||
module.exports = { icons, info, metadata, chars }
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import chars from './chars.json' with { type: 'json' }
|
||||
import icons from './icons.json' with { type: 'json' }
|
||||
import info from './info.json' with { type: 'json' }
|
||||
import metadata from './metadata.json' with { type: 'json' }
|
||||
import chars from './chars.json' with { type: 'json' }
|
||||
|
||||
export { chars, icons, info, metadata }
|
||||
export { icons, info, metadata, chars }
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"prefix": "custom-vender",
|
||||
"name": "Dify Custom Vender",
|
||||
"total": 319,
|
||||
"total": 326,
|
||||
"version": "0.0.0-private",
|
||||
"author": {
|
||||
"name": "LangGenius, Inc.",
|
||||
@ -13,12 +13,12 @@
|
||||
"url": "https://github.com/langgenius/dify/blob/main/LICENSE"
|
||||
},
|
||||
"samples": [
|
||||
"features-citations",
|
||||
"features-content-moderation",
|
||||
"features-document",
|
||||
"features-folder-upload",
|
||||
"features-love-message",
|
||||
"features-message-fast"
|
||||
"agent-v2-access-point",
|
||||
"agent-v2-end-user-auth",
|
||||
"agent-v2-plan",
|
||||
"agent-v2-prompt-insert",
|
||||
"agent-v2-robot-3",
|
||||
"features-citations"
|
||||
],
|
||||
"palette": false
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import Loading from '@/app/components/base/loading'
|
||||
import { redirect, usePathname } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
|
||||
const datasetOperatorRedirectRoutes = ['/', '/apps', '/app', '/snippets', '/explore', '/tools', '/integrations'] as const
|
||||
const datasetOperatorRedirectRoutes = ['/', '/apps', '/app', '/snippets', '/roster', '/explore', '/tools', '/integrations'] as const
|
||||
|
||||
const isPathUnderRoute = (pathname: string, route: string) => pathname === route || pathname.startsWith(`${route}/`)
|
||||
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ agentId: string }>
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: PageProps) {
|
||||
const { agentId } = await params
|
||||
|
||||
return <AgentDetailPage agentId={agentId} section="access" />
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ agentId: string }>
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: PageProps) {
|
||||
const { agentId } = await params
|
||||
|
||||
return <AgentDetailPage agentId={agentId} section="configure" />
|
||||
}
|
||||
20
web/app/(commonLayout)/roster/agent/[agentId]/layout.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { AgentDetailLayout } from '@/features/agent-v2/agent-detail/layout'
|
||||
|
||||
type LayoutProps = {
|
||||
children: ReactNode
|
||||
params: Promise<{ agentId: string }>
|
||||
}
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
params,
|
||||
}: LayoutProps) {
|
||||
const { agentId } = await params
|
||||
|
||||
return (
|
||||
<AgentDetailLayout agentId={agentId}>
|
||||
{children}
|
||||
</AgentDetailLayout>
|
||||
)
|
||||
}
|
||||
13
web/app/(commonLayout)/roster/agent/[agentId]/logs/page.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ agentId: string }>
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: PageProps) {
|
||||
const { agentId } = await params
|
||||
|
||||
return <AgentDetailPage agentId={agentId} section="logs" />
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ agentId: string }>
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: PageProps) {
|
||||
const { agentId } = await params
|
||||
|
||||
return <AgentDetailPage agentId={agentId} section="monitoring" />
|
||||
}
|
||||
13
web/app/(commonLayout)/roster/agent/[agentId]/page.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { redirect } from '@/next/navigation'
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ agentId: string }>
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: PageProps) {
|
||||
const { agentId } = await params
|
||||
|
||||
redirect(`/roster/agent/${agentId}/configure`)
|
||||
}
|
||||
5
web/app/(commonLayout)/roster/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import RosterPage from '@/features/agent-v2/roster/page'
|
||||
|
||||
export default function Page() {
|
||||
return <RosterPage />
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
import { redirect } from '@/next/navigation'
|
||||
|
||||
const Page = async (props: {
|
||||
params: Promise<{ snippetId: string }>
|
||||
|
||||
@ -186,6 +186,20 @@ describe('SettingBuiltInTool', () => {
|
||||
expect(screen.getByTestId('mock-form')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render a masked drawer with balanced vertical offsets', async () => {
|
||||
const { baseElement } = renderComponent()
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('mock-form')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(baseElement.querySelector('.bg-background-overlay')).toBeInTheDocument()
|
||||
const drawerPopup = baseElement.querySelector('[role="dialog"]')
|
||||
expect(drawerPopup).toHaveClass(
|
||||
'data-[swipe-direction=right]:top-6',
|
||||
'data-[swipe-direction=right]:bottom-6',
|
||||
)
|
||||
})
|
||||
|
||||
it('should call onSave with updated values when save button clicked', async () => {
|
||||
const { onSave } = renderComponent()
|
||||
await waitFor(() => expect(screen.getByTestId('mock-form')).toBeInTheDocument())
|
||||
|
||||
@ -181,9 +181,9 @@ const SettingBuiltInTool: FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className="bg-transparent" />
|
||||
<DrawerBackdrop className="bg-background-overlay" />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-6 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-6 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
{isLoading && <Loading type="app" />}
|
||||
{!isLoading && (
|
||||
|
||||
@ -19,6 +19,7 @@ import { useInfiniteDatasets } from '@/service/knowledge/use-dataset'
|
||||
|
||||
type ISelectDataSetProps = {
|
||||
isShow: boolean
|
||||
modal?: boolean
|
||||
onClose: () => void
|
||||
selectedIds: string[]
|
||||
onSelect: (dataSet: DataSet[]) => void
|
||||
@ -26,6 +27,7 @@ type ISelectDataSetProps = {
|
||||
|
||||
const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||
isShow,
|
||||
modal,
|
||||
onClose,
|
||||
selectedIds,
|
||||
onSelect,
|
||||
@ -90,8 +92,8 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||
}, [handleClose])
|
||||
|
||||
return (
|
||||
<Dialog open={isShow} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="w-100 overflow-hidden">
|
||||
<Dialog modal={modal} open={isShow} onOpenChange={handleOpenChange}>
|
||||
<DialogContent backdropProps={{ forceRender: true }} className="w-100 overflow-hidden">
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary">
|
||||
{t('feature.dataSet.selectTitle', { ns: 'appDebug' })}
|
||||
</DialogTitle>
|
||||
|
||||
@ -30,6 +30,7 @@ import { RetrievalChangeTip, RetrievalSection } from './retrieval-section'
|
||||
|
||||
type SettingsModalProps = {
|
||||
currentDataset: DataSet
|
||||
height?: string
|
||||
onCancel: () => void
|
||||
onSave: (newDataset: DataSet) => void
|
||||
}
|
||||
@ -44,6 +45,7 @@ const labelClass = `
|
||||
|
||||
const SettingsModal: FC<SettingsModalProps> = ({
|
||||
currentDataset,
|
||||
height = 'calc(100vh - 72px)',
|
||||
onCancel,
|
||||
onSave,
|
||||
}) => {
|
||||
@ -186,9 +188,9 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex w-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl"
|
||||
className="flex min-h-0 w-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl"
|
||||
style={{
|
||||
height: 'calc(100vh - 72px)',
|
||||
height,
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
|
||||
@ -417,15 +417,17 @@ describe('List', () => {
|
||||
expect(screen.getByRole('button', { name: 'common.operation.create' }))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render link to snippets before the create button', () => {
|
||||
it('should render sort filter before search and the snippets link', () => {
|
||||
renderList()
|
||||
|
||||
const sortButton = screen.getByRole('button', { name: 'Sort by Last modified' })
|
||||
const searchInput = screen.getByRole('searchbox', { name: 'app.gotoAnything.actions.searchApplications' })
|
||||
const snippetsLink = screen.getByRole('link', { name: 'app.studio.viewSnippets' })
|
||||
const createButton = screen.getByRole('button', { name: 'common.operation.create' })
|
||||
|
||||
expect(snippetsLink).toHaveAttribute('href', '/snippets')
|
||||
expect(sortButton.compareDocumentPosition(snippetsLink) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
expect(sortButton.compareDocumentPosition(searchInput) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
expect(searchInput.compareDocumentPosition(snippetsLink) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
expect(snippetsLink.compareDocumentPosition(createButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
})
|
||||
|
||||
|
||||
@ -62,6 +62,7 @@ export function AppListHeaderFilters({
|
||||
showLeadingIcon={false}
|
||||
/>
|
||||
<CreatorsFilter value={creatorIDs} onChange={onCreatorIDsChange} />
|
||||
<AppSortFilter value={sortBy} onChange={onSortByChange} />
|
||||
<SearchInput
|
||||
className="w-50"
|
||||
value={keywords}
|
||||
@ -70,7 +71,6 @@ export function AppListHeaderFilters({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<AppSortFilter value={sortBy} onChange={onSortByChange} />
|
||||
<Link
|
||||
href="/snippets"
|
||||
className="flex h-8 items-center rounded-lg px-3 text-sm font-semibold text-text-secondary outline-hidden hover:bg-state-base-hover hover:text-text-primary focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
|
||||
@ -8,10 +8,18 @@ import { useHover } from 'ahooks'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { init } from 'emoji-mart'
|
||||
import * as React from 'react'
|
||||
import { useRef } from 'react'
|
||||
import { useRef, useSyncExternalStore } from 'react'
|
||||
|
||||
init({ data })
|
||||
|
||||
const subscribeHydrationState = () => () => {}
|
||||
|
||||
const useIsHydrated = () => useSyncExternalStore(
|
||||
subscribeHydrationState,
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
|
||||
type AppIconProps = {
|
||||
size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' | 'xl' | 'xxl'
|
||||
rounded?: boolean
|
||||
@ -105,9 +113,20 @@ const AppIcon: FC<AppIconProps> = ({
|
||||
}) => {
|
||||
const isValidImageIcon = iconType === 'image' && imageUrl
|
||||
const emojiIcon = (icon && icon !== '') ? icon : '🤖'
|
||||
const Icon = <em-emoji key={emojiIcon} id={emojiIcon} />
|
||||
const isHydrated = useIsHydrated()
|
||||
const Icon = isHydrated ? <em-emoji key={emojiIcon} id={emojiIcon} /> : emojiIcon
|
||||
const wrapperRef = useRef<HTMLSpanElement>(null)
|
||||
const isHovering = useHover(wrapperRef)
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
|
||||
if (!onClick)
|
||||
return
|
||||
|
||||
if (event.key !== 'Enter' && event.key !== ' ')
|
||||
return
|
||||
|
||||
event.preventDefault()
|
||||
onClick()
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
@ -115,6 +134,9 @@ const AppIcon: FC<AppIconProps> = ({
|
||||
className={cn(appIconVariants({ size, rounded }), className)}
|
||||
style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }}
|
||||
onClick={onClick}
|
||||
onKeyDown={onClick ? handleKeyDown : undefined}
|
||||
role={onClick ? 'button' : undefined}
|
||||
tabIndex={onClick ? 0 : undefined}
|
||||
>
|
||||
{
|
||||
isValidImageIcon
|
||||
|
||||
@ -57,6 +57,7 @@ const renderPanel = (props: Partial<{
|
||||
onClose: () => void
|
||||
inWorkflow: boolean
|
||||
showFileUpload: boolean
|
||||
showAnnotationReply: boolean
|
||||
}> = {}) => {
|
||||
return render(
|
||||
<FeaturesProvider features={defaultFeatures}>
|
||||
@ -68,6 +69,7 @@ const renderPanel = (props: Partial<{
|
||||
onClose={props.onClose ?? vi.fn()}
|
||||
inWorkflow={props.inWorkflow}
|
||||
showFileUpload={props.showFileUpload}
|
||||
showAnnotationReply={props.showAnnotationReply}
|
||||
/>
|
||||
</FeaturesProvider>,
|
||||
)
|
||||
@ -191,5 +193,11 @@ describe('NewFeaturePanel', () => {
|
||||
|
||||
expect(screen.queryByText(/feature\.annotation\.title/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render AnnotationReply when showAnnotationReply is false', () => {
|
||||
renderPanel({ isChatMode: true, inWorkflow: false, showAnnotationReply: false })
|
||||
|
||||
expect(screen.queryByText(/feature\.annotation\.title/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
@ -26,9 +27,14 @@ type Props = Readonly<{
|
||||
onClose: () => void
|
||||
inWorkflow?: boolean
|
||||
showFileUpload?: boolean
|
||||
showModeration?: boolean
|
||||
showAnnotationReply?: boolean
|
||||
promptVariables?: PromptVariable[]
|
||||
workflowVariables?: InputVar[]
|
||||
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
|
||||
title?: ReactNode
|
||||
description?: ReactNode
|
||||
drawerClassName?: string
|
||||
}>
|
||||
|
||||
const NewFeaturePanel = ({
|
||||
@ -39,9 +45,14 @@ const NewFeaturePanel = ({
|
||||
onClose,
|
||||
inWorkflow = true,
|
||||
showFileUpload = true,
|
||||
showModeration = true,
|
||||
showAnnotationReply = true,
|
||||
promptVariables,
|
||||
workflowVariables,
|
||||
onAutoAddPromptVariable,
|
||||
title,
|
||||
description,
|
||||
drawerClassName,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
|
||||
@ -52,13 +63,14 @@ const NewFeaturePanel = ({
|
||||
show={show}
|
||||
onClose={onClose}
|
||||
inWorkflow={inWorkflow}
|
||||
className={drawerClassName}
|
||||
>
|
||||
<div className="flex h-full grow flex-col">
|
||||
{/* header */}
|
||||
<div className="flex shrink-0 justify-between p-4 pb-3">
|
||||
<div>
|
||||
<div className="system-xl-semibold text-text-primary">{t('common.features', { ns: 'workflow' })}</div>
|
||||
<div className="body-xs-regular text-text-tertiary">{t('common.featuresDescription', { ns: 'workflow' })}</div>
|
||||
<div className="system-xl-semibold text-text-primary">{title ?? t('common.features', { ns: 'workflow' })}</div>
|
||||
<div className="body-xs-regular text-text-tertiary">{description ?? t('common.featuresDescription', { ns: 'workflow' })}</div>
|
||||
</div>
|
||||
<DrawerCloseButton
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
@ -93,8 +105,8 @@ const NewFeaturePanel = ({
|
||||
{isChatMode && (
|
||||
<Citation disabled={disabled} onChange={onChange} />
|
||||
)}
|
||||
{(isChatMode || !inWorkflow) && <Moderation disabled={disabled} onChange={onChange} />}
|
||||
{!inWorkflow && isChatMode && (
|
||||
{showModeration && (isChatMode || !inWorkflow) && <Moderation disabled={disabled} onChange={onChange} />}
|
||||
{showAnnotationReply && !inWorkflow && isChatMode && (
|
||||
<AnnotationReply disabled={disabled} onChange={onChange} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -302,6 +302,37 @@ describe('ModerationSettingModal', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('should save the latest preset response when content textarea changes', async () => {
|
||||
const data: ModerationConfig = {
|
||||
...defaultData,
|
||||
config: {
|
||||
keywords: 'bad',
|
||||
inputs_config: { enabled: true, preset_response: 'blocked' },
|
||||
outputs_config: { enabled: false, preset_response: '' },
|
||||
},
|
||||
}
|
||||
await renderModal(
|
||||
<ModerationSettingModal
|
||||
data={data}
|
||||
onCancel={vi.fn()}
|
||||
onSave={onSave}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByRole('textbox', { name: /feature\.moderation\.modal\.content\.preset/ }), {
|
||||
target: { value: 'updated blocked response' },
|
||||
})
|
||||
fireEvent.click(screen.getByText(/operation\.save/))
|
||||
|
||||
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
inputs_config: expect.objectContaining({
|
||||
preset_response: 'updated blocked response',
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
})
|
||||
|
||||
it('should show api selector when api type is selected', async () => {
|
||||
await renderModal(
|
||||
<ModerationSettingModal
|
||||
@ -702,7 +733,10 @@ describe('ModerationSettingModal', () => {
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalled()
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider' })
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: 'provider',
|
||||
onCancelCallback: expect.any(Function),
|
||||
})
|
||||
})
|
||||
|
||||
it('should not save when OpenAI type is selected but not configured', async () => {
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC } from 'react'
|
||||
import type { ModerationContentConfig } from '@/models/debug'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type ModerationContentProps = {
|
||||
@ -19,57 +20,71 @@ const ModerationContent: FC<ModerationContentProps> = ({
|
||||
onConfigChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [presetResponse, setPresetResponse] = useState(config.preset_response || '')
|
||||
|
||||
const handleConfigChange = (field: string, value: boolean | string) => {
|
||||
if (field === 'preset_response' && typeof value === 'string')
|
||||
value = value.slice(0, 100)
|
||||
onConfigChange({ ...config, [field]: value })
|
||||
|
||||
onConfigChange({
|
||||
...config,
|
||||
preset_response: field === 'preset_response' ? value as string : presetResponse,
|
||||
[field]: value,
|
||||
})
|
||||
}
|
||||
|
||||
const handlePresetResponseChange = (value: string) => {
|
||||
const nextValue = value.slice(0, 100)
|
||||
setPresetResponse(nextValue)
|
||||
handleConfigChange('preset_response', nextValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="rounded-lg border border-components-panel-border bg-components-panel-bg">
|
||||
<div className="flex h-10 items-center justify-between rounded-lg px-3">
|
||||
<div className="shrink-0 text-sm font-medium text-text-primary">{title}</div>
|
||||
<div className="flex grow items-center justify-end">
|
||||
{
|
||||
info && (
|
||||
<div className="mr-2 truncate text-xs text-text-tertiary" title={info}>{info}</div>
|
||||
)
|
||||
}
|
||||
<Switch
|
||||
size="lg"
|
||||
checked={config.enabled}
|
||||
onCheckedChange={v => handleConfigChange('enabled', v)}
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg shadow-xs">
|
||||
<div className="flex min-h-10 items-center gap-2 px-3 py-2">
|
||||
<div className="min-w-0 flex-1 system-sm-medium text-text-secondary">{title}</div>
|
||||
<div className="flex min-w-0 shrink-0 items-center justify-end">
|
||||
{
|
||||
info && (
|
||||
<div className="mr-2 truncate system-xs-regular text-text-tertiary" title={info}>{info}</div>
|
||||
)
|
||||
}
|
||||
<Switch
|
||||
checked={config.enabled}
|
||||
onCheckedChange={v => handleConfigChange('enabled', v)}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
config.enabled && showPreset && (
|
||||
<div className="rounded-lg bg-components-panel-bg px-3 pt-1 pb-3">
|
||||
<div className="flex h-8 items-center justify-between text-[13px] font-medium text-text-secondary">
|
||||
</div>
|
||||
{
|
||||
config.enabled && showPreset && (
|
||||
<div className="px-3 pt-0.5 pb-3">
|
||||
<div className="flex h-8 items-center justify-between gap-2">
|
||||
<span className="system-2xs-medium-uppercase text-text-secondary">
|
||||
{t('feature.moderation.modal.content.preset', { ns: 'appDebug' })}
|
||||
<span className="text-xs font-normal text-text-tertiary">{t('feature.moderation.modal.content.supportMarkdown', { ns: 'appDebug' })}</span>
|
||||
</div>
|
||||
{/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */}
|
||||
<div className="relative h-20">
|
||||
<Textarea
|
||||
aria-label={t('feature.moderation.modal.content.preset', { ns: 'appDebug' }) as string}
|
||||
value={config.preset_response || ''}
|
||||
className="size-full resize-none pb-8"
|
||||
placeholder={t('feature.moderation.modal.content.placeholder', { ns: 'appDebug' }) || ''}
|
||||
onValueChange={value => handleConfigChange('preset_response', value)}
|
||||
/>
|
||||
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 text-xs font-medium text-text-quaternary">
|
||||
<span>{(config.preset_response || '').length}</span>
|
||||
/
|
||||
<span className="text-text-tertiary">100</span>
|
||||
</div>
|
||||
</span>
|
||||
<span className="flex shrink-0 items-center gap-0.5 rounded bg-background-section px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
||||
<span className="i-ri-markdown-line size-3" aria-hidden />
|
||||
{t('feature.moderation.modal.content.supportMarkdown', { ns: 'appDebug' })}
|
||||
</span>
|
||||
</div>
|
||||
{/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */}
|
||||
<div className="relative h-20">
|
||||
<Textarea
|
||||
aria-label={t('feature.moderation.modal.content.preset', { ns: 'appDebug' }) as string}
|
||||
value={presetResponse}
|
||||
className="size-full resize-none pb-8"
|
||||
placeholder={t('feature.moderation.modal.content.placeholder', { ns: 'appDebug' }) || ''}
|
||||
onValueChange={handlePresetResponseChange}
|
||||
/>
|
||||
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 system-2xs-medium-uppercase text-text-quaternary">
|
||||
<span>{presetResponse.length}</span>
|
||||
/
|
||||
<span className="text-text-tertiary">100</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { FC } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import type { CodeBasedExtensionItem } from '@/models/common'
|
||||
import type { ModerationConfig, ModerationContentConfig } from '@/models/debug'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
@ -6,7 +6,7 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { ApiBasedExtensionSelector } from '@/app/components/header/account-setting/api-based-extension-page/selector'
|
||||
@ -27,6 +27,27 @@ type Provider = {
|
||||
form_schema?: CodeBasedExtensionItem['form_schema']
|
||||
}
|
||||
|
||||
function ProviderIcon({ type }: { type: string }) {
|
||||
if (type === 'openai_moderation')
|
||||
return <span className="i-ri-openai-fill size-4 text-text-secondary" aria-hidden />
|
||||
|
||||
if (type === 'keywords')
|
||||
return <span className="i-ri-search-line size-4 text-util-colors-green-green-600" aria-hidden />
|
||||
|
||||
return <span className="i-ri-image-line size-4 text-util-colors-violet-violet-600" aria-hidden />
|
||||
}
|
||||
|
||||
function LabeledDivider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<span className="shrink-0 system-xs-medium-uppercase text-text-tertiary">
|
||||
{children}
|
||||
</span>
|
||||
<Divider bgStyle="gradient" className="my-0 h-px flex-1" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ModerationSettingModalProps = {
|
||||
data: ModerationConfig
|
||||
onCancel: () => void
|
||||
@ -41,12 +62,27 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const locale = useLocale()
|
||||
const { data: modelProviders, isPending: isLoading } = useModelProviders()
|
||||
const { data: modelProviders, isPending: isLoading, refetch: refetchModelProviders } = useModelProviders()
|
||||
const localeDataRef = useRef<ModerationConfig>(data)
|
||||
const [localeData, setLocaleData] = useState<ModerationConfig>(data)
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const updateLocaleData = useCallback((
|
||||
update: ModerationConfig | ((current: ModerationConfig) => ModerationConfig),
|
||||
options: { render?: boolean } = {},
|
||||
) => {
|
||||
const nextLocaleData = typeof update === 'function'
|
||||
? update(localeDataRef.current)
|
||||
: update
|
||||
|
||||
localeDataRef.current = nextLocaleData
|
||||
|
||||
if (options.render !== false)
|
||||
setLocaleData(nextLocaleData)
|
||||
}, [])
|
||||
const handleOpenSettingsModal = () => {
|
||||
openIntegrationsSetting({
|
||||
payload: ACCOUNT_SETTING_TAB.PROVIDER,
|
||||
onCancelCallback: refetchModelProviders,
|
||||
})
|
||||
}
|
||||
const { data: codeBasedExtensionList } = useCodeBasedExtensions('moderation')
|
||||
@ -85,20 +121,20 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
const currentProvider = providers.find(provider => provider.key === localeData.type)
|
||||
|
||||
const handleDataTypeChange = (type: string) => {
|
||||
let config: undefined | Record<string, any>
|
||||
let config: undefined | Record<string, string>
|
||||
const currProvider = providers.find(provider => provider.key === type)
|
||||
|
||||
if (systemTypes.findIndex(t => t === type) < 0 && currProvider?.form_schema) {
|
||||
config = currProvider?.form_schema.reduce((prev, next) => {
|
||||
prev[next.variable] = next.default
|
||||
return prev
|
||||
}, {} as Record<string, any>)
|
||||
}, {} as Record<string, string>)
|
||||
}
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
updateLocaleData(current => ({
|
||||
...current,
|
||||
type,
|
||||
config,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
const handleDataKeywordsChange = (value: string) => {
|
||||
@ -111,43 +147,46 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
return prev
|
||||
}, [])
|
||||
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
updateLocaleData(current => ({
|
||||
...current,
|
||||
config: {
|
||||
...localeData.config,
|
||||
...current.config,
|
||||
keywords: arr.slice(0, 100).join('\n'),
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
const handleDataContentChange = (contentType: string, contentConfig: ModerationContentConfig) => {
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
const previousContentConfig = localeDataRef.current.config?.[contentType] as ModerationContentConfig | undefined
|
||||
const shouldRender = previousContentConfig?.enabled !== contentConfig.enabled
|
||||
|
||||
updateLocaleData(current => ({
|
||||
...current,
|
||||
config: {
|
||||
...localeData.config,
|
||||
...current.config,
|
||||
[contentType]: contentConfig,
|
||||
},
|
||||
})
|
||||
}), { render: shouldRender })
|
||||
}
|
||||
|
||||
const handleDataApiBasedChange = (apiBasedExtensionId: string) => {
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
updateLocaleData(current => ({
|
||||
...current,
|
||||
config: {
|
||||
...localeData.config,
|
||||
...current.config,
|
||||
api_based_extension_id: apiBasedExtensionId,
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
const handleDataExtraChange = (extraValue: Record<string, string>) => {
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
updateLocaleData(current => ({
|
||||
...current,
|
||||
config: {
|
||||
...localeData.config,
|
||||
...current.config,
|
||||
...extraValue,
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
const formatData = (originData: ModerationConfig) => {
|
||||
@ -179,115 +218,116 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
const currentLocaleData = localeDataRef.current
|
||||
const providerForSave = providers.find(provider => provider.key === currentLocaleData.type)
|
||||
|
||||
/* v8 ignore next -- UI-invariant guard: same condition is used in Save button disabled logic, so when true handleSave has no user-triggerable invocation path. @preserve */
|
||||
if (localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured)
|
||||
if (currentLocaleData.type === 'openai_moderation' && !isOpenAIProviderConfigured)
|
||||
return
|
||||
|
||||
if (!localeData.config?.inputs_config?.enabled && !localeData.config?.outputs_config?.enabled) {
|
||||
if (!currentLocaleData.config?.inputs_config?.enabled && !currentLocaleData.config?.outputs_config?.enabled) {
|
||||
toast.error(t('feature.moderation.modal.content.condition', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.type === 'keywords' && !localeData.config.keywords) {
|
||||
if (currentLocaleData.type === 'keywords' && !currentLocaleData.config.keywords) {
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'keywords' : '关键词' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.type === 'api' && !localeData.config.api_based_extension_id) {
|
||||
if (currentLocaleData.type === 'api' && !currentLocaleData.config.api_based_extension_id) {
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (systemTypes.findIndex(t => t === localeData.type) < 0 && currentProvider?.form_schema) {
|
||||
for (let i = 0; i < currentProvider.form_schema.length; i++) {
|
||||
if (!localeData.config?.[currentProvider.form_schema[i]!.variable] && currentProvider.form_schema[i]!.required) {
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i]!.label['en-US'] : currentProvider.form_schema[i]!.label['zh-Hans'] }))
|
||||
if (systemTypes.findIndex(t => t === currentLocaleData.type) < 0 && providerForSave?.form_schema) {
|
||||
for (let i = 0; i < providerForSave.form_schema.length; i++) {
|
||||
if (!currentLocaleData.config?.[providerForSave.form_schema[i]!.variable] && providerForSave.form_schema[i]!.required) {
|
||||
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? providerForSave.form_schema[i]!.label['en-US'] : providerForSave.form_schema[i]!.label['zh-Hans'] }))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (localeData.config.inputs_config?.enabled && !localeData.config.inputs_config.preset_response && localeData.type !== 'api') {
|
||||
if (currentLocaleData.config.inputs_config?.enabled && !currentLocaleData.config.inputs_config.preset_response && currentLocaleData.type !== 'api') {
|
||||
toast.error(t('feature.moderation.modal.content.errorMessage', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.config.outputs_config?.enabled && !localeData.config.outputs_config.preset_response && localeData.type !== 'api') {
|
||||
if (currentLocaleData.config.outputs_config?.enabled && !currentLocaleData.config.outputs_config.preset_response && currentLocaleData.type !== 'api') {
|
||||
toast.error(t('feature.moderation.modal.content.errorMessage', { ns: 'appDebug' }))
|
||||
return
|
||||
}
|
||||
|
||||
onSave(formatData(localeData))
|
||||
onSave(formatData(currentLocaleData))
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open>
|
||||
<DialogContent className="mt-14! w-[600px]! max-w-none! border-none p-6! text-left align-middle">
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="title-2xl-semi-bold text-text-primary">{t('feature.moderation.modal.title', { ns: 'appDebug' })}</div>
|
||||
<DialogContent className="mt-14! w-[600px]! max-w-none! overflow-hidden border-[0.5px]! border-components-panel-border! p-0! text-left align-middle">
|
||||
<div className="flex items-start gap-2 px-6 pt-6 pr-14 pb-3">
|
||||
<div className="title-2xl-semi-bold text-text-primary">
|
||||
{t('feature.moderation.modal.title', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
className="cursor-pointer border-none bg-transparent p-1 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
className="absolute top-5 right-5 flex size-8 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<span className="i-ri-close-line size-4 text-text-tertiary" aria-hidden="true" />
|
||||
<span className="i-ri-close-line size-[18px]" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<div className="text-sm/9 font-medium text-text-primary">
|
||||
{t('feature.moderation.modal.provider.title', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2.5">
|
||||
{
|
||||
providers.map(provider => (
|
||||
<div
|
||||
<div className="flex flex-col gap-4 px-6 py-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="system-sm-medium text-text-secondary">
|
||||
{t('feature.moderation.modal.provider.title', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{providers.map(provider => (
|
||||
<button
|
||||
type="button"
|
||||
key={provider.key}
|
||||
className={cn(
|
||||
'flex h-8 cursor-default items-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 system-sm-regular text-text-secondary',
|
||||
localeData.type !== provider.key && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
||||
localeData.type === provider.key && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg system-sm-medium shadow-xs',
|
||||
'flex min-h-[68px] flex-col items-start justify-center gap-1.5 rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg px-3 py-2 text-left text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
|
||||
localeData.type !== provider.key && 'hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
||||
localeData.type === provider.key && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs',
|
||||
localeData.type === 'openai_moderation' && provider.key === 'openai_moderation' && !isOpenAIProviderConfigured && 'text-text-disabled',
|
||||
)}
|
||||
onClick={() => handleDataTypeChange(provider.key)}
|
||||
>
|
||||
<div className={cn(
|
||||
'mr-2 size-4 rounded-full border border-components-radio-border bg-components-radio-bg shadow-xs',
|
||||
localeData.type === provider.key && 'border-[5px] border-components-radio-border-checked',
|
||||
)}
|
||||
>
|
||||
<div className="flex size-8 items-center justify-center rounded-lg border-[0.5px] border-divider-regular bg-background-default-dodge">
|
||||
<ProviderIcon type={provider.key} />
|
||||
</div>
|
||||
{provider.name}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!isLoading && !isOpenAIProviderConfigured && localeData.type === 'openai_moderation' && (
|
||||
<span className="w-full truncate system-xs-regular">
|
||||
{provider.name}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!isLoading && !isOpenAIProviderConfigured && localeData.type === 'openai_moderation' && (
|
||||
<div className="mt-2 flex items-center rounded-lg border border-[#FEF0C7] bg-[#FFFAEB] px-3 py-2">
|
||||
<span className="mr-1 i-custom-vender-line-general-info-circle h-4 w-4 text-[#F79009]" />
|
||||
<div className="flex items-center text-xs font-medium text-gray-700">
|
||||
{t('feature.moderation.modal.openaiNotConfig.before', { ns: 'appDebug' })}
|
||||
<span
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer text-primary-600"
|
||||
onClick={handleOpenSettingsModal}
|
||||
>
|
||||
|
||||
|
||||
{t('settings.provider', { ns: 'common' })}
|
||||
|
||||
</span>
|
||||
|
||||
</button>
|
||||
{t('feature.moderation.modal.openaiNotConfig.after', { ns: 'appDebug' })}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
localeData.type === 'keywords' && (
|
||||
<div className="py-2">
|
||||
<div className="mb-1 text-sm font-medium text-text-primary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div>
|
||||
<div className="mb-2 text-xs text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div>
|
||||
)}
|
||||
</div>
|
||||
{localeData.type === 'keywords' && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="system-sm-medium text-text-secondary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div>
|
||||
{/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */}
|
||||
<div className="relative h-[88px]">
|
||||
<Textarea
|
||||
@ -297,7 +337,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
className="size-full resize-none pb-8"
|
||||
placeholder={t('feature.moderation.modal.keywords.placeholder', { ns: 'appDebug' }) || ''}
|
||||
/>
|
||||
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 text-xs font-medium text-text-quaternary">
|
||||
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 system-2xs-medium-uppercase text-text-quaternary">
|
||||
<span>{(localeData.config?.keywords || '').split('\n').filter(Boolean).length}</span>
|
||||
/
|
||||
<span className="text-text-tertiary">
|
||||
@ -307,18 +347,16 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
localeData.type === 'api' && (
|
||||
<div className="py-2">
|
||||
<div className="flex h-9 items-center justify-between">
|
||||
<div className="text-sm font-medium text-text-primary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
|
||||
)}
|
||||
{localeData.type === 'api' && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex h-6 items-center justify-between">
|
||||
<div className="system-sm-medium text-text-secondary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
|
||||
<a
|
||||
href={docLink('/use-dify/workspace/api-extension/api-extension')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center text-xs text-text-tertiary hover:text-primary-600"
|
||||
className="group flex items-center system-xs-regular text-text-tertiary hover:text-primary-600"
|
||||
>
|
||||
<span className="mr-1 i-custom-vender-line-education-book-open-01 size-3 text-text-tertiary group-hover:text-primary-600" />
|
||||
{t('apiBasedExtension.link', { ns: 'common' })}
|
||||
@ -329,46 +367,51 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
onChange={handleDataApiBasedChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
systemTypes.findIndex(t => t === localeData.type) < 0
|
||||
&& currentProvider?.form_schema
|
||||
&& (
|
||||
<FormGeneration
|
||||
forms={currentProvider?.form_schema}
|
||||
value={localeData.config}
|
||||
onChange={handleDataExtraChange}
|
||||
)}
|
||||
{systemTypes.findIndex(t => t === localeData.type) < 0
|
||||
&& currentProvider?.form_schema
|
||||
&& (
|
||||
<FormGeneration
|
||||
forms={currentProvider?.form_schema}
|
||||
value={localeData.config}
|
||||
onChange={handleDataExtraChange}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<LabeledDivider>{t('feature.moderation.title', { ns: 'appDebug' })}</LabeledDivider>
|
||||
<ModerationContent
|
||||
key={`inputs-${localeData.type}-${localeData.config?.inputs_config?.preset_response ?? ''}`}
|
||||
title={t('feature.moderation.modal.content.input', { ns: 'appDebug' }) || ''}
|
||||
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
|
||||
onConfigChange={config => handleDataContentChange('inputs_config', config)}
|
||||
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
|
||||
showPreset={localeData.type !== 'api'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<Divider bgStyle="gradient" className="my-3 h-px" />
|
||||
<ModerationContent
|
||||
title={t('feature.moderation.modal.content.input', { ns: 'appDebug' }) || ''}
|
||||
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
|
||||
onConfigChange={config => handleDataContentChange('inputs_config', config)}
|
||||
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
|
||||
showPreset={localeData.type !== 'api'}
|
||||
/>
|
||||
<ModerationContent
|
||||
title={t('feature.moderation.modal.content.output', { ns: 'appDebug' }) || ''}
|
||||
config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }}
|
||||
onConfigChange={config => handleDataContentChange('outputs_config', config)}
|
||||
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
|
||||
showPreset={localeData.type !== 'api'}
|
||||
/>
|
||||
<div className="mt-1 mb-8 text-xs font-medium text-text-tertiary">{t('feature.moderation.modal.content.condition', { ns: 'appDebug' })}</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<ModerationContent
|
||||
key={`outputs-${localeData.type}-${localeData.config?.outputs_config?.preset_response ?? ''}`}
|
||||
title={t('feature.moderation.modal.content.output', { ns: 'appDebug' }) || ''}
|
||||
config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }}
|
||||
onConfigChange={config => handleDataContentChange('outputs_config', config)}
|
||||
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
|
||||
showPreset={localeData.type !== 'api'}
|
||||
/>
|
||||
<div className="py-0.5 system-xs-regular text-text-tertiary">{t('feature.moderation.modal.content.condition', { ns: 'appDebug' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-[76px] items-center justify-end gap-2 px-6 pt-5 pb-6">
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
className="mr-2"
|
||||
size="medium"
|
||||
className="min-w-[72px]"
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="medium"
|
||||
onClick={handleSave}
|
||||
disabled={localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured}
|
||||
className="min-w-[72px]"
|
||||
>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 16 16",
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
@ -15,35 +15,27 @@
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "agent"
|
||||
"id": "agent",
|
||||
"transform": "translate(2.5 1)"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector"
|
||||
"d": "M3.3178 20.9524V15.7184C3.31774 15.7136 3.31456 15.6845 3.28404 15.6468L3.24312 15.6069C1.26589 13.9971 0 11.5398 0 8.78813C0.000122972 3.93464 3.93464 0.000122969 8.78813 0C13.5146 0 17.3698 3.73066 17.5691 8.40858C17.5712 8.45846 17.5841 8.48161 17.5865 8.48531L19.3226 11.089C19.7937 11.7956 19.6309 12.7481 18.9513 13.2579L17.5998 14.2707C17.5851 14.2819 17.5763 14.2996 17.5763 14.3178V15.4237C17.576 17.2235 16.1176 18.682 14.3178 18.6822H13.2119C13.1798 18.6822 13.1537 18.7085 13.1536 18.7405V20.9524C13.1536 21.5309 12.6845 21.9999 12.1059 22C11.5274 22 11.0583 21.531 11.0583 20.9524V18.7405C11.0584 17.5513 12.0226 16.587 13.2119 16.587H14.3178C14.9604 16.5868 15.4808 16.0663 15.481 15.4237V14.3178C15.481 13.64 15.8006 13.0016 16.3424 12.595L17.3195 11.8614L15.8432 9.64853C15.603 9.28835 15.4913 8.88291 15.4749 8.49758C15.323 4.93639 12.3872 2.09524 8.78813 2.09524C5.09181 2.09536 2.09536 5.09181 2.09524 8.78813C2.09524 10.883 3.05647 12.7533 4.56594 13.9822C5.0571 14.3822 5.41299 15.0012 5.41304 15.7184V20.9524C5.41304 21.5309 4.94385 21.9998 4.36542 22C3.78684 22 3.3178 21.531 3.3178 20.9524Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M14.7401 5.80454C14.5765 4.77996 14.1638 3.79808 13.5306 2.97273C12.8973 2.14738 12.0648 1.48568 11.1185 1.06589C10.1722 0.646098 9.12632 0.461106 8.08751 0.546487C7.05582 0.624753 6.04548 0.966277 5.17744 1.53548C4.3094 2.09758 3.58366 2.88024 3.09272 3.79808C2.59466 4.70881 2.33852 5.7405 2.33852 6.7793V7.22756L1.25703 9.3692C1.04357 9.80322 1.22145 10.3368 1.65547 10.5574L2.3314 10.8989V12.3006C2.3314 12.82 2.53063 13.3038 2.90061 13.6738C3.2706 14.0367 3.75442 14.243 4.27382 14.243H6.01702V14.7624C6.01702 15.1538 6.3372 15.4739 6.72853 15.4739C7.11986 15.4739 7.44004 15.1538 7.44004 14.7624V13.7094C7.44004 13.2185 7.04159 12.82 6.55065 12.82H4.27382C4.13864 12.82 4.00345 12.7631 3.91095 12.6706C3.81846 12.5781 3.76154 12.4429 3.76154 12.3077V10.5716C3.76154 10.2301 3.56943 9.92417 3.2706 9.77476L2.77254 9.52573L3.66904 7.73984C3.72596 7.61889 3.76154 7.4837 3.76154 7.34851V6.77219C3.76154 5.96818 3.96076 5.17129 4.34498 4.4669C4.72919 3.76251 5.28417 3.15772 5.9601 2.7237C6.63603 2.28968 7.41158 2.02643 8.20847 1.96239C9.00536 1.89835 9.81648 2.04066 10.5493 2.36795C11.2822 2.69524 11.9225 3.20042 12.4135 3.84077C12.8973 4.47402 13.2246 5.23533 13.3456 6.02511C13.4665 6.81488 13.3954 7.63312 13.125 8.38731C12.8617 9.12017 12.4206 9.78187 11.8585 10.3084C11.6735 10.4792 11.5668 10.7139 11.5668 10.9701V14.7624C11.5668 15.1538 11.887 15.4739 12.2783 15.4739C12.6696 15.4739 12.9898 15.1538 12.9898 14.7624V11.1978C13.6515 10.5432 14.1567 9.73918 14.4697 8.87114C14.8184 7.89637 14.918 6.83623 14.7615 5.81165L14.7401 5.80454Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M10.8055 7.99599C10.8909 7.83234 10.962 7.66158 11.0189 7.4837H11.6522C12.0435 7.4837 12.3637 7.16352 12.3637 6.77219C12.3637 6.38086 12.0435 6.06068 11.6522 6.06068H11.0189C10.9691 5.8828 10.898 5.71204 10.8055 5.54839L11.2537 5.10014C11.5312 4.82266 11.5312 4.3744 11.2537 4.09692C10.9762 3.81943 10.528 3.81943 10.2505 4.09692L9.80225 4.54517C9.6386 4.45267 9.46784 4.38863 9.28996 4.33171V3.69847C9.28996 3.30714 8.96978 2.98696 8.57845 2.98696C8.18712 2.98696 7.86694 3.30714 7.86694 3.69847V4.33171C7.68907 4.38152 7.5183 4.45267 7.35466 4.54517L6.90641 4.09692C6.62892 3.81943 6.18067 3.81943 5.90318 4.09692C5.62569 4.3744 5.62569 4.82266 5.90318 5.10014L6.35143 5.54839C6.26605 5.71204 6.1949 5.8828 6.13798 6.06068H5.50473C5.1134 6.06068 4.79323 6.38086 4.79323 6.77219C4.79323 7.16352 5.1134 7.4837 5.50473 7.4837H6.13798C6.18778 7.66158 6.25893 7.83234 6.35143 7.99599L5.90318 8.44424C5.62569 8.72172 5.62569 9.16997 5.90318 9.44746C6.04548 9.58976 6.22336 9.6538 6.40835 9.6538C6.59334 9.6538 6.77122 9.58265 6.91352 9.44746L7.36177 8.99921C7.52542 9.08459 7.69618 9.15574 7.87406 9.21267V9.84591C7.87406 10.2372 8.19424 10.5574 8.58557 10.5574C8.9769 10.5574 9.29708 10.2372 9.29708 9.84591V9.21267C9.47496 9.16286 9.64572 9.09171 9.80936 8.99921L10.2576 9.44746C10.3999 9.58976 10.5778 9.6538 10.7628 9.6538C10.9478 9.6538 11.1257 9.58265 11.268 9.44746C11.5454 9.16997 11.5454 8.72172 11.268 8.44424L10.8197 7.99599H10.8055ZM7.44004 6.77219C7.44004 6.14606 7.94521 5.64089 8.57134 5.64089C9.19747 5.64089 9.70264 6.14606 9.70264 6.77219C9.70264 7.39832 9.19747 7.90349 8.57134 7.90349C7.94521 7.90349 7.44004 7.39832 7.44004 6.77219Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M9.79012 6.5163L9.31429 5.27923C9.23058 5.06159 9.02158 4.91799 8.78836 4.91799C8.55514 4.91799 8.34614 5.06159 8.26243 5.27923L7.7866 6.5163C7.56194 7.10037 7.10038 7.56194 6.5163 7.78659L5.27923 8.26239C5.06159 8.34609 4.91799 8.55519 4.91799 8.78836C4.91799 9.02158 5.06159 9.23058 5.27923 9.31429L6.5163 9.79012C7.10037 10.0148 7.56194 10.4764 7.7866 11.0604L8.26243 12.2975C8.34614 12.5151 8.55514 12.6587 8.78836 12.6587C9.02158 12.6587 9.23058 12.5151 9.31429 12.2975L9.79012 11.0604C10.0148 10.4764 10.4764 10.0148 11.0604 9.79012L12.2975 9.31429C12.5151 9.23058 12.6587 9.02158 12.6587 8.78836C12.6587 8.55519 12.5151 8.34609 12.2975 8.26239L11.0604 7.78659C10.4764 7.56194 10.0148 7.10038 9.79012 6.5163Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -9,6 +9,13 @@ import {
|
||||
UPDATE_HISTORY_EVENT_EMITTER,
|
||||
} from '../constants'
|
||||
import PromptEditor from '../index'
|
||||
import { CustomTextNode } from '../plugins/custom-text/node'
|
||||
|
||||
type MockNodeReplacementConfig = {
|
||||
replace?: unknown
|
||||
with?: (arg: { __text: string }) => void
|
||||
withKlass?: unknown
|
||||
}
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const commandHandlers = new Map<unknown, (payload: unknown) => boolean>()
|
||||
@ -18,6 +25,7 @@ const mocks = vi.hoisted(() => {
|
||||
return {
|
||||
emit: vi.fn(),
|
||||
rootLines: ['first line', 'second line'],
|
||||
nodeReplacementConfig: undefined as MockNodeReplacementConfig | undefined,
|
||||
commandHandlers,
|
||||
subscriptions,
|
||||
rootElement,
|
||||
@ -86,7 +94,7 @@ vi.mock('@lexical/react/LexicalComposer', () => ({
|
||||
LexicalComposer: ({ initialConfig, children }: {
|
||||
initialConfig: {
|
||||
onError?: (error: Error) => void
|
||||
nodes?: Array<{ replace?: unknown, with: (arg: { __text: string }) => void }>
|
||||
nodes?: unknown[]
|
||||
}
|
||||
children: ReactNode
|
||||
}) => {
|
||||
@ -99,9 +107,11 @@ vi.mock('@lexical/react/LexicalComposer', () => ({
|
||||
}
|
||||
}
|
||||
if (initialConfig?.nodes) {
|
||||
const textNodeConf = initialConfig.nodes.find((n: { replace?: unknown, with: (arg: { __text: string }) => void }) => n?.replace)
|
||||
if (textNodeConf)
|
||||
textNodeConf.with({ __text: 'test' })
|
||||
const textNodeConf = initialConfig.nodes.find((node): node is MockNodeReplacementConfig => {
|
||||
return typeof node === 'object' && node !== null && 'replace' in node
|
||||
})
|
||||
mocks.nodeReplacementConfig = textNodeConf
|
||||
textNodeConf?.with?.({ __text: 'test' })
|
||||
}
|
||||
return <div data-testid="lexical-composer">{children}</div>
|
||||
},
|
||||
@ -173,10 +183,17 @@ describe('PromptEditor', () => {
|
||||
mocks.commandHandlers.clear()
|
||||
mocks.subscriptions.length = 0
|
||||
mocks.rootLines = ['first line', 'second line']
|
||||
mocks.nodeReplacementConfig = undefined
|
||||
})
|
||||
|
||||
// Rendering shell and text output from lexical state.
|
||||
describe('Rendering', () => {
|
||||
it('should register CustomTextNode as the TextNode replacement class', () => {
|
||||
render(<PromptEditor />)
|
||||
|
||||
expect(mocks.nodeReplacementConfig?.withKlass).toBe(CustomTextNode)
|
||||
})
|
||||
|
||||
it('should render placeholder and call onChange with joined lexical text', async () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ import { HITLInputNode } from '../plugins/hitl-input-block'
|
||||
import { LastRunBlockNode } from '../plugins/last-run-block'
|
||||
import { QueryBlockNode } from '../plugins/query-block'
|
||||
import { RequestURLBlockNode } from '../plugins/request-url-block'
|
||||
import { RosterReferenceBlockNode } from '../plugins/roster-reference-block/node'
|
||||
import { PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER } from '../plugins/update-block'
|
||||
import { VariableValueBlockNode } from '../plugins/variable-value-block/node'
|
||||
import { WorkflowVariableBlockNode } from '../plugins/workflow-variable-block'
|
||||
@ -108,6 +109,7 @@ const PromptEditorContentHarness = ({
|
||||
RequestURLBlockNode,
|
||||
WorkflowVariableBlockNode,
|
||||
VariableValueBlockNode,
|
||||
RosterReferenceBlockNode,
|
||||
HITLInputNode,
|
||||
CurrentBlockNode,
|
||||
ErrorMessageBlockNode,
|
||||
@ -291,5 +293,29 @@ describe('PromptEditorContent', () => {
|
||||
expect(screen.queryByTestId('draggable-target-line')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('common.promptEditor.placeholder')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render roster references as inline token pills when enabled', async () => {
|
||||
const captures: Captures = { editor: null, eventEmitter: null }
|
||||
|
||||
const { container } = render(
|
||||
<PromptEditorContentHarness
|
||||
captures={captures}
|
||||
shortcutPopups={[]}
|
||||
initialText="Use [§file:file-1:qna_report.pdf§]"
|
||||
floatingAnchorElem={null}
|
||||
onEditorChange={vi.fn()}
|
||||
rosterReferenceBlock={{ show: true }}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(captures.editor).not.toBeNull()
|
||||
})
|
||||
|
||||
const token = container.querySelector('[data-roster-reference-kind="file"]') as HTMLElement
|
||||
expect(token).toBeInTheDocument()
|
||||
expect(token).toHaveTextContent('qna_report.pdf')
|
||||
expect(token.querySelector('.i-ri-file-pdf-2-fill')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -16,6 +16,7 @@ import type {
|
||||
LastRunBlockType,
|
||||
QueryBlockType,
|
||||
RequestURLBlockType,
|
||||
RosterReferenceBlockType,
|
||||
VariableBlockType,
|
||||
WorkflowVariableBlockType,
|
||||
} from './types'
|
||||
@ -60,6 +61,7 @@ import {
|
||||
import {
|
||||
RequestURLBlockNode,
|
||||
} from './plugins/request-url-block'
|
||||
import { RosterReferenceBlockNode } from './plugins/roster-reference-block/node'
|
||||
import { VariableValueBlockNode } from './plugins/variable-value-block/node'
|
||||
import {
|
||||
WorkflowVariableBlockNode,
|
||||
@ -108,6 +110,7 @@ const EditableSyncPlugin: FC<{ editable: boolean }> = ({ editable }) => {
|
||||
|
||||
export type PromptEditorProps = {
|
||||
instanceId?: string
|
||||
children?: React.ReactNode
|
||||
compact?: boolean
|
||||
wrapperClassName?: string
|
||||
className?: string
|
||||
@ -124,6 +127,7 @@ export type PromptEditorProps = {
|
||||
requestURLBlock?: RequestURLBlockType
|
||||
historyBlock?: HistoryBlockType
|
||||
variableBlock?: VariableBlockType
|
||||
rosterReferenceBlock?: RosterReferenceBlockType
|
||||
externalToolBlock?: ExternalToolBlockType
|
||||
workflowVariableBlock?: WorkflowVariableBlockType
|
||||
hitlInputBlock?: HITLInputBlockType
|
||||
@ -131,6 +135,8 @@ export type PromptEditorProps = {
|
||||
errorMessageBlock?: ErrorMessageBlockType
|
||||
lastRunBlock?: LastRunBlockType
|
||||
isSupportFileVar?: boolean
|
||||
disableSlashPicker?: boolean
|
||||
disableBracePicker?: boolean
|
||||
shortcutPopups?: Array<{
|
||||
hotkey: Hotkey
|
||||
displayMode?: ShortcutPopupDisplayMode
|
||||
@ -140,6 +146,7 @@ export type PromptEditorProps = {
|
||||
|
||||
const PromptEditor: FC<PromptEditorProps> = ({
|
||||
instanceId,
|
||||
children,
|
||||
compact,
|
||||
wrapperClassName,
|
||||
className,
|
||||
@ -156,6 +163,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
requestURLBlock,
|
||||
historyBlock,
|
||||
variableBlock,
|
||||
rosterReferenceBlock,
|
||||
externalToolBlock,
|
||||
workflowVariableBlock,
|
||||
hitlInputBlock,
|
||||
@ -163,6 +171,8 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
isSupportFileVar,
|
||||
disableSlashPicker = false,
|
||||
disableBracePicker = false,
|
||||
shortcutPopups = [],
|
||||
}) => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
@ -177,6 +187,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
{
|
||||
replace: TextNode,
|
||||
with: (node: TextNode) => new CustomTextNode(node.__text),
|
||||
withKlass: CustomTextNode,
|
||||
},
|
||||
ContextBlockNode,
|
||||
HistoryBlockNode,
|
||||
@ -184,6 +195,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
RequestURLBlockNode,
|
||||
WorkflowVariableBlockNode,
|
||||
VariableValueBlockNode,
|
||||
RosterReferenceBlockNode,
|
||||
HITLInputNode,
|
||||
CurrentBlockNode,
|
||||
ErrorMessageBlockNode,
|
||||
@ -242,6 +254,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
requestURLBlock={requestURLBlock}
|
||||
historyBlock={historyBlock}
|
||||
variableBlock={variableBlock}
|
||||
rosterReferenceBlock={rosterReferenceBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
hitlInputBlock={hitlInputBlock}
|
||||
@ -249,6 +262,8 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
disableSlashPicker={disableSlashPicker}
|
||||
disableBracePicker={disableBracePicker}
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
instanceId={instanceId}
|
||||
@ -257,6 +272,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
/>
|
||||
<ValueSyncPlugin value={value} />
|
||||
<EditableSyncPlugin editable={editable} />
|
||||
{children}
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
)
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
import type {
|
||||
Klass,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
} from 'lexical'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { createEditor } from 'lexical'
|
||||
import RosterReferenceBlockComponent from '../component'
|
||||
import {
|
||||
$createRosterReferenceBlockNode,
|
||||
$isRosterReferenceBlockNode,
|
||||
RosterReferenceBlockNode,
|
||||
} from '../node'
|
||||
import {
|
||||
getRosterReferenceFileIconType,
|
||||
parseRosterReferenceToken,
|
||||
} from '../utils'
|
||||
|
||||
describe('RosterReferenceBlockNode', () => {
|
||||
let editor: LexicalEditor
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
editor = createEditor({
|
||||
nodes: [RosterReferenceBlockNode as unknown as Klass<LexicalNode>],
|
||||
})
|
||||
})
|
||||
|
||||
const runInEditor = (callback: () => void) => {
|
||||
editor.update(callback, { discrete: true })
|
||||
}
|
||||
|
||||
it('should parse roster reference tokens and infer icon classes', () => {
|
||||
expect(parseRosterReferenceToken('[§skill:2c3176de8a01:tender-analyzer§]')).toEqual({
|
||||
kind: 'skill',
|
||||
id: '2c3176de8a01',
|
||||
label: 'tender-analyzer',
|
||||
})
|
||||
expect(parseRosterReferenceToken('[§file:1f0ad3e2:qna_report:final.pdf§]')).toEqual({
|
||||
kind: 'file',
|
||||
id: '1f0ad3e2',
|
||||
label: 'qna_report:final.pdf',
|
||||
})
|
||||
expect(parseRosterReferenceToken('[§unknown:1:item§]')).toBeNull()
|
||||
expect(getRosterReferenceFileIconType('qna_report.pdf')).toBe('pdf')
|
||||
})
|
||||
|
||||
it('should render a non-editable token pill component', () => {
|
||||
const { container } = render(
|
||||
<RosterReferenceBlockComponent text="[§tool-all:tavily/tavily:tavily§]" />,
|
||||
)
|
||||
|
||||
const token = screen.getByTitle('tavily')
|
||||
expect(token).toHaveAttribute('contenteditable', 'false')
|
||||
expect(token).toHaveAttribute('data-roster-reference-kind', 'tool-all')
|
||||
expect(token).toHaveAttribute('data-roster-reference-id', 'tavily/tavily')
|
||||
expect(token).toHaveClass('border-state-accent-hover-alt')
|
||||
expect(token).toHaveClass('bg-state-accent-hover')
|
||||
expect(token).toHaveTextContent('tavily')
|
||||
expect(container.querySelector('.i-custom-public-other-default-tool-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render knowledge icon with the configured retrieval row style', () => {
|
||||
const { container } = render(
|
||||
<RosterReferenceBlockComponent text="[§knowledge:manual-1:产品手册§]" />,
|
||||
)
|
||||
|
||||
const iconShell = container.querySelector('.bg-util-colors-green-green-500')
|
||||
expect(iconShell).toBeInTheDocument()
|
||||
expect(iconShell).toHaveClass('text-text-primary-on-surface')
|
||||
expect(iconShell?.querySelector('.i-ri-book-open-line')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should expose DecoratorNode behavior and preserve raw text content', () => {
|
||||
runInEditor(() => {
|
||||
const node = new RosterReferenceBlockNode('[§tool-all:tavily/tavily:tavily§]', 'node-key')
|
||||
const cloned = RosterReferenceBlockNode.clone(node)
|
||||
const dom = node.createDOM()
|
||||
|
||||
expect(RosterReferenceBlockNode.getType()).toBe('roster-reference-block')
|
||||
expect(cloned).toBeInstanceOf(RosterReferenceBlockNode)
|
||||
expect(cloned.getKey()).toBe('node-key')
|
||||
expect(node.isInline()).toBe(true)
|
||||
expect(dom).toHaveClass('inline-flex')
|
||||
expect(dom).toHaveClass('align-middle')
|
||||
expect(node.getTextContent()).toBe('[§tool-all:tavily/tavily:tavily§]')
|
||||
})
|
||||
})
|
||||
|
||||
it('should import and export serialized node text', () => {
|
||||
runInEditor(() => {
|
||||
const imported = RosterReferenceBlockNode.importJSON({
|
||||
text: '[§knowledge:manual-1:产品手册§]',
|
||||
type: 'roster-reference-block',
|
||||
version: 1,
|
||||
})
|
||||
const exported = imported.exportJSON()
|
||||
|
||||
expect(exported).toEqual({
|
||||
text: '[§knowledge:manual-1:产品手册§]',
|
||||
type: 'roster-reference-block',
|
||||
version: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should create node with helper and support type guard checks', () => {
|
||||
runInEditor(() => {
|
||||
const node = $createRosterReferenceBlockNode('[§skill:playwright:Playwright§]')
|
||||
|
||||
expect(node).toBeInstanceOf(RosterReferenceBlockNode)
|
||||
expect(node.getTextContent()).toBe('[§skill:playwright:Playwright§]')
|
||||
expect($isRosterReferenceBlockNode(node)).toBe(true)
|
||||
expect($isRosterReferenceBlockNode(null)).toBe(false)
|
||||
expect($isRosterReferenceBlockNode(undefined)).toBe(false)
|
||||
expect($isRosterReferenceBlockNode({} as LexicalNode)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,55 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { FileTreeIcon } from '@langgenius/dify-ui/file-tree'
|
||||
import { use } from 'react'
|
||||
import { RosterReferenceBlockContext } from './context'
|
||||
import {
|
||||
getRosterReferenceFileIconType,
|
||||
getRosterReferenceIconClassName,
|
||||
parseRosterReferenceToken,
|
||||
} from './utils'
|
||||
|
||||
type RosterReferenceBlockComponentProps = {
|
||||
text: string
|
||||
}
|
||||
|
||||
const RosterReferenceBlockComponent = ({
|
||||
text,
|
||||
}: RosterReferenceBlockComponentProps) => {
|
||||
const rosterReferenceBlock = use(RosterReferenceBlockContext)
|
||||
const token = parseRosterReferenceToken(text)
|
||||
|
||||
if (!token)
|
||||
return null
|
||||
|
||||
const isKnowledge = token.kind === 'knowledge'
|
||||
const customIcon = rosterReferenceBlock?.renderIcon?.(token)
|
||||
const defaultIcon = token.kind === 'file'
|
||||
? <FileTreeIcon type={getRosterReferenceFileIconType(token.label)} className="size-4" />
|
||||
: <span className={cn(isKnowledge ? 'size-3.5' : 'size-3.5 shrink-0', getRosterReferenceIconClassName(token))} />
|
||||
|
||||
return (
|
||||
<span
|
||||
contentEditable={false}
|
||||
data-roster-reference-kind={token.kind}
|
||||
data-roster-reference-id={token.id}
|
||||
title={token.label}
|
||||
className="inline-flex min-w-[18px] items-center gap-0.5 overflow-hidden rounded-[5px] border border-state-accent-hover-alt bg-state-accent-hover py-px pr-1 pl-px align-middle shadow-xs shadow-shadow-shadow-3"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'inline-flex size-4 shrink-0 items-center justify-center rounded-[5px] border-[0.5px] border-divider-subtle bg-background-default-dodge',
|
||||
token.kind === 'cli_tool' && 'border-divider-regular bg-text-tertiary',
|
||||
isKnowledge && 'border-divider-subtle bg-util-colors-green-green-500 p-[3px] text-text-primary-on-surface shadow-xs shadow-shadow-shadow-3',
|
||||
)}
|
||||
>
|
||||
{customIcon || defaultIcon}
|
||||
</span>
|
||||
<span className="max-w-48 truncate system-xs-medium text-text-accent">
|
||||
{token.label}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default RosterReferenceBlockComponent
|
||||
@ -0,0 +1,4 @@
|
||||
import type { RosterReferenceBlockType } from '../../types'
|
||||
import { createContext } from 'react'
|
||||
|
||||
export const RosterReferenceBlockContext = createContext<RosterReferenceBlockType | undefined>(undefined)
|
||||
@ -0,0 +1,63 @@
|
||||
import type { EntityMatch } from '@lexical/text'
|
||||
import type { LexicalEditor, TextNode } from 'lexical'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import { decoratorTransform } from '../../utils'
|
||||
import { CustomTextNode } from '../custom-text/node'
|
||||
import { RosterReferenceBlockNode } from './node'
|
||||
import { ROSTER_REFERENCE_REGEX } from './utils'
|
||||
|
||||
type RosterReferenceNodeRegistry = {
|
||||
_nodes: Map<string, { klass: typeof RosterReferenceBlockNode }>
|
||||
}
|
||||
|
||||
function createRegisteredRosterReferenceBlockNode(editor: LexicalEditor, textNode: TextNode): RosterReferenceBlockNode {
|
||||
const RegisteredRosterReferenceBlockNode = (editor as unknown as RosterReferenceNodeRegistry)
|
||||
._nodes
|
||||
.get(RosterReferenceBlockNode.getType())
|
||||
?.klass ?? RosterReferenceBlockNode
|
||||
|
||||
return $applyNodeReplacement(new RegisteredRosterReferenceBlockNode(textNode.getTextContent()))
|
||||
}
|
||||
|
||||
const RosterReferenceBlock = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([RosterReferenceBlockNode]))
|
||||
throw new Error('RosterReferenceBlockPlugin: RosterReferenceBlockNode not registered on editor')
|
||||
}, [editor])
|
||||
|
||||
const createRosterReferenceBlockNode = useCallback((textNode: CustomTextNode): RosterReferenceBlockNode => (
|
||||
createRegisteredRosterReferenceBlockNode(editor, textNode)
|
||||
), [editor])
|
||||
|
||||
const getRosterReferenceMatch = useCallback((text: string): EntityMatch | null => {
|
||||
const matchArr = ROSTER_REFERENCE_REGEX.exec(text)
|
||||
|
||||
if (matchArr === null)
|
||||
return null
|
||||
|
||||
const startOffset = matchArr.index
|
||||
const endOffset = startOffset + matchArr[0].length
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getRosterReferenceMatch, createRosterReferenceBlockNode)),
|
||||
)
|
||||
}, [createRosterReferenceBlockNode, editor, getRosterReferenceMatch])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default RosterReferenceBlock
|
||||
@ -0,0 +1,76 @@
|
||||
import type {
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedLexicalNode,
|
||||
} from 'lexical'
|
||||
import type { JSX } from 'react'
|
||||
import {
|
||||
$applyNodeReplacement,
|
||||
DecoratorNode,
|
||||
} from 'lexical'
|
||||
import RosterReferenceBlockComponent from './component'
|
||||
|
||||
type SerializedRosterReferenceBlockNode = SerializedLexicalNode & {
|
||||
text: string
|
||||
}
|
||||
|
||||
export class RosterReferenceBlockNode extends DecoratorNode<JSX.Element> {
|
||||
__text: string
|
||||
|
||||
static override getType(): string {
|
||||
return 'roster-reference-block'
|
||||
}
|
||||
|
||||
static override clone(node: RosterReferenceBlockNode): RosterReferenceBlockNode {
|
||||
return new RosterReferenceBlockNode(node.__text, node.__key)
|
||||
}
|
||||
|
||||
constructor(text: string, key?: NodeKey) {
|
||||
super(key)
|
||||
this.__text = text
|
||||
}
|
||||
|
||||
override isInline(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override createDOM(): HTMLElement {
|
||||
const span = document.createElement('span')
|
||||
span.classList.add('inline-flex', 'items-center', 'align-middle')
|
||||
return span
|
||||
}
|
||||
|
||||
override updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
override decorate(): JSX.Element {
|
||||
return <RosterReferenceBlockComponent text={this.getTextContent()} />
|
||||
}
|
||||
|
||||
static override importJSON(serializedNode: SerializedRosterReferenceBlockNode): RosterReferenceBlockNode {
|
||||
return $createRosterReferenceBlockNode(serializedNode.text)
|
||||
}
|
||||
|
||||
override exportJSON(): SerializedRosterReferenceBlockNode {
|
||||
return {
|
||||
text: this.getTextContent(),
|
||||
type: 'roster-reference-block',
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
override getTextContent(): string {
|
||||
return this.getLatest().__text
|
||||
}
|
||||
}
|
||||
|
||||
export function $createRosterReferenceBlockNode(text = ''): RosterReferenceBlockNode {
|
||||
return $applyNodeReplacement(new RosterReferenceBlockNode(text))
|
||||
}
|
||||
|
||||
export function $isRosterReferenceBlockNode(
|
||||
node: LexicalNode | null | undefined,
|
||||
): node is RosterReferenceBlockNode {
|
||||
return node instanceof RosterReferenceBlockNode
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
import type { FileTreeIconType } from '@langgenius/dify-ui/file-tree'
|
||||
|
||||
export type RosterReferenceKind = 'skill' | 'file' | 'tool-all' | 'tool' | 'cli_tool' | 'knowledge'
|
||||
|
||||
export type RosterReferenceToken = {
|
||||
kind: RosterReferenceKind
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export const ROSTER_REFERENCE_REGEX = /\[§(?:skill|file|tool-all|tool|cli_tool|knowledge):[^\]§\n\r]+§\]/
|
||||
|
||||
const KNOWN_KINDS = new Set<RosterReferenceKind>([
|
||||
'skill',
|
||||
'file',
|
||||
'tool-all',
|
||||
'tool',
|
||||
'cli_tool',
|
||||
'knowledge',
|
||||
])
|
||||
|
||||
export function parseRosterReferenceToken(text: string): RosterReferenceToken | null {
|
||||
if (!text.startsWith('[§') || !text.endsWith('§]'))
|
||||
return null
|
||||
|
||||
const body = text.slice(2, -2)
|
||||
const firstColonIndex = body.indexOf(':')
|
||||
if (firstColonIndex === -1)
|
||||
return null
|
||||
|
||||
const kind = body.slice(0, firstColonIndex) as RosterReferenceKind
|
||||
if (!KNOWN_KINDS.has(kind))
|
||||
return null
|
||||
|
||||
const rest = body.slice(firstColonIndex + 1)
|
||||
const secondColonIndex = rest.indexOf(':')
|
||||
const id = secondColonIndex === -1 ? rest : rest.slice(0, secondColonIndex)
|
||||
const label = secondColonIndex === -1 ? id : rest.slice(secondColonIndex + 1)
|
||||
|
||||
if (!id || !label)
|
||||
return null
|
||||
|
||||
return {
|
||||
kind,
|
||||
id,
|
||||
label,
|
||||
}
|
||||
}
|
||||
|
||||
const codeFileExtensions = new Set([
|
||||
'css',
|
||||
'go',
|
||||
'html',
|
||||
'htm',
|
||||
'js',
|
||||
'jsx',
|
||||
'py',
|
||||
'rb',
|
||||
'rs',
|
||||
'scss',
|
||||
'sh',
|
||||
'ts',
|
||||
'tsx',
|
||||
'vue',
|
||||
'yaml',
|
||||
'yml',
|
||||
])
|
||||
const imageFileExtensions = new Set(['apng', 'avif', 'bmp', 'gif', 'ico', 'jpeg', 'jpg', 'png', 'svg', 'webp'])
|
||||
const tableFileExtensions = new Set(['csv', 'xls', 'xlsx'])
|
||||
const archiveFileExtensions = new Set(['7z', 'gz', 'rar', 'tar', 'zip'])
|
||||
|
||||
export function getRosterReferenceFileIconType(label: string): FileTreeIconType {
|
||||
const extension = label.includes('.') ? label.split('.').pop()?.toLowerCase() : undefined
|
||||
|
||||
if (!extension)
|
||||
return 'folder'
|
||||
if (imageFileExtensions.has(extension))
|
||||
return 'image'
|
||||
if (extension === 'pdf')
|
||||
return 'pdf'
|
||||
if (extension === 'md' || extension === 'markdown' || extension === 'mdx')
|
||||
return 'markdown'
|
||||
if (extension === 'json')
|
||||
return 'json'
|
||||
if (tableFileExtensions.has(extension))
|
||||
return 'table'
|
||||
if (archiveFileExtensions.has(extension))
|
||||
return 'archive'
|
||||
if (codeFileExtensions.has(extension))
|
||||
return 'code'
|
||||
if (extension === 'txt')
|
||||
return 'text'
|
||||
|
||||
return 'file'
|
||||
}
|
||||
|
||||
export function getRosterReferenceIconClassName(token: RosterReferenceToken) {
|
||||
switch (token.kind) {
|
||||
case 'skill':
|
||||
return 'i-custom-public-agent-building-blocks text-text-tertiary'
|
||||
case 'file':
|
||||
return ''
|
||||
case 'tool-all':
|
||||
case 'tool':
|
||||
return 'i-custom-public-other-default-tool-icon text-[#ef5b39]'
|
||||
case 'cli_tool':
|
||||
return 'i-ri-terminal-box-line text-text-primary-on-surface'
|
||||
case 'knowledge':
|
||||
return 'i-ri-book-open-line'
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ import type {
|
||||
LastRunBlockType,
|
||||
QueryBlockType,
|
||||
RequestURLBlockType,
|
||||
RosterReferenceBlockType,
|
||||
VariableBlockType,
|
||||
WorkflowVariableBlockType,
|
||||
} from './types'
|
||||
@ -57,6 +58,8 @@ import {
|
||||
RequestURLBlock,
|
||||
RequestURLBlockReplacementBlock,
|
||||
} from './plugins/request-url-block'
|
||||
import RosterReferenceBlock from './plugins/roster-reference-block'
|
||||
import { RosterReferenceBlockContext } from './plugins/roster-reference-block/context'
|
||||
import ShortcutsPopupPlugin from './plugins/shortcuts-popup-plugin'
|
||||
import UpdateBlock from './plugins/update-block'
|
||||
import VariableBlock from './plugins/variable-block'
|
||||
@ -84,6 +87,7 @@ type PromptEditorContentProps = {
|
||||
requestURLBlock?: RequestURLBlockType
|
||||
historyBlock?: HistoryBlockType
|
||||
variableBlock?: VariableBlockType
|
||||
rosterReferenceBlock?: RosterReferenceBlockType
|
||||
externalToolBlock?: ExternalToolBlockType
|
||||
workflowVariableBlock?: WorkflowVariableBlockType
|
||||
hitlInputBlock?: HITLInputBlockType
|
||||
@ -91,6 +95,8 @@ type PromptEditorContentProps = {
|
||||
errorMessageBlock?: ErrorMessageBlockType
|
||||
lastRunBlock?: LastRunBlockType
|
||||
isSupportFileVar?: boolean
|
||||
disableSlashPicker?: boolean
|
||||
disableBracePicker?: boolean
|
||||
onBlur?: () => void
|
||||
onFocus?: () => void
|
||||
instanceId?: string
|
||||
@ -110,6 +116,7 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
|
||||
requestURLBlock,
|
||||
historyBlock,
|
||||
variableBlock,
|
||||
rosterReferenceBlock,
|
||||
externalToolBlock,
|
||||
workflowVariableBlock,
|
||||
hitlInputBlock,
|
||||
@ -117,6 +124,8 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
|
||||
errorMessageBlock,
|
||||
lastRunBlock,
|
||||
isSupportFileVar,
|
||||
disableSlashPicker,
|
||||
disableBracePicker,
|
||||
onBlur,
|
||||
onFocus,
|
||||
instanceId,
|
||||
@ -124,7 +133,7 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
|
||||
onEditorChange,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<RosterReferenceBlockContext value={rosterReferenceBlock}>
|
||||
<RichTextPlugin
|
||||
contentEditable={(
|
||||
<ContentEditable
|
||||
@ -150,34 +159,38 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
|
||||
{(closePortal, onInsert) => <Popup onClose={closePortal} onInsert={onInsert} />}
|
||||
</ShortcutsPopupPlugin>
|
||||
))}
|
||||
<ComponentPickerBlock
|
||||
triggerString="/"
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
requestURLBlock={requestURLBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
currentBlock={currentBlock}
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
<ComponentPickerBlock
|
||||
triggerString="{"
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
requestURLBlock={requestURLBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
currentBlock={currentBlock}
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
{!disableSlashPicker && (
|
||||
<ComponentPickerBlock
|
||||
triggerString="/"
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
requestURLBlock={requestURLBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
currentBlock={currentBlock}
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
)}
|
||||
{!disableBracePicker && (
|
||||
<ComponentPickerBlock
|
||||
triggerString="{"
|
||||
contextBlock={contextBlock}
|
||||
historyBlock={historyBlock}
|
||||
queryBlock={queryBlock}
|
||||
requestURLBlock={requestURLBlock}
|
||||
variableBlock={variableBlock}
|
||||
externalToolBlock={externalToolBlock}
|
||||
workflowVariableBlock={workflowVariableBlock}
|
||||
currentBlock={currentBlock}
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
)}
|
||||
{contextBlock?.show && (
|
||||
<>
|
||||
<ContextBlock {...contextBlock} />
|
||||
@ -202,6 +215,9 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
|
||||
<VariableValueBlock />
|
||||
</>
|
||||
)}
|
||||
{rosterReferenceBlock?.show && (
|
||||
<RosterReferenceBlock />
|
||||
)}
|
||||
{workflowVariableBlock?.show && (
|
||||
<>
|
||||
<WorkflowVariableBlock {...workflowVariableBlock} />
|
||||
@ -248,7 +264,7 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
|
||||
{floatingAnchorElem && (
|
||||
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
|
||||
)}
|
||||
</>
|
||||
</RosterReferenceBlockContext>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import type { FormInputItem } from '../../workflow/nodes/human-input/types'
|
||||
import type { Type } from '../../workflow/nodes/llm/types'
|
||||
import type { Dataset } from './plugins/context-block'
|
||||
import type { RoleName } from './plugins/history-block'
|
||||
import type { RosterReferenceToken } from './plugins/roster-reference-block/utils'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
@ -59,6 +60,11 @@ export type VariableBlockType = {
|
||||
variables?: Option[]
|
||||
}
|
||||
|
||||
export type RosterReferenceBlockType = {
|
||||
show?: boolean
|
||||
renderIcon?: (token: RosterReferenceToken) => React.ReactNode
|
||||
}
|
||||
|
||||
export type ExternalToolBlockType = {
|
||||
show?: boolean
|
||||
externalTools?: ExternalToolOption[]
|
||||
|
||||
@ -40,4 +40,25 @@ describe('useIntegrationsSetting', () => {
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'mcp' })
|
||||
})
|
||||
|
||||
it('should preserve the agent source for agent-scoped settings', () => {
|
||||
const { result } = renderHook(() => useIntegrationsSetting())
|
||||
|
||||
act(() => {
|
||||
result.current({ payload: ACCOUNT_SETTING_TAB.PROVIDER, source: 'agent' })
|
||||
})
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider', source: 'agent' })
|
||||
})
|
||||
|
||||
it('should preserve the cancel callback for migrated integrations settings', () => {
|
||||
const onCancelCallback = vi.fn()
|
||||
const { result } = renderHook(() => useIntegrationsSetting())
|
||||
|
||||
act(() => {
|
||||
result.current({ payload: ACCOUNT_SETTING_TAB.PROVIDER, onCancelCallback })
|
||||
})
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider', onCancelCallback })
|
||||
})
|
||||
})
|
||||
|
||||
@ -4,6 +4,7 @@ import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
type DialogProps = {
|
||||
backdropClassName?: string
|
||||
className?: string
|
||||
children: ReactNode
|
||||
show: boolean
|
||||
@ -11,6 +12,7 @@ type DialogProps = {
|
||||
}
|
||||
|
||||
const MenuDialog = ({
|
||||
backdropClassName,
|
||||
className,
|
||||
children,
|
||||
show,
|
||||
@ -27,7 +29,7 @@ const MenuDialog = ({
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
backdropClassName="z-40 bg-transparent"
|
||||
backdropClassName={cn('z-40 bg-transparent', backdropClassName)}
|
||||
className={cn(
|
||||
'top-0 left-0 z-40 h-full max-h-none w-full max-w-none translate-x-0 translate-y-0 scale-100 overflow-hidden rounded-none border-none bg-background-sidenav-bg p-0 shadow-none backdrop-blur-md transition-opacity data-ending-style:scale-100 data-starting-style:scale-100',
|
||||
className,
|
||||
|
||||
@ -247,6 +247,7 @@ export type DefaultModelResponse = {
|
||||
export type DefaultModel = {
|
||||
provider: string
|
||||
model: string
|
||||
plugin_id?: string
|
||||
}
|
||||
|
||||
export type CustomConfigurationModelFixedFields = {
|
||||
|
||||
@ -108,7 +108,11 @@ describe('ModelSelector', () => {
|
||||
fireEvent.click(screen.getByRole('combobox'))
|
||||
fireEvent.click(screen.getByText('select'))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith({ provider: 'openai', model: 'gpt-4' })
|
||||
expect(onSelect).toHaveBeenCalledWith({
|
||||
provider: 'openai',
|
||||
model: 'gpt-4',
|
||||
plugin_id: 'langgenius/openai',
|
||||
})
|
||||
})
|
||||
|
||||
it('should close popup when popup requests hide', () => {
|
||||
|
||||
@ -11,6 +11,15 @@ import ModelSelectorTrigger from './model-selector-trigger'
|
||||
import Popup from './popup'
|
||||
import { getModelSelectorValueLabel, isSameModelSelectorValue } from './types'
|
||||
|
||||
const getModelProviderPluginId = (provider: string) => {
|
||||
const [organization, pluginName] = provider.split('/').filter(Boolean)
|
||||
|
||||
if (organization && pluginName)
|
||||
return `${organization}/${pluginName}`
|
||||
|
||||
return provider ? `langgenius/${provider}` : ''
|
||||
}
|
||||
|
||||
type ModelSelectorProps = {
|
||||
defaultModel?: DefaultModel
|
||||
modelList: Model[]
|
||||
@ -24,6 +33,7 @@ type ModelSelectorProps = {
|
||||
showDeprecatedWarnIcon?: boolean
|
||||
hideProviderSettingsFooter?: boolean
|
||||
onConfigureEmptyState?: () => void
|
||||
providerSettingsSource?: 'agent'
|
||||
showModelMeta?: boolean
|
||||
}
|
||||
function ModelSelector({
|
||||
@ -39,6 +49,7 @@ function ModelSelector({
|
||||
showDeprecatedWarnIcon = true,
|
||||
hideProviderSettingsFooter,
|
||||
onConfigureEmptyState,
|
||||
providerSettingsSource,
|
||||
showModelMeta,
|
||||
}: ModelSelectorProps) {
|
||||
const { t } = useTranslation()
|
||||
@ -74,8 +85,13 @@ function ModelSelector({
|
||||
setOpen(false)
|
||||
setInputValue('')
|
||||
|
||||
if (onSelect)
|
||||
onSelect({ provider, model: model.model })
|
||||
if (onSelect) {
|
||||
onSelect({
|
||||
provider,
|
||||
model: model.model,
|
||||
plugin_id: getModelProviderPluginId(provider),
|
||||
})
|
||||
}
|
||||
}, [onSelect])
|
||||
|
||||
const handleValueChange = useCallback((value: ModelSelectorValue | null) => {
|
||||
@ -150,6 +166,7 @@ function ModelSelector({
|
||||
modelList={modelList}
|
||||
scopeFeatures={scopeFeatures}
|
||||
hideProviderSettingsFooter={hideProviderSettingsFooter}
|
||||
providerSettingsSource={providerSettingsSource}
|
||||
onConfigureEmptyState={onConfigureEmptyState ? handleConfigureEmptyState : undefined}
|
||||
onInputValueChange={setInputValue}
|
||||
onHide={handleHide}
|
||||
|
||||
@ -38,6 +38,7 @@ export type PopupProps = {
|
||||
modelList: Model[]
|
||||
scopeFeatures?: ModelFeatureEnum[]
|
||||
hideProviderSettingsFooter?: boolean
|
||||
providerSettingsSource?: 'agent'
|
||||
onConfigureEmptyState?: () => void
|
||||
onInputValueChange: (value: string) => void
|
||||
onHide: () => void
|
||||
@ -48,6 +49,7 @@ function Popup({
|
||||
modelList,
|
||||
scopeFeatures = [],
|
||||
hideProviderSettingsFooter,
|
||||
providerSettingsSource,
|
||||
onConfigureEmptyState,
|
||||
onInputValueChange,
|
||||
onHide,
|
||||
@ -173,8 +175,8 @@ function Popup({
|
||||
|
||||
const handleOpenSettings = useCallback(() => {
|
||||
onHide()
|
||||
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
}, [onHide, openIntegrationsSetting])
|
||||
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER, source: providerSettingsSource })
|
||||
}, [onHide, openIntegrationsSetting, providerSettingsSource])
|
||||
const handleClosePreviewCard = useCallback(() => {
|
||||
previewCardHandle.close()
|
||||
}, [previewCardHandle])
|
||||
|
||||
@ -7,8 +7,8 @@ import { useModalContext } from '@/context/modal-context'
|
||||
import { integrationSectionByMovedAccountSettingTab } from './destinations'
|
||||
|
||||
type IntegrationsSettingState
|
||||
= | { payload: MovedAccountSettingTab }
|
||||
| { section: IntegrationSection }
|
||||
= | { payload: MovedAccountSettingTab, source?: 'agent', onCancelCallback?: () => void }
|
||||
| { section: IntegrationSection, source?: 'agent', onCancelCallback?: () => void }
|
||||
|
||||
export const useIntegrationsSetting = () => {
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
@ -19,7 +19,12 @@ export const useIntegrationsSetting = () => {
|
||||
? state.section
|
||||
: integrationSectionByMovedAccountSettingTab[state.payload]
|
||||
|
||||
if (section)
|
||||
setShowAccountSettingModal({ payload: section })
|
||||
if (section) {
|
||||
setShowAccountSettingModal({
|
||||
payload: section,
|
||||
...(state.source ? { source: state.source } : {}),
|
||||
...(state.onCancelCallback ? { onCancelCallback: state.onCancelCallback } : {}),
|
||||
})
|
||||
}
|
||||
}, [setShowAccountSettingModal])
|
||||
}
|
||||
|
||||
@ -159,6 +159,15 @@ vi.mock('@/app/components/app-sidebar/dataset-detail-top', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/features/agent-v2/agent-detail/navigation', () => ({
|
||||
AgentDetailSection: ({ expand }: { expand: boolean }) => <div data-testid="agent-detail-section" data-expand={expand} />,
|
||||
AgentDetailTop: ({ expand, onToggle }: { expand: boolean, onToggle: () => void }) => (
|
||||
<div data-testid="agent-detail-top" data-expand={expand}>
|
||||
<button type="button" data-testid="agent-detail-toggle" onClick={onToggle}>Toggle</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: () => 'en-US',
|
||||
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
|
||||
@ -320,6 +329,7 @@ describe('MainNav', () => {
|
||||
expect(screen.getByRole('button', { name: 'common.account.account' })).not.toHaveTextContent(Plan.team)
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/')
|
||||
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
|
||||
expect(screen.getByRole('link', { name: /common.menus.roster/ })).toHaveAttribute('href', '/roster')
|
||||
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute('href', '/datasets')
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toHaveAttribute('href', '/integrations/model-provider')
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toHaveAttribute('href', '/marketplace')
|
||||
@ -433,6 +443,7 @@ describe('MainNav', () => {
|
||||
|
||||
expect(screen.queryByRole('link', { name: /common.mainNav.home/ })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: /common.menus.apps/ })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: /common.menus.roster/ })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute('href', '/datasets')
|
||||
expect(screen.queryByRole('link', { name: /common.mainNav.integrations/ })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toHaveAttribute('href', '/marketplace')
|
||||
@ -456,6 +467,7 @@ describe('MainNav', () => {
|
||||
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.menus.roster/ })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: /common.menus.datasets/ })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toBeInTheDocument()
|
||||
@ -484,6 +496,21 @@ describe('MainNav', () => {
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).not.toHaveAttribute('aria-current')
|
||||
})
|
||||
|
||||
it('hides the main menu on snippet detail routes while keeping account settings available', () => {
|
||||
mockPathname = '/snippets/snippet-1/orchestrate'
|
||||
|
||||
renderMainNav()
|
||||
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-16')
|
||||
expect(screen.queryByLabelText('Dify')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'common.mainNav.workspace.openMenu' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: /common.mainNav.home/ })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: /common.menus.apps/ })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'explore.sidebar.webApps' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.account.account' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.mainNav.help.openMenu' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('replaces global navigation with app detail navigation on app routes', () => {
|
||||
mockPathname = '/app/app-1/overview'
|
||||
|
||||
@ -609,6 +636,35 @@ describe('MainNav', () => {
|
||||
expect(screen.getByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
})
|
||||
|
||||
it('replaces global navigation with agent detail navigation on roster detail routes', () => {
|
||||
mockPathname = '/roster/agent/agent-1/configure'
|
||||
|
||||
renderMainNav()
|
||||
|
||||
expect(screen.getByTestId('agent-detail-top')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('agent-detail-section')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-[248px]')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('p-1')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('bg-background-body')
|
||||
expect(screen.queryByRole('button', { name: 'common.mainNav.workspace.openMenu' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: /common.menus.roster/ })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('collapses agent detail navigation from the top-right toggle', () => {
|
||||
mockPathname = '/roster/agent/agent-1/configure'
|
||||
|
||||
renderMainNav()
|
||||
fireEvent.click(screen.getByTestId('agent-detail-toggle'))
|
||||
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-16')
|
||||
expect(screen.getByRole('complementary')).toHaveClass('p-1')
|
||||
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'false')
|
||||
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'false')
|
||||
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
|
||||
})
|
||||
|
||||
it('registers the detail navigation shortcut to run while inputs are focused', () => {
|
||||
mockPathname = '/app/app-1/overview'
|
||||
|
||||
@ -618,6 +674,21 @@ describe('MainNav', () => {
|
||||
expect.objectContaining({ ignoreInputs: false }),
|
||||
)
|
||||
})
|
||||
|
||||
it('shows agent detail navigation as a floating preview when hovering the collapsed top toggle', () => {
|
||||
mockPathname = '/roster/agent/agent-1/configure'
|
||||
|
||||
renderMainNav()
|
||||
fireEvent.click(screen.getByTestId('agent-detail-toggle'))
|
||||
fireEvent.mouseEnter(screen.getByTestId('agent-detail-top').parentElement!)
|
||||
|
||||
expect(screen.getByRole('complementary')).toHaveClass('w-16', 'overflow-visible')
|
||||
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
|
||||
expect(screen.getAllByTestId('agent-detail-top')).toHaveLength(1)
|
||||
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'true')
|
||||
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'true')
|
||||
})
|
||||
|
||||
it.each([
|
||||
'/datasets/create',
|
||||
'/datasets/create-from-pipeline',
|
||||
@ -644,6 +715,16 @@ describe('MainNav', () => {
|
||||
expect(marketplaceLink).toHaveClass(activeEdgeClassName)
|
||||
})
|
||||
|
||||
it('marks roster active on roster routes', () => {
|
||||
mockPathname = '/roster'
|
||||
|
||||
renderMainNav()
|
||||
|
||||
const rosterLink = screen.getByRole('link', { name: /common.menus.roster/ })
|
||||
expect(rosterLink).toHaveClass(activeEdgeClassName)
|
||||
expect(rosterLink).toHaveAttribute('aria-current', 'page')
|
||||
})
|
||||
|
||||
it('applies the Figma glass active state to the Home route', () => {
|
||||
mockPathname = '/'
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||
import EnvNav from '@/app/components/header/env-nav'
|
||||
import { buildIntegrationPath } from '@/app/components/integrations/routes'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { AgentDetailSection, AgentDetailTop } from '@/features/agent-v2/agent-detail/navigation'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import Link from '@/next/link'
|
||||
import { usePathname } from '@/next/navigation'
|
||||
@ -60,6 +61,18 @@ const isDatasetDetailPathname = (pathname: string) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const isAgentDetailPathname = (pathname: string) => {
|
||||
const [section, type, agentId] = pathname.split('/').filter(Boolean)
|
||||
|
||||
return section === 'roster' && type === 'agent' && !!agentId
|
||||
}
|
||||
|
||||
const isSnippetDetailPathname = (pathname: string) => {
|
||||
const [section, snippetId] = pathname.split('/').filter(Boolean)
|
||||
|
||||
return section === 'snippets' && !!snippetId
|
||||
}
|
||||
|
||||
const MainNav = ({
|
||||
className,
|
||||
}: MainNavProps) => {
|
||||
@ -70,7 +83,9 @@ const MainNav = ({
|
||||
const showEnvTag = langGeniusVersionInfo.current_env === 'TESTING' || langGeniusVersionInfo.current_env === 'DEVELOPMENT'
|
||||
const showAppDetailNavigation = !isCurrentWorkspaceDatasetOperator && pathname.startsWith('/app/')
|
||||
const showDatasetDetailNavigation = isDatasetDetailPathname(pathname)
|
||||
const showDetailNavigation = showAppDetailNavigation || showDatasetDetailNavigation
|
||||
const showAgentDetailNavigation = !isCurrentWorkspaceDatasetOperator && isAgentDetailPathname(pathname)
|
||||
const showSnippetDetailBottomNavigation = isSnippetDetailPathname(pathname)
|
||||
const showDetailNavigation = showAppDetailNavigation || showDatasetDetailNavigation || showAgentDetailNavigation
|
||||
const { hasAppDetail, appSidebarExpand, setAppDetail, setAppSidebarExpand } = useAppStore(useShallow(state => ({
|
||||
hasAppDetail: !!state.appDetail,
|
||||
appSidebarExpand: state.appSidebarExpand,
|
||||
@ -87,7 +102,9 @@ const MainNav = ({
|
||||
const detailNavigationTransitionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const isDetailNavigationHoverPreviewOpen = isCollapsedDetailNavigation && detailNavigationHoverPreviewOpen
|
||||
const detailNavigationVisibleExpanded = detailNavigationExpanded || isDetailNavigationHoverPreviewOpen
|
||||
const bottomNavigationExpanded = !showDetailNavigation || detailNavigationVisibleExpanded
|
||||
const bottomNavigationExpanded = showSnippetDetailBottomNavigation
|
||||
? false
|
||||
: !showDetailNavigation || detailNavigationVisibleExpanded
|
||||
const handleToggleDetailNavigation = useCallback(() => {
|
||||
if (isDetailNavigationHoverPreviewOpen) {
|
||||
if (detailNavigationTransitionTimerRef.current)
|
||||
@ -173,6 +190,13 @@ const MainNav = ({
|
||||
icon: 'i-custom-vender-main-nav-studio',
|
||||
activeIcon: 'i-custom-vender-main-nav-studio-active',
|
||||
},
|
||||
{
|
||||
href: '/roster',
|
||||
label: t('menus.roster', { ns: 'common' }),
|
||||
active: (path: string) => path.startsWith('/roster'),
|
||||
icon: 'i-custom-vender-main-nav-roster',
|
||||
activeIcon: 'i-custom-vender-main-nav-roster-active',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...((isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator)
|
||||
@ -206,23 +230,27 @@ const MainNav = ({
|
||||
},
|
||||
], [isCurrentWorkspaceDatasetOperator, isCurrentWorkspaceEditor, t])
|
||||
|
||||
const renderLogo = () => (
|
||||
<Link
|
||||
href="/"
|
||||
className="flex h-8 shrink-0 items-center overflow-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
aria-label={systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'}
|
||||
>
|
||||
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
|
||||
? (
|
||||
<img
|
||||
src={systemFeatures.branding.workspace_logo}
|
||||
className="block h-5.5 w-auto object-contain"
|
||||
alt=""
|
||||
/>
|
||||
)
|
||||
: <DifyLogo alt="" />}
|
||||
</Link>
|
||||
)
|
||||
const renderLogo = () => {
|
||||
const appTitle = systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'
|
||||
|
||||
return (
|
||||
<Link
|
||||
href="/"
|
||||
className="flex h-8 shrink-0 items-center overflow-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
aria-label={appTitle}
|
||||
>
|
||||
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
|
||||
? (
|
||||
<img
|
||||
src={systemFeatures.branding.workspace_logo}
|
||||
className="block h-5.5 w-auto object-contain"
|
||||
alt=""
|
||||
/>
|
||||
)
|
||||
: <DifyLogo alt="" />}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<aside
|
||||
@ -234,7 +262,9 @@ const MainNav = ({
|
||||
? detailNavigationExpanded
|
||||
? 'w-[248px] bg-background-body p-1'
|
||||
: 'w-16 bg-background-body p-1'
|
||||
: 'w-60 flex-col',
|
||||
: showSnippetDetailBottomNavigation
|
||||
? 'w-16 bg-background-body p-1'
|
||||
: 'w-60 flex-col',
|
||||
'bg-background-body',
|
||||
className,
|
||||
)}
|
||||
@ -261,38 +291,51 @@ const MainNav = ({
|
||||
onToggle={handleToggleDetailNavigation}
|
||||
/>
|
||||
)
|
||||
: showAgentDetailNavigation
|
||||
? (
|
||||
<AgentDetailTop
|
||||
expand={detailNavigationVisibleExpanded}
|
||||
onToggle={handleToggleDetailNavigation}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<DatasetDetailTop
|
||||
expand={detailNavigationVisibleExpanded}
|
||||
onToggle={handleToggleDetailNavigation}
|
||||
/>
|
||||
)
|
||||
: showSnippetDetailBottomNavigation
|
||||
? null
|
||||
: (
|
||||
<DatasetDetailTop
|
||||
expand={detailNavigationVisibleExpanded}
|
||||
onToggle={handleToggleDetailNavigation}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<div className="flex items-center justify-between pt-3 pr-2 pb-2 pl-4">
|
||||
{renderLogo()}
|
||||
<MainNavSearchButton />
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<WorkspaceCard />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<div className="flex items-center justify-between pt-3 pr-2 pb-2 pl-4">
|
||||
{renderLogo()}
|
||||
<MainNavSearchButton />
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<WorkspaceCard />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{showDetailNavigation
|
||||
? showAppDetailNavigation
|
||||
? <AppDetailSection expand={detailNavigationVisibleExpanded} />
|
||||
: <DatasetDetailSection expand={detailNavigationVisibleExpanded} />
|
||||
: (
|
||||
<>
|
||||
<nav className="flex flex-col gap-px p-2">
|
||||
{navItems.map(item => (
|
||||
<MainNavLink key={item.href} item={item} pathname={pathname} />
|
||||
))}
|
||||
</nav>
|
||||
{!isCurrentWorkspaceDatasetOperator && <WebAppsSection />}
|
||||
</>
|
||||
)}
|
||||
{showEnvTag && detailNavigationVisibleExpanded && (
|
||||
: showAgentDetailNavigation
|
||||
? <AgentDetailSection expand={detailNavigationVisibleExpanded} />
|
||||
: <DatasetDetailSection expand={detailNavigationVisibleExpanded} />
|
||||
: showSnippetDetailBottomNavigation
|
||||
? null
|
||||
: (
|
||||
<>
|
||||
<nav className="flex flex-col gap-px p-2">
|
||||
{navItems.map(item => (
|
||||
<MainNavLink key={item.href} item={item} pathname={pathname} />
|
||||
))}
|
||||
</nav>
|
||||
{!isCurrentWorkspaceDatasetOperator && <WebAppsSection />}
|
||||
</>
|
||||
)}
|
||||
{showEnvTag && !showSnippetDetailBottomNavigation && detailNavigationVisibleExpanded && (
|
||||
<div className="relative z-30 mt-auto shrink-0 px-3 pb-2">
|
||||
<EnvNav />
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Credential, PluginPayload } from '../types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AuthCategory, CredentialTypeEnum } from '../types'
|
||||
|
||||
@ -118,6 +118,24 @@ describe('AuthorizedInNode Component', () => {
|
||||
expect(screen.getByText('plugin.auth.workspaceDefault'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should expose the workspace default credential id when requested', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
const onDefaultCredentialChange = vi.fn()
|
||||
const pluginPayload = createPluginPayload()
|
||||
render(
|
||||
<AuthorizedInNode
|
||||
pluginPayload={pluginPayload}
|
||||
onAuthorizationItemClick={vi.fn()}
|
||||
onDefaultCredentialChange={onDefaultCredentialChange}
|
||||
/>,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onDefaultCredentialChange).toHaveBeenCalledWith('test-credential-id')
|
||||
})
|
||||
})
|
||||
|
||||
it('should render credential name when credentialId matches', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
const credential = createCredential({ id: 'selected-id', name: 'My Credential' })
|
||||
|
||||
@ -10,6 +10,7 @@ import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -22,11 +23,13 @@ type AuthorizedInNodeProps = {
|
||||
pluginPayload: PluginPayload
|
||||
onAuthorizationItemClick: (id: string) => void
|
||||
credentialId?: string
|
||||
onDefaultCredentialChange?: (id?: string) => void
|
||||
}
|
||||
const AuthorizedInNode = ({
|
||||
pluginPayload,
|
||||
onAuthorizationItemClick,
|
||||
credentialId,
|
||||
onDefaultCredentialChange,
|
||||
}: AuthorizedInNodeProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
@ -38,6 +41,12 @@ const AuthorizedInNode = ({
|
||||
invalidPluginCredentialInfo,
|
||||
notAllowCustomCredential,
|
||||
} = usePluginAuth(pluginPayload, true, credentialId ? [credentialId] : undefined)
|
||||
const defaultCredentialId = credentials.find(c => c.is_default)?.id
|
||||
|
||||
useEffect(() => {
|
||||
onDefaultCredentialChange?.(defaultCredentialId)
|
||||
}, [defaultCredentialId, onDefaultCredentialChange])
|
||||
|
||||
const renderTrigger = useCallback((open?: boolean) => {
|
||||
let label = ''
|
||||
let removed = false
|
||||
@ -108,9 +117,15 @@ const AuthorizedInNode = ({
|
||||
},
|
||||
]
|
||||
const handleAuthorizationItemClick = useCallback((id: string) => {
|
||||
onAuthorizationItemClick(id)
|
||||
onAuthorizationItemClick(
|
||||
id === '__workspace_default__' && onDefaultCredentialChange
|
||||
? defaultCredentialId || id
|
||||
: id,
|
||||
)
|
||||
setIsOpen(false)
|
||||
}, [
|
||||
defaultCredentialId,
|
||||
onDefaultCredentialChange,
|
||||
onAuthorizationItemClick,
|
||||
setIsOpen,
|
||||
])
|
||||
|
||||
@ -3,7 +3,6 @@ import type { CredentialFormSchemaBase } from '../header/account-setting/model-p
|
||||
import type { AutoUpdateConfig } from './reference-setting-modal/auto-update-setting/types'
|
||||
import type { TypeWithI18N } from '@/app/components/base/form/types'
|
||||
import type { Collection, ToolCredential } from '@/app/components/tools/types'
|
||||
import type { AgentFeature } from '@/app/components/workflow/nodes/agent/types'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
|
||||
export enum PluginCategoryEnum {
|
||||
@ -567,6 +566,12 @@ export type StrategyDetail = {
|
||||
features: AgentFeature[]
|
||||
}
|
||||
|
||||
export const AgentFeature = {
|
||||
HISTORY_MESSAGES: 'history-messages',
|
||||
} as const
|
||||
|
||||
export type AgentFeature = typeof AgentFeature[keyof typeof AgentFeature]
|
||||
|
||||
type Identity = {
|
||||
author: string
|
||||
name: string
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
|
||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
|
||||
@ -33,7 +34,7 @@ export const useAvailableNodesMetaData = () => {
|
||||
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
|
||||
const { metaData } = node
|
||||
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
|
||||
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
|
||||
const description = t(`blocksAbout.${metaData.type}` as I18nKeysWithPrefix<'workflow', 'blocksAbout.'>, { ns: 'workflow' })
|
||||
return {
|
||||
...node,
|
||||
metaData: {
|
||||
@ -44,7 +45,7 @@ export const useAvailableNodesMetaData = () => {
|
||||
},
|
||||
defaultValue: {
|
||||
...node.defaultValue,
|
||||
type: metaData.type,
|
||||
type: metaData.type === BlockEnum.AgentV2 ? BlockEnum.Agent : metaData.type,
|
||||
title,
|
||||
},
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import type { IntegrationSection } from '@/app/components/integrations/routes'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MenuDialog from '@/app/components/header/account-setting/menu-dialog'
|
||||
@ -10,23 +11,35 @@ import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
type IntegrationsSettingModalProps = {
|
||||
section: IntegrationSection
|
||||
source?: 'agent'
|
||||
onCancel: () => void
|
||||
onSectionChange: (section: IntegrationSection) => void
|
||||
}
|
||||
|
||||
export default function IntegrationsSettingModal({
|
||||
section,
|
||||
source,
|
||||
onCancel,
|
||||
onSectionChange,
|
||||
}: IntegrationsSettingModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const isAgentSource = source === 'agent'
|
||||
const handleSwitchToMarketplace = useCallback((path: string) => {
|
||||
window.open(getMarketplaceUrl(path), '_blank', 'noopener,noreferrer')
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<MenuDialog show onClose={onCancel}>
|
||||
<div className="mx-auto flex h-dvh w-[min(1440px,calc(100vw-48px))] shrink-0 py-6">
|
||||
<MenuDialog
|
||||
show
|
||||
backdropClassName={isAgentSource ? 'bg-background-overlay' : undefined}
|
||||
className={isAgentSource ? 'bg-transparent backdrop-blur-none' : undefined}
|
||||
onClose={onCancel}
|
||||
>
|
||||
<div className={cn(
|
||||
'mx-auto flex h-dvh w-[min(1440px,calc(100vw-48px))] shrink-0 py-6',
|
||||
isAgentSource && 'w-full p-6',
|
||||
)}
|
||||
>
|
||||
<div className="relative flex min-h-0 w-full shrink-0 overflow-hidden rounded-2xl border border-divider-subtle bg-components-panel-bg shadow-2xl">
|
||||
<div className="fixed top-6 right-6 z-9999 flex flex-col items-center">
|
||||
<Button
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type {
|
||||
PluginDefaultValue,
|
||||
BlockDefaultValue,
|
||||
TriggerDefaultValue,
|
||||
} from '@/app/components/workflow/block-selector/types'
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
@ -98,7 +98,7 @@ const WorkflowChildren = () => {
|
||||
handleOnboardingClose()
|
||||
}, [handleOnboardingClose])
|
||||
|
||||
const handleSelectStartNode = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => {
|
||||
const handleSelectStartNode = useCallback((nodeType: BlockEnum, toolConfig?: BlockDefaultValue) => {
|
||||
const nodeDefault = availableNodesMetaData.nodesMap?.[nodeType]
|
||||
if (!nodeDefault?.defaultValue)
|
||||
return
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { PluginDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||
import type { BlockDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
@ -9,7 +9,7 @@ import StartNodeSelectionPanel from './start-node-selection-panel'
|
||||
type WorkflowOnboardingModalProps = {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
onSelectStartNode: (nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => void
|
||||
onSelectStartNode: (nodeType: BlockEnum, toolConfig?: BlockDefaultValue) => void
|
||||
}
|
||||
|
||||
const WorkflowOnboardingModal: FC<WorkflowOnboardingModalProps> = ({
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { PluginDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||
import type { BlockDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NodeSelector from '@/app/components/workflow/block-selector'
|
||||
@ -10,7 +10,7 @@ import StartNodeOption from './start-node-option'
|
||||
|
||||
type StartNodeSelectionPanelProps = {
|
||||
onSelectUserInput: () => void
|
||||
onSelectTrigger: (nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => void
|
||||
onSelectTrigger: (nodeType: BlockEnum, toolConfig?: BlockDefaultValue) => void
|
||||
}
|
||||
|
||||
const StartNodeSelectionPanel: FC<StartNodeSelectionPanelProps> = ({
|
||||
@ -20,7 +20,7 @@ const StartNodeSelectionPanel: FC<StartNodeSelectionPanelProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const [showTriggerSelector, setShowTriggerSelector] = useState(false)
|
||||
|
||||
const handleTriggerSelect = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => {
|
||||
const handleTriggerSelect = useCallback((nodeType: BlockEnum, toolConfig?: BlockDefaultValue) => {
|
||||
setShowTriggerSelector(false)
|
||||
onSelectTrigger(nodeType, toolConfig)
|
||||
}, [onSelectTrigger])
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
|
||||
import type { DocPathWithoutLang } from '@/types/doc-paths'
|
||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
|
||||
@ -48,7 +49,7 @@ export const useAvailableNodesMetaData = () => {
|
||||
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
|
||||
const { metaData } = node
|
||||
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
|
||||
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
|
||||
const description = t(`blocksAbout.${metaData.type}` as I18nKeysWithPrefix<'workflow', 'blocksAbout.'>, { ns: 'workflow' })
|
||||
const helpLinkPath = `/use-dify/nodes/${metaData.helpLinkUri}` as DocPathWithoutLang
|
||||
return {
|
||||
...node,
|
||||
@ -60,7 +61,7 @@ export const useAvailableNodesMetaData = () => {
|
||||
},
|
||||
defaultValue: {
|
||||
...node.defaultValue,
|
||||
type: metaData.type,
|
||||
type: metaData.type === BlockEnum.AgentV2 ? BlockEnum.Agent : metaData.type,
|
||||
title,
|
||||
},
|
||||
}
|
||||
|
||||
@ -16,6 +16,8 @@ const mockCustomNode = vi.hoisted(() => vi.fn())
|
||||
const mockCustomNoteNode = vi.hoisted(() => vi.fn())
|
||||
const mockGetIterationStartNode = vi.hoisted(() => vi.fn())
|
||||
const mockGetLoopStartNode = vi.hoisted(() => vi.fn())
|
||||
const mockCreateInlineAgentBinding = vi.hoisted(() => vi.fn())
|
||||
const mockSetOpenInlineAgentPanelNodeId = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
useEventListener: (...args: unknown[]) => mockUseEventListener(...args),
|
||||
@ -68,6 +70,12 @@ vi.mock('@/app/components/workflow/note-node', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/agent-v2/hooks', () => ({
|
||||
useCreateInlineAgentBinding: () => ({
|
||||
createInlineAgentBinding: mockCreateInlineAgentBinding,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/utils', () => ({
|
||||
getIterationStartNode: (...args: unknown[]) => mockGetIterationStartNode(...args),
|
||||
getLoopStartNode: (...args: unknown[]) => mockGetLoopStartNode(...args),
|
||||
@ -102,6 +110,9 @@ describe('CandidateNodeMain', () => {
|
||||
mockUseEventListener.mockImplementation((event: 'click' | 'contextmenu', handler: (event: { preventDefault: () => void }) => void) => {
|
||||
eventHandlers[event] = handler
|
||||
})
|
||||
mockSetNodes.mockImplementation((nextNodes) => {
|
||||
nodes = nextNodes
|
||||
})
|
||||
mockUseStoreApi.mockReturnValue({
|
||||
getState: () => ({
|
||||
getNodes: () => nodes,
|
||||
@ -126,6 +137,9 @@ describe('CandidateNodeMain', () => {
|
||||
},
|
||||
}))
|
||||
mockUseWorkflowStore.mockReturnValue({
|
||||
getState: () => ({
|
||||
setOpenInlineAgentPanelNodeId: mockSetOpenInlineAgentPanelNodeId,
|
||||
}),
|
||||
setState: mockWorkflowStoreSetState,
|
||||
})
|
||||
mockUseHooks.mockReturnValue({
|
||||
@ -137,6 +151,17 @@ describe('CandidateNodeMain', () => {
|
||||
mockHandleSyncWorkflowDraft.mockImplementation((_isSync: boolean, _force: boolean, options?: { onSuccess?: () => void }) => {
|
||||
options?.onSuccess?.()
|
||||
})
|
||||
mockCreateInlineAgentBinding.mockImplementation((_nodeId: string, options?: { onSuccess?: (binding: {
|
||||
binding_type: 'inline_agent'
|
||||
agent_id: string
|
||||
current_snapshot_id: string
|
||||
}) => void }) => {
|
||||
options?.onSuccess?.({
|
||||
binding_type: 'inline_agent',
|
||||
agent_id: 'inline-agent-1',
|
||||
current_snapshot_id: 'inline-snapshot-1',
|
||||
})
|
||||
})
|
||||
mockGetIterationStartNode.mockReturnValue(createNode({ id: 'iteration-start' }))
|
||||
mockGetLoopStartNode.mockReturnValue(createNode({ id: 'loop-start' }))
|
||||
})
|
||||
@ -201,6 +226,97 @@ describe('CandidateNodeMain', () => {
|
||||
expect(mockHandleNodeSelect).toHaveBeenCalledWith('candidate-note')
|
||||
})
|
||||
|
||||
it('should sync draft immediately when committing an Agent v2 node', () => {
|
||||
const candidateNode = createNode({
|
||||
id: 'candidate-agent-v2',
|
||||
type: CUSTOM_NODE,
|
||||
data: {
|
||||
type: BlockEnum.Agent,
|
||||
title: 'Agent Candidate',
|
||||
agent_binding: {
|
||||
binding_type: 'roster_agent',
|
||||
agent_id: 'agent-1',
|
||||
},
|
||||
agent_node_kind: 'dify_agent',
|
||||
version: '2',
|
||||
_isCandidate: true,
|
||||
},
|
||||
})
|
||||
|
||||
render(<CandidateNodeMain candidateNode={candidateNode} />)
|
||||
|
||||
eventHandlers.click?.({ preventDefault: vi.fn() })
|
||||
|
||||
expect(mockSetNodes).toHaveBeenCalledWith(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'candidate-agent-v2',
|
||||
data: expect.objectContaining({
|
||||
agent_binding: {
|
||||
binding_type: 'roster_agent',
|
||||
agent_id: 'agent-1',
|
||||
},
|
||||
agent_node_kind: 'dify_agent',
|
||||
version: '2',
|
||||
_isCandidate: false,
|
||||
}),
|
||||
}),
|
||||
]))
|
||||
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true, true)
|
||||
})
|
||||
|
||||
it('should create inline binding before syncing a start-from-scratch Agent v2 node', () => {
|
||||
const candidateNode = createNode({
|
||||
id: 'candidate-inline-agent-v2',
|
||||
type: CUSTOM_NODE,
|
||||
data: {
|
||||
type: BlockEnum.Agent,
|
||||
title: 'Agent Candidate',
|
||||
agent_binding: {
|
||||
binding_type: 'inline_agent',
|
||||
},
|
||||
agent_node_kind: 'dify_agent',
|
||||
version: '2',
|
||||
_isCandidate: true,
|
||||
},
|
||||
})
|
||||
|
||||
render(<CandidateNodeMain candidateNode={candidateNode} />)
|
||||
|
||||
eventHandlers.click?.({ preventDefault: vi.fn() })
|
||||
|
||||
expect(mockCreateInlineAgentBinding).toHaveBeenCalledWith('candidate-inline-agent-v2', expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
}))
|
||||
expect(mockSetNodes.mock.calls[0]?.[0]).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'candidate-inline-agent-v2',
|
||||
data: expect.objectContaining({
|
||||
agent_binding: {
|
||||
binding_type: 'inline_agent',
|
||||
},
|
||||
_isTempNode: true,
|
||||
}),
|
||||
}),
|
||||
]))
|
||||
expect(mockSetNodes).toHaveBeenLastCalledWith(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'candidate-inline-agent-v2',
|
||||
data: expect.objectContaining({
|
||||
agent_binding: {
|
||||
binding_type: 'inline_agent',
|
||||
agent_id: 'inline-agent-1',
|
||||
current_snapshot_id: 'inline-snapshot-1',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]))
|
||||
const finalNodes = mockSetNodes.mock.calls.at(-1)?.[0]
|
||||
const finalAgentNode = finalNodes.find((node: { id: string }) => node.id === 'candidate-inline-agent-v2')
|
||||
expect(finalAgentNode.data._isTempNode).toBeUndefined()
|
||||
expect(mockSetOpenInlineAgentPanelNodeId).toHaveBeenCalledWith('candidate-inline-agent-v2')
|
||||
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true, true)
|
||||
})
|
||||
|
||||
it('should append iteration and loop start helper nodes for control-flow candidates', () => {
|
||||
const iterationNode = createNode({
|
||||
id: 'candidate-iteration',
|
||||
|
||||
@ -68,6 +68,7 @@ const DEFAULT_ICON_MAP: Record<BlockEnum, React.ComponentType<{ className: strin
|
||||
[BlockEnum.DocExtractor]: DocsExtractor,
|
||||
[BlockEnum.ListFilter]: ListFilter,
|
||||
[BlockEnum.Agent]: Agent,
|
||||
[BlockEnum.AgentV2]: Agent,
|
||||
[BlockEnum.KnowledgeBase]: KnowledgeBase,
|
||||
[BlockEnum.DataSource]: Datasource,
|
||||
[BlockEnum.DataSourceEmpty]: () => null,
|
||||
@ -118,6 +119,7 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
|
||||
[BlockEnum.DocExtractor]: 'bg-util-colors-green-green-500',
|
||||
[BlockEnum.ListFilter]: 'bg-util-colors-cyan-cyan-500',
|
||||
[BlockEnum.Agent]: 'bg-util-colors-indigo-indigo-500',
|
||||
[BlockEnum.AgentV2]: 'bg-util-colors-indigo-indigo-500',
|
||||
[BlockEnum.HumanInput]: 'bg-util-colors-cyan-cyan-500',
|
||||
[BlockEnum.KnowledgeBase]: 'bg-util-colors-warning-warning-500',
|
||||
[BlockEnum.DataSource]: 'bg-components-icon-bg-midnight-solid',
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { FlowType } from '@/types/common'
|
||||
import { HooksStoreContext } from '../../hooks-store/provider'
|
||||
import { createHooksStore } from '../../hooks-store/store'
|
||||
import { BlockEnum } from '../../types'
|
||||
import { AgentSelectorContent } from '../agent-selector'
|
||||
import Blocks from '../blocks'
|
||||
import { BlockClassificationEnum } from '../types'
|
||||
|
||||
@ -10,6 +15,12 @@ const runtimeState = vi.hoisted(() => ({
|
||||
nodes: [] as Array<{ data: { type?: BlockEnum } }>,
|
||||
}))
|
||||
|
||||
const queryMocks = vi.hoisted(() => ({
|
||||
inviteOptionsQueryFn: vi.fn(),
|
||||
versionDetailGet: vi.fn(),
|
||||
toastError: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useStoreApi: () => ({
|
||||
getState: () => ({
|
||||
@ -26,10 +37,48 @@ vi.mock('@/app/components/app/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
const createBlock = (type: BlockEnum, title: string, classification = BlockClassificationEnum.Default): NodeDefault => ({
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
agent: {
|
||||
byAgentId: {
|
||||
versions: {
|
||||
byVersionId: {
|
||||
get: {
|
||||
queryOptions: ({ input }: { input: unknown }) => ({
|
||||
queryKey: ['agent-version-detail', input],
|
||||
queryFn: () => queryMocks.versionDetailGet(input),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
inviteOptions: {
|
||||
get: {
|
||||
queryOptions: (options: unknown) => ({
|
||||
queryKey: ['agents', 'invite-options', options],
|
||||
queryFn: () => queryMocks.inviteOptionsQueryFn(options),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
toast: {
|
||||
error: (message: string) => queryMocks.toastError(message),
|
||||
},
|
||||
}))
|
||||
|
||||
const createBlock = (
|
||||
type: BlockEnum,
|
||||
title: string,
|
||||
classification = BlockClassificationEnum.Default,
|
||||
sort = 0,
|
||||
): NodeDefault => ({
|
||||
metaData: {
|
||||
classification,
|
||||
sort: 0,
|
||||
sort,
|
||||
type,
|
||||
title,
|
||||
author: 'Dify',
|
||||
@ -39,6 +88,17 @@ const createBlock = (type: BlockEnum, title: string, classification = BlockClass
|
||||
checkValid: () => ({ isValid: true }),
|
||||
})
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
let reject!: (reason?: unknown) => void
|
||||
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise
|
||||
reject = rejectPromise
|
||||
})
|
||||
|
||||
return { promise, reject, resolve }
|
||||
}
|
||||
|
||||
describe('Blocks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -65,12 +125,12 @@ describe('Blocks', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('LLM')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'LLM' })).toBeInTheDocument()
|
||||
expect(screen.getByText('Exit Loop')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.nodes.loop.loopNode')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Knowledge Retrieval')).not.toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByText('LLM'))
|
||||
await user.click(screen.getByRole('button', { name: 'LLM' }))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(BlockEnum.LLM)
|
||||
})
|
||||
@ -87,4 +147,396 @@ describe('Blocks', () => {
|
||||
|
||||
expect(screen.getByText('workflow.tabs.noResult')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens the agent selector on Agent block hover', async () => {
|
||||
const user = userEvent.setup()
|
||||
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
|
||||
data: [],
|
||||
has_more: false,
|
||||
limit: 8,
|
||||
page: 1,
|
||||
total: 0,
|
||||
})
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
const hooksStore = createHooksStore({
|
||||
configsMap: {
|
||||
flowId: 'app-1',
|
||||
flowType: FlowType.appFlow,
|
||||
fileSettings: {} as never,
|
||||
},
|
||||
})
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HooksStoreContext value={hooksStore}>
|
||||
<Blocks
|
||||
searchText=""
|
||||
onSelect={vi.fn()}
|
||||
availableBlocksTypes={[BlockEnum.AgentV2]}
|
||||
blocks={[createBlock(BlockEnum.AgentV2, 'Agent')]}
|
||||
/>
|
||||
</HooksStoreContext>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
await user.hover(screen.getByRole('button', { name: 'Agent' }))
|
||||
|
||||
expect(await screen.findByRole('dialog', { name: 'agentV2.roster.nodeSelector.dialogLabel' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens the agent selector from the Agent block and selects an agent', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: 'agent-1',
|
||||
name: 'Nadia',
|
||||
description: 'Clarification Drafter',
|
||||
active_config_snapshot_id: 'version-1',
|
||||
role: 'Researcher',
|
||||
agent_kind: 'dify_agent',
|
||||
icon: 'A',
|
||||
icon_background: '#E9D7FE',
|
||||
icon_type: 'emoji',
|
||||
scope: 'roster',
|
||||
source: 'workflow',
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
has_more: false,
|
||||
limit: 8,
|
||||
page: 1,
|
||||
total: 1,
|
||||
})
|
||||
queryMocks.versionDetailGet.mockResolvedValue({
|
||||
config_snapshot: {
|
||||
model: {
|
||||
model: 'gpt-4o',
|
||||
model_provider: 'openai',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
const hooksStore = createHooksStore({
|
||||
configsMap: {
|
||||
flowId: 'app-1',
|
||||
flowType: FlowType.appFlow,
|
||||
fileSettings: {} as never,
|
||||
},
|
||||
})
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HooksStoreContext value={hooksStore}>
|
||||
<Blocks
|
||||
searchText=""
|
||||
onSelect={onSelect}
|
||||
availableBlocksTypes={[BlockEnum.LLM, BlockEnum.AgentV2]}
|
||||
blocks={[
|
||||
createBlock(BlockEnum.LLM, 'LLM', BlockClassificationEnum.Default, 0),
|
||||
createBlock(BlockEnum.AgentV2, 'Agent', BlockClassificationEnum.Default, 3),
|
||||
]}
|
||||
/>
|
||||
</HooksStoreContext>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByText('Agent').compareDocumentPosition(screen.getByText('LLM')) & Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Agent' }))
|
||||
|
||||
expect(await screen.findByRole('dialog', { name: 'agentV2.roster.nodeSelector.dialogLabel' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('combobox', { name: 'agentV2.roster.searchLabel' })).toBeInTheDocument()
|
||||
expect(await screen.findByText('Nadia')).toBeInTheDocument()
|
||||
expect(screen.getByText('Researcher')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('option', { name: 'Nadia Researcher' }))
|
||||
|
||||
await waitFor(() => expect(queryMocks.versionDetailGet).toHaveBeenCalledWith({
|
||||
params: {
|
||||
agent_id: 'agent-1',
|
||||
version_id: 'version-1',
|
||||
},
|
||||
}))
|
||||
expect(onSelect).toHaveBeenCalledWith(BlockEnum.AgentV2, {
|
||||
agent_binding: {
|
||||
binding_type: 'roster_agent',
|
||||
agent_id: 'agent-1',
|
||||
},
|
||||
agent_node_kind: 'dify_agent',
|
||||
version: '2',
|
||||
})
|
||||
expect(queryMocks.inviteOptionsQueryFn).toHaveBeenCalledWith({
|
||||
input: {
|
||||
query: {
|
||||
app_id: 'app-1',
|
||||
limit: 8,
|
||||
page: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the agent list visible while validating a selected agent', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
const versionDetail = createDeferred<{
|
||||
config_snapshot: {
|
||||
model: {
|
||||
model: string
|
||||
model_provider: string
|
||||
}
|
||||
}
|
||||
}>()
|
||||
|
||||
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: 'agent-1',
|
||||
name: 'Nadia',
|
||||
description: 'Clarification Drafter',
|
||||
active_config_snapshot_id: 'version-1',
|
||||
role: 'Researcher',
|
||||
agent_kind: 'dify_agent',
|
||||
icon: 'A',
|
||||
icon_background: '#E9D7FE',
|
||||
icon_type: 'emoji',
|
||||
scope: 'roster',
|
||||
source: 'workflow',
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
has_more: false,
|
||||
limit: 8,
|
||||
page: 1,
|
||||
total: 1,
|
||||
})
|
||||
queryMocks.versionDetailGet.mockReturnValue(versionDetail.promise)
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
const hooksStore = createHooksStore({
|
||||
configsMap: {
|
||||
flowId: 'app-1',
|
||||
flowType: FlowType.appFlow,
|
||||
fileSettings: {} as never,
|
||||
},
|
||||
})
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HooksStoreContext value={hooksStore}>
|
||||
<AgentSelectorContent
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</HooksStoreContext>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
expect(await screen.findByText('Nadia')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('option', { name: 'Nadia Researcher' }))
|
||||
|
||||
await waitFor(() => expect(queryMocks.versionDetailGet).toHaveBeenCalled())
|
||||
expect(screen.getByText('Nadia')).toBeInTheDocument()
|
||||
expect(screen.queryByText('common.loading')).not.toBeInTheDocument()
|
||||
|
||||
versionDetail.resolve({
|
||||
config_snapshot: {
|
||||
model: {
|
||||
model: 'gpt-4o',
|
||||
model_provider: 'openai',
|
||||
},
|
||||
},
|
||||
})
|
||||
await waitFor(() => expect(onSelect).toHaveBeenCalled())
|
||||
})
|
||||
|
||||
it('does not select an Agent v2 roster agent without model config', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: 'agent-1',
|
||||
name: 'Nadia',
|
||||
description: 'Clarification Drafter',
|
||||
active_config_snapshot_id: 'version-1',
|
||||
role: 'Researcher',
|
||||
agent_kind: 'dify_agent',
|
||||
icon: 'A',
|
||||
icon_background: '#E9D7FE',
|
||||
icon_type: 'emoji',
|
||||
scope: 'roster',
|
||||
source: 'workflow',
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
has_more: false,
|
||||
limit: 8,
|
||||
page: 1,
|
||||
total: 1,
|
||||
})
|
||||
queryMocks.versionDetailGet.mockResolvedValue({
|
||||
config_snapshot: {},
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
const hooksStore = createHooksStore({
|
||||
configsMap: {
|
||||
flowId: 'app-1',
|
||||
flowType: FlowType.appFlow,
|
||||
fileSettings: {} as never,
|
||||
},
|
||||
})
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HooksStoreContext value={hooksStore}>
|
||||
<Blocks
|
||||
searchText=""
|
||||
onSelect={onSelect}
|
||||
availableBlocksTypes={[BlockEnum.AgentV2]}
|
||||
blocks={[createBlock(BlockEnum.AgentV2, 'Agent', BlockClassificationEnum.Default, 3)]}
|
||||
/>
|
||||
</HooksStoreContext>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Agent' }))
|
||||
expect(await screen.findByText('Nadia')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('option', { name: 'Nadia Researcher' }))
|
||||
|
||||
await waitFor(() => expect(queryMocks.toastError).toHaveBeenCalledWith('workflow.nodes.agent.modelNotSelected'))
|
||||
expect(onSelect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('inserts an inline Agent v2 node from the selector start action', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelect = vi.fn()
|
||||
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
|
||||
data: [],
|
||||
has_more: false,
|
||||
limit: 8,
|
||||
page: 1,
|
||||
total: 0,
|
||||
})
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
const hooksStore = createHooksStore({
|
||||
configsMap: {
|
||||
flowId: 'app-1',
|
||||
flowType: FlowType.appFlow,
|
||||
fileSettings: {} as never,
|
||||
},
|
||||
})
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HooksStoreContext value={hooksStore}>
|
||||
<Blocks
|
||||
searchText=""
|
||||
onSelect={onSelect}
|
||||
availableBlocksTypes={[BlockEnum.AgentV2]}
|
||||
blocks={[createBlock(BlockEnum.AgentV2, 'Agent', BlockClassificationEnum.Default, 3)]}
|
||||
/>
|
||||
</HooksStoreContext>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Agent' }))
|
||||
await user.click(await screen.findByRole('button', { name: 'agentV2.roster.nodeSelector.startFromScratch' }))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(BlockEnum.AgentV2, {
|
||||
agent_binding: {
|
||||
binding_type: 'inline_agent',
|
||||
},
|
||||
agent_node_kind: 'dify_agent',
|
||||
version: '2',
|
||||
})
|
||||
})
|
||||
|
||||
it('closes the agent selector when Escape closes the combobox', async () => {
|
||||
const user = userEvent.setup()
|
||||
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
|
||||
data: [],
|
||||
has_more: false,
|
||||
limit: 8,
|
||||
page: 1,
|
||||
total: 0,
|
||||
})
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
const hooksStore = createHooksStore({
|
||||
configsMap: {
|
||||
flowId: 'app-1',
|
||||
flowType: FlowType.appFlow,
|
||||
fileSettings: {} as never,
|
||||
},
|
||||
})
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HooksStoreContext value={hooksStore}>
|
||||
<Blocks
|
||||
searchText=""
|
||||
onSelect={vi.fn()}
|
||||
availableBlocksTypes={[BlockEnum.AgentV2]}
|
||||
blocks={[createBlock(BlockEnum.AgentV2, 'Agent')]}
|
||||
/>
|
||||
</HooksStoreContext>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Agent' }))
|
||||
|
||||
expect(await screen.findByRole('dialog', { name: 'agentV2.roster.nodeSelector.dialogLabel' })).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('combobox', { name: 'agentV2.roster.searchLabel' }))
|
||||
await user.keyboard('{Escape}')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog', { name: 'agentV2.roster.nodeSelector.dialogLabel' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
346
web/app/components/workflow/block-selector/agent-selector.tsx
Normal file
@ -0,0 +1,346 @@
|
||||
import type { AgentInviteOptionResponse } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { ComboboxRootChangeEventDetails } from '@langgenius/dify-ui/combobox'
|
||||
import type { NodeDefault } from '../types'
|
||||
import type { AgentRosterNodeData } from './types'
|
||||
import { AvatarFallback, AvatarImage, AvatarRoot } from '@langgenius/dify-ui/avatar'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxEmpty,
|
||||
ComboboxInput,
|
||||
ComboboxInputGroup,
|
||||
ComboboxItem,
|
||||
ComboboxItemText,
|
||||
ComboboxList,
|
||||
ComboboxStatus,
|
||||
} from '@langgenius/dify-ui/combobox'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTitle,
|
||||
PopoverTrigger,
|
||||
} from '@langgenius/dify-ui/popover'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import Link from '@/next/link'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import BlockIcon from '../block-icon'
|
||||
|
||||
const AGENT_SELECTOR_PAGE_SIZE = 8
|
||||
|
||||
export function AgentSelectorContent({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSelect,
|
||||
onStartFromScratch,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSelect: (agent: AgentRosterNodeData) => void
|
||||
onStartFromScratch?: () => void
|
||||
}) {
|
||||
const { t } = useTranslation(['agentV2', 'common', 'workflow'])
|
||||
const queryClient = useQueryClient()
|
||||
const appId = useHooksStore(s => s.configsMap?.flowId)
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [validatingAgentId, setValidatingAgentId] = useState<string>()
|
||||
const debouncedSearchText = useDebounce(searchText.trim(), { wait: 300 })
|
||||
const agentsQuery = useQuery({
|
||||
...consoleQuery.agent.inviteOptions.get.queryOptions({
|
||||
input: {
|
||||
query: {
|
||||
limit: AGENT_SELECTOR_PAGE_SIZE,
|
||||
page: 1,
|
||||
...(appId ? { app_id: appId } : {}),
|
||||
...(debouncedSearchText ? { keyword: debouncedSearchText } : {}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
const agents = agentsQuery.data?.data ?? []
|
||||
const handleInputValueChange = (nextSearchText: string, details: ComboboxRootChangeEventDetails) => {
|
||||
if (details.reason !== 'item-press')
|
||||
setSearchText(nextSearchText)
|
||||
}
|
||||
const handleValueChange = async (agent: AgentInviteOptionResponse | null) => {
|
||||
if (!agent || validatingAgentId)
|
||||
return
|
||||
|
||||
if (!agent.active_config_snapshot_id) {
|
||||
toast.error(t('nodes.agent.modelNotSelected', { ns: 'workflow' }))
|
||||
return
|
||||
}
|
||||
|
||||
setValidatingAgentId(agent.id)
|
||||
try {
|
||||
const activeConfigSnapshot = await queryClient.fetchQuery(consoleQuery.agent.byAgentId.versions.byVersionId.get.queryOptions({
|
||||
input: {
|
||||
params: {
|
||||
agent_id: agent.id,
|
||||
version_id: agent.active_config_snapshot_id,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
if (!activeConfigSnapshot.config_snapshot.model) {
|
||||
toast.error(t('nodes.agent.modelNotSelected', { ns: 'workflow' }))
|
||||
return
|
||||
}
|
||||
|
||||
onSelect(toAgentRosterNodeData(agent))
|
||||
}
|
||||
catch {
|
||||
toast.error(t('roster.loadingError', { ns: 'agentV2' }))
|
||||
}
|
||||
finally {
|
||||
setValidatingAgentId(undefined)
|
||||
}
|
||||
}
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen)
|
||||
onOpenChange(false)
|
||||
}
|
||||
const isLoading = agentsQuery.isPending
|
||||
|
||||
return (
|
||||
<div className="w-60 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm">
|
||||
<Combobox<AgentInviteOptionResponse>
|
||||
filter={null}
|
||||
inputValue={searchText}
|
||||
items={agents}
|
||||
itemToStringLabel={getAgentLabel}
|
||||
itemToStringValue={getAgentValue}
|
||||
open={open}
|
||||
value={null}
|
||||
onInputValueChange={handleInputValueChange}
|
||||
onOpenChange={handleOpenChange}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<div className="bg-components-panel-bg-blur p-2 pb-1">
|
||||
<ComboboxInputGroup className="h-8 min-h-8 px-2">
|
||||
<span aria-hidden className="mr-0.5 i-ri-search-line size-4 shrink-0 text-components-input-text-placeholder" />
|
||||
<ComboboxInput
|
||||
aria-label={t('roster.searchLabel', { ns: 'agentV2' })}
|
||||
placeholder={t('roster.nodeSelector.searchPlaceholder', { ns: 'agentV2' })}
|
||||
className="block h-4.5 grow px-1 py-0 system-sm-regular text-components-input-text-filled"
|
||||
/>
|
||||
</ComboboxInputGroup>
|
||||
</div>
|
||||
<div className="max-h-54 overflow-y-auto p-1">
|
||||
{isLoading && (
|
||||
<AgentSelectorLoadingSkeleton label={t('loading', { ns: 'common' })} />
|
||||
)}
|
||||
{!isLoading && agentsQuery.isError && (
|
||||
<ComboboxStatus className="px-3 py-2 system-xs-regular">
|
||||
{t('roster.loadingError', { ns: 'agentV2' })}
|
||||
</ComboboxStatus>
|
||||
)}
|
||||
{!isLoading && !agentsQuery.isError && (
|
||||
<>
|
||||
<ComboboxList className="max-h-none overflow-visible p-0">
|
||||
{(agent: AgentInviteOptionResponse) => (
|
||||
<AgentSelectorItem key={agent.id} agent={agent} />
|
||||
)}
|
||||
</ComboboxList>
|
||||
<ComboboxEmpty className="px-3 py-2 system-xs-regular">
|
||||
{debouncedSearchText
|
||||
? t('roster.emptySearch', { ns: 'agentV2' })
|
||||
: t('roster.empty', { ns: 'agentV2' })}
|
||||
</ComboboxEmpty>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Combobox>
|
||||
<div className="border-t border-divider-subtle p-1">
|
||||
{onStartFromScratch && (
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-h-7 w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left system-sm-regular text-text-secondary hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
onClick={onStartFromScratch}
|
||||
>
|
||||
<span aria-hidden className="i-ri-add-line size-4 shrink-0 text-text-tertiary" />
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{t('roster.nodeSelector.startFromScratch', { ns: 'agentV2' })}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
href="/roster"
|
||||
className="flex min-h-7 w-full items-center gap-2 rounded-md px-2 py-1.5 system-sm-regular text-text-secondary hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
>
|
||||
<span aria-hidden className="i-ri-arrow-right-up-line size-4 shrink-0 text-text-tertiary" />
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{t('roster.nodeSelector.manageInAgentConsole', { ns: 'agentV2' })}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentSelectorLoadingSkeleton({
|
||||
label,
|
||||
}: {
|
||||
label: string
|
||||
}) {
|
||||
return (
|
||||
<ComboboxStatus className="p-0">
|
||||
<span className="sr-only">{label}</span>
|
||||
<div className="relative overflow-hidden" aria-hidden>
|
||||
<div className="p-1">
|
||||
{['skeleton-1', 'skeleton-2', 'skeleton-3', 'skeleton-4'].map((key, index) => (
|
||||
<div
|
||||
key={key}
|
||||
className={cn(
|
||||
'flex items-center gap-2 py-1.5 pr-3 pl-2 opacity-20',
|
||||
index === 3 && 'opacity-10',
|
||||
)}
|
||||
>
|
||||
<div className="size-8 shrink-0 rounded-full bg-text-quaternary" />
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||
<div className="h-2 w-20 rounded-xs bg-text-quaternary" />
|
||||
<div className="h-2 w-28 rounded-xs bg-text-quaternary" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-0 bg-linear-to-b from-components-panel-bg-transparent to-background-default-subtle" />
|
||||
</div>
|
||||
</ComboboxStatus>
|
||||
)
|
||||
}
|
||||
|
||||
function getAgentLabel(agent: AgentInviteOptionResponse) {
|
||||
return agent.name
|
||||
}
|
||||
|
||||
function getAgentValue(agent: AgentInviteOptionResponse) {
|
||||
return agent.id
|
||||
}
|
||||
|
||||
function toAgentRosterNodeData(agent: AgentInviteOptionResponse): AgentRosterNodeData {
|
||||
return {
|
||||
description: agent.description,
|
||||
icon: agent.icon,
|
||||
icon_background: agent.icon_background,
|
||||
icon_type: agent.icon_type,
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
role: agent.role,
|
||||
}
|
||||
}
|
||||
|
||||
function AgentSelectorAvatar({
|
||||
agent,
|
||||
}: {
|
||||
agent: AgentInviteOptionResponse
|
||||
}) {
|
||||
const imageUrl = (agent.icon_type === 'image' || agent.icon_type === 'link') ? agent.icon : undefined
|
||||
|
||||
return (
|
||||
<AvatarRoot
|
||||
size="md"
|
||||
className="border-[0.5px] border-divider-regular text-lg"
|
||||
style={{ background: imageUrl ? undefined : (agent.icon_background || '#FFEAD5') }}
|
||||
>
|
||||
{imageUrl && (
|
||||
<AvatarImage
|
||||
src={imageUrl}
|
||||
alt={agent.name}
|
||||
/>
|
||||
)}
|
||||
<AvatarFallback size="md" className="text-lg text-text-primary-on-surface">
|
||||
{agent.icon_type === 'emoji' && agent.icon ? agent.icon : agent.name[0]?.toLocaleUpperCase()}
|
||||
</AvatarFallback>
|
||||
</AvatarRoot>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentSelectorItem({
|
||||
agent,
|
||||
}: {
|
||||
agent: AgentInviteOptionResponse
|
||||
}) {
|
||||
return (
|
||||
<ComboboxItem
|
||||
value={agent}
|
||||
className="grid-cols-[1fr] gap-0 py-1.5 pr-3 pl-2"
|
||||
>
|
||||
<ComboboxItemText className="flex items-center gap-2 px-0">
|
||||
<span aria-hidden className="shrink-0">
|
||||
<AgentSelectorAvatar agent={agent} />
|
||||
</span>
|
||||
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<span className="truncate system-sm-medium text-text-secondary">
|
||||
{agent.name}
|
||||
</span>
|
||||
<span className="truncate system-xs-regular text-text-tertiary">
|
||||
{agent.role || agent.description}
|
||||
</span>
|
||||
</span>
|
||||
</ComboboxItemText>
|
||||
</ComboboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
export function AgentBlockItem({
|
||||
block,
|
||||
onSelect,
|
||||
onStartFromScratch,
|
||||
}: {
|
||||
block: NodeDefault
|
||||
onSelect: (agent: AgentRosterNodeData) => void
|
||||
onStartFromScratch: () => void
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const [open, setOpen] = useState(false)
|
||||
const handleSelect = (agent: AgentRosterNodeData) => {
|
||||
setOpen(false)
|
||||
onSelect(agent)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
openOnHover
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 text-left hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-popup-open:bg-state-base-hover"
|
||||
>
|
||||
<BlockIcon
|
||||
className="mr-2 shrink-0"
|
||||
type={block.metaData.type}
|
||||
/>
|
||||
<span className="min-w-0 grow truncate system-sm-medium text-text-secondary">
|
||||
{block.metaData.title}
|
||||
</span>
|
||||
<span aria-hidden className="i-custom-vender-solid-general-arrow-down-round-fill size-4 shrink-0 -rotate-90 text-text-tertiary" />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="right-start"
|
||||
sideOffset={4}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
>
|
||||
<PopoverTitle className="sr-only">
|
||||
{t('roster.nodeSelector.dialogLabel')}
|
||||
</PopoverTitle>
|
||||
<AgentSelectorContent
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onSelect={handleSelect}
|
||||
onStartFromScratch={() => {
|
||||
setOpen(false)
|
||||
onStartFromScratch()
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { NodeDefault } from '../types'
|
||||
import type { NodeDefault, OnSelectBlock } from '../types'
|
||||
import type { BlockClassificationEnum } from './types'
|
||||
import {
|
||||
createPreviewCardHandle,
|
||||
@ -17,12 +17,13 @@ import { useStoreApi } from 'reactflow'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { BlockEnum } from '../types'
|
||||
import { AgentBlockItem } from './agent-selector'
|
||||
import { BLOCK_CLASSIFICATIONS } from './constants'
|
||||
import { useBlocks } from './hooks'
|
||||
|
||||
type BlocksProps = {
|
||||
searchText: string
|
||||
onSelect: (type: BlockEnum) => void
|
||||
onSelect: OnSelectBlock
|
||||
availableBlocksTypes?: BlockEnum[]
|
||||
blocks?: NodeDefault[]
|
||||
}
|
||||
@ -79,7 +80,13 @@ const Blocks = ({
|
||||
const isEmpty = Object.values(groups).every(list => !list.length)
|
||||
|
||||
const renderGroup = useCallback((classification: BlockClassificationEnum) => {
|
||||
const list = groups[classification]!.sort((a, b) => (a.metaData.sort || 0) - (b.metaData.sort || 0))
|
||||
const list = [...groups[classification]!].sort((a, b) => {
|
||||
if (a.metaData.type === BlockEnum.AgentV2)
|
||||
return -1
|
||||
if (b.metaData.type === BlockEnum.AgentV2)
|
||||
return 1
|
||||
return (a.metaData.sort || 0) - (b.metaData.sort || 0)
|
||||
})
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const hasKnowledgeBaseNode = nodes.some(node => node.data.type === BlockEnum.KnowledgeBase)
|
||||
@ -102,39 +109,64 @@ const Blocks = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
// Preview is supplementary: icon/title/description are all reachable
|
||||
// from the node that gets added on click (inspector + canvas), so
|
||||
// hover/focus-only activation is a11y-safe. See
|
||||
// packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
|
||||
filteredList.map(block => (
|
||||
<PreviewCardTrigger
|
||||
key={block.metaData.type}
|
||||
delay={150}
|
||||
closeDelay={150}
|
||||
handle={previewCardHandle}
|
||||
payload={{ block }}
|
||||
render={(
|
||||
<div
|
||||
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
|
||||
onClick={() => onSelect(block.metaData.type)}
|
||||
>
|
||||
<BlockIcon
|
||||
className="mr-2 shrink-0"
|
||||
type={block.metaData.type}
|
||||
/>
|
||||
<div className="grow text-sm text-text-secondary">{block.metaData.title}</div>
|
||||
{
|
||||
block.metaData.type === BlockEnum.LoopEnd && (
|
||||
<Badge
|
||||
text={t('nodes.loop.loopNode', { ns: 'workflow' })}
|
||||
className="ml-2 shrink-0"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))
|
||||
filteredList.map((block) => {
|
||||
if (block.metaData.type === BlockEnum.AgentV2) {
|
||||
return (
|
||||
<AgentBlockItem
|
||||
key={block.metaData.type}
|
||||
block={block}
|
||||
onSelect={agent =>
|
||||
onSelect(BlockEnum.AgentV2, {
|
||||
agent_binding: {
|
||||
binding_type: 'roster_agent',
|
||||
agent_id: agent.id,
|
||||
},
|
||||
agent_node_kind: 'dify_agent',
|
||||
version: '2',
|
||||
})}
|
||||
onStartFromScratch={() =>
|
||||
onSelect(BlockEnum.AgentV2, {
|
||||
agent_binding: {
|
||||
binding_type: 'inline_agent',
|
||||
},
|
||||
agent_node_kind: 'dify_agent',
|
||||
version: '2',
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PreviewCardTrigger
|
||||
key={block.metaData.type}
|
||||
delay={150}
|
||||
closeDelay={150}
|
||||
handle={previewCardHandle}
|
||||
payload={{ block }}
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 text-left hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
onClick={() => onSelect(block.metaData.type)}
|
||||
>
|
||||
<BlockIcon
|
||||
className="mr-2 shrink-0"
|
||||
type={block.metaData.type}
|
||||
/>
|
||||
<span className="min-w-0 grow truncate text-sm text-text-secondary">{block.metaData.title}</span>
|
||||
{
|
||||
block.metaData.type === BlockEnum.LoopEnd && (
|
||||
<Badge
|
||||
text={t('nodes.loop.loopNode', { ns: 'workflow' })}
|
||||
className="ml-2 shrink-0"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -61,6 +61,16 @@ export const ENTRY_NODE_TYPES = [
|
||||
] as const
|
||||
|
||||
export const BLOCKS = [
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.Agent,
|
||||
title: 'Old Agent',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.AgentV2,
|
||||
title: 'Agent',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.LLM,
|
||||
@ -147,9 +157,4 @@ export const BLOCKS = [
|
||||
type: BlockEnum.ListFilter,
|
||||
title: 'List Filter',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.Agent,
|
||||
title: 'Agent',
|
||||
},
|
||||
] as const satisfies readonly Block[]
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { OffsetOptions } from '@floating-ui/react'
|
||||
import type { Placement } from '@langgenius/dify-ui/popover'
|
||||
import type { FC } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ToolDefaultValue, ToolValue } from './types'
|
||||
import type { CustomCollectionBackend } from '@/app/components/tools/types'
|
||||
import type { BlockEnum, OnSelectBlock } from '@/app/components/workflow/types'
|
||||
@ -14,7 +14,6 @@ import {
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SearchBox from '@/app/components/plugins/marketplace/search-box'
|
||||
@ -37,13 +36,17 @@ import {
|
||||
} from '@/service/use-tools'
|
||||
|
||||
type Props = Readonly<{
|
||||
panelClassName?: string
|
||||
disabled: boolean
|
||||
trigger: React.ReactNode
|
||||
trigger: ReactNode
|
||||
placement?: Placement
|
||||
offset?: OffsetOptions
|
||||
isShow: boolean
|
||||
onShowChange: (isShow: boolean) => void
|
||||
}> & ToolPickerContentProps
|
||||
|
||||
export type ToolPickerContentProps = Readonly<{
|
||||
focusSearchOnMount?: boolean
|
||||
panelClassName?: string
|
||||
onSelect: (tool: ToolDefaultValue) => void
|
||||
onSelectMultiple: (tools: ToolDefaultValue[]) => void
|
||||
supportAddCustomTool?: boolean
|
||||
@ -51,25 +54,18 @@ type Props = Readonly<{
|
||||
selectedTools?: ToolValue[]
|
||||
}>
|
||||
|
||||
const ToolPicker: FC<Props> = ({
|
||||
disabled,
|
||||
trigger,
|
||||
placement = 'right-start',
|
||||
offset = 0,
|
||||
isShow,
|
||||
onShowChange,
|
||||
export function ToolPickerContent({
|
||||
focusSearchOnMount = false,
|
||||
onSelect,
|
||||
onSelectMultiple,
|
||||
supportAddCustomTool,
|
||||
scope = 'all',
|
||||
selectedTools,
|
||||
panelClassName,
|
||||
}) => {
|
||||
}: ToolPickerContentProps) {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
const sideOffset = typeof offset === 'number' ? offset : (typeof offset === 'function' ? 0 : (offset?.mainAxis ?? 0))
|
||||
const alignOffset = typeof offset === 'number' ? 0 : (typeof offset === 'function' ? 0 : (offset?.crossAxis ?? 0))
|
||||
|
||||
const { data: enable_marketplace } = useSuspenseQuery({
|
||||
...systemFeaturesQueryOptions(),
|
||||
@ -120,12 +116,6 @@ const ToolPicker: FC<Props> = ({
|
||||
|
||||
const handleAddedCustomTool = invalidateCustomTools
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (nextOpen && disabled)
|
||||
return
|
||||
onShowChange(nextOpen)
|
||||
}
|
||||
|
||||
const handleSelect = (_type: BlockEnum, tool?: ToolDefaultValue) => {
|
||||
onSelect(tool!)
|
||||
}
|
||||
@ -157,6 +147,70 @@ const ToolPicker: FC<Props> = ({
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('relative min-h-20 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-xs', panelClassName)}>
|
||||
<div className="p-2 pb-1">
|
||||
<SearchBox
|
||||
search={searchText}
|
||||
onSearchChange={setSearchText}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
placeholder={t('searchTools', { ns: 'plugin' })!}
|
||||
supportAddCustomTool={supportAddCustomTool}
|
||||
onAddedCustomTool={handleAddedCustomTool}
|
||||
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
|
||||
// The picker replaces the focused menu item inside an already-open popover.
|
||||
// Focusing search keeps keyboard users in the same add-tool workflow.
|
||||
/* eslint-disable-next-line jsx-a11y/no-autofocus */
|
||||
autoFocus={focusSearchOnMount}
|
||||
inputClassName="grow"
|
||||
/>
|
||||
</div>
|
||||
<AllTools
|
||||
className="mt-1"
|
||||
toolContentClassName="max-w-full"
|
||||
tags={tags}
|
||||
searchText={searchText}
|
||||
onSelect={handleSelect as OnSelectBlock}
|
||||
onSelectMultiple={handleSelectMultiple}
|
||||
buildInTools={builtinToolList || []}
|
||||
customTools={customToolList || []}
|
||||
workflowTools={workflowToolList || []}
|
||||
mcpTools={mcpTools || []}
|
||||
selectedTools={selectedTools}
|
||||
onTagsChange={setTags}
|
||||
featuredPlugins={featuredPlugins}
|
||||
featuredLoading={isFeaturedLoading}
|
||||
showFeatured={scope === 'all' && enable_marketplace}
|
||||
onFeaturedInstallSuccess={async () => {
|
||||
invalidateBuiltInTools()
|
||||
invalidateCustomTools()
|
||||
invalidateWorkflowTools()
|
||||
invalidateMcpTools()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ToolPicker({
|
||||
disabled,
|
||||
trigger,
|
||||
placement = 'right-start',
|
||||
offset = 0,
|
||||
isShow,
|
||||
onShowChange,
|
||||
...contentProps
|
||||
}: Props) {
|
||||
const sideOffset = typeof offset === 'number' ? offset : (typeof offset === 'function' ? 0 : (offset?.mainAxis ?? 0))
|
||||
const alignOffset = typeof offset === 'number' ? 0 : (typeof offset === 'function' ? 0 : (offset?.crossAxis ?? 0))
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (nextOpen && disabled)
|
||||
return
|
||||
onShowChange(nextOpen)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={isShow}
|
||||
@ -175,47 +229,10 @@ const ToolPicker: FC<Props> = ({
|
||||
alignOffset={alignOffset}
|
||||
popupClassName="border-none bg-transparent shadow-none"
|
||||
>
|
||||
<div className={cn('relative min-h-20 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-xs', panelClassName)}>
|
||||
<div className="p-2 pb-1">
|
||||
<SearchBox
|
||||
search={searchText}
|
||||
onSearchChange={setSearchText}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
placeholder={t('searchTools', { ns: 'plugin' })!}
|
||||
supportAddCustomTool={supportAddCustomTool}
|
||||
onAddedCustomTool={handleAddedCustomTool}
|
||||
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
|
||||
inputClassName="grow"
|
||||
/>
|
||||
</div>
|
||||
<AllTools
|
||||
className="mt-1"
|
||||
toolContentClassName="max-w-full"
|
||||
tags={tags}
|
||||
searchText={searchText}
|
||||
onSelect={handleSelect as OnSelectBlock}
|
||||
onSelectMultiple={handleSelectMultiple}
|
||||
buildInTools={builtinToolList || []}
|
||||
customTools={customToolList || []}
|
||||
workflowTools={workflowToolList || []}
|
||||
mcpTools={mcpTools || []}
|
||||
selectedTools={selectedTools}
|
||||
onTagsChange={setTags}
|
||||
featuredPlugins={featuredPlugins}
|
||||
featuredLoading={isFeaturedLoading}
|
||||
showFeatured={scope === 'all' && enable_marketplace}
|
||||
onFeaturedInstallSuccess={async () => {
|
||||
invalidateBuiltInTools()
|
||||
invalidateCustomTools()
|
||||
invalidateWorkflowTools()
|
||||
invalidateMcpTools()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ToolPickerContent {...contentProps} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ToolPicker)
|
||||
export default ToolPicker
|
||||
|
||||
@ -85,6 +85,7 @@ const ToolItem: FC<Props> = ({
|
||||
provider_id: provider.id,
|
||||
provider_type: provider.type,
|
||||
provider_name: provider.name,
|
||||
provider_show_name: provider.label[language],
|
||||
plugin_id: provider.plugin_id,
|
||||
plugin_unique_identifier: provider.plugin_unique_identifier,
|
||||
provider_icon: normalizedIcon,
|
||||
|
||||
@ -118,6 +118,7 @@ const Tool: FC<Props> = ({
|
||||
provider_id: payload.id,
|
||||
provider_type: payload.type,
|
||||
provider_name: payload.name,
|
||||
provider_show_name: payload.label[language],
|
||||
plugin_id: payload.plugin_id!,
|
||||
plugin_unique_identifier: payload.plugin_unique_identifier!,
|
||||
provider_icon: normalizedIcon,
|
||||
@ -148,7 +149,7 @@ const Tool: FC<Props> = ({
|
||||
: `${selectedToolsNum} / ${totalToolsNum}`}
|
||||
</span>
|
||||
)
|
||||
}, [actions, getIsDisabled, isAllSelected, isHovering, language, onSelectMultiple, payload.id, payload.is_team_authorization, payload.name, payload.type, selectedToolsNum, t, totalToolsNum])
|
||||
}, [actions, getIsDisabled, isAllSelected, isHovering, language, normalizedIcon, normalizedIconDark, onSelectMultiple, payload.id, payload.is_team_authorization, payload.label, payload.name, payload.plugin_id, payload.plugin_unique_identifier, payload.type, selectedToolsNum, t, totalToolsNum])
|
||||
|
||||
if (isFoldHasSearchText !== hasSearchText) {
|
||||
setIsFoldHasSearchText(hasSearchText)
|
||||
@ -196,6 +197,7 @@ const Tool: FC<Props> = ({
|
||||
provider_id: payload.id,
|
||||
provider_type: payload.type,
|
||||
provider_name: payload.name,
|
||||
provider_show_name: payload.label[language],
|
||||
plugin_id: payload.plugin_id,
|
||||
plugin_unique_identifier: payload.plugin_unique_identifier,
|
||||
provider_icon: normalizedIcon,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { AgentInviteOptionResponse } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { ParametersSchema, PluginMeta, PluginTriggerSubscriptionConstructor, SupportedCreationMethods, TriggerEvent } from '../../plugins/types'
|
||||
import type { Collection, Event } from '../../tools/types'
|
||||
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
@ -48,6 +49,7 @@ export type TriggerDefaultValue = PluginCommonDefaultValue & {
|
||||
}
|
||||
|
||||
export type ToolDefaultValue = PluginCommonDefaultValue & {
|
||||
provider_show_name?: string
|
||||
tool_name: string
|
||||
tool_label: string
|
||||
tool_description: string
|
||||
@ -75,8 +77,34 @@ export type DataSourceDefaultValue = Omit<PluginCommonDefaultValue, 'provider_id
|
||||
plugin_unique_identifier?: string
|
||||
}
|
||||
|
||||
export type AgentRosterNodeData = Pick<
|
||||
AgentInviteOptionResponse,
|
||||
'description' | 'icon' | 'icon_background' | 'icon_type' | 'id' | 'name' | 'role'
|
||||
>
|
||||
|
||||
export type AgentRosterBinding = {
|
||||
binding_type: 'roster_agent'
|
||||
agent_id: string
|
||||
}
|
||||
|
||||
export type AgentInlineBinding = {
|
||||
binding_type: 'inline_agent'
|
||||
agent_id?: string | null
|
||||
current_snapshot_id?: string | null
|
||||
}
|
||||
|
||||
export type AgentBinding = AgentRosterBinding | AgentInlineBinding
|
||||
|
||||
export type AgentDefaultValue = {
|
||||
agent_binding: AgentBinding
|
||||
agent_node_kind: 'dify_agent'
|
||||
version: '2'
|
||||
}
|
||||
|
||||
export type PluginDefaultValue = ToolDefaultValue | DataSourceDefaultValue | TriggerDefaultValue
|
||||
|
||||
export type BlockDefaultValue = PluginDefaultValue | AgentDefaultValue
|
||||
|
||||
export type ToolValue = {
|
||||
provider_name: string
|
||||
provider_show_name?: string
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type {
|
||||
ComponentProps,
|
||||
FC,
|
||||
} from 'react'
|
||||
import type {
|
||||
@ -17,6 +18,8 @@ import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-co
|
||||
import { CUSTOM_NODE } from './constants'
|
||||
import { useAutoGenerateWebhookUrl, useNodesInteractions, useNodesSyncDraft, useWorkflowHistory, WorkflowHistoryEvent } from './hooks'
|
||||
import CustomNode from './nodes'
|
||||
import { useCreateInlineAgentBinding } from './nodes/agent-v2/hooks'
|
||||
import { isAgentV2NodeData, needsInlineAgentBindingCreation } from './nodes/agent-v2/types'
|
||||
import CustomNoteNode from './note-node'
|
||||
import { CUSTOM_NOTE_NODE } from './note-node/constants'
|
||||
import {
|
||||
@ -41,18 +44,21 @@ const CandidateNodeMain: FC<Props> = ({
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
const { createInlineAgentBinding } = useCreateInlineAgentBinding()
|
||||
|
||||
useEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
const { screenToFlowPosition } = reactflow
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
||||
const shouldCreateInlineAgentBinding = isAgentV2NodeData(candidateNode.data) && needsInlineAgentBindingCreation(candidateNode.data)
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.push({
|
||||
...candidateNode,
|
||||
data: {
|
||||
...candidateNode.data,
|
||||
_isCandidate: false,
|
||||
_isTempNode: shouldCreateInlineAgentBinding ? true : candidateNode.data._isTempNode,
|
||||
},
|
||||
position: {
|
||||
x,
|
||||
@ -81,6 +87,32 @@ const CandidateNodeMain: FC<Props> = ({
|
||||
onSuccess: () => autoGenerateWebhookUrl(candidateNode.id),
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldCreateInlineAgentBinding) {
|
||||
createInlineAgentBinding(candidateNode.id, {
|
||||
onError: () => {
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
setNodes(nodes.filter(node => node.id !== candidateNode.id))
|
||||
},
|
||||
onSuccess: (binding) => {
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
setNodes(produce(nodes, (draft) => {
|
||||
const node = draft.find(node => node.id === candidateNode.id)
|
||||
if (node) {
|
||||
if (isAgentV2NodeData(node.data) && needsInlineAgentBindingCreation(node.data))
|
||||
node.data.agent_binding = binding
|
||||
delete node.data._isTempNode
|
||||
}
|
||||
}))
|
||||
workflowStore.getState().setOpenInlineAgentPanelNodeId(candidateNode.id)
|
||||
handleSyncWorkflowDraft(true, true)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isAgentV2NodeData(candidateNode.data))
|
||||
handleSyncWorkflowDraft(true, true)
|
||||
})
|
||||
|
||||
useEventListener('contextmenu', (e) => {
|
||||
@ -100,12 +132,12 @@ const CandidateNodeMain: FC<Props> = ({
|
||||
>
|
||||
{
|
||||
candidateNode.type === CUSTOM_NODE && (
|
||||
<CustomNode {...candidateNode as any} />
|
||||
<CustomNode {...candidateNode as unknown as ComponentProps<typeof CustomNode>} />
|
||||
)
|
||||
}
|
||||
{
|
||||
candidateNode.type === CUSTOM_NOTE_NODE && (
|
||||
<CustomNoteNode {...candidateNode as any} />
|
||||
<CustomNoteNode {...candidateNode as unknown as ComponentProps<typeof CustomNoteNode>} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -121,13 +121,22 @@ export const SUPPORT_OUTPUT_VARS_NODE = [
|
||||
BlockEnum.DocExtractor,
|
||||
BlockEnum.ListFilter,
|
||||
BlockEnum.Agent,
|
||||
BlockEnum.AgentV2,
|
||||
BlockEnum.DataSource,
|
||||
BlockEnum.HumanInput,
|
||||
]
|
||||
|
||||
export const AGENT_OUTPUT_STRUCT: Var[] = [
|
||||
{
|
||||
variable: 'usage',
|
||||
variable: 'text',
|
||||
type: VarType.string,
|
||||
},
|
||||
{
|
||||
variable: 'files',
|
||||
type: VarType.arrayFile,
|
||||
},
|
||||
{
|
||||
variable: 'json',
|
||||
type: VarType.object,
|
||||
},
|
||||
]
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import agentV2Default from '@/app/components/workflow/nodes/agent-v2/default'
|
||||
import agentDefault from '@/app/components/workflow/nodes/agent/default'
|
||||
import assignerDefault from '@/app/components/workflow/nodes/assigner/default'
|
||||
import codeDefault from '@/app/components/workflow/nodes/code/default'
|
||||
@ -26,6 +27,7 @@ export const WORKFLOW_COMMON_NODES = [
|
||||
llmDefault,
|
||||
knowledgeRetrievalDefault,
|
||||
agentDefault,
|
||||
agentV2Default,
|
||||
questionClassifierDefault,
|
||||
ifElseDefault,
|
||||
iterationDefault,
|
||||
|
||||
@ -20,6 +20,7 @@ const mockHandleNodeLoopChildrenCopy = vi.hoisted(() => vi.fn(() => ({
|
||||
copyChildren: [],
|
||||
newIdMapping: {},
|
||||
})))
|
||||
const mockCreateInlineAgentBinding = vi.hoisted(() => vi.fn())
|
||||
const runtimeNodesMetaDataMap = vi.hoisted(() => ({
|
||||
value: {} as Record<string, unknown>,
|
||||
}))
|
||||
@ -78,6 +79,12 @@ vi.mock('../use-inspect-vars-crud', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../nodes/agent-v2/hooks', () => ({
|
||||
useCreateInlineAgentBinding: () => ({
|
||||
createInlineAgentBinding: mockCreateInlineAgentBinding,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../nodes/iteration/use-interactions', () => ({
|
||||
useNodeIterationInteractions: () => ({
|
||||
handleNodeIterationChildDrag: () => ({ restrictPosition: {} }),
|
||||
@ -107,6 +114,17 @@ describe('useNodesInteractions', () => {
|
||||
resetReactFlowMockState()
|
||||
runtimeState.nodesReadOnly = false
|
||||
runtimeState.workflowReadOnly = false
|
||||
mockCreateInlineAgentBinding.mockImplementation((_nodeId: string, options?: { onSuccess?: (binding: {
|
||||
binding_type: 'inline_agent'
|
||||
agent_id: string
|
||||
current_snapshot_id: string
|
||||
}) => void }) => {
|
||||
options?.onSuccess?.({
|
||||
binding_type: 'inline_agent',
|
||||
agent_id: 'inline-agent-1',
|
||||
current_snapshot_id: 'inline-snapshot-1',
|
||||
})
|
||||
})
|
||||
currentNodes = [
|
||||
createNode({
|
||||
id: 'node-1',
|
||||
@ -452,6 +470,84 @@ describe('useNodesInteractions', () => {
|
||||
expect(rfState.setEdges).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('creates an inline agent binding before syncing an added Agent v2 node', () => {
|
||||
currentNodes = [
|
||||
createNode({
|
||||
id: 'node-1',
|
||||
width: 100,
|
||||
data: {
|
||||
type: BlockEnum.Code,
|
||||
title: 'Code',
|
||||
desc: '',
|
||||
},
|
||||
}),
|
||||
]
|
||||
rfState.nodes = currentNodes as unknown as typeof rfState.nodes
|
||||
rfState.edges = []
|
||||
rfState.setNodes.mockImplementation((nextNodes) => {
|
||||
rfState.nodes = nextNodes
|
||||
})
|
||||
rfState.setEdges.mockImplementation((nextEdges) => {
|
||||
rfState.edges = nextEdges
|
||||
})
|
||||
runtimeNodesMetaDataMap.value = {
|
||||
[BlockEnum.AgentV2]: {
|
||||
defaultValue: {
|
||||
type: BlockEnum.AgentV2,
|
||||
title: 'Agent',
|
||||
desc: '',
|
||||
agent_node_kind: 'dify_agent',
|
||||
version: '2',
|
||||
},
|
||||
metaData: {
|
||||
isSingleton: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const { result, store } = renderWorkflowHook(() => useNodesInteractions(), {
|
||||
historyStore: {
|
||||
nodes: currentNodes,
|
||||
edges: [],
|
||||
},
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleNodeAdd(
|
||||
{
|
||||
nodeType: BlockEnum.AgentV2,
|
||||
pluginDefaultValue: {
|
||||
agent_binding: {
|
||||
binding_type: 'inline_agent',
|
||||
},
|
||||
agent_node_kind: 'dify_agent',
|
||||
version: '2',
|
||||
},
|
||||
},
|
||||
{ prevNodeId: 'node-1' },
|
||||
)
|
||||
})
|
||||
|
||||
const agentNode = rfState.nodes.find(node => node.data.type === BlockEnum.AgentV2)
|
||||
const firstSetNodesPayload = rfState.setNodes.mock.calls[0]?.[0]
|
||||
const pendingAgentNode = firstSetNodesPayload.find((node: Node) => node.data.type === BlockEnum.AgentV2)
|
||||
const finalSetNodesPayload = rfState.setNodes.mock.calls.at(-1)?.[0]
|
||||
const finalAgentNode = finalSetNodesPayload.find((node: Node) => node.data.type === BlockEnum.AgentV2)
|
||||
|
||||
expect(pendingAgentNode?.data._isTempNode).toBe(true)
|
||||
expect(agentNode?.data.agent_binding).toEqual({
|
||||
binding_type: 'inline_agent',
|
||||
agent_id: 'inline-agent-1',
|
||||
current_snapshot_id: 'inline-snapshot-1',
|
||||
})
|
||||
expect(finalAgentNode?.data._isTempNode).toBeUndefined()
|
||||
expect(mockCreateInlineAgentBinding).toHaveBeenCalledWith(agentNode?.id, expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
}))
|
||||
expect(store.getState().openInlineAgentPanelNodeId).toBe(agentNode?.id)
|
||||
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true, true)
|
||||
})
|
||||
|
||||
it('cancels selection state with collaborative nodes snapshot', () => {
|
||||
currentNodes = [
|
||||
createNode({
|
||||
|
||||
@ -56,6 +56,7 @@ import {
|
||||
} from '../hooks'
|
||||
import { useHooksStore } from '../hooks-store/store'
|
||||
import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils'
|
||||
import { isAgentV2NodeData } from '../nodes/agent-v2/types'
|
||||
import { IndexMethodEnum } from '../nodes/knowledge-base/types'
|
||||
import { getLLMModelIssue, isLLMModelProviderInstalled, LLMModelIssueCode } from '../nodes/llm/utils'
|
||||
import {
|
||||
@ -98,6 +99,10 @@ const withFlowType = (moreDataForCheckValid: CheckValidExtraData, flowType?: Flo
|
||||
}
|
||||
}
|
||||
|
||||
const getNodeMetaType = (data: CommonNodeType) => {
|
||||
return isAgentV2NodeData(data) ? BlockEnum.AgentV2 : data.type
|
||||
}
|
||||
|
||||
const START_NODE_TYPES: BlockEnum[] = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
@ -249,7 +254,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
|
||||
moreDataForCheckValid = getTriggerCheckParams(node!.data as PluginTriggerNodeType, triggerPlugins, language)
|
||||
|
||||
const toolIcon = getToolIcon(node!.data)
|
||||
if (node!.data.type === BlockEnum.Agent) {
|
||||
if (node!.data.type === BlockEnum.Agent && !isAgentV2NodeData(node!.data)) {
|
||||
const data = node!.data as AgentNodeType
|
||||
const isReadyForCheckValid = !!strategyProviders
|
||||
const provider = strategyProviders?.find(provider => provider.declaration.identity.name === data.agent_strategy_provider_name)
|
||||
@ -267,7 +272,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
|
||||
|
||||
if (node!.type === CUSTOM_NODE) {
|
||||
const checkData = getCheckData(node!.data)
|
||||
const validator = nodesExtraData?.[node!.data.type as BlockEnum]?.checkValid
|
||||
const validator = nodesExtraData?.[getNodeMetaType(node!.data) as BlockEnum]?.checkValid
|
||||
const isPluginMissing = isNodePluginMissing(node!.data, { builtInTools: buildInTools, customTools, workflowTools, mcpTools, triggerPlugins, dataSourceList })
|
||||
|
||||
const errorMessages: string[] = []
|
||||
@ -522,7 +527,7 @@ export const useChecklistBeforePublish = () => {
|
||||
if (node!.data.type === BlockEnum.DataSource)
|
||||
moreDataForCheckValid = getDataSourceCheckParams(node!.data as DataSourceNodeType, dataSourceList || [], language)
|
||||
|
||||
if (node!.data.type === BlockEnum.Agent) {
|
||||
if (node!.data.type === BlockEnum.Agent && !isAgentV2NodeData(node!.data)) {
|
||||
const data = node!.data as AgentNodeType
|
||||
const isReadyForCheckValid = !!strategyProviders
|
||||
const provider = strategyProviders?.find(provider => provider.declaration.identity.name === data.agent_strategy_provider_name)
|
||||
@ -551,7 +556,7 @@ export const useChecklistBeforePublish = () => {
|
||||
}
|
||||
|
||||
const checkData = getCheckData(node!.data, datasets, embeddingProviderModelMap)
|
||||
const { errorMessage } = nodesExtraData![node!.data.type as BlockEnum].checkValid(checkData, t, withFlowType(moreDataForCheckValid, flowType))
|
||||
const { errorMessage } = nodesExtraData![getNodeMetaType(node!.data) as BlockEnum].checkValid(checkData, t, withFlowType(moreDataForCheckValid, flowType))
|
||||
|
||||
if (errorMessage) {
|
||||
toast.error(`[${node!.data.title}] ${errorMessage}`)
|
||||
|
||||
@ -7,7 +7,7 @@ import type {
|
||||
OnConnectStart,
|
||||
ResizeParamsWithDirection,
|
||||
} from 'reactflow'
|
||||
import type { PluginDefaultValue } from '../block-selector/types'
|
||||
import type { BlockDefaultValue } from '../block-selector/types'
|
||||
import type { IterationNodeType } from '../nodes/iteration/types'
|
||||
import type { LoopNodeType } from '../nodes/loop/types'
|
||||
import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
|
||||
@ -36,6 +36,8 @@ import {
|
||||
Y_OFFSET,
|
||||
} from '../constants'
|
||||
import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
|
||||
import { useCreateInlineAgentBinding } from '../nodes/agent-v2/hooks'
|
||||
import { isAgentV2NodeData, needsInlineAgentBindingCreation } from '../nodes/agent-v2/types'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
|
||||
import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
|
||||
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
|
||||
@ -50,6 +52,7 @@ import {
|
||||
getNestedNodePosition,
|
||||
getNodeCustomTypeByNodeDataType,
|
||||
getNodesConnectedSourceOrTargetHandleIdsMap,
|
||||
getNodesWithSameDefaultDataType,
|
||||
getTopLeftNodePosition,
|
||||
isClipboardEdgeStructurallyValid,
|
||||
isClipboardNodeStructurallyValid,
|
||||
@ -84,6 +87,16 @@ const ENTRY_NODE_WRAPPER_OFFSET = {
|
||||
y: 21, // Adjusted based on visual testing feedback
|
||||
} as const
|
||||
|
||||
function needsPendingInlineAgentBinding(defaultValue?: BlockDefaultValue) {
|
||||
if (!defaultValue || !('agent_binding' in defaultValue))
|
||||
return false
|
||||
|
||||
const binding = defaultValue.agent_binding
|
||||
|
||||
return binding.binding_type === 'inline_agent'
|
||||
&& (!binding.agent_id || !binding.current_snapshot_id)
|
||||
}
|
||||
|
||||
const pruneClipboardNodesWithFilteredAncestors = (
|
||||
sourceNodes: Node[],
|
||||
candidateNodes: Node[],
|
||||
@ -174,6 +187,30 @@ export const useNodesInteractions = () => {
|
||||
redo,
|
||||
} = useWorkflowHistory()
|
||||
const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl()
|
||||
const { createInlineAgentBinding } = useCreateInlineAgentBinding()
|
||||
|
||||
const createInlineAgentBindingForNode = useCallback((nodeId: string, options?: {
|
||||
onError?: () => void
|
||||
}) => {
|
||||
createInlineAgentBinding(nodeId, {
|
||||
onError: () => {
|
||||
options?.onError?.()
|
||||
},
|
||||
onSuccess: (binding) => {
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
setNodes(produce(nodes, (draft) => {
|
||||
const node = draft.find(node => node.id === nodeId)
|
||||
if (node) {
|
||||
if (isAgentV2NodeData(node.data) && needsInlineAgentBindingCreation(node.data))
|
||||
node.data.agent_binding = binding
|
||||
delete node.data._isTempNode
|
||||
}
|
||||
}))
|
||||
workflowStore.getState().setOpenInlineAgentPanelNodeId(nodeId)
|
||||
handleSyncWorkflowDraft(true, true)
|
||||
},
|
||||
})
|
||||
}, [collaborativeWorkflow, createInlineAgentBinding, handleSyncWorkflowDraft, workflowStore])
|
||||
|
||||
const handleNodeDragStart = useCallback<NodeDragHandler>(
|
||||
(_, node) => {
|
||||
@ -233,12 +270,12 @@ export const useNodesInteractions = () => {
|
||||
const currentNode = draft.find(n => n.id === node.id)!
|
||||
|
||||
// Check if current dragging node is an entry node
|
||||
const isCurrentEntryNode = isTriggerNode(node.data.type as any) || node.data.type === BlockEnum.Start
|
||||
const isCurrentEntryNode = isTriggerNode(node.data.type as BlockEnum) || node.data.type === BlockEnum.Start
|
||||
|
||||
// X-axis alignment with offset consideration
|
||||
if (showVerticalHelpLineNodesLength > 0) {
|
||||
const targetNode = showVerticalHelpLineNodes[0]
|
||||
const isTargetEntryNode = isTriggerNode(targetNode!.data.type as any) || targetNode!.data.type === BlockEnum.Start
|
||||
const isTargetEntryNode = isTriggerNode(targetNode!.data.type as BlockEnum) || targetNode!.data.type === BlockEnum.Start
|
||||
|
||||
// Calculate the wrapper position needed to align the inner nodes
|
||||
// Target inner position = target.position + target.offset
|
||||
@ -262,7 +299,7 @@ export const useNodesInteractions = () => {
|
||||
// Y-axis alignment with offset consideration
|
||||
if (showHorizontalHelpLineNodesLength > 0) {
|
||||
const targetNode = showHorizontalHelpLineNodes[0]
|
||||
const isTargetEntryNode = isTriggerNode(targetNode!.data.type as any) || targetNode!.data.type === BlockEnum.Start
|
||||
const isTargetEntryNode = isTriggerNode(targetNode!.data.type as BlockEnum) || targetNode!.data.type === BlockEnum.Start
|
||||
|
||||
const targetOffset = isTargetEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0
|
||||
const currentOffset = isCurrentEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0
|
||||
@ -619,7 +656,7 @@ export const useNodesInteractions = () => {
|
||||
)
|
||||
|
||||
const handleNodeConnectEnd = useCallback<OnConnectEnd>(
|
||||
(e: any) => {
|
||||
(e) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
@ -645,7 +682,8 @@ export const useNodesInteractions = () => {
|
||||
if (fromNode.parentId !== toNode.parentId)
|
||||
return
|
||||
|
||||
const { x, y } = screenToFlowPosition({ x: e.x, y: e.y })
|
||||
const pointer = e as { x: number, y: number }
|
||||
const { x, y } = screenToFlowPosition({ x: pointer.x, y: pointer.y })
|
||||
|
||||
if (
|
||||
fromHandleType === 'source'
|
||||
@ -880,24 +918,24 @@ export const useNodesInteractions = () => {
|
||||
return
|
||||
|
||||
const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
|
||||
const nodesWithSameType = nodes.filter(
|
||||
node => node.data.type === nodeType,
|
||||
)
|
||||
const nodeMetaData = nodesMetaDataMap?.[nodeType]
|
||||
if (!nodeMetaData)
|
||||
return
|
||||
const { defaultValue } = nodeMetaData
|
||||
const nodesWithSameType = getNodesWithSameDefaultDataType(nodes, nodeType, defaultValue)
|
||||
const shouldCreateInlineAgentBinding = nodeType === BlockEnum.AgentV2 && needsPendingInlineAgentBinding(pluginDefaultValue)
|
||||
const { newNode, newIterationStartNode, newLoopStartNode }
|
||||
= generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(nodeType),
|
||||
data: {
|
||||
...(defaultValue as any),
|
||||
...(defaultValue as Node['data']),
|
||||
title:
|
||||
nodesWithSameType.length > 0
|
||||
? `${defaultValue.title} ${nodesWithSameType.length + 1}`
|
||||
: defaultValue.title,
|
||||
...pluginDefaultValue,
|
||||
selected: true,
|
||||
...(shouldCreateInlineAgentBinding ? { _isTempNode: true } : {}),
|
||||
_showAddVariablePopup:
|
||||
(nodeType === BlockEnum.VariableAssigner
|
||||
|| nodeType === BlockEnum.VariableAggregator)
|
||||
@ -1141,7 +1179,7 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
}
|
||||
|
||||
let nodesConnectedSourceOrTargetHandleIdsMap: Record<string, any>
|
||||
let nodesConnectedSourceOrTargetHandleIdsMap: ReturnType<typeof getNodesConnectedSourceOrTargetHandleIdsMap>
|
||||
if (newEdge) {
|
||||
nodesConnectedSourceOrTargetHandleIdsMap
|
||||
= getNodesConnectedSourceOrTargetHandleIdsMap(
|
||||
@ -1420,6 +1458,18 @@ export const useNodesInteractions = () => {
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}
|
||||
if (isAgentV2NodeData(newNode.data) && needsInlineAgentBindingCreation(newNode.data)) {
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNode.id })
|
||||
createInlineAgentBindingForNode(newNode.id, {
|
||||
onError: () => {
|
||||
const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
|
||||
setNodes(nodes.filter(node => node.id !== newNode.id))
|
||||
setEdges(edges.filter(edge => edge.source !== newNode.id && edge.target !== newNode.id))
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
handleSyncWorkflowDraft()
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNode.id })
|
||||
},
|
||||
@ -1427,6 +1477,7 @@ export const useNodesInteractions = () => {
|
||||
getNodesReadOnly,
|
||||
collaborativeWorkflow,
|
||||
handleSyncWorkflowDraft,
|
||||
createInlineAgentBindingForNode,
|
||||
saveStateToHistory,
|
||||
workflowStore,
|
||||
getAfterNodesInSameBranch,
|
||||
@ -1439,7 +1490,7 @@ export const useNodesInteractions = () => {
|
||||
currentNodeId: string,
|
||||
nodeType: BlockEnum,
|
||||
sourceHandle: string,
|
||||
pluginDefaultValue?: PluginDefaultValue,
|
||||
pluginDefaultValue?: BlockDefaultValue,
|
||||
) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
@ -1447,13 +1498,12 @@ export const useNodesInteractions = () => {
|
||||
const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
|
||||
const currentNode = nodes.find(node => node.id === currentNodeId)!
|
||||
const connectedEdges = getConnectedEdges([currentNode], edges)
|
||||
const nodesWithSameType = nodes.filter(
|
||||
node => node.data.type === nodeType,
|
||||
)
|
||||
const nodeMetaData = nodesMetaDataMap?.[nodeType]
|
||||
if (!nodeMetaData)
|
||||
return
|
||||
const { defaultValue } = nodeMetaData
|
||||
const nodesWithSameType = getNodesWithSameDefaultDataType(nodes, nodeType, defaultValue)
|
||||
const shouldCreateInlineAgentBinding = nodeType === BlockEnum.AgentV2 && needsPendingInlineAgentBinding(pluginDefaultValue)
|
||||
const {
|
||||
newNode: newCurrentNode,
|
||||
newIterationStartNode,
|
||||
@ -1461,12 +1511,13 @@ export const useNodesInteractions = () => {
|
||||
} = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(nodeType),
|
||||
data: {
|
||||
...(defaultValue as any),
|
||||
...(defaultValue as Node['data']),
|
||||
title:
|
||||
nodesWithSameType.length > 0
|
||||
? `${defaultValue.title} ${nodesWithSameType.length + 1}`
|
||||
: defaultValue.title,
|
||||
...pluginDefaultValue,
|
||||
...(shouldCreateInlineAgentBinding ? { _isTempNode: true } : {}),
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
selected: currentNode.data.selected,
|
||||
@ -1651,6 +1702,15 @@ export const useNodesInteractions = () => {
|
||||
onSuccess: () => autoGenerateWebhookUrl(newCurrentNode.id),
|
||||
})
|
||||
}
|
||||
else if (isAgentV2NodeData(newCurrentNode.data) && needsInlineAgentBindingCreation(newCurrentNode.data)) {
|
||||
createInlineAgentBindingForNode(newCurrentNode.id, {
|
||||
onError: () => {
|
||||
const { setNodes, setEdges } = collaborativeWorkflow.getState()
|
||||
setNodes(nodes)
|
||||
setEdges(edges)
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
@ -1663,6 +1723,7 @@ export const useNodesInteractions = () => {
|
||||
getNodesReadOnly,
|
||||
collaborativeWorkflow,
|
||||
handleSyncWorkflowDraft,
|
||||
createInlineAgentBindingForNode,
|
||||
saveStateToHistory,
|
||||
nodesMetaDataMap,
|
||||
autoGenerateWebhookUrl,
|
||||
@ -1729,7 +1790,7 @@ export const useNodesInteractions = () => {
|
||||
if (node.type === CUSTOM_NOTE_NODE)
|
||||
return true
|
||||
|
||||
const nodeMeta = nodesMetaDataMap?.[node.data.type as BlockEnum]
|
||||
const nodeMeta = nodesMetaDataMap?.[isAgentV2NodeData(node.data) ? BlockEnum.AgentV2 : node.data.type as BlockEnum]
|
||||
if (!nodeMeta)
|
||||
return false
|
||||
|
||||
@ -1741,7 +1802,7 @@ export const useNodesInteractions = () => {
|
||||
if (node.type === CUSTOM_NOTE_NODE)
|
||||
return {}
|
||||
|
||||
const nodeMeta = nodesMetaDataMap?.[node.data.type as BlockEnum]
|
||||
const nodeMeta = nodesMetaDataMap?.[isAgentV2NodeData(node.data) ? BlockEnum.AgentV2 : node.data.type as BlockEnum]
|
||||
return nodeMeta?.defaultValue
|
||||
}, [nodesMetaDataMap])
|
||||
|
||||
@ -2359,7 +2420,7 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const currentNode = nodes.find(n => n.id === nodeId)!
|
||||
const childrenNodes = nodes.filter(n =>
|
||||
currentNode.data._children?.find((c: any) => c.nodeId === n.id),
|
||||
currentNode.data._children?.find((child: NonNullable<Node['data']['_children']>[number]) => child.nodeId === n.id),
|
||||
)
|
||||
let rightNode: Node
|
||||
let bottomNode: Node
|
||||
|
||||