Compare commits
	
		
			770 Commits
		
	
	
		
			v2.14.2
			...
			51189b1c24
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					51189b1c24 | ||
| 
						 | 
					b9e3f47243 | ||
| 
						 | 
					3809375694 | ||
| 
						 | 
					1b0de25986 | ||
| 
						 | 
					865c45dd29 | ||
| 
						 | 
					1f5d8e6d9c | ||
| 
						 | 
					c9ef6d58ed | ||
| 
						 | 
					2d7229d2b8 | ||
| 
						 | 
					5ac2a404a4 | ||
| 
						 | 
					11b37c15bd | ||
| 
						 | 
					1d0038f17d | ||
| 
						 | 
					619fa519c0 | ||
| 
						 | 
					48469bd8ca | ||
| 
						 | 
					5a5e887f2b | ||
| 
						 | 
					b6f5d75656 | ||
| 
						 | 
					0d41a17ef6 | ||
| 
						 | 
					f7cde17919 | ||
| 
						 | 
					570cbb34b6 | ||
| 
						 | 
					7aa9ae0a3e | ||
| 
						 | 
					2d4180f5be | ||
| 
						 | 
					9f0182b55e | ||
| 
						 | 
					ad6666eeaf | ||
| 
						 | 
					a2c4e468a0 | ||
| 
						 | 
					2167076652 | ||
| 
						 | 
					e123076250 | ||
| 
						 | 
					ebcb4db245 | ||
| 
						 | 
					0a25a1a8cb | ||
| 
						 | 
					f3154b20a5 | ||
| 
						 | 
					b709ee3983 | ||
| 
						 | 
					f5f3ce94f6 | ||
| 
						 | 
					2b5f600308 | ||
| 
						 | 
					24b2a0c14b | ||
| 
						 | 
					b966107117 | ||
| 
						 | 
					377480b448 | ||
| 
						 | 
					8bd0d6a1a7 | ||
| 
						 | 
					90827fc593 | ||
| 
						 | 
					008e339b6d | ||
| 
						 | 
					12863f5213 | ||
| 
						 | 
					cf140d4228 | ||
| 
						 | 
					476d946f96 | ||
| 
						 | 
					9714258322 | ||
| 
						 | 
					48cd4b11b5 | ||
| 
						 | 
					77c78b230a | ||
| 
						 | 
					b44686b887 | ||
| 
						 | 
					34bdd4b945 | ||
| 
						 | 
					b0758cccde | ||
| 
						 | 
					98a11e56d2 | ||
| 
						 | 
					86f86962fb | ||
| 
						 | 
					2137aa65bf | ||
| 
						 | 
					18fa2cc30d | ||
| 
						 | 
					0bfc648085 | ||
| 
						 | 
					9f91c2d05c | ||
| 
						 | 
					a029b4330b | ||
| 
						 | 
					2842b264e0 | ||
| 
						 | 
					c2edfec16f | ||
| 
						 | 
					6406ac99a3 | ||
| 
						 | 
					97a4aafc92 | ||
| 
						 | 
					d8f533e1f3 | ||
| 
						 | 
					c6199dbf9f | ||
| 
						 | 
					4273aa0803 | ||
| 
						 | 
					acf75ce68f | ||
| 
						 | 
					1ae5fdbf01 | ||
| 
						 | 
					2a3996e0d6 | ||
| 
						 | 
					fdbaddde37 | ||
| 
						 | 
					d74f79e9c5 | ||
| 
						 | 
					c4e9cb03a9 | ||
| 
						 | 
					bf265d3375 | ||
| 
						 | 
					17f391d929 | ||
| 
						 | 
					78186c27fb | ||
| 
						 | 
					a5a9768245 | ||
| 
						 | 
					3fe55b4f7f | ||
| 
						 | 
					f156430cc5 | ||
| 
						 | 
					f30c6a4348 | ||
| 
						 | 
					a780b39c17 | ||
| 
						 | 
					1010db834c | ||
| 
						 | 
					51384ddc5f | ||
| 
						 | 
					e5e5fde924 | ||
| 
						 | 
					add9ca200c | ||
| 
						 | 
					5225a6e192 | ||
| 
						 | 
					28cbe56cec | ||
| 
						 | 
					ad9ab9d45a | ||
| 
						 | 
					bb4832e6e7 | ||
| 
						 | 
					39b3487ea0 | ||
| 
						 | 
					32b60909ae | ||
| 
						 | 
					5db6775cb8 | ||
| 
						 | 
					b6881c7797 | ||
| 
						 | 
					9943a52295 | ||
| 
						 | 
					1db4d25370 | ||
| 
						 | 
					92f57fb18f | ||
| 
						 | 
					4c4d44e2f8 | ||
| 
						 | 
					8f12beb8f0 | ||
| 
						 | 
					2e7cac3218 | ||
| 
						 | 
					60fa358010 | ||
| 
						 | 
					034b7d4655 | ||
| 
						 | 
					1e20b64048 | ||
| 
						 | 
					4f28fca506 | ||
| 
						 | 
					3ef5993085 | ||
| 
						 | 
					09ad7c1875 | ||
| 
						 | 
					31e52cb47e | ||
| 
						 | 
					9a69c5bd7c | ||
| 
						 | 
					be645aab37 | ||
| 
						 | 
					c41e86faa6 | ||
| 
						 | 
					143be69a7f | ||
| 
						 | 
					63b7626656 | ||
| 
						 | 
					dabb7c70d5 | ||
| 
						 | 
					c449737127 | ||
| 
						 | 
					553b8c9f28 | ||
| 
						 | 
					19314793b8 | ||
| 
						 | 
					8680182921 | ||
| 
						 | 
					2173c82bb5 | ||
| 
						 | 
					0d5e66a9ae | ||
| 
						 | 
					2f9cb5a68f | ||
| 
						 | 
					55cacfb7e2 | ||
| 
						 | 
					6a862372f7 | ||
| 
						 | 
					81bd83eb44 | ||
| 
						 | 
					b2b6fd81be | ||
| 
						 | 
					f22cfd7b33 | ||
| 
						 | 
					8111acff34 | ||
| 
						 | 
					4cad55379d | ||
| 
						 | 
					a3d3ce3f4c | ||
| 
						 | 
					611e97e641 | ||
| 
						 | 
					bfeea4ed49 | ||
| 
						 | 
					bc71ae247b | ||
| 
						 | 
					0112b54bc7 | ||
| 
						 | 
					65810d918b | ||
| 
						 | 
					4d535b1cd0 | ||
| 
						 | 
					588d81e8f1 | ||
| 
						 | 
					d4f499ee41 | ||
| 
						 | 
					4d63d73b2e | ||
| 
						 | 
					07c63497dc | ||
| 
						 | 
					e440ff56c8 | ||
| 
						 | 
					c89e4883b2 | ||
| 
						 | 
					ac3d940de8 | ||
| 
						 | 
					be59de56f0 | ||
| 
						 | 
					a70e9a3c01 | ||
| 
						 | 
					8aa9a500fd | ||
| 
						 | 
					93652db688 | ||
| 
						 | 
					8421c483e8 | ||
| 
						 | 
					4ac27fdd4d | ||
| 
						 | 
					b6b2c501fd | ||
| 
						 | 
					ce13cf61a7 | ||
| 
						 | 
					a3af563e89 | ||
| 
						 | 
					e95c94d7be | ||
| 
						 | 
					125a71fead | ||
| 
						 | 
					b410ec399c | ||
| 
						 | 
					7d51bfd42e | ||
| 
						 | 
					0c14ce6417 | ||
| 
						 | 
					f2a2b40d2c | ||
| 
						 | 
					77be190d76 | ||
| 
						 | 
					c56587c438 | ||
| 
						 | 
					840c151ab9 | ||
| 
						 | 
					0af04e0f2f | ||
| 
						 | 
					d184eb6458 | ||
| 
						 | 
					c5d9b1131e | ||
| 
						 | 
					e13408dd24 | ||
| 
						 | 
					aba4baf384 | ||
| 
						 | 
					6d84f9d3ae | ||
| 
						 | 
					63c5baaa80 | ||
| 
						 | 
					defefba925 | ||
| 
						 | 
					90c531c224 | ||
| 
						 | 
					266e9efd2e | ||
| 
						 | 
					57c88c0717 | ||
| 
						 | 
					5b5dea1c59 | ||
| 
						 | 
					d56566cd73 | ||
| 
						 | 
					b5d104c908 | ||
| 
						 | 
					f9e9129d52 | ||
| 
						 | 
					2a8a18391e | ||
| 
						 | 
					e1cb8e36fa | ||
| 
						 | 
					b948d6bf86 | ||
| 
						 | 
					fe67f79050 | ||
| 
						 | 
					67338ff9b7 | ||
| 
						 | 
					7380c8a2c1 | ||
| 
						 | 
					e1ba8f1b0f | ||
| 
						 | 
					c0062ff280 | ||
| 
						 | 
					39e593da48 | ||
| 
						 | 
					f8b10ad8b1 | ||
| 
						 | 
					8a22c9d6db | ||
| 
						 | 
					5f96804f3b | ||
| 
						 | 
					13430ea3e2 | ||
| 
						 | 
					664879b9df | ||
| 
						 | 
					9df24e568b | ||
| 
						 | 
					bc322be448 | ||
| 
						 | 
					a867adaf04 | ||
| 
						 | 
					0cb186846a | ||
| 
						 | 
					e467ce028d | ||
| 
						 | 
					cdfe907fb5 | ||
| 
						 | 
					d91af7f983 | ||
| 
						 | 
					c3108ad333 | ||
| 
						 | 
					081daf937e | ||
| 
						 | 
					0c3d4462ca | ||
| 
						 | 
					3c859fc29f | ||
| 
						 | 
					e1c7c54dfa | ||
| 
						 | 
					87b5e3bf62 | ||
| 
						 | 
					1d15666713 | ||
| 
						 | 
					a127ae1fb4 | ||
| 
						 | 
					ea1329f73e | ||
| 
						 | 
					149d732cb7 | ||
| 
						 | 
					210b29bfbe | ||
| 
						 | 
					acc2e97aab | ||
| 
						 | 
					93ac0e5017 | ||
| 
						 | 
					ed8c3580c8 | ||
| 
						 | 
					0a056a7c5c | ||
| 
						 | 
					74c4711cdd | ||
| 
						 | 
					eceec092cf | ||
| 
						 | 
					42743410a8 | ||
| 
						 | 
					0f04756d4c | ||
| 
						 | 
					acdded8161 | ||
| 
						 | 
					e939ce5a02 | ||
| 
						 | 
					46a0b100f7 | ||
| 
						 | 
					e27e8fb0e1 | ||
| 
						 | 
					93c5320bf2 | ||
| 
						 | 
					a433d1606c | ||
| 
						 | 
					cc5e16b045 | ||
| 
						 | 
					54f6feb2d7 | ||
| 
						 | 
					e1ac0538b8 | ||
| 
						 | 
					1a678cb4d8 | ||
| 
						 | 
					83cea3a90d | ||
| 
						 | 
					759a09a76c | ||
| 
						 | 
					2623a92763 | ||
| 
						 | 
					3932c594c7 | ||
| 
						 | 
					b7acb89096 | ||
| 
						 | 
					ef24d3e633 | ||
| 
						 | 
					23350c842b | ||
| 
						 | 
					a2adfbbd32 | ||
| 
						 | 
					f22cec1eb4 | ||
| 
						 | 
					e56216549e | ||
| 
						 | 
					19facc7c85 | ||
| 
						 | 
					b08ce5630c | ||
| 
						 | 
					b41c012d27 | ||
| 
						 | 
					a392daab71 | ||
| 
						 | 
					0628ddfc6f | ||
| 
						 | 
					7eda14f138 | ||
| 
						 | 
					9a86c42c95 | ||
| 
						 | 
					819d249a09 | ||
| 
						 | 
					8d66fedb1f | ||
| 
						 | 
					7cf89b53ce | ||
| 
						 | 
					459c373f13 | ||
| 
						 | 
					1d14a991ee | ||
| 
						 | 
					05ef5adfa7 | ||
| 
						 | 
					38fa3056df | ||
| 
						 | 
					289aeec8af | ||
| 
						 | 
					7d71da938f | ||
| 
						 | 
					f8f6954115 | ||
| 
						 | 
					6e03f32871 | ||
| 
						 | 
					18a6571883 | ||
| 
						 | 
					14f444e1f0 | ||
| 
						 | 
					2b0f2e5f9d | ||
| 
						 | 
					4629b39c29 | ||
| 
						 | 
					d33e772fa5 | ||
| 
						 | 
					89136fba32 | ||
| 
						 | 
					8b4ca133fd | ||
| 
						 | 
					a4c9eaf6cd | ||
| 
						 | 
					50e63109a3 | ||
| 
						 | 
					48a1e8a584 | ||
| 
						 | 
					e44ebe3f0e | ||
| 
						 | 
					108069a0c6 | ||
| 
						 | 
					d5bda2904d | ||
| 
						 | 
					283caba8ce | ||
| 
						 | 
					b78e5db817 | ||
| 
						 | 
					46c469b2d7 | ||
| 
						 | 
					c00ebbea4f | ||
| 
						 | 
					c526ff80b5 | ||
| 
						 | 
					0037b0c944 | ||
| 
						 | 
					6f81bb3b8a | ||
| 
						 | 
					7bdc45ed3e | ||
| 
						 | 
					88cd3ac122 | ||
| 
						 | 
					4988d2ee26 | ||
| 
						 | 
					8deb7a92ee | ||
| 
						 | 
					db060d732a | ||
| 
						 | 
					522627820a | ||
| 
						 | 
					cf46d5ad63 | ||
| 
						 | 
					a4941521d0 | ||
| 
						 | 
					f6e1f8398b | ||
| 
						 | 
					d544eead38 | ||
| 
						 | 
					fbb9385f23 | ||
| 
						 | 
					18144c3d9c | ||
| 
						 | 
					64aa760e58 | ||
| 
						 | 
					e0bbb8bb68 | ||
| 
						 | 
					6667ee1c7f | ||
| 
						 | 
					6ded4e96e7 | ||
| 
						 | 
					85cdcab850 | ||
| 
						 | 
					f4c9410c29 | ||
| 
						 | 
					adf7d8200b | ||
| 
						 | 
					3086a2fa77 | ||
| 
						 | 
					f526d6f560 | ||
| 
						 | 
					106461a1e7 | ||
| 
						 | 
					c4e19dbc59 | ||
| 
						 | 
					f3603e59fa | ||
| 
						 | 
					8e2484fcdf | ||
| 
						 | 
					00d6cb27f7 | ||
| 
						 | 
					b844045d23 | ||
| 
						 | 
					e49fe976d9 | ||
| 
						 | 
					14f751965f | ||
| 
						 | 
					0ec423389f | ||
| 
						 | 
					820ab54e2d | ||
| 
						 | 
					a6c1eb27a8 | ||
| 
						 | 
					0dc4071ccc | ||
| 
						 | 
					4d3949718a | ||
| 
						 | 
					aef535f1a7 | ||
| 
						 | 
					686a80e727 | ||
| 
						 | 
					e49466fa05 | ||
| 
						 | 
					4b93370814 | ||
| 
						 | 
					5733e3c588 | ||
| 
						 | 
					44fc5b5cbf | ||
| 
						 | 
					2d3f7c922f | ||
| 
						 | 
					fe8cca3730 | ||
| 
						 | 
					fbb7a1e853 | ||
| 
						 | 
					fb2c15567d | ||
| 
						 | 
					c2c52a1f60 | ||
| 
						 | 
					106ddc17cd | ||
| 
						 | 
					17d5209738 | ||
| 
						 | 
					d66bfc6352 | ||
| 
						 | 
					4d75b23ed1 | ||
| 
						 | 
					36bfa2ef7c | ||
| 
						 | 
					afe12c212e | ||
| 
						 | 
					adf97c6d8b | ||
| 
						 | 
					7a8d557ea3 | ||
| 
						 | 
					d3f0a77830 | ||
| 
						 | 
					0581e37236 | ||
| 
						 | 
					44383a8b33 | ||
| 
						 | 
					7c466c9b9c | ||
| 
						 | 
					a0fa4d7e72 | ||
| 
						 | 
					d357b45e84 | ||
| 
						 | 
					d0bd1bf8fd | ||
| 
						 | 
					86ffa1e643 | ||
| 
						 | 
					b0d28eb77e | ||
| 
						 | 
					736cbdbdd1 | ||
| 
						 | 
					613d67eada | ||
| 
						 | 
					89cea18955 | ||
| 
						 | 
					56bc77d20b | ||
| 
						 | 
					6d93d37963 | ||
| 
						 | 
					24df85cf9d | ||
| 
						 | 
					a4d7a2c6e3 | ||
| 
						 | 
					49d42bb45d | ||
| 
						 | 
					4f49626303 | ||
| 
						 | 
					45db20c1c3 | ||
| 
						 | 
					82994843f5 | ||
| 
						 | 
					1110a087a0 | ||
| 
						 | 
					f0b3e10a6c | ||
| 
						 | 
					f89872b833 | ||
| 
						 | 
					90ced92876 | ||
| 
						 | 
					2c74559010 | ||
| 
						 | 
					e3ca7e8b44 | ||
| 
						 | 
					4745706c42 | ||
| 
						 | 
					801dc412f9 | ||
| 
						 | 
					c7c2c0211a | ||
| 
						 | 
					65bb962fc0 | ||
| 
						 | 
					e791cd441d | ||
| 
						 | 
					8455fefc8a | ||
| 
						 | 
					06f897f32f | ||
| 
						 | 
					deb1e76c41 | ||
| 
						 | 
					463fa743e9 | ||
| 
						 | 
					cda4494cec | ||
| 
						 | 
					87d85c10c3 | ||
| 
						 | 
					22f83c9e11 | ||
| 
						 | 
					7f454cbcec | ||
| 
						 | 
					426269d795 | ||
| 
						 | 
					370f143157 | ||
| 
						 | 
					103106bb93 | ||
| 
						 | 
					2419083adf | ||
| 
						 | 
					c25903bfb4 | ||
| 
						 | 
					e34c266438 | ||
| 
						 | 
					8c39a687b5 | ||
| 
						 | 
					592f62005b | ||
| 
						 | 
					12e7caa209 | ||
| 
						 | 
					b016771555 | ||
| 
						 | 
					a84383f919 | ||
| 
						 | 
					7f68fb1ff2 | ||
| 
						 | 
					8d2003fe68 | ||
| 
						 | 
					9961b513cc | ||
| 
						 | 
					819238acaf | ||
| 
						 | 
					ad49916b1c | ||
| 
						 | 
					d18bd8a48a | ||
| 
						 | 
					4a1319f2c0 | ||
| 
						 | 
					8fd843d228 | ||
| 
						 | 
					6792d6e475 | ||
| 
						 | 
					c139038e01 | ||
| 
						 | 
					4a7fd3a380 | ||
| 
						 | 
					c98dc31cdf | ||
| 
						 | 
					bd43af3a8d | ||
| 
						 | 
					be98aa2078 | ||
| 
						 | 
					a0d4a04192 | ||
| 
						 | 
					bd9de4dc4d | ||
| 
						 | 
					2eebfcf6fe | ||
| 
						 | 
					c5074f0aa4 | ||
| 
						 | 
					ba58018a15 | ||
| 
						 | 
					63ab83c3c8 | ||
| 
						 | 
					268cf3b606 | ||
| 
						 | 
					fbc68fa776 | ||
| 
						 | 
					4ae34ea3ee | ||
| 
						 | 
					96273fd75e | ||
| 
						 | 
					3e63d405c1 | ||
| 
						 | 
					19b42aac5d | ||
| 
						 | 
					b67a23200e | ||
| 
						 | 
					1dac02e4d6 | ||
| 
						 | 
					acad5b1d08 | ||
| 
						 | 
					4e9bb51d2f | ||
| 
						 | 
					c0c8cdbbf3 | ||
| 
						 | 
					cbdc611b54 | ||
| 
						 | 
					93ca303b6c | ||
| 
						 | 
					a925b424a8 | ||
| 
						 | 
					5b4d423b58 | ||
| 
						 | 
					6c1cbe120c | ||
| 
						 | 
					77a58bc4b0 | ||
| 
						 | 
					7d55a6d0e4 | ||
| 
						 | 
					8ad63a6c25 | ||
| 
						 | 
					acf9fa36f9 | ||
| 
						 | 
					461154bb03 | ||
| 
						 | 
					cd75461f9e | ||
| 
						 | 
					2bac174e6f | ||
| 
						 | 
					65f80f81ad | ||
| 
						 | 
					450766a44b | ||
| 
						 | 
					05e6e4bffb | ||
| 
						 | 
					fbb66a4a5d | ||
| 
						 | 
					d51d31a559 | ||
| 
						 | 
					919ee51dca | ||
| 
						 | 
					9c577ad9d5 | ||
| 
						 | 
					953114041b | ||
| 
						 | 
					d830c23dab | ||
| 
						 | 
					fd3568c459 | ||
| 
						 | 
					3029dcb2f6 | ||
| 
						 | 
					35e03e1bca | ||
| 
						 | 
					cea5b91f96 | ||
| 
						 | 
					d2984db6e7 | ||
| 
						 | 
					deb215ccd1 | ||
| 
						 | 
					7173cf2184 | ||
| 
						 | 
					0c697e123d | ||
| 
						 | 
					edfa6d14ee | ||
| 
						 | 
					b6d9ba93fa | ||
| 
						 | 
					6293b95a3b | ||
| 
						 | 
					ef4665cd8b | ||
| 
						 | 
					8030e71a5a | ||
| 
						 | 
					f42488d4cb | ||
| 
						 | 
					af49ed4fdc | ||
| 
						 | 
					b174a40634 | ||
| 
						 | 
					3c01738c29 | ||
| 
						 | 
					9be58f3eb4 | ||
| 
						 | 
					a50c282d01 | ||
| 
						 | 
					5141145e4d | ||
| 
						 | 
					b5f6e5a598 | ||
| 
						 | 
					7df308d655 | ||
| 
						 | 
					f5ad51a35e | ||
| 
						 | 
					f9d4105170 | ||
| 
						 | 
					9e6ee50fa6 | ||
| 
						 | 
					dd77ad5d74 | ||
| 
						 | 
					3898c507c4 | ||
| 
						 | 
					fcba50f041 | ||
| 
						 | 
					452fc86ad1 | ||
| 
						 | 
					5bdf411399 | ||
| 
						 | 
					2d920f7ccc | ||
| 
						 | 
					d84d51b475 | ||
| 
						 | 
					f9d6f4f9da | ||
| 
						 | 
					a13bd624e8 | ||
| 
						 | 
					8fb019b2e2 | ||
| 
						 | 
					2f3457e73d | ||
| 
						 | 
					c6ebd6e73c | ||
| 
						 | 
					2333a47c55 | ||
| 
						 | 
					b35895b551 | ||
| 
						 | 
					19c4ed4463 | ||
| 
						 | 
					22aa1698b4 | ||
| 
						 | 
					07d089a2bd | ||
| 
						 | 
					870ad913cc | ||
| 
						 | 
					3fb389551b | ||
| 
						 | 
					d12a4adfb5 | ||
| 
						 | 
					702e17c96b | ||
| 
						 | 
					93ff7d26cc | ||
| 
						 | 
					13777786c4 | ||
| 
						 | 
					6655c64e55 | ||
| 
						 | 
					7f3f6f1aaf | ||
| 
						 | 
					ea04595c5e | ||
| 
						 | 
					13c68bd810 | ||
| 
						 | 
					1d2f44fba8 | ||
| 
						 | 
					68702bfb1f | ||
| 
						 | 
					e83f61e74d | ||
| 
						 | 
					10d472e79e | ||
| 
						 | 
					a6b920d9af | ||
| 
						 | 
					064e964d75 | ||
| 
						 | 
					47fb40d572 | ||
| 
						 | 
					b7892b58f5 | ||
| 
						 | 
					77f037a3c4 | ||
| 
						 | 
					248d27680d | ||
| 
						 | 
					4c84182e7a | ||
| 
						 | 
					9e18cc260b | ||
| 
						 | 
					e8581c8f3c | ||
| 
						 | 
					fe4cba8baf | ||
| 
						 | 
					9bbd7d3185 | ||
| 
						 | 
					dbabb2c403 | ||
| 
						 | 
					6c37d04591 | ||
| 
						 | 
					649c5be64e | ||
| 
						 | 
					fc0042a799 | ||
| 
						 | 
					269d064e0a | ||
| 
						 | 
					6c8143b7de | ||
| 
						 | 
					f9f99639db | ||
| 
						 | 
					6d5bf490ab | ||
| 
						 | 
					46fc2a5012 | ||
| 
						 | 
					90e7b5aecf | ||
| 
						 | 
					ed20fd2962 | ||
| 
						 | 
					4c3fd55a75 | ||
| 
						 | 
					d95d509046 | ||
| 
						 | 
					c15c852668 | ||
| 
						 | 
					4a60512ae7 | ||
| 
						 | 
					0e210cf8de | ||
| 
						 | 
					35aa2c7270 | ||
| 
						 | 
					518e0d90a5 | ||
| 
						 | 
					51f7b02b27 | ||
| 
						 | 
					23f2b6213c | ||
| 
						 | 
					3a969054e3 | ||
| 
						 | 
					4d1f9e49d4 | ||
| 
						 | 
					702f5bd362 | ||
| 
						 | 
					2474d5b6d2 | ||
| 
						 | 
					9858d1f958 | ||
| 
						 | 
					62efab987b | ||
| 
						 | 
					4c63ee23cd | ||
| 
						 | 
					c75d9e3de4 | ||
| 
						 | 
					df222ded12 | ||
| 
						 | 
					7dc0f81d3f | ||
| 
						 | 
					23793e834d | ||
| 
						 | 
					f4f3c6ad5a | ||
| 
						 | 
					775794e0e4 | ||
| 
						 | 
					03268ce4d8 | ||
| 
						 | 
					212d15fdd0 | ||
| 
						 | 
					065f015f7b | ||
| 
						 | 
					b5ba05dd83 | ||
| 
						 | 
					e4fda6cacf | ||
| 
						 | 
					accb526cd6 | ||
| 
						 | 
					2f0d94a46b | ||
| 
						 | 
					8dc24403d8 | ||
| 
						 | 
					a8c70d84a9 | ||
| 
						 | 
					10d7a64f88 | ||
| 
						 | 
					d51bbb4a81 | ||
| 
						 | 
					848f794149 | ||
| 
						 | 
					7f1b44befe | ||
| 
						 | 
					9ddd5a0566 | ||
| 
						 | 
					a3b664763e | ||
| 
						 | 
					fd47bc1dc3 | ||
| 
						 | 
					dfaafe3adb | ||
| 
						 | 
					b4dc4d34eb | ||
| 
						 | 
					3ae8ec1af6 | ||
| 
						 | 
					5c34666334 | ||
| 
						 | 
					212605a7e3 | ||
| 
						 | 
					4ddfa9af8d | ||
| 
						 | 
					a69234e715 | ||
| 
						 | 
					36a0c7b8a3 | ||
| 
						 | 
					9e1e0a7252 | ||
| 
						 | 
					027e5adf67 | ||
| 
						 | 
					e986088bec | ||
| 
						 | 
					63ffd473d5 | ||
| 
						 | 
					9e5d92dc58 | ||
| 
						 | 
					8ac9141a29 | ||
| 
						 | 
					313c942350 | ||
| 
						 | 
					c90d65a98d | ||
| 
						 | 
					26c3edd023 | ||
| 
						 | 
					9a5a3d4ce4 | ||
| 
						 | 
					6e79b9a7a2 | ||
| 
						 | 
					b32d82e6c1 | ||
| 
						 | 
					a3585685df | ||
| 
						 | 
					f379865e2c | ||
| 
						 | 
					4eb4c31438 | ||
| 
						 | 
					84a7afcd94 | ||
| 
						 | 
					ca9aa50ad1 | ||
| 
						 | 
					fa48ace39b | ||
| 
						 | 
					1b869d9305 | ||
| 
						 | 
					93bc2f5870 | ||
| 
						 | 
					37c0cfe1e9 | ||
| 
						 | 
					fc27441561 | ||
| 
						 | 
					79cfbac11f | ||
| 
						 | 
					df62736ff6 | ||
| 
						 | 
					6a464b3e5f | ||
| 
						 | 
					57fcda80df | ||
| 
						 | 
					db39fbc419 | ||
| 
						 | 
					3dabe47c78 | ||
| 
						 | 
					affc194cde | ||
| 
						 | 
					03fa580a55 | ||
| 
						 | 
					d0dce654bf | ||
| 
						 | 
					169323e238 | ||
| 
						 | 
					71df415b14 | ||
| 
						 | 
					6bb01bc564 | ||
| 
						 | 
					22616b0ea0 | ||
| 
						 | 
					07c6fe5975 | ||
| 
						 | 
					5964181cb2 | ||
| 
						 | 
					4b8288a2b2 | ||
| 
						 | 
					88b1c1c6a5 | ||
| 
						 | 
					1234deabfa | ||
| 
						 | 
					ebaeb5a0d5 | ||
| 
						 | 
					45306bbb6c | ||
| 
						 | 
					18e2403b01 | ||
| 
						 | 
					e578c5f3ad | ||
| 
						 | 
					c13f9b0f21 | ||
| 
						 | 
					61245e3d7e | ||
| 
						 | 
					7804182d0d | ||
| 
						 | 
					f2195154f6 | ||
| 
						 | 
					35f77f45a2 | ||
| 
						 | 
					ba123dfcdf | ||
| 
						 | 
					992c3a5d3a | ||
| 
						 | 
					d51d7b6797 | ||
| 
						 | 
					aa16dfe59d | ||
| 
						 | 
					23ac2efd89 | ||
| 
						 | 
					daeffb2dc6 | ||
| 
						 | 
					db58ca6c1d | ||
| 
						 | 
					2ff292cbfa | ||
| 
						 | 
					5a81393863 | ||
| 
						 | 
					116a73d398 | ||
| 
						 | 
					cf0c057164 | ||
| 
						 | 
					fe5a4f4447 | ||
| 
						 | 
					82aa3c483c | ||
| 
						 | 
					c1b74201e4 | ||
| 
						 | 
					27828d9ca8 | ||
| 
						 | 
					2bd799fac6 | ||
| 
						 | 
					9275f2d753 | ||
| 
						 | 
					7455978ee5 | ||
| 
						 | 
					7c0acc7b77 | ||
| 
						 | 
					f32dd69acf | ||
| 
						 | 
					80b8f956a9 | ||
| 
						 | 
					caf50b6e6c | ||
| 
						 | 
					b590d0857c | ||
| 
						 | 
					1f53f41d53 | ||
| 
						 | 
					982019307c | ||
| 
						 | 
					09aec7b22e | ||
| 
						 | 
					f9a047aad4 | ||
| 
						 | 
					85704570f3 | ||
| 
						 | 
					53dcae9e9c | ||
| 
						 | 
					04e1ab63bb | ||
| 
						 | 
					ed9aae531e | ||
| 
						 | 
					6ab6b3dbca | ||
| 
						 | 
					7180ed9a60 | ||
| 
						 | 
					0a5522d28c | ||
| 
						 | 
					c7bc93b32b | ||
| 
						 | 
					d30351e7b0 | ||
| 
						 | 
					886ffc0af8 | ||
| 
						 | 
					4fdd997108 | ||
| 
						 | 
					236736deea | ||
| 
						 | 
					2b317f60c8 | ||
| 
						 | 
					3ec67f9f47 | ||
| 
						 | 
					6435e7a30e | ||
| 
						 | 
					078305f5ac | ||
| 
						 | 
					801b62543a | ||
| 
						 | 
					877668b629 | ||
| 
						 | 
					f652f73260 | ||
| 
						 | 
					b2965e1deb | ||
| 
						 | 
					2214689920 | ||
| 
						 | 
					9326ff9d08 | ||
| 
						 | 
					271f58d9cf | ||
| 
						 | 
					cac99e3908 | ||
| 
						 | 
					08119f9087 | ||
| 
						 | 
					97a4a910e0 | ||
| 
						 | 
					19c7a84548 | ||
| 
						 | 
					571ce11e53 | ||
| 
						 | 
					d2cb984ced | ||
| 
						 | 
					7fc0d11931 | ||
| 
						 | 
					341a52a615 | ||
| 
						 | 
					d58b99d602 | ||
| 
						 | 
					f7a5f836db | ||
| 
						 | 
					d212df8b95 | ||
| 
						 | 
					f3f6dc57c3 | ||
| 
						 | 
					29b5cd9436 | ||
| 
						 | 
					f5209fc344 | ||
| 
						 | 
					c5168c2132 | ||
| 
						 | 
					318e0989a2 | ||
| 
						 | 
					d8b1781d7b | ||
| 
						 | 
					ed5aea0521 | ||
| 
						 | 
					217026bd5d | ||
| 
						 | 
					e9f90a4d82 | ||
| 
						 | 
					f86b220c92 | ||
| 
						 | 
					b6bb1673d4 | ||
| 
						 | 
					93f1762e6c | ||
| 
						 | 
					2f410fc09f | ||
| 
						 | 
					7b6fe66f2a | ||
| 
						 | 
					c2fc0b4979 | ||
| 
						 | 
					0b758941a4 | ||
| 
						 | 
					492b55c893 | ||
| 
						 | 
					4060e367ad | ||
| 
						 | 
					27a315fad5 | ||
| 
						 | 
					c99cd31b6b | ||
| 
						 | 
					718782f5b1 | ||
| 
						 | 
					0c3fb5b2ce | ||
| 
						 | 
					3ad56ad304 | ||
| 
						 | 
					4ec6b067e7 | ||
| 
						 | 
					1748dd6a3b | ||
| 
						 | 
					95332e50ed | ||
| 
						 | 
					56eb9d1430 | ||
| 
						 | 
					8496980cf9 | ||
| 
						 | 
					ffe32694b0 | ||
| 
						 | 
					3d5b21154b | ||
| 
						 | 
					4b9697e336 | ||
| 
						 | 
					b0e9a542ba | ||
| 
						 | 
					8b67536c23 | ||
| 
						 | 
					cd49c12181 | ||
| 
						 | 
					9cb7c4907f | ||
| 
						 | 
					a6b14c7910 | ||
| 
						 | 
					e275abdb9c | ||
| 
						 | 
					09a90665d5 | ||
| 
						 | 
					6649fbdfd0 | ||
| 
						 | 
					64a0ffee7b | ||
| 
						 | 
					b529118f31 | ||
| 
						 | 
					39d7d9f13a | ||
| 
						 | 
					fcd55df969 | ||
| 
						 | 
					4b3e664cee | ||
| 
						 | 
					1e59948358 | ||
| 
						 | 
					1102ef6e6b | ||
| 
						 | 
					7ce2e8f4c4 | ||
| 
						 | 
					fd1c656bdd | ||
| 
						 | 
					82298a760a | ||
| 
						 | 
					b84bb72e07 | ||
| 
						 | 
					81d9584338 | ||
| 
						 | 
					e3f499be0c | ||
| 
						 | 
					000f750d7c | ||
| 
						 | 
					86220573b6 | ||
| 
						 | 
					65ed6b02a4 | ||
| 
						 | 
					98093a1f31 | ||
| 
						 | 
					00990dc195 | ||
| 
						 | 
					3da5284a07 | ||
| 
						 | 
					cd920364f8 | ||
| 
						 | 
					e644718b8a | ||
| 
						 | 
					4acb5f624c | ||
| 
						 | 
					6ee6671028 | ||
| 
						 | 
					3d7b39f6ff | ||
| 
						 | 
					1287e39cc6 | ||
| 
						 | 
					1ef2aa35e9 | ||
| 
						 | 
					f414d2d41e | ||
| 
						 | 
					a2cf5eb9d7 | ||
| 
						 | 
					27f8d4e463 | ||
| 
						 | 
					207737fd61 | ||
| 
						 | 
					d5b8a0d191 | ||
| 
						 | 
					25c42a02c8 | ||
| 
						 | 
					0ed5dc627a | ||
| 
						 | 
					58a2cf4d0e | ||
| 
						 | 
					3f9f78b823 | ||
| 
						 | 
					79aeffed01 | ||
| 
						 | 
					daab120d4c | ||
| 
						 | 
					9d4d486dac | ||
| 
						 | 
					9bbaa7cebd | ||
| 
						 | 
					9d623dc11d | ||
| 
						 | 
					4357db6a65 | ||
| 
						 | 
					1e59be5f06 | ||
| 
						 | 
					eb945f1c64 | ||
| 
						 | 
					e07af01413 | ||
| 
						 | 
					59e1449b2f | ||
| 
						 | 
					9d217bdaf5 | ||
| 
						 | 
					90ecefad85 | ||
| 
						 | 
					f1bf2c612d | ||
| 
						 | 
					e5590697aa | ||
| 
						 | 
					5bb365c383 | ||
| 
						 | 
					b09ee3ffc7 | ||
| 
						 | 
					372e6f5f4f | ||
| 
						 | 
					4c7affabd0 | ||
| 
						 | 
					fba2dd3e6c | ||
| 
						 | 
					e57a71a90b | ||
| 
						 | 
					c7d3287432 | ||
| 
						 | 
					4a9e125185 | ||
| 
						 | 
					3014a9765f | ||
| 
						 | 
					cf1e4e3a23 | ||
| 
						 | 
					ff84b2d397 | ||
| 
						 | 
					ddc45b9e09 | ||
| 
						 | 
					7aa0bd7686 | ||
| 
						 | 
					ad4c7531cb | ||
| 
						 | 
					e679f0b9db | ||
| 
						 | 
					a988bb91c7 | ||
| 
						 | 
					6137cc25c7 | ||
| 
						 | 
					e5f4bde59e | ||
| 
						 | 
					f081ad2c43 | ||
| 
						 | 
					087dc20091 | ||
| 
						 | 
					9f805096d0 | ||
| 
						 | 
					43b0b5b9e4 | ||
| 
						 | 
					462b6ea279 | ||
| 
						 | 
					e3ed3d3189 | ||
| 
						 | 
					07be7804a6 | ||
| 
						 | 
					915d533a2b | ||
| 
						 | 
					7738cc3af0 | ||
| 
						 | 
					59c25231ce | ||
| 
						 | 
					4d6b981a54 | 
@@ -1,12 +1,20 @@
 | 
				
			|||||||
# Your openai api key. (required)
 | 
					# Your openai api key. (required)
 | 
				
			||||||
OPENAI_API_KEY=sk-xxxx
 | 
					OPENAI_API_KEY=sk-xxxx
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# DeepSeek Api Key. (Optional)
 | 
				
			||||||
 | 
					DEEPSEEK_API_KEY=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Access password, separated by comma. (optional)
 | 
					# Access password, separated by comma. (optional)
 | 
				
			||||||
CODE=your-password
 | 
					CODE=your-password
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# You can start service behind a proxy. (optional)
 | 
					# You can start service behind a proxy. (optional)
 | 
				
			||||||
PROXY_URL=http://localhost:7890
 | 
					PROXY_URL=http://localhost:7890
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Enable MCP functionality (optional)
 | 
				
			||||||
 | 
					# Default: Empty (disabled)
 | 
				
			||||||
 | 
					# Set to "true" to enable MCP functionality
 | 
				
			||||||
 | 
					ENABLE_MCP=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# (optional)
 | 
					# (optional)
 | 
				
			||||||
# Default: Empty
 | 
					# Default: Empty
 | 
				
			||||||
# Google Gemini Pro API key, set if you want to use Google Gemini Pro API.
 | 
					# Google Gemini Pro API key, set if you want to use Google Gemini Pro API.
 | 
				
			||||||
@@ -66,4 +74,10 @@ ANTHROPIC_API_VERSION=
 | 
				
			|||||||
ANTHROPIC_URL=
 | 
					ANTHROPIC_URL=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### (optional)
 | 
					### (optional)
 | 
				
			||||||
WHITE_WEBDEV_ENDPOINTS=
 | 
					WHITE_WEBDAV_ENDPOINTS=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### siliconflow Api key (optional)
 | 
				
			||||||
 | 
					SILICONFLOW_API_KEY=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### siliconflow Api url (optional)
 | 
				
			||||||
 | 
					SILICONFLOW_URL=
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +1,3 @@
 | 
				
			|||||||
public/serviceWorker.js
 | 
					public/serviceWorker.js
 | 
				
			||||||
 | 
					app/mcp/mcp_config.json
 | 
				
			||||||
 | 
					app/mcp/mcp_config.default.json
 | 
				
			||||||
@@ -1,4 +1,7 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "extends": "next/core-web-vitals",
 | 
					  "extends": "next/core-web-vitals",
 | 
				
			||||||
  "plugins": ["prettier"]
 | 
					  "plugins": ["prettier", "unused-imports"],
 | 
				
			||||||
 | 
					  "rules": {
 | 
				
			||||||
 | 
					    "unused-imports/no-unused-imports": "warn"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										6
									
								
								.github/workflows/deploy_preview.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -3,9 +3,7 @@ name: VercelPreviewDeployment
 | 
				
			|||||||
on:
 | 
					on:
 | 
				
			||||||
  pull_request_target:
 | 
					  pull_request_target:
 | 
				
			||||||
    types:
 | 
					    types:
 | 
				
			||||||
      - opened
 | 
					      - review_requested
 | 
				
			||||||
      - synchronize
 | 
					 | 
				
			||||||
      - reopened
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
env:
 | 
					env:
 | 
				
			||||||
  VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
 | 
					  VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
 | 
				
			||||||
@@ -49,7 +47,7 @@ jobs:
 | 
				
			|||||||
        run: npm install --global vercel@latest
 | 
					        run: npm install --global vercel@latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Cache dependencies
 | 
					      - name: Cache dependencies
 | 
				
			||||||
        uses: actions/cache@v2
 | 
					        uses: actions/cache@v4
 | 
				
			||||||
        id: cache-npm
 | 
					        id: cache-npm
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          path: ~/.npm
 | 
					          path: ~/.npm
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										39
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					name: Run Tests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - main
 | 
				
			||||||
 | 
					    tags:
 | 
				
			||||||
 | 
					      - "!*"
 | 
				
			||||||
 | 
					  pull_request:
 | 
				
			||||||
 | 
					    types:
 | 
				
			||||||
 | 
					      - review_requested
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  test:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: Checkout repository
 | 
				
			||||||
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Set up Node.js
 | 
				
			||||||
 | 
					        uses: actions/setup-node@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          node-version: 18
 | 
				
			||||||
 | 
					          cache: "yarn"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Cache node_modules
 | 
				
			||||||
 | 
					        uses: actions/cache@v4
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          path: node_modules
 | 
				
			||||||
 | 
					          key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
 | 
				
			||||||
 | 
					          restore-keys: |
 | 
				
			||||||
 | 
					            ${{ runner.os }}-node_modules-
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Install dependencies
 | 
				
			||||||
 | 
					        run: yarn install
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Run Jest tests
 | 
				
			||||||
 | 
					        run: yarn test:ci
 | 
				
			||||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -46,3 +46,6 @@ dev
 | 
				
			|||||||
*.key.pub
 | 
					*.key.pub
 | 
				
			||||||
 | 
					
 | 
				
			||||||
masks.json
 | 
					masks.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# mcp config
 | 
				
			||||||
 | 
					app/mcp/mcp_config.json
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,12 +34,16 @@ ENV PROXY_URL=""
 | 
				
			|||||||
ENV OPENAI_API_KEY=""
 | 
					ENV OPENAI_API_KEY=""
 | 
				
			||||||
ENV GOOGLE_API_KEY=""
 | 
					ENV GOOGLE_API_KEY=""
 | 
				
			||||||
ENV CODE=""
 | 
					ENV CODE=""
 | 
				
			||||||
 | 
					ENV ENABLE_MCP=""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY --from=builder /app/public ./public
 | 
					COPY --from=builder /app/public ./public
 | 
				
			||||||
COPY --from=builder /app/.next/standalone ./
 | 
					COPY --from=builder /app/.next/standalone ./
 | 
				
			||||||
COPY --from=builder /app/.next/static ./.next/static
 | 
					COPY --from=builder /app/.next/static ./.next/static
 | 
				
			||||||
COPY --from=builder /app/.next/server ./.next/server
 | 
					COPY --from=builder /app/.next/server ./.next/server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUN mkdir -p /app/app/mcp && chmod 777 /app/app/mcp
 | 
				
			||||||
 | 
					COPY --from=builder /app/app/mcp/mcp_config.default.json /app/app/mcp/mcp_config.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
EXPOSE 3000
 | 
					EXPOSE 3000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CMD if [ -n "$PROXY_URL" ]; then \
 | 
					CMD if [ -n "$PROXY_URL" ]; then \
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						@@ -1,6 +1,6 @@
 | 
				
			|||||||
MIT License
 | 
					MIT License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Copyright (c) 2023-2024 Zhang Yifei
 | 
					Copyright (c) 2023-2025 NextChat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
					Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
				
			||||||
of this software and associated documentation files (the "Software"), to deal
 | 
					of this software and associated documentation files (the "Software"), to deal
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										202
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -1,26 +1,32 @@
 | 
				
			|||||||
<div align="center">
 | 
					<div align="center">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<a href='#企业版'>
 | 
					<a href='https://nextchat.club'>
 | 
				
			||||||
  <img src="./docs/images/ent.svg" alt="icon"/>
 | 
					  <img src="https://github.com/user-attachments/assets/83bdcc07-ae5e-4954-a53a-ac151ba6ccf3" width="1000" alt="icon"/>
 | 
				
			||||||
</a>
 | 
					</a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<h1 align="center">NextChat (ChatGPT Next Web)</h1>
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h1 align="center">NextChat</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
English / [简体中文](./README_CN.md)
 | 
					English / [简体中文](./README_CN.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 & Gemini Pro support.
 | 
					<a href="https://trendshift.io/repositories/5973" target="_blank"><img src="https://trendshift.io/api/badge/repositories/5973" alt="ChatGPTNextWeb%2FChatGPT-Next-Web | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
一键免费部署你的跨平台私人 ChatGPT 应用, 支持 GPT3, GPT4 & Gemini Pro 模型。
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					✨ Light and Fast AI Assistant,with Claude, DeepSeek, GPT4 & Gemini Pro support. 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[![Saas][Saas-image]][saas-url]
 | 
				
			||||||
[![Web][Web-image]][web-url]
 | 
					[![Web][Web-image]][web-url]
 | 
				
			||||||
[![Windows][Windows-image]][download-url]
 | 
					[![Windows][Windows-image]][download-url]
 | 
				
			||||||
[![MacOS][MacOS-image]][download-url]
 | 
					[![MacOS][MacOS-image]][download-url]
 | 
				
			||||||
[![Linux][Linux-image]][download-url]
 | 
					[![Linux][Linux-image]][download-url]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[Web App](https://app.nextchat.dev/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev)
 | 
					[NextChatAI](https://nextchat.club?utm_source=readme) / [iOS APP](https://apps.apple.com/us/app/nextchat-ai/id6743085599) / [Web App Demo](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Enterprise Edition](#enterprise-edition) 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[saas-url]: https://nextchat.club?utm_source=readme
 | 
				
			||||||
 | 
					[saas-image]: https://img.shields.io/badge/NextChat-Saas-green?logo=microsoftedge
 | 
				
			||||||
[web-url]: https://app.nextchat.dev/
 | 
					[web-url]: https://app.nextchat.dev/
 | 
				
			||||||
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
 | 
					[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
 | 
				
			||||||
[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge
 | 
					[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge
 | 
				
			||||||
@@ -28,10 +34,26 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
 | 
				
			|||||||
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
 | 
					[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
 | 
				
			||||||
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
 | 
					[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA)  [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
					[<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://vercel.com/button" alt="Deploy on Vercel" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat)  [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/ChatGPTNextWeb/NextChat) 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="50" width="" >](https://monica.im/?utm=nxcrp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 🥳 Cheer for NextChat iOS Version Online!
 | 
				
			||||||
 | 
					> [👉 Click Here to Install Now](https://apps.apple.com/us/app/nextchat-ai/id6743085599)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> [❤️ Source Code Coming Soon](https://github.com/ChatGPTNextWeb/NextChat-iOS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					## 🫣 NextChat Support MCP  ! 
 | 
				
			||||||
 | 
					> Before build, please set env ENABLE_MCP=true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<img src="https://github.com/user-attachments/assets/d8851f40-4e36-4335-b1a4-ec1e11488c7e"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Enterprise Edition
 | 
					## Enterprise Edition
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Meeting Your Company's Privatization and Customization Deployment Requirements:
 | 
					Meeting Your Company's Privatization and Customization Deployment Requirements:
 | 
				
			||||||
@@ -45,20 +67,12 @@ Meeting Your Company's Privatization and Customization Deployment Requirements:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
For enterprise inquiries, please contact: **business@nextchat.dev**
 | 
					For enterprise inquiries, please contact: **business@nextchat.dev**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 企业版
 | 
					## Screenshots
 | 
				
			||||||
 | 
					
 | 
				
			||||||
满足企业用户私有化部署和个性化定制需求:
 | 
					
 | 
				
			||||||
- **品牌定制**:企业量身定制 VI/UI,与企业品牌形象无缝契合
 | 
					 | 
				
			||||||
- **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用
 | 
					 | 
				
			||||||
- **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制
 | 
					 | 
				
			||||||
- **知识接入**:企业内部知识库与 AI 能力相结合,比通用 AI 更贴近企业自身业务需求
 | 
					 | 
				
			||||||
- **安全审计**:自动拦截敏感提问,支持追溯全部历史对话记录,让 AI 也能遵循企业信息安全规范
 | 
					 | 
				
			||||||
- **私有部署**:企业级私有部署,支持各类主流私有云部署,确保数据安全和隐私保护
 | 
					 | 
				
			||||||
- **持续更新**:提供多模态、智能体等前沿能力持续更新升级服务,常用常新、持续先进
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
企业版咨询: **business@nextchat.dev**
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Features
 | 
					## Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -89,13 +103,15 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 | 
				
			|||||||
- [x] Desktop App with tauri
 | 
					- [x] Desktop App with tauri
 | 
				
			||||||
- [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
 | 
					- [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
 | 
				
			||||||
- [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
 | 
					- [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
 | 
				
			||||||
- [x] Plugins: support artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 | 
					- [x] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
 | 
				
			||||||
  - [x] artifacts
 | 
					  - [x] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
 | 
				
			||||||
  - [ ] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 | 
					- [x] Supports Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
 | 
				
			||||||
- [ ] local knowledge base
 | 
					- [ ] local knowledge base
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## What's New
 | 
					## What's New
 | 
				
			||||||
 | 
					- 🚀 v2.15.8 Now supports Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
 | 
				
			||||||
 | 
					- 🚀 v2.15.4 The Application supports using Tauri fetch LLM API, MORE SECURITY! [#5379](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5379)
 | 
				
			||||||
 | 
					- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
 | 
				
			||||||
- 🚀 v2.14.0 Now supports  Artifacts & SD 
 | 
					- 🚀 v2.14.0 Now supports  Artifacts & SD 
 | 
				
			||||||
- 🚀 v2.10.1 support Google Gemini Pro model.
 | 
					- 🚀 v2.10.1 support Google Gemini Pro model.
 | 
				
			||||||
- 🚀 v2.9.11 you can use azure endpoint now.
 | 
					- 🚀 v2.9.11 you can use azure endpoint now.
 | 
				
			||||||
@@ -103,48 +119,8 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 | 
				
			|||||||
- 🚀 v2.7 let's share conversations as image, or share to ShareGPT!
 | 
					- 🚀 v2.7 let's share conversations as image, or share to ShareGPT!
 | 
				
			||||||
- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
 | 
					- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 主要功能
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- 在 1 分钟内使用 Vercel **免费一键部署**
 | 
					 | 
				
			||||||
- 提供体积极小(~5MB)的跨平台客户端(Linux/Windows/MacOS), [下载地址](https://github.com/Yidadaa/ChatGPT-Next-Web/releases)
 | 
					 | 
				
			||||||
- 完整的 Markdown 支持:LaTex 公式、Mermaid 流程图、代码高亮等等
 | 
					 | 
				
			||||||
- 精心设计的 UI,响应式设计,支持深色模式,支持 PWA
 | 
					 | 
				
			||||||
- 极快的首屏加载速度(~100kb),支持流式响应
 | 
					 | 
				
			||||||
- 隐私安全,所有数据保存在用户浏览器本地
 | 
					 | 
				
			||||||
- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话
 | 
					 | 
				
			||||||
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
 | 
					 | 
				
			||||||
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
 | 
					 | 
				
			||||||
- 多国语言支持:English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
 | 
					 | 
				
			||||||
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 开发计划
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- [x] 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
 | 
					 | 
				
			||||||
- [x] 允许用户自行编辑内置 Prompt 列表
 | 
					 | 
				
			||||||
- [x] 预制角色:使用预制角色快速定制新对话 [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993)
 | 
					 | 
				
			||||||
- [x] 分享为图片,分享到 ShareGPT 链接 [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
 | 
					 | 
				
			||||||
- [x] 使用 tauri 打包桌面应用
 | 
					 | 
				
			||||||
- [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm)
 | 
					 | 
				
			||||||
- [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
 | 
					 | 
				
			||||||
- [x] 插件机制,支持 artifacts,联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 | 
					 | 
				
			||||||
   - [x] artifacts
 | 
					 | 
				
			||||||
   - [ ] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 | 
					 | 
				
			||||||
 - [ ] 本地知识库
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 最新动态
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- 🚀 v2.14.0 现在支持 Artifacts & SD 了。
 | 
					 | 
				
			||||||
- 🚀 v2.10.1 现在支持 Gemini Pro 模型。
 | 
					 | 
				
			||||||
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
 | 
					 | 
				
			||||||
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
 | 
					 | 
				
			||||||
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
 | 
					 | 
				
			||||||
- 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。
 | 
					 | 
				
			||||||
- 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Get Started
 | 
					## Get Started
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> [简体中文 > 如何开始使用](./README_CN.md#开始使用)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1. Get [OpenAI API Key](https://platform.openai.com/account/api-keys);
 | 
					1. Get [OpenAI API Key](https://platform.openai.com/account/api-keys);
 | 
				
			||||||
2. Click
 | 
					2. Click
 | 
				
			||||||
   [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web), remember that `CODE` is your page password;
 | 
					   [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web), remember that `CODE` is your page password;
 | 
				
			||||||
@@ -152,14 +128,10 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## FAQ
 | 
					## FAQ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[简体中文 > 常见问题](./docs/faq-cn.md)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[English > FAQ](./docs/faq-en.md)
 | 
					[English > FAQ](./docs/faq-en.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Keep Updated
 | 
					## Keep Updated
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> [简体中文 > 如何保持代码更新](./README_CN.md#保持更新)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
If you have deployed your own project with just one click following the steps above, you may encounter the issue of "Updates Available" constantly showing up. This is because Vercel will create a new project for you by default instead of forking this project, resulting in the inability to detect updates correctly.
 | 
					If you have deployed your own project with just one click following the steps above, you may encounter the issue of "Updates Available" constantly showing up. This is because Vercel will create a new project for you by default instead of forking this project, resulting in the inability to detect updates correctly.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
We recommend that you follow the steps below to re-deploy:
 | 
					We recommend that you follow the steps below to re-deploy:
 | 
				
			||||||
@@ -170,7 +142,7 @@ We recommend that you follow the steps below to re-deploy:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### Enable Automatic Updates
 | 
					### Enable Automatic Updates
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> If you encounter a failure of Upstream Sync execution, please manually sync fork once.
 | 
					> If you encounter a failure of Upstream Sync execution, please [manually update code](./README.md#manually-updating-code).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
 | 
					After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -186,8 +158,6 @@ You can star or watch this project or follow author to get release notifications
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Access Password
 | 
					## Access Password
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> [简体中文 > 如何增加访问密码](./README_CN.md#配置页面访问密码)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
This project provides limited access control. Please add an environment variable named `CODE` on the vercel environment variables page. The value should be passwords separated by comma like this:
 | 
					This project provides limited access control. Please add an environment variable named `CODE` on the vercel environment variables page. The value should be passwords separated by comma like this:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
@@ -198,8 +168,6 @@ After adding or modifying this environment variable, please redeploy the project
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Environment Variables
 | 
					## Environment Variables
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### `CODE` (optional)
 | 
					### `CODE` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Access password, separated by comma.
 | 
					Access password, separated by comma.
 | 
				
			||||||
@@ -294,6 +262,22 @@ iflytek Api Key.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
iflytek Api Secret.
 | 
					iflytek Api Secret.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `CHATGLM_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ChatGLM Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `CHATGLM_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ChatGLM Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DEEPSEEK_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DeepSeek Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DEEPSEEK_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DeepSeek Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `HIDE_USER_API_KEY` (optional)
 | 
					### `HIDE_USER_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> Default: Empty
 | 
					> Default: Empty
 | 
				
			||||||
@@ -327,9 +311,9 @@ To control custom models, use `+` to add a custom model, use `-` to hide a model
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
User `-all` to disable all default models, `+all` to enable all default models.
 | 
					User `-all` to disable all default models, `+all` to enable all default models.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
For Azure: use `modelName@azure=deploymentName` to customize model name and deployment name.
 | 
					For Azure: use `modelName@Azure=deploymentName` to customize model name and deployment name.
 | 
				
			||||||
> Example: `+gpt-3.5-turbo@azure=gpt35` will show option `gpt35(Azure)` in model list.
 | 
					> Example: `+gpt-3.5-turbo@Azure=gpt35` will show option `gpt35(Azure)` in model list.
 | 
				
			||||||
> If you only can use Azure model, `-all,+gpt-3.5-turbo@azure=gpt35` will `gpt35(Azure)` the only option in model list.
 | 
					> If you only can use Azure model, `-all,+gpt-3.5-turbo@Azure=gpt35` will `gpt35(Azure)` the only option in model list.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
 | 
					For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
 | 
				
			||||||
> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.
 | 
					> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.
 | 
				
			||||||
@@ -338,7 +322,14 @@ For ByteDance: use `modelName@bytedance=deploymentName` to customize model name
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Change default model
 | 
					Change default model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `WHITE_WEBDEV_ENDPOINTS` (optional)
 | 
					### `VISION_MODELS` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> Default: Empty
 | 
				
			||||||
 | 
					> Example: `gpt-4-vision,claude-3-opus,my-custom-model` means add vision capabilities to these models in addition to the default pattern matches (which detect models containing keywords like "vision", "claude-3", "gemini-1.5", etc).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Add additional models to have vision capabilities, beyond the default pattern matching. Multiple models should be separated by commas.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `WHITE_WEBDAV_ENDPOINTS` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format:
 | 
					You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format:
 | 
				
			||||||
- Each address must be a complete endpoint 
 | 
					- Each address must be a complete endpoint 
 | 
				
			||||||
@@ -357,13 +348,25 @@ Stability API key.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Customize Stability API url.
 | 
					Customize Stability API url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ENABLE_MCP` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Enable MCP(Model Context Protocol)Feature
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `SILICONFLOW_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SiliconFlow API Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `SILICONFLOW_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SiliconFlow API URL.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Requirements
 | 
					## Requirements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
NodeJS >= 18, Docker >= 20
 | 
					NodeJS >= 18, Docker >= 20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Development
 | 
					## Development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
					[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -388,7 +391,6 @@ yarn dev
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Deployment
 | 
					## Deployment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> [简体中文 > 如何部署到私人服务器](./README_CN.md#部署)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Docker (Recommended)
 | 
					### Docker (Recommended)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -417,6 +419,16 @@ If your proxy needs password, use:
 | 
				
			|||||||
-e PROXY_URL="http://127.0.0.1:7890 user pass"
 | 
					-e PROXY_URL="http://127.0.0.1:7890 user pass"
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If enable MCP, use:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					docker run -d -p 3000:3000 \
 | 
				
			||||||
 | 
					   -e OPENAI_API_KEY=sk-xxxx \
 | 
				
			||||||
 | 
					   -e CODE=your-password \
 | 
				
			||||||
 | 
					   -e ENABLE_MCP=true \
 | 
				
			||||||
 | 
					   yidadaa/chatgpt-next-web
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Shell
 | 
					### Shell
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```shell
 | 
					```shell
 | 
				
			||||||
@@ -437,11 +449,7 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
 | 
				
			|||||||
- [How to use Vercel (No English)](./docs/vercel-cn.md)
 | 
					- [How to use Vercel (No English)](./docs/vercel-cn.md)
 | 
				
			||||||
- [User Manual (Only Chinese, WIP)](./docs/user-manual-cn.md)
 | 
					- [User Manual (Only Chinese, WIP)](./docs/user-manual-cn.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Screenshots
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||

 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||

 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Translation
 | 
					## Translation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -453,37 +461,7 @@ If you want to add a new translation, read this [document](./docs/translation.md
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Special Thanks
 | 
					## Special Thanks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Sponsor
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
> 仅列出捐赠金额 >= 100RMB 的用户。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[@mushan0x0](https://github.com/mushan0x0)
 | 
					 | 
				
			||||||
[@ClarenceDan](https://github.com/ClarenceDan)
 | 
					 | 
				
			||||||
[@zhangjia](https://github.com/zhangjia)
 | 
					 | 
				
			||||||
[@hoochanlon](https://github.com/hoochanlon)
 | 
					 | 
				
			||||||
[@relativequantum](https://github.com/relativequantum)
 | 
					 | 
				
			||||||
[@desenmeng](https://github.com/desenmeng)
 | 
					 | 
				
			||||||
[@webees](https://github.com/webees)
 | 
					 | 
				
			||||||
[@chazzhou](https://github.com/chazzhou)
 | 
					 | 
				
			||||||
[@hauy](https://github.com/hauy)
 | 
					 | 
				
			||||||
[@Corwin006](https://github.com/Corwin006)
 | 
					 | 
				
			||||||
[@yankunsong](https://github.com/yankunsong)
 | 
					 | 
				
			||||||
[@ypwhs](https://github.com/ypwhs)
 | 
					 | 
				
			||||||
[@fxxxchao](https://github.com/fxxxchao)
 | 
					 | 
				
			||||||
[@hotic](https://github.com/hotic)
 | 
					 | 
				
			||||||
[@WingCH](https://github.com/WingCH)
 | 
					 | 
				
			||||||
[@jtung4](https://github.com/jtung4)
 | 
					 | 
				
			||||||
[@micozhu](https://github.com/micozhu)
 | 
					 | 
				
			||||||
[@jhansion](https://github.com/jhansion)
 | 
					 | 
				
			||||||
[@Sha1rholder](https://github.com/Sha1rholder)
 | 
					 | 
				
			||||||
[@AnsonHyq](https://github.com/AnsonHyq)
 | 
					 | 
				
			||||||
[@synwith](https://github.com/synwith)
 | 
					 | 
				
			||||||
[@piksonGit](https://github.com/piksonGit)
 | 
					 | 
				
			||||||
[@ouyangzhiping](https://github.com/ouyangzhiping)
 | 
					 | 
				
			||||||
[@wenjiavv](https://github.com/wenjiavv)
 | 
					 | 
				
			||||||
[@LeXwDeX](https://github.com/LeXwDeX)
 | 
					 | 
				
			||||||
[@Licoy](https://github.com/Licoy)
 | 
					 | 
				
			||||||
[@shangmin2009](https://github.com/shangmin2009)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Contributors
 | 
					### Contributors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										65
									
								
								README_CN.md
									
									
									
									
									
								
							
							
						
						@@ -6,9 +6,9 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<h1 align="center">NextChat</h1>
 | 
					<h1 align="center">NextChat</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
 | 
					一键免费部署你的私人 ChatGPT 网页应用,支持 Claude, GPT4 & Gemini Pro 模型。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) /[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
 | 
					[NextChatAI](https://nextchat.club?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA)  [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
					[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA)  [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -27,7 +27,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
企业版咨询: **business@nextchat.dev**
 | 
					企业版咨询: **business@nextchat.dev**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
 | 
					<img width="300" src="https://github.com/user-attachments/assets/bb29a11d-ff75-48a8-b1f8-d2d7238cf987">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 开始使用
 | 
					## 开始使用
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -54,7 +55,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### 打开自动更新
 | 
					### 打开自动更新
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> 如果你遇到了 Upstream Sync 执行错误,请手动 Sync Fork 一次!
 | 
					> 如果你遇到了 Upstream Sync 执行错误,请[手动 Sync Fork 一次](./README_CN.md#手动更新代码)!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新:
 | 
					当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -88,7 +89,7 @@ code1,code2,code3
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### `OPENAI_API_KEY` (必填项)
 | 
					### `OPENAI_API_KEY` (必填项)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
OpanAI 密钥,你在 openai 账户页面申请的 api key,使用英文逗号隔开多个 key,这样可以随机轮询这些 key。
 | 
					OpenAI 密钥,你在 openai 账户页面申请的 api key,使用英文逗号隔开多个 key,这样可以随机轮询这些 key。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `CODE` (可选)
 | 
					### `CODE` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -184,6 +185,21 @@ ByteDance Api Url.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
讯飞星火Api Secret.
 | 
					讯飞星火Api Secret.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `CHATGLM_API_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ChatGLM Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `CHATGLM_URL` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ChatGLM Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DEEPSEEK_API_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DeepSeek Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `DEEPSEEK_URL` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DeepSeek Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `HIDE_USER_API_KEY` (可选)
 | 
					### `HIDE_USER_API_KEY` (可选)
 | 
				
			||||||
@@ -202,7 +218,7 @@ ByteDance Api Url.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
 | 
					如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `WHITE_WEBDEV_ENDPOINTS` (可选)
 | 
					### `WHITE_WEBDAV_ENDPOINTS` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求:
 | 
					如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求:
 | 
				
			||||||
- 每一个地址必须是一个完整的 endpoint
 | 
					- 每一个地址必须是一个完整的 endpoint
 | 
				
			||||||
@@ -216,9 +232,9 @@ ByteDance Api Url.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
 | 
					用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
在Azure的模式下,支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
 | 
					在Azure的模式下,支持使用`modelName@Azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
 | 
				
			||||||
> 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
 | 
					> 示例:`+gpt-3.5-turbo@Azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
 | 
				
			||||||
> 如果你只能使用Azure模式,那么设置 `-all,+gpt-3.5-turbo@azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
 | 
					> 如果你只能使用Azure模式,那么设置 `-all,+gpt-3.5-turbo@Azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
 | 
					在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
 | 
				
			||||||
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
 | 
					> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
 | 
				
			||||||
@@ -228,6 +244,13 @@ ByteDance Api Url.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
更改默认模型
 | 
					更改默认模型
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `VISION_MODELS` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> 默认值:空
 | 
				
			||||||
 | 
					> 示例:`gpt-4-vision,claude-3-opus,my-custom-model` 表示为这些模型添加视觉能力,作为对默认模式匹配的补充(默认会检测包含"vision"、"claude-3"、"gemini-1.5"等关键词的模型)。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					在默认模式匹配之外,添加更多具有视觉能力的模型。多个模型用逗号分隔。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `DEFAULT_INPUT_TEMPLATE` (可选)
 | 
					### `DEFAULT_INPUT_TEMPLATE` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项
 | 
					自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项
 | 
				
			||||||
@@ -240,6 +263,17 @@ Stability API密钥
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
自定义的Stability API请求地址
 | 
					自定义的Stability API请求地址
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ENABLE_MCP` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					启用MCP(Model Context Protocol)功能
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `SILICONFLOW_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SiliconFlow API Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `SILICONFLOW_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SiliconFlow API URL.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 开发
 | 
					## 开发
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -264,6 +298,9 @@ BASE_URL=https://b.nextweb.fun/api/proxy
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## 部署
 | 
					## 部署
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 宝塔面板部署
 | 
				
			||||||
 | 
					> [简体中文 > 如何通过宝塔一键部署](./docs/bt-cn.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 容器部署 (推荐)
 | 
					### 容器部署 (推荐)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。
 | 
					> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。
 | 
				
			||||||
@@ -290,6 +327,16 @@ docker run -d -p 3000:3000 \
 | 
				
			|||||||
   yidadaa/chatgpt-next-web
 | 
					   yidadaa/chatgpt-next-web
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					如需启用 MCP 功能,可以使用:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```shell
 | 
				
			||||||
 | 
					docker run -d -p 3000:3000 \
 | 
				
			||||||
 | 
					   -e OPENAI_API_KEY=sk-xxxx \
 | 
				
			||||||
 | 
					   -e CODE=页面访问密码 \
 | 
				
			||||||
 | 
					   -e ENABLE_MCP=true \
 | 
				
			||||||
 | 
					   yidadaa/chatgpt-next-web
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
如果你的本地代理需要账号密码,可以使用:
 | 
					如果你的本地代理需要账号密码,可以使用:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```shell
 | 
					```shell
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								README_JA.md
									
									
									
									
									
								
							
							
						
						@@ -5,7 +5,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
 | 
					ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
 | 
					[NextChatAI](https://nextchat.club?utm_source=readme) / [企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[<img src="https://vercel.com/button" alt="Zeaburでデプロイ" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA)  [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
					[<img src="https://vercel.com/button" alt="Zeaburでデプロイ" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA)  [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -54,7 +54,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### 自動更新を開く
 | 
					### 自動更新を開く
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> Upstream Sync の実行エラーが発生した場合は、手動で Sync Fork してください!
 | 
					> Upstream Sync の実行エラーが発生した場合は、[手動で Sync Fork](./README_JA.md#手動でコードを更新する) してください!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
プロジェクトを fork した後、GitHub の制限により、fork 後のプロジェクトの Actions ページで Workflows を手動で有効にし、Upstream Sync Action を有効にする必要があります。有効化後、毎時の定期自動更新が可能になります:
 | 
					プロジェクトを fork した後、GitHub の制限により、fork 後のプロジェクトの Actions ページで Workflows を手動で有効にし、Upstream Sync Action を有効にする必要があります。有効化後、毎時の定期自動更新が可能になります:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -193,7 +193,7 @@ ByteDance API の URL。
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
リンクからのプリセット設定解析を無効にしたい場合は、この環境変数を 1 に設定します。
 | 
					リンクからのプリセット設定解析を無効にしたい場合は、この環境変数を 1 に設定します。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `WHITE_WEBDEV_ENDPOINTS` (オプション)
 | 
					### `WHITE_WEBDAV_ENDPOINTS` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
アクセス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件:
 | 
					アクセス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件:
 | 
				
			||||||
- 各アドレスは完全なエンドポイントでなければなりません。
 | 
					- 各アドレスは完全なエンドポイントでなければなりません。
 | 
				
			||||||
@@ -207,8 +207,8 @@ ByteDance API の URL。
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
 | 
					モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Azure モードでは、`modelName@azure=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
 | 
					Azure モードでは、`modelName@Azure=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
 | 
				
			||||||
> 例:`+gpt-3.5-turbo@azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
 | 
					> 例:`+gpt-3.5-turbo@Azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
 | 
					ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
 | 
				
			||||||
> 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。
 | 
					> 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。
 | 
				
			||||||
@@ -217,6 +217,13 @@ ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデ
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
デフォルトのモデルを変更します。
 | 
					デフォルトのモデルを変更します。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `VISION_MODELS` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> デフォルト:空
 | 
				
			||||||
 | 
					> 例:`gpt-4-vision,claude-3-opus,my-custom-model` は、これらのモデルにビジョン機能を追加します。これはデフォルトのパターンマッチング("vision"、"claude-3"、"gemini-1.5"などのキーワードを含むモデルを検出)に加えて適用されます。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					デフォルトのパターンマッチングに加えて、追加のモデルにビジョン機能を付与します。複数のモデルはカンマで区切ります。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `DEFAULT_INPUT_TEMPLATE` (オプション)
 | 
					### `DEFAULT_INPUT_TEMPLATE` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
『設定』の『ユーザー入力前処理』の初期設定に使用するテンプレートをカスタマイズします。
 | 
					『設定』の『ユーザー入力前処理』の初期設定に使用するテンプレートをカスタマイズします。
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import { ApiPath } from "@/app/constant";
 | 
					import { ApiPath } from "@/app/constant";
 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextRequest } from "next/server";
 | 
				
			||||||
import { handle as openaiHandler } from "../../openai";
 | 
					import { handle as openaiHandler } from "../../openai";
 | 
				
			||||||
import { handle as azureHandler } from "../../azure";
 | 
					import { handle as azureHandler } from "../../azure";
 | 
				
			||||||
import { handle as googleHandler } from "../../google";
 | 
					import { handle as googleHandler } from "../../google";
 | 
				
			||||||
@@ -10,6 +10,12 @@ import { handle as alibabaHandler } from "../../alibaba";
 | 
				
			|||||||
import { handle as moonshotHandler } from "../../moonshot";
 | 
					import { handle as moonshotHandler } from "../../moonshot";
 | 
				
			||||||
import { handle as stabilityHandler } from "../../stability";
 | 
					import { handle as stabilityHandler } from "../../stability";
 | 
				
			||||||
import { handle as iflytekHandler } from "../../iflytek";
 | 
					import { handle as iflytekHandler } from "../../iflytek";
 | 
				
			||||||
 | 
					import { handle as deepseekHandler } from "../../deepseek";
 | 
				
			||||||
 | 
					import { handle as siliconflowHandler } from "../../siliconflow";
 | 
				
			||||||
 | 
					import { handle as xaiHandler } from "../../xai";
 | 
				
			||||||
 | 
					import { handle as chatglmHandler } from "../../glm";
 | 
				
			||||||
 | 
					import { handle as proxyHandler } from "../../proxy";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function handle(
 | 
					async function handle(
 | 
				
			||||||
  req: NextRequest,
 | 
					  req: NextRequest,
 | 
				
			||||||
  { params }: { params: { provider: string; path: string[] } },
 | 
					  { params }: { params: { provider: string; path: string[] } },
 | 
				
			||||||
@@ -36,8 +42,18 @@ async function handle(
 | 
				
			|||||||
      return stabilityHandler(req, { params });
 | 
					      return stabilityHandler(req, { params });
 | 
				
			||||||
    case ApiPath.Iflytek:
 | 
					    case ApiPath.Iflytek:
 | 
				
			||||||
      return iflytekHandler(req, { params });
 | 
					      return iflytekHandler(req, { params });
 | 
				
			||||||
    default:
 | 
					    case ApiPath.DeepSeek:
 | 
				
			||||||
 | 
					      return deepseekHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.XAI:
 | 
				
			||||||
 | 
					      return xaiHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.ChatGLM:
 | 
				
			||||||
 | 
					      return chatglmHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.SiliconFlow:
 | 
				
			||||||
 | 
					      return siliconflowHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.OpenAI:
 | 
				
			||||||
      return openaiHandler(req, { params });
 | 
					      return openaiHandler(req, { params });
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return proxyHandler(req, { params });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Alibaba,
 | 
					 | 
				
			||||||
  ALIBABA_BASE_URL,
 | 
					  ALIBABA_BASE_URL,
 | 
				
			||||||
  ApiPath,
 | 
					  ApiPath,
 | 
				
			||||||
  ModelProvider,
 | 
					  ModelProvider,
 | 
				
			||||||
@@ -9,8 +8,7 @@ import {
 | 
				
			|||||||
import { prettyObject } from "@/app/utils/format";
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
import { isModelAvailableInServer } from "@/app/utils/model";
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
import type { RequestPayload } from "@/app/client/platforms/openai";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -91,7 +89,7 @@ async function request(req: NextRequest) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // not undefined and is false
 | 
					      // not undefined and is false
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
        isModelAvailableInServer(
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
          serverConfig.customModels,
 | 
					          serverConfig.customModels,
 | 
				
			||||||
          jsonBody?.model as string,
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
          ServiceProvider.Alibaba as string,
 | 
					          ServiceProvider.Alibaba as string,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,14 +3,13 @@ import {
 | 
				
			|||||||
  ANTHROPIC_BASE_URL,
 | 
					  ANTHROPIC_BASE_URL,
 | 
				
			||||||
  Anthropic,
 | 
					  Anthropic,
 | 
				
			||||||
  ApiPath,
 | 
					  ApiPath,
 | 
				
			||||||
  DEFAULT_MODELS,
 | 
					 | 
				
			||||||
  ServiceProvider,
 | 
					  ServiceProvider,
 | 
				
			||||||
  ModelProvider,
 | 
					  ModelProvider,
 | 
				
			||||||
} from "@/app/constant";
 | 
					} from "@/app/constant";
 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
import { auth } from "./auth";
 | 
					import { auth } from "./auth";
 | 
				
			||||||
import { isModelAvailableInServer } from "@/app/utils/model";
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
					import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
 | 
					const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
 | 
				
			||||||
@@ -98,6 +97,7 @@ async function request(req: NextRequest) {
 | 
				
			|||||||
    headers: {
 | 
					    headers: {
 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
      "Cache-Control": "no-store",
 | 
					      "Cache-Control": "no-store",
 | 
				
			||||||
 | 
					      "anthropic-dangerous-direct-browser-access": "true",
 | 
				
			||||||
      [authHeaderName]: authValue,
 | 
					      [authHeaderName]: authValue,
 | 
				
			||||||
      "anthropic-version":
 | 
					      "anthropic-version":
 | 
				
			||||||
        req.headers.get("anthropic-version") ||
 | 
					        req.headers.get("anthropic-version") ||
 | 
				
			||||||
@@ -122,7 +122,7 @@ async function request(req: NextRequest) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // not undefined and is false
 | 
					      // not undefined and is false
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
        isModelAvailableInServer(
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
          serverConfig.customModels,
 | 
					          serverConfig.customModels,
 | 
				
			||||||
          jsonBody?.model as string,
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
          ServiceProvider.Anthropic as string,
 | 
					          ServiceProvider.Anthropic as string,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -92,6 +92,18 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
 | 
				
			|||||||
        systemApiKey =
 | 
					        systemApiKey =
 | 
				
			||||||
          serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
 | 
					          serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.DeepSeek:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.deepseekApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.XAI:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.xaiApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.ChatGLM:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.chatglmApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.SiliconFlow:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.siliconFlowApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
      case ModelProvider.GPT:
 | 
					      case ModelProvider.GPT:
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        if (req.nextUrl.pathname.includes("azure/deployments")) {
 | 
					        if (req.nextUrl.pathname.includes("azure/deployments")) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					 | 
				
			||||||
import { ModelProvider } from "@/app/constant";
 | 
					import { ModelProvider } from "@/app/constant";
 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,13 +3,12 @@ import {
 | 
				
			|||||||
  BAIDU_BASE_URL,
 | 
					  BAIDU_BASE_URL,
 | 
				
			||||||
  ApiPath,
 | 
					  ApiPath,
 | 
				
			||||||
  ModelProvider,
 | 
					  ModelProvider,
 | 
				
			||||||
  BAIDU_OATUH_URL,
 | 
					 | 
				
			||||||
  ServiceProvider,
 | 
					  ServiceProvider,
 | 
				
			||||||
} from "@/app/constant";
 | 
					} from "@/app/constant";
 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
import { isModelAvailableInServer } from "@/app/utils/model";
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
import { getAccessToken } from "@/app/utils/baidu";
 | 
					import { getAccessToken } from "@/app/utils/baidu";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
@@ -105,7 +104,7 @@ async function request(req: NextRequest) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // not undefined and is false
 | 
					      // not undefined and is false
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
        isModelAvailableInServer(
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
          serverConfig.customModels,
 | 
					          serverConfig.customModels,
 | 
				
			||||||
          jsonBody?.model as string,
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
          ServiceProvider.Baidu as string,
 | 
					          ServiceProvider.Baidu as string,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ import {
 | 
				
			|||||||
import { prettyObject } from "@/app/utils/format";
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
import { isModelAvailableInServer } from "@/app/utils/model";
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -88,7 +88,7 @@ async function request(req: NextRequest) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // not undefined and is false
 | 
					      // not undefined and is false
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
        isModelAvailableInServer(
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
          serverConfig.customModels,
 | 
					          serverConfig.customModels,
 | 
				
			||||||
          jsonBody?.model as string,
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
          ServiceProvider.ByteDance as string,
 | 
					          ServiceProvider.ByteDance as string,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,8 @@
 | 
				
			|||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
import { getServerSideConfig } from "../config/server";
 | 
					import { getServerSideConfig } from "../config/server";
 | 
				
			||||||
import {
 | 
					import { OPENAI_BASE_URL, ServiceProvider } from "../constant";
 | 
				
			||||||
  DEFAULT_MODELS,
 | 
					 | 
				
			||||||
  OPENAI_BASE_URL,
 | 
					 | 
				
			||||||
  GEMINI_BASE_URL,
 | 
					 | 
				
			||||||
  ServiceProvider,
 | 
					 | 
				
			||||||
} from "../constant";
 | 
					 | 
				
			||||||
import { isModelAvailableInServer } from "../utils/model";
 | 
					 | 
				
			||||||
import { cloudflareAIGatewayUrl } from "../utils/cloudflare";
 | 
					import { cloudflareAIGatewayUrl } from "../utils/cloudflare";
 | 
				
			||||||
 | 
					import { getModelProvider, isModelNotavailableInServer } from "../utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,10 +27,7 @@ export async function requestOpenai(req: NextRequest) {
 | 
				
			|||||||
    authHeaderName = "Authorization";
 | 
					    authHeaderName = "Authorization";
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll("/api/openai/", "");
 | 
				
			||||||
    "/api/openai/",
 | 
					 | 
				
			||||||
    "",
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let baseUrl =
 | 
					  let baseUrl =
 | 
				
			||||||
    (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;
 | 
					    (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;
 | 
				
			||||||
@@ -79,7 +71,7 @@ export async function requestOpenai(req: NextRequest) {
 | 
				
			|||||||
        .filter((v) => !!v && !v.startsWith("-") && v.includes(modelName))
 | 
					        .filter((v) => !!v && !v.startsWith("-") && v.includes(modelName))
 | 
				
			||||||
        .forEach((m) => {
 | 
					        .forEach((m) => {
 | 
				
			||||||
          const [fullName, displayName] = m.split("=");
 | 
					          const [fullName, displayName] = m.split("=");
 | 
				
			||||||
          const [_, providerName] = fullName.split("@");
 | 
					          const [_, providerName] = getModelProvider(fullName);
 | 
				
			||||||
          if (providerName === "azure" && !displayName) {
 | 
					          if (providerName === "azure" && !displayName) {
 | 
				
			||||||
            const [_, deployId] = (serverConfig?.azureUrl ?? "").split(
 | 
					            const [_, deployId] = (serverConfig?.azureUrl ?? "").split(
 | 
				
			||||||
              "deployments/",
 | 
					              "deployments/",
 | 
				
			||||||
@@ -126,15 +118,14 @@ export async function requestOpenai(req: NextRequest) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // not undefined and is false
 | 
					      // not undefined and is false
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
        isModelAvailableInServer(
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
          serverConfig.customModels,
 | 
					          serverConfig.customModels,
 | 
				
			||||||
          jsonBody?.model as string,
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
          ServiceProvider.OpenAI as string,
 | 
					          [
 | 
				
			||||||
        ) ||
 | 
					            ServiceProvider.OpenAI,
 | 
				
			||||||
        isModelAvailableInServer(
 | 
					            ServiceProvider.Azure,
 | 
				
			||||||
          serverConfig.customModels,
 | 
					            jsonBody?.model as string, // support provider-unspecified model
 | 
				
			||||||
          jsonBody?.model as string,
 | 
					          ],
 | 
				
			||||||
          ServiceProvider.Azure as string,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      ) {
 | 
					      ) {
 | 
				
			||||||
        return NextResponse.json(
 | 
					        return NextResponse.json(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,7 @@ const DANGER_CONFIG = {
 | 
				
			|||||||
  disableFastLink: serverConfig.disableFastLink,
 | 
					  disableFastLink: serverConfig.disableFastLink,
 | 
				
			||||||
  customModels: serverConfig.customModels,
 | 
					  customModels: serverConfig.customModels,
 | 
				
			||||||
  defaultModel: serverConfig.defaultModel,
 | 
					  defaultModel: serverConfig.defaultModel,
 | 
				
			||||||
 | 
					  visionModels: serverConfig.visionModels,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										128
									
								
								app/api/deepseek.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DEEPSEEK_BASE_URL,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[DeepSeek Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.DeepSeek);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[DeepSeek] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // alibaba use base url or just remove the path
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.DeepSeek, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.deepseekUrl || DEEPSEEK_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}`;
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Authorization: req.headers.get("Authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.DeepSeek as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[DeepSeek] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										129
									
								
								app/api/glm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  CHATGLM_BASE_URL,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[GLM Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.ChatGLM);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[GLM] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // alibaba use base url or just remove the path
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ChatGLM, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.chatglmUrl || CHATGLM_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}`;
 | 
				
			||||||
 | 
					  console.log("[Fetch Url] ", fetchUrl);
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Authorization: req.headers.get("Authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.ChatGLM as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[GLM] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,12 +1,7 @@
 | 
				
			|||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
import { auth } from "./auth";
 | 
					import { auth } from "./auth";
 | 
				
			||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
import {
 | 
					import { ApiPath, GEMINI_BASE_URL, ModelProvider } from "@/app/constant";
 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  GEMINI_BASE_URL,
 | 
					 | 
				
			||||||
  Google,
 | 
					 | 
				
			||||||
  ModelProvider,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
@@ -28,7 +23,8 @@ export async function handle(
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const bearToken = req.headers.get("Authorization") ?? "";
 | 
					  const bearToken =
 | 
				
			||||||
 | 
					    req.headers.get("x-goog-api-key") || req.headers.get("Authorization") || "";
 | 
				
			||||||
  const token = bearToken.trim().replaceAll("Bearer ", "").trim();
 | 
					  const token = bearToken.trim().replaceAll("Bearer ", "").trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const apiKey = token ? token : serverConfig.googleApiKey;
 | 
					  const apiKey = token ? token : serverConfig.googleApiKey;
 | 
				
			||||||
@@ -96,8 +92,8 @@ async function request(req: NextRequest, apiKey: string) {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    10 * 60 * 1000,
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  const fetchUrl = `${baseUrl}${path}?key=${apiKey}${
 | 
					  const fetchUrl = `${baseUrl}${path}${
 | 
				
			||||||
    req?.nextUrl?.searchParams?.get("alt") === "sse" ? "&alt=sse" : ""
 | 
					    req?.nextUrl?.searchParams?.get("alt") === "sse" ? "?alt=sse" : ""
 | 
				
			||||||
  }`;
 | 
					  }`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log("[Fetch Url] ", fetchUrl);
 | 
					  console.log("[Fetch Url] ", fetchUrl);
 | 
				
			||||||
@@ -105,6 +101,9 @@ async function request(req: NextRequest, apiKey: string) {
 | 
				
			|||||||
    headers: {
 | 
					    headers: {
 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
      "Cache-Control": "no-store",
 | 
					      "Cache-Control": "no-store",
 | 
				
			||||||
 | 
					      "x-goog-api-key":
 | 
				
			||||||
 | 
					        req.headers.get("x-goog-api-key") ||
 | 
				
			||||||
 | 
					        (req.headers.get("Authorization") ?? "").replace("Bearer ", ""),
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    method: req.method,
 | 
					    method: req.method,
 | 
				
			||||||
    body: req.body,
 | 
					    body: req.body,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Iflytek,
 | 
					 | 
				
			||||||
  IFLYTEK_BASE_URL,
 | 
					  IFLYTEK_BASE_URL,
 | 
				
			||||||
  ApiPath,
 | 
					  ApiPath,
 | 
				
			||||||
  ModelProvider,
 | 
					  ModelProvider,
 | 
				
			||||||
@@ -9,8 +8,7 @@ import {
 | 
				
			|||||||
import { prettyObject } from "@/app/utils/format";
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
import { isModelAvailableInServer } from "@/app/utils/model";
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
import type { RequestPayload } from "@/app/client/platforms/openai";
 | 
					 | 
				
			||||||
// iflytek
 | 
					// iflytek
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
@@ -91,7 +89,7 @@ async function request(req: NextRequest) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // not undefined and is false
 | 
					      // not undefined and is false
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
        isModelAvailableInServer(
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
          serverConfig.customModels,
 | 
					          serverConfig.customModels,
 | 
				
			||||||
          jsonBody?.model as string,
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
          ServiceProvider.Iflytek as string,
 | 
					          ServiceProvider.Iflytek as string,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Moonshot,
 | 
					 | 
				
			||||||
  MOONSHOT_BASE_URL,
 | 
					  MOONSHOT_BASE_URL,
 | 
				
			||||||
  ApiPath,
 | 
					  ApiPath,
 | 
				
			||||||
  ModelProvider,
 | 
					  ModelProvider,
 | 
				
			||||||
@@ -9,8 +8,7 @@ import {
 | 
				
			|||||||
import { prettyObject } from "@/app/utils/format";
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
import { isModelAvailableInServer } from "@/app/utils/model";
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
import type { RequestPayload } from "@/app/client/platforms/openai";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -90,7 +88,7 @@ async function request(req: NextRequest) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // not undefined and is false
 | 
					      // not undefined and is false
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
        isModelAvailableInServer(
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
          serverConfig.customModels,
 | 
					          serverConfig.customModels,
 | 
				
			||||||
          jsonBody?.model as string,
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
          ServiceProvider.Moonshot as string,
 | 
					          ServiceProvider.Moonshot as string,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,14 +6,20 @@ import { NextRequest, NextResponse } from "next/server";
 | 
				
			|||||||
import { auth } from "./auth";
 | 
					import { auth } from "./auth";
 | 
				
			||||||
import { requestOpenai } from "./common";
 | 
					import { requestOpenai } from "./common";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
 | 
					const ALLOWED_PATH = new Set(Object.values(OpenaiPath));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getModels(remoteModelRes: OpenAIListModelResponse) {
 | 
					function getModels(remoteModelRes: OpenAIListModelResponse) {
 | 
				
			||||||
  const config = getServerSideConfig();
 | 
					  const config = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (config.disableGPT4) {
 | 
					  if (config.disableGPT4) {
 | 
				
			||||||
    remoteModelRes.data = remoteModelRes.data.filter(
 | 
					    remoteModelRes.data = remoteModelRes.data.filter(
 | 
				
			||||||
      (m) => !m.id.startsWith("gpt-4") || m.id.startsWith("gpt-4o-mini"),
 | 
					      (m) =>
 | 
				
			||||||
 | 
					        !(
 | 
				
			||||||
 | 
					          m.id.startsWith("gpt-4") ||
 | 
				
			||||||
 | 
					          m.id.startsWith("chatgpt-4o") ||
 | 
				
			||||||
 | 
					          m.id.startsWith("o1") ||
 | 
				
			||||||
 | 
					          m.id.startsWith("o3")
 | 
				
			||||||
 | 
					        ) || m.id.startsWith("gpt-4o-mini"),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,7 +38,7 @@ export async function handle(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const subpath = params.path.join("/");
 | 
					  const subpath = params.path.join("/");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!ALLOWD_PATH.has(subpath)) {
 | 
					  if (!ALLOWED_PATH.has(subpath)) {
 | 
				
			||||||
    console.log("[OpenAI Route] forbidden path ", subpath);
 | 
					    console.log("[OpenAI Route] forbidden path ", subpath);
 | 
				
			||||||
    return NextResponse.json(
 | 
					    return NextResponse.json(
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										89
									
								
								app/api/proxy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,89 @@
 | 
				
			|||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[Proxy Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // remove path params from searchParams
 | 
				
			||||||
 | 
					  req.nextUrl.searchParams.delete("path");
 | 
				
			||||||
 | 
					  req.nextUrl.searchParams.delete("provider");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const subpath = params.path.join("/");
 | 
				
			||||||
 | 
					  const fetchUrl = `${req.headers.get(
 | 
				
			||||||
 | 
					    "x-base-url",
 | 
				
			||||||
 | 
					  )}/${subpath}?${req.nextUrl.searchParams.toString()}`;
 | 
				
			||||||
 | 
					  const skipHeaders = ["connection", "host", "origin", "referer", "cookie"];
 | 
				
			||||||
 | 
					  const headers = new Headers(
 | 
				
			||||||
 | 
					    Array.from(req.headers.entries()).filter((item) => {
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        item[0].indexOf("x-") > -1 ||
 | 
				
			||||||
 | 
					        item[0].indexOf("sec-") > -1 ||
 | 
				
			||||||
 | 
					        skipHeaders.includes(item[0])
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  // if dalle3 use openai api key
 | 
				
			||||||
 | 
					    const baseUrl = req.headers.get("x-base-url");
 | 
				
			||||||
 | 
					    if (baseUrl?.includes("api.openai.com")) {
 | 
				
			||||||
 | 
					      if (!serverConfig.apiKey) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          { error: "OpenAI API key not configured" },
 | 
				
			||||||
 | 
					          { status: 500 },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      headers.set("Authorization", `Bearer ${serverConfig.apiKey}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers,
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // The latest version of the OpenAI API forced the content-encoding to be "br" in json response
 | 
				
			||||||
 | 
					    // So if the streaming is disabled, we need to remove the content-encoding header
 | 
				
			||||||
 | 
					    // Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
 | 
				
			||||||
 | 
					    // The browser will try to decode the response with brotli and fail
 | 
				
			||||||
 | 
					    newHeaders.delete("content-encoding");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										128
									
								
								app/api/siliconflow.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  SILICONFLOW_BASE_URL,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[SiliconFlow Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.SiliconFlow);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[SiliconFlow] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // alibaba use base url or just remove the path
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.SiliconFlow, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.siliconFlowUrl || SILICONFLOW_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}`;
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Authorization: req.headers.get("Authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.SiliconFlow as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[SiliconFlow] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,15 +1,8 @@
 | 
				
			|||||||
import { getServerSideConfig } from "@/app/config/server";
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
import {
 | 
					import { TENCENT_BASE_URL, ModelProvider } from "@/app/constant";
 | 
				
			||||||
  TENCENT_BASE_URL,
 | 
					 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  ModelProvider,
 | 
					 | 
				
			||||||
  ServiceProvider,
 | 
					 | 
				
			||||||
  Tencent,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
import { NextRequest, NextResponse } from "next/server";
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
import { auth } from "@/app/api/auth";
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
import { isModelAvailableInServer } from "@/app/utils/model";
 | 
					 | 
				
			||||||
import { getHeader } from "@/app/utils/tencent";
 | 
					import { getHeader } from "@/app/utils/tencent";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ const config = getServerSideConfig();
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const mergedAllowedWebDavEndpoints = [
 | 
					const mergedAllowedWebDavEndpoints = [
 | 
				
			||||||
  ...internalAllowedWebDavEndpoints,
 | 
					  ...internalAllowedWebDavEndpoints,
 | 
				
			||||||
  ...config.allowedWebDevEndpoints,
 | 
					  ...config.allowedWebDavEndpoints,
 | 
				
			||||||
].filter((domain) => Boolean(domain.trim()));
 | 
					].filter((domain) => Boolean(domain.trim()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const normalizeUrl = (url: string) => {
 | 
					const normalizeUrl = (url: string) => {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										128
									
								
								app/api/xai.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  XAI_BASE_URL,
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  ModelProvider,
 | 
				
			||||||
 | 
					  ServiceProvider,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { isModelNotavailableInServer } from "@/app/utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[XAI Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.XAI);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[XAI] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // alibaba use base url or just remove the path
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.XAI, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.xaiUrl || XAI_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}${path}`;
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Authorization: req.headers.get("Authorization") ?? "",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body: req.body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // #1815 try to refuse some request to some models
 | 
				
			||||||
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const clonedBody = await req.text();
 | 
				
			||||||
 | 
					      fetchOptions.body = clonedBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // not undefined and is false
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        isModelNotavailableInServer(
 | 
				
			||||||
 | 
					          serverConfig.customModels,
 | 
				
			||||||
 | 
					          jsonBody?.model as string,
 | 
				
			||||||
 | 
					          ServiceProvider.XAI as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[XAI] filter`, e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const res = await fetch(fetchUrl, fetchOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // to prevent browser prompt for credentials
 | 
				
			||||||
 | 
					    const newHeaders = new Headers(res.headers);
 | 
				
			||||||
 | 
					    newHeaders.delete("www-authenticate");
 | 
				
			||||||
 | 
					    // to disable nginx buffering
 | 
				
			||||||
 | 
					    newHeaders.set("X-Accel-Buffering", "no");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: newHeaders,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,11 +1,16 @@
 | 
				
			|||||||
import { getClientConfig } from "../config/client";
 | 
					import { getClientConfig } from "../config/client";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ACCESS_CODE_PREFIX,
 | 
					  ACCESS_CODE_PREFIX,
 | 
				
			||||||
  Azure,
 | 
					 | 
				
			||||||
  ModelProvider,
 | 
					  ModelProvider,
 | 
				
			||||||
  ServiceProvider,
 | 
					  ServiceProvider,
 | 
				
			||||||
} from "../constant";
 | 
					} from "../constant";
 | 
				
			||||||
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
 | 
					import {
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  ChatMessage,
 | 
				
			||||||
 | 
					  ModelType,
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					} from "../store";
 | 
				
			||||||
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
 | 
					import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
 | 
				
			||||||
import { GeminiProApi } from "./platforms/google";
 | 
					import { GeminiProApi } from "./platforms/google";
 | 
				
			||||||
import { ClaudeApi } from "./platforms/anthropic";
 | 
					import { ClaudeApi } from "./platforms/anthropic";
 | 
				
			||||||
@@ -15,11 +20,16 @@ import { QwenApi } from "./platforms/alibaba";
 | 
				
			|||||||
import { HunyuanApi } from "./platforms/tencent";
 | 
					import { HunyuanApi } from "./platforms/tencent";
 | 
				
			||||||
import { MoonshotApi } from "./platforms/moonshot";
 | 
					import { MoonshotApi } from "./platforms/moonshot";
 | 
				
			||||||
import { SparkApi } from "./platforms/iflytek";
 | 
					import { SparkApi } from "./platforms/iflytek";
 | 
				
			||||||
 | 
					import { DeepSeekApi } from "./platforms/deepseek";
 | 
				
			||||||
 | 
					import { XAIApi } from "./platforms/xai";
 | 
				
			||||||
 | 
					import { ChatGLMApi } from "./platforms/glm";
 | 
				
			||||||
 | 
					import { SiliconflowApi } from "./platforms/siliconflow";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ROLES = ["system", "user", "assistant"] as const;
 | 
					export const ROLES = ["system", "user", "assistant"] as const;
 | 
				
			||||||
export type MessageRole = (typeof ROLES)[number];
 | 
					export type MessageRole = (typeof ROLES)[number];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Models = ["gpt-3.5-turbo", "gpt-4"] as const;
 | 
					export const Models = ["gpt-3.5-turbo", "gpt-4"] as const;
 | 
				
			||||||
 | 
					export const TTSModels = ["tts-1", "tts-1-hd"] as const;
 | 
				
			||||||
export type ChatModel = ModelType;
 | 
					export type ChatModel = ModelType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface MultimodalContent {
 | 
					export interface MultimodalContent {
 | 
				
			||||||
@@ -30,6 +40,11 @@ export interface MultimodalContent {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface MultimodalContentForAlibaba {
 | 
				
			||||||
 | 
					  text?: string;
 | 
				
			||||||
 | 
					  image?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface RequestMessage {
 | 
					export interface RequestMessage {
 | 
				
			||||||
  role: MessageRole;
 | 
					  role: MessageRole;
 | 
				
			||||||
  content: string | MultimodalContent[];
 | 
					  content: string | MultimodalContent[];
 | 
				
			||||||
@@ -48,14 +63,25 @@ export interface LLMConfig {
 | 
				
			|||||||
  style?: DalleRequestPayload["style"];
 | 
					  style?: DalleRequestPayload["style"];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface SpeechOptions {
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					  input: string;
 | 
				
			||||||
 | 
					  voice: string;
 | 
				
			||||||
 | 
					  response_format?: string;
 | 
				
			||||||
 | 
					  speed?: number;
 | 
				
			||||||
 | 
					  onController?: (controller: AbortController) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ChatOptions {
 | 
					export interface ChatOptions {
 | 
				
			||||||
  messages: RequestMessage[];
 | 
					  messages: RequestMessage[];
 | 
				
			||||||
  config: LLMConfig;
 | 
					  config: LLMConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onUpdate?: (message: string, chunk: string) => void;
 | 
					  onUpdate?: (message: string, chunk: string) => void;
 | 
				
			||||||
  onFinish: (message: string) => void;
 | 
					  onFinish: (message: string, responseRes: Response) => void;
 | 
				
			||||||
  onError?: (err: Error) => void;
 | 
					  onError?: (err: Error) => void;
 | 
				
			||||||
  onController?: (controller: AbortController) => void;
 | 
					  onController?: (controller: AbortController) => void;
 | 
				
			||||||
 | 
					  onBeforeTool?: (tool: ChatMessageTool) => void;
 | 
				
			||||||
 | 
					  onAfterTool?: (tool: ChatMessageTool) => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface LLMUsage {
 | 
					export interface LLMUsage {
 | 
				
			||||||
@@ -80,6 +106,7 @@ export interface LLMModelProvider {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export abstract class LLMApi {
 | 
					export abstract class LLMApi {
 | 
				
			||||||
  abstract chat(options: ChatOptions): Promise<void>;
 | 
					  abstract chat(options: ChatOptions): Promise<void>;
 | 
				
			||||||
 | 
					  abstract speech(options: SpeechOptions): Promise<ArrayBuffer>;
 | 
				
			||||||
  abstract usage(): Promise<LLMUsage>;
 | 
					  abstract usage(): Promise<LLMUsage>;
 | 
				
			||||||
  abstract models(): Promise<LLMModel[]>;
 | 
					  abstract models(): Promise<LLMModel[]>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -134,6 +161,18 @@ export class ClientApi {
 | 
				
			|||||||
      case ModelProvider.Iflytek:
 | 
					      case ModelProvider.Iflytek:
 | 
				
			||||||
        this.llm = new SparkApi();
 | 
					        this.llm = new SparkApi();
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.DeepSeek:
 | 
				
			||||||
 | 
					        this.llm = new DeepSeekApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.XAI:
 | 
				
			||||||
 | 
					        this.llm = new XAIApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.ChatGLM:
 | 
				
			||||||
 | 
					        this.llm = new ChatGLMApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.SiliconFlow:
 | 
				
			||||||
 | 
					        this.llm = new SiliconflowApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        this.llm = new ChatGPTApi();
 | 
					        this.llm = new ChatGPTApi();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -198,19 +237,22 @@ export function validString(x: string): boolean {
 | 
				
			|||||||
  return x?.length > 0;
 | 
					  return x?.length > 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getHeaders() {
 | 
					export function getHeaders(ignoreHeaders: boolean = false) {
 | 
				
			||||||
  const accessStore = useAccessStore.getState();
 | 
					  const accessStore = useAccessStore.getState();
 | 
				
			||||||
  const chatStore = useChatStore.getState();
 | 
					  const chatStore = useChatStore.getState();
 | 
				
			||||||
  const headers: Record<string, string> = {
 | 
					  let headers: Record<string, string> = {};
 | 
				
			||||||
    "Content-Type": "application/json",
 | 
					  if (!ignoreHeaders) {
 | 
				
			||||||
    Accept: "application/json",
 | 
					    headers = {
 | 
				
			||||||
  };
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      Accept: "application/json",
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const clientConfig = getClientConfig();
 | 
					  const clientConfig = getClientConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function getConfig() {
 | 
					  function getConfig() {
 | 
				
			||||||
    const modelConfig = chatStore.currentSession().mask.modelConfig;
 | 
					    const modelConfig = chatStore.currentSession().mask.modelConfig;
 | 
				
			||||||
    const isGoogle = modelConfig.providerName == ServiceProvider.Google;
 | 
					    const isGoogle = modelConfig.providerName === ServiceProvider.Google;
 | 
				
			||||||
    const isAzure = modelConfig.providerName === ServiceProvider.Azure;
 | 
					    const isAzure = modelConfig.providerName === ServiceProvider.Azure;
 | 
				
			||||||
    const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
 | 
					    const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
 | 
				
			||||||
    const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
 | 
					    const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
 | 
				
			||||||
@@ -218,6 +260,11 @@ export function getHeaders() {
 | 
				
			|||||||
    const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
 | 
					    const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
 | 
				
			||||||
    const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
 | 
					    const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
 | 
				
			||||||
    const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
 | 
					    const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
 | 
				
			||||||
 | 
					    const isDeepSeek = modelConfig.providerName === ServiceProvider.DeepSeek;
 | 
				
			||||||
 | 
					    const isXAI = modelConfig.providerName === ServiceProvider.XAI;
 | 
				
			||||||
 | 
					    const isChatGLM = modelConfig.providerName === ServiceProvider.ChatGLM;
 | 
				
			||||||
 | 
					    const isSiliconFlow =
 | 
				
			||||||
 | 
					      modelConfig.providerName === ServiceProvider.SiliconFlow;
 | 
				
			||||||
    const isEnabledAccessControl = accessStore.enabledAccessControl();
 | 
					    const isEnabledAccessControl = accessStore.enabledAccessControl();
 | 
				
			||||||
    const apiKey = isGoogle
 | 
					    const apiKey = isGoogle
 | 
				
			||||||
      ? accessStore.googleApiKey
 | 
					      ? accessStore.googleApiKey
 | 
				
			||||||
@@ -231,6 +278,14 @@ export function getHeaders() {
 | 
				
			|||||||
      ? accessStore.alibabaApiKey
 | 
					      ? accessStore.alibabaApiKey
 | 
				
			||||||
      : isMoonshot
 | 
					      : isMoonshot
 | 
				
			||||||
      ? accessStore.moonshotApiKey
 | 
					      ? accessStore.moonshotApiKey
 | 
				
			||||||
 | 
					      : isXAI
 | 
				
			||||||
 | 
					      ? accessStore.xaiApiKey
 | 
				
			||||||
 | 
					      : isDeepSeek
 | 
				
			||||||
 | 
					      ? accessStore.deepseekApiKey
 | 
				
			||||||
 | 
					      : isChatGLM
 | 
				
			||||||
 | 
					      ? accessStore.chatglmApiKey
 | 
				
			||||||
 | 
					      : isSiliconFlow
 | 
				
			||||||
 | 
					      ? accessStore.siliconflowApiKey
 | 
				
			||||||
      : isIflytek
 | 
					      : isIflytek
 | 
				
			||||||
      ? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
 | 
					      ? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
 | 
				
			||||||
        ? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
 | 
					        ? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
 | 
				
			||||||
@@ -245,13 +300,23 @@ export function getHeaders() {
 | 
				
			|||||||
      isAlibaba,
 | 
					      isAlibaba,
 | 
				
			||||||
      isMoonshot,
 | 
					      isMoonshot,
 | 
				
			||||||
      isIflytek,
 | 
					      isIflytek,
 | 
				
			||||||
 | 
					      isDeepSeek,
 | 
				
			||||||
 | 
					      isXAI,
 | 
				
			||||||
 | 
					      isChatGLM,
 | 
				
			||||||
 | 
					      isSiliconFlow,
 | 
				
			||||||
      apiKey,
 | 
					      apiKey,
 | 
				
			||||||
      isEnabledAccessControl,
 | 
					      isEnabledAccessControl,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function getAuthHeader(): string {
 | 
					  function getAuthHeader(): string {
 | 
				
			||||||
    return isAzure ? "api-key" : isAnthropic ? "x-api-key" : "Authorization";
 | 
					    return isAzure
 | 
				
			||||||
 | 
					      ? "api-key"
 | 
				
			||||||
 | 
					      : isAnthropic
 | 
				
			||||||
 | 
					      ? "x-api-key"
 | 
				
			||||||
 | 
					      : isGoogle
 | 
				
			||||||
 | 
					      ? "x-goog-api-key"
 | 
				
			||||||
 | 
					      : "Authorization";
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
@@ -259,17 +324,26 @@ export function getHeaders() {
 | 
				
			|||||||
    isAzure,
 | 
					    isAzure,
 | 
				
			||||||
    isAnthropic,
 | 
					    isAnthropic,
 | 
				
			||||||
    isBaidu,
 | 
					    isBaidu,
 | 
				
			||||||
 | 
					    isByteDance,
 | 
				
			||||||
 | 
					    isAlibaba,
 | 
				
			||||||
 | 
					    isMoonshot,
 | 
				
			||||||
 | 
					    isIflytek,
 | 
				
			||||||
 | 
					    isDeepSeek,
 | 
				
			||||||
 | 
					    isXAI,
 | 
				
			||||||
 | 
					    isChatGLM,
 | 
				
			||||||
 | 
					    isSiliconFlow,
 | 
				
			||||||
    apiKey,
 | 
					    apiKey,
 | 
				
			||||||
    isEnabledAccessControl,
 | 
					    isEnabledAccessControl,
 | 
				
			||||||
  } = getConfig();
 | 
					  } = getConfig();
 | 
				
			||||||
  // when using google api in app, not set auth header
 | 
					 | 
				
			||||||
  if (isGoogle && clientConfig?.isApp) return headers;
 | 
					 | 
				
			||||||
  // when using baidu api in app, not set auth header
 | 
					  // when using baidu api in app, not set auth header
 | 
				
			||||||
  if (isBaidu && clientConfig?.isApp) return headers;
 | 
					  if (isBaidu && clientConfig?.isApp) return headers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const authHeader = getAuthHeader();
 | 
					  const authHeader = getAuthHeader();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const bearerToken = getBearerToken(apiKey, isAzure || isAnthropic);
 | 
					  const bearerToken = getBearerToken(
 | 
				
			||||||
 | 
					    apiKey,
 | 
				
			||||||
 | 
					    isAzure || isAnthropic || isGoogle,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (bearerToken) {
 | 
					  if (bearerToken) {
 | 
				
			||||||
    headers[authHeader] = bearerToken;
 | 
					    headers[authHeader] = bearerToken;
 | 
				
			||||||
@@ -300,6 +374,14 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
 | 
				
			|||||||
      return new ClientApi(ModelProvider.Moonshot);
 | 
					      return new ClientApi(ModelProvider.Moonshot);
 | 
				
			||||||
    case ServiceProvider.Iflytek:
 | 
					    case ServiceProvider.Iflytek:
 | 
				
			||||||
      return new ClientApi(ModelProvider.Iflytek);
 | 
					      return new ClientApi(ModelProvider.Iflytek);
 | 
				
			||||||
 | 
					    case ServiceProvider.DeepSeek:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.DeepSeek);
 | 
				
			||||||
 | 
					    case ServiceProvider.XAI:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.XAI);
 | 
				
			||||||
 | 
					    case ServiceProvider.ChatGLM:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.ChatGLM);
 | 
				
			||||||
 | 
					    case ServiceProvider.SiliconFlow:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.SiliconFlow);
 | 
				
			||||||
    default:
 | 
					    default:
 | 
				
			||||||
      return new ClientApi(ModelProvider.GPT);
 | 
					      return new ClientApi(ModelProvider.GPT);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,27 +1,33 @@
 | 
				
			|||||||
"use client";
 | 
					"use client";
 | 
				
			||||||
 | 
					import { ApiPath, Alibaba, ALIBABA_BASE_URL } from "@/app/constant";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ApiPath,
 | 
					  useAccessStore,
 | 
				
			||||||
  Alibaba,
 | 
					  useAppConfig,
 | 
				
			||||||
  ALIBABA_BASE_URL,
 | 
					  useChatStore,
 | 
				
			||||||
  REQUEST_TIMEOUT_MS,
 | 
					  ChatMessageTool,
 | 
				
			||||||
} from "@/app/constant";
 | 
					  usePluginStore,
 | 
				
			||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  preProcessImageContentForAlibabaDashScope,
 | 
				
			||||||
 | 
					  streamWithThink,
 | 
				
			||||||
 | 
					} from "@/app/utils/chat";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ChatOptions,
 | 
					  ChatOptions,
 | 
				
			||||||
  getHeaders,
 | 
					  getHeaders,
 | 
				
			||||||
  LLMApi,
 | 
					  LLMApi,
 | 
				
			||||||
  LLMModel,
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
  MultimodalContent,
 | 
					  MultimodalContent,
 | 
				
			||||||
 | 
					  MultimodalContentForAlibaba,
 | 
				
			||||||
} from "../api";
 | 
					} from "../api";
 | 
				
			||||||
import Locale from "../../locales";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  EventStreamContentType,
 | 
					 | 
				
			||||||
  fetchEventSource,
 | 
					 | 
				
			||||||
} from "@fortaine/fetch-event-source";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
import { getMessageTextContent } from "@/app/utils";
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  getMessageTextContentWithoutThinking,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					  isVisionModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface OpenAIListModelResponse {
 | 
					export interface OpenAIListModelResponse {
 | 
				
			||||||
  object: string;
 | 
					  object: string;
 | 
				
			||||||
@@ -83,12 +89,11 @@ export class QwenApi implements LLMApi {
 | 
				
			|||||||
    return res?.output?.choices?.at(0)?.message?.content ?? "";
 | 
					    return res?.output?.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
    const messages = options.messages.map((v) => ({
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
      role: v.role,
 | 
					  }
 | 
				
			||||||
      content: getMessageTextContent(v),
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
    const modelConfig = {
 | 
					    const modelConfig = {
 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
@@ -97,6 +102,21 @@ export class QwenApi implements LLMApi {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const visionModel = isVisionModel(options.config.model);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      const content = (
 | 
				
			||||||
 | 
					        visionModel
 | 
				
			||||||
 | 
					          ? await preProcessImageContentForAlibabaDashScope(v.content)
 | 
				
			||||||
 | 
					          : v.role === "assistant"
 | 
				
			||||||
 | 
					          ? getMessageTextContentWithoutThinking(v)
 | 
				
			||||||
 | 
					          : getMessageTextContent(v)
 | 
				
			||||||
 | 
					      ) as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const shouldStream = !!options.config.stream;
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
    const requestPayload: RequestPayload = {
 | 
					    const requestPayload: RequestPayload = {
 | 
				
			||||||
      model: modelConfig.model,
 | 
					      model: modelConfig.model,
 | 
				
			||||||
@@ -116,138 +136,127 @@ export class QwenApi implements LLMApi {
 | 
				
			|||||||
    options.onController?.(controller);
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const chatPath = this.path(Alibaba.ChatPath);
 | 
					      const headers = {
 | 
				
			||||||
 | 
					        ...getHeaders(),
 | 
				
			||||||
 | 
					        "X-DashScope-SSE": shouldStream ? "enable" : "disable",
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const chatPath = this.path(Alibaba.ChatPath(modelConfig.model));
 | 
				
			||||||
      const chatPayload = {
 | 
					      const chatPayload = {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        body: JSON.stringify(requestPayload),
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
        signal: controller.signal,
 | 
					        signal: controller.signal,
 | 
				
			||||||
        headers: {
 | 
					        headers: headers,
 | 
				
			||||||
          ...getHeaders(),
 | 
					 | 
				
			||||||
          "X-DashScope-SSE": shouldStream ? "enable" : "disable",
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // make a fetch request
 | 
					      // make a fetch request
 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
        () => controller.abort(),
 | 
					        () => controller.abort(),
 | 
				
			||||||
        REQUEST_TIMEOUT_MS,
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (shouldStream) {
 | 
					      if (shouldStream) {
 | 
				
			||||||
        let responseText = "";
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
        let remainText = "";
 | 
					          .getState()
 | 
				
			||||||
        let finished = false;
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return streamWithThink(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          headers,
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.output.choices as Array<{
 | 
				
			||||||
 | 
					              message: {
 | 
				
			||||||
 | 
					                content: string | null | MultimodalContentForAlibaba[];
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					                reasoning_content: string | null;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // animate response to make it looks smooth
 | 
					            if (!choices?.length) return { isThinking: false, content: "" };
 | 
				
			||||||
        function animateResponseText() {
 | 
					 | 
				
			||||||
          if (finished || controller.signal.aborted) {
 | 
					 | 
				
			||||||
            responseText += remainText;
 | 
					 | 
				
			||||||
            console.log("[Response Animation] finished");
 | 
					 | 
				
			||||||
            if (responseText?.length === 0) {
 | 
					 | 
				
			||||||
              options.onError?.(new Error("empty response from server"));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (remainText.length > 0) {
 | 
					            const tool_calls = choices[0]?.message?.tool_calls;
 | 
				
			||||||
            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
            const fetchText = remainText.slice(0, fetchCount);
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
            responseText += fetchText;
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
            remainText = remainText.slice(fetchCount);
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
            options.onUpdate?.(responseText, fetchText);
 | 
					              if (id) {
 | 
				
			||||||
          }
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
          requestAnimationFrame(animateResponseText);
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
        }
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
        // start animaion
 | 
					                    arguments: args,
 | 
				
			||||||
        animateResponseText();
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
        const finish = () => {
 | 
					              } else {
 | 
				
			||||||
          if (!finished) {
 | 
					                // @ts-ignore
 | 
				
			||||||
            finished = true;
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
            options.onFinish(responseText + remainText);
 | 
					              }
 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        controller.signal.onabort = finish;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        fetchEventSource(chatPath, {
 | 
					 | 
				
			||||||
          ...chatPayload,
 | 
					 | 
				
			||||||
          async onopen(res) {
 | 
					 | 
				
			||||||
            clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
            const contentType = res.headers.get("content-type");
 | 
					 | 
				
			||||||
            console.log(
 | 
					 | 
				
			||||||
              "[Alibaba] request response content type: ",
 | 
					 | 
				
			||||||
              contentType,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					 | 
				
			||||||
              responseText = await res.clone().text();
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const reasoning = choices[0]?.message?.reasoning_content;
 | 
				
			||||||
 | 
					            const content = choices[0]?.message?.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Skip if both content and reasoning_content are empty or null
 | 
				
			||||||
            if (
 | 
					            if (
 | 
				
			||||||
              !res.ok ||
 | 
					              (!reasoning || reasoning.length === 0) &&
 | 
				
			||||||
              !res.headers
 | 
					              (!content || content.length === 0)
 | 
				
			||||||
                .get("content-type")
 | 
					 | 
				
			||||||
                ?.startsWith(EventStreamContentType) ||
 | 
					 | 
				
			||||||
              res.status !== 200
 | 
					 | 
				
			||||||
            ) {
 | 
					            ) {
 | 
				
			||||||
              const responseTexts = [responseText];
 | 
					              return {
 | 
				
			||||||
              let extraInfo = await res.clone().text();
 | 
					                isThinking: false,
 | 
				
			||||||
              try {
 | 
					                content: "",
 | 
				
			||||||
                const resJson = await res.clone().json();
 | 
					              };
 | 
				
			||||||
                extraInfo = prettyObject(resJson);
 | 
					 | 
				
			||||||
              } catch {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (res.status === 401) {
 | 
					 | 
				
			||||||
                responseTexts.push(Locale.Error.Unauthorized);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (extraInfo) {
 | 
					 | 
				
			||||||
                responseTexts.push(extraInfo);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              responseText = responseTexts.join("\n\n");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          },
 | 
					
 | 
				
			||||||
          onmessage(msg) {
 | 
					            if (reasoning && reasoning.length > 0) {
 | 
				
			||||||
            if (msg.data === "[DONE]" || finished) {
 | 
					              return {
 | 
				
			||||||
              return finish();
 | 
					                isThinking: true,
 | 
				
			||||||
            }
 | 
					                content: reasoning,
 | 
				
			||||||
            const text = msg.data;
 | 
					              };
 | 
				
			||||||
            try {
 | 
					            } else if (content && content.length > 0) {
 | 
				
			||||||
              const json = JSON.parse(text);
 | 
					              return {
 | 
				
			||||||
              const choices = json.output.choices as Array<{
 | 
					                isThinking: false,
 | 
				
			||||||
                message: { content: string };
 | 
					                content: Array.isArray(content)
 | 
				
			||||||
              }>;
 | 
					                  ? content.map((item) => item.text).join(",")
 | 
				
			||||||
              const delta = choices[0]?.message?.content;
 | 
					                  : content,
 | 
				
			||||||
              if (delta) {
 | 
					              };
 | 
				
			||||||
                remainText += delta;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            } catch (e) {
 | 
					 | 
				
			||||||
              console.error("[Request] parse error", text, msg);
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              isThinking: false,
 | 
				
			||||||
 | 
					              content: "",
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          onclose() {
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
            finish();
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            requestPayload?.input?.messages?.splice(
 | 
				
			||||||
 | 
					              requestPayload?.input?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          onerror(e) {
 | 
					          options,
 | 
				
			||||||
            options.onError?.(e);
 | 
					        );
 | 
				
			||||||
            throw e;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          openWhenHidden: true,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const resJson = await res.json();
 | 
					        const resJson = await res.json();
 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
        options.onFinish(message);
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,19 @@
 | 
				
			|||||||
import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant";
 | 
					import { Anthropic, ApiPath } from "@/app/constant";
 | 
				
			||||||
import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api";
 | 
					import { ChatOptions, getHeaders, LLMApi, SpeechOptions } from "../api";
 | 
				
			||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import { DEFAULT_API_HOST } from "@/app/constant";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  EventStreamContentType,
 | 
					  useAccessStore,
 | 
				
			||||||
  fetchEventSource,
 | 
					  useAppConfig,
 | 
				
			||||||
} from "@fortaine/fetch-event-source";
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
import Locale from "../../locales";
 | 
					  ChatMessageTool,
 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import { ANTHROPIC_BASE_URL } from "@/app/constant";
 | 
				
			||||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
 | 
					import { getMessageTextContent, isVisionModel } from "@/app/utils";
 | 
				
			||||||
import { preProcessImageContent } from "@/app/utils/chat";
 | 
					import { preProcessImageContent, stream } from "@/app/utils/chat";
 | 
				
			||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
					import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type MultiBlockContent = {
 | 
					export type MultiBlockContent = {
 | 
				
			||||||
  type: "image" | "text";
 | 
					  type: "image" | "text";
 | 
				
			||||||
@@ -73,6 +74,10 @@ const ClaudeMapper = {
 | 
				
			|||||||
const keys = ["claude-2, claude-instant-1"];
 | 
					const keys = ["claude-2, claude-instant-1"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ClaudeApi implements LLMApi {
 | 
					export class ClaudeApi implements LLMApi {
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  extractMessage(res: any) {
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
    console.log("[Response] claude response: ", res);
 | 
					    console.log("[Response] claude response: ", res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -191,120 +196,135 @@ export class ClaudeApi implements LLMApi {
 | 
				
			|||||||
    const controller = new AbortController();
 | 
					    const controller = new AbortController();
 | 
				
			||||||
    options.onController?.(controller);
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const payload = {
 | 
					 | 
				
			||||||
      method: "POST",
 | 
					 | 
				
			||||||
      body: JSON.stringify(requestBody),
 | 
					 | 
				
			||||||
      signal: controller.signal,
 | 
					 | 
				
			||||||
      headers: {
 | 
					 | 
				
			||||||
        ...getHeaders(), // get common headers
 | 
					 | 
				
			||||||
        "anthropic-version": accessStore.anthropicApiVersion,
 | 
					 | 
				
			||||||
        // do not send `anthropicApiKey` in browser!!!
 | 
					 | 
				
			||||||
        // Authorization: getAuthKey(accessStore.anthropicApiKey),
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (shouldStream) {
 | 
					    if (shouldStream) {
 | 
				
			||||||
      try {
 | 
					      let index = -1;
 | 
				
			||||||
        const context = {
 | 
					      const [tools, funcs] = usePluginStore
 | 
				
			||||||
          text: "",
 | 
					        .getState()
 | 
				
			||||||
          finished: false,
 | 
					        .getAsTools(
 | 
				
			||||||
        };
 | 
					          useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
        const finish = () => {
 | 
					      return stream(
 | 
				
			||||||
          if (!context.finished) {
 | 
					        path,
 | 
				
			||||||
            options.onFinish(context.text);
 | 
					        requestBody,
 | 
				
			||||||
            context.finished = true;
 | 
					        {
 | 
				
			||||||
          }
 | 
					          ...getHeaders(),
 | 
				
			||||||
        };
 | 
					          "anthropic-version": accessStore.anthropicApiVersion,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        controller.signal.onabort = finish;
 | 
					        // @ts-ignore
 | 
				
			||||||
        fetchEventSource(path, {
 | 
					        tools.map((tool) => ({
 | 
				
			||||||
          ...payload,
 | 
					          name: tool?.function?.name,
 | 
				
			||||||
          async onopen(res) {
 | 
					          description: tool?.function?.description,
 | 
				
			||||||
            const contentType = res.headers.get("content-type");
 | 
					          input_schema: tool?.function?.parameters,
 | 
				
			||||||
            console.log("response content type: ", contentType);
 | 
					        })),
 | 
				
			||||||
 | 
					        funcs,
 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					        controller,
 | 
				
			||||||
              context.text = await res.clone().text();
 | 
					        // parseSSE
 | 
				
			||||||
              return finish();
 | 
					        (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
            }
 | 
					          // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					          let chunkJson:
 | 
				
			||||||
            if (
 | 
					            | undefined
 | 
				
			||||||
              !res.ok ||
 | 
					            | {
 | 
				
			||||||
              !res.headers
 | 
					                type: "content_block_delta" | "content_block_stop";
 | 
				
			||||||
                .get("content-type")
 | 
					                content_block?: {
 | 
				
			||||||
                ?.startsWith(EventStreamContentType) ||
 | 
					                  type: "tool_use";
 | 
				
			||||||
              res.status !== 200
 | 
					                  id: string;
 | 
				
			||||||
            ) {
 | 
					                  name: string;
 | 
				
			||||||
              const responseTexts = [context.text];
 | 
					 | 
				
			||||||
              let extraInfo = await res.clone().text();
 | 
					 | 
				
			||||||
              try {
 | 
					 | 
				
			||||||
                const resJson = await res.clone().json();
 | 
					 | 
				
			||||||
                extraInfo = prettyObject(resJson);
 | 
					 | 
				
			||||||
              } catch {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (res.status === 401) {
 | 
					 | 
				
			||||||
                responseTexts.push(Locale.Error.Unauthorized);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (extraInfo) {
 | 
					 | 
				
			||||||
                responseTexts.push(extraInfo);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              context.text = responseTexts.join("\n\n");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onmessage(msg) {
 | 
					 | 
				
			||||||
            let chunkJson:
 | 
					 | 
				
			||||||
              | undefined
 | 
					 | 
				
			||||||
              | {
 | 
					 | 
				
			||||||
                  type: "content_block_delta" | "content_block_stop";
 | 
					 | 
				
			||||||
                  delta?: {
 | 
					 | 
				
			||||||
                    type: "text_delta";
 | 
					 | 
				
			||||||
                    text: string;
 | 
					 | 
				
			||||||
                  };
 | 
					 | 
				
			||||||
                  index: number;
 | 
					 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
            try {
 | 
					                delta?: {
 | 
				
			||||||
              chunkJson = JSON.parse(msg.data);
 | 
					                  type: "text_delta" | "input_json_delta";
 | 
				
			||||||
            } catch (e) {
 | 
					                  text?: string;
 | 
				
			||||||
              console.error("[Response] parse error", msg.data);
 | 
					                  partial_json?: string;
 | 
				
			||||||
            }
 | 
					                };
 | 
				
			||||||
 | 
					                index: number;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					          chunkJson = JSON.parse(text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!chunkJson || chunkJson.type === "content_block_stop") {
 | 
					          if (chunkJson?.content_block?.type == "tool_use") {
 | 
				
			||||||
              return finish();
 | 
					            index += 1;
 | 
				
			||||||
            }
 | 
					            const id = chunkJson?.content_block.id;
 | 
				
			||||||
 | 
					            const name = chunkJson?.content_block.name;
 | 
				
			||||||
            const { delta } = chunkJson;
 | 
					            runTools.push({
 | 
				
			||||||
            if (delta?.text) {
 | 
					              id,
 | 
				
			||||||
              context.text += delta.text;
 | 
					              type: "function",
 | 
				
			||||||
              options.onUpdate?.(context.text, delta.text);
 | 
					              function: {
 | 
				
			||||||
            }
 | 
					                name,
 | 
				
			||||||
          },
 | 
					                arguments: "",
 | 
				
			||||||
          onclose() {
 | 
					              },
 | 
				
			||||||
            finish();
 | 
					            });
 | 
				
			||||||
          },
 | 
					          }
 | 
				
			||||||
          onerror(e) {
 | 
					          if (
 | 
				
			||||||
            options.onError?.(e);
 | 
					            chunkJson?.delta?.type == "input_json_delta" &&
 | 
				
			||||||
            throw e;
 | 
					            chunkJson?.delta?.partial_json
 | 
				
			||||||
          },
 | 
					          ) {
 | 
				
			||||||
          openWhenHidden: true,
 | 
					            // @ts-ignore
 | 
				
			||||||
        });
 | 
					            runTools[index]["function"]["arguments"] +=
 | 
				
			||||||
      } catch (e) {
 | 
					              chunkJson?.delta?.partial_json;
 | 
				
			||||||
        console.error("failed to chat", e);
 | 
					          }
 | 
				
			||||||
        options.onError?.(e as Error);
 | 
					          return chunkJson?.delta?.text;
 | 
				
			||||||
      }
 | 
					        },
 | 
				
			||||||
 | 
					        // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					          requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					          toolCallMessage: any,
 | 
				
			||||||
 | 
					          toolCallResult: any[],
 | 
				
			||||||
 | 
					        ) => {
 | 
				
			||||||
 | 
					          // reset index value
 | 
				
			||||||
 | 
					          index = -1;
 | 
				
			||||||
 | 
					          // @ts-ignore
 | 
				
			||||||
 | 
					          requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					            0,
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              role: "assistant",
 | 
				
			||||||
 | 
					              content: toolCallMessage.tool_calls.map(
 | 
				
			||||||
 | 
					                (tool: ChatMessageTool) => ({
 | 
				
			||||||
 | 
					                  type: "tool_use",
 | 
				
			||||||
 | 
					                  id: tool.id,
 | 
				
			||||||
 | 
					                  name: tool?.function?.name,
 | 
				
			||||||
 | 
					                  input: tool?.function?.arguments
 | 
				
			||||||
 | 
					                    ? JSON.parse(tool?.function?.arguments)
 | 
				
			||||||
 | 
					                    : {},
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            ...toolCallResult.map((result) => ({
 | 
				
			||||||
 | 
					              role: "user",
 | 
				
			||||||
 | 
					              content: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                  type: "tool_result",
 | 
				
			||||||
 | 
					                  tool_use_id: result.tool_call_id,
 | 
				
			||||||
 | 
					                  content: result.content,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            })),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        options,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
 | 
					      const payload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestBody),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: {
 | 
				
			||||||
 | 
					          ...getHeaders(), // get common headers
 | 
				
			||||||
 | 
					          "anthropic-version": accessStore.anthropicApiVersion,
 | 
				
			||||||
 | 
					          // do not send `anthropicApiKey` in browser!!!
 | 
				
			||||||
 | 
					          // Authorization: getAuthKey(accessStore.anthropicApiKey),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        controller.signal.onabort = () => options.onFinish("");
 | 
					        controller.signal.onabort = () =>
 | 
				
			||||||
 | 
					          options.onFinish("", new Response(null, { status: 400 }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const res = await fetch(path, payload);
 | 
					        const res = await fetch(path, payload);
 | 
				
			||||||
        const resJson = await res.json();
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
        options.onFinish(message);
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        console.error("failed to chat", e);
 | 
					        console.error("failed to chat", e);
 | 
				
			||||||
        options.onError?.(e as Error);
 | 
					        options.onError?.(e as Error);
 | 
				
			||||||
@@ -370,9 +390,7 @@ export class ClaudeApi implements LLMApi {
 | 
				
			|||||||
    if (baseUrl.trim().length === 0) {
 | 
					    if (baseUrl.trim().length === 0) {
 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      baseUrl = isApp
 | 
					      baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
 | 
				
			||||||
        ? DEFAULT_API_HOST + "/api/proxy/anthropic"
 | 
					 | 
				
			||||||
        : ApiPath.Anthropic;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,5 @@
 | 
				
			|||||||
"use client";
 | 
					"use client";
 | 
				
			||||||
import {
 | 
					import { ApiPath, Baidu, BAIDU_BASE_URL } from "@/app/constant";
 | 
				
			||||||
  ApiPath,
 | 
					 | 
				
			||||||
  Baidu,
 | 
					 | 
				
			||||||
  BAIDU_BASE_URL,
 | 
					 | 
				
			||||||
  REQUEST_TIMEOUT_MS,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					 | 
				
			||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
					import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
				
			||||||
import { getAccessToken } from "@/app/utils/baidu";
 | 
					import { getAccessToken } from "@/app/utils/baidu";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -14,6 +9,7 @@ import {
 | 
				
			|||||||
  LLMApi,
 | 
					  LLMApi,
 | 
				
			||||||
  LLMModel,
 | 
					  LLMModel,
 | 
				
			||||||
  MultimodalContent,
 | 
					  MultimodalContent,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
} from "../api";
 | 
					} from "../api";
 | 
				
			||||||
import Locale from "../../locales";
 | 
					import Locale from "../../locales";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@@ -22,7 +18,8 @@ import {
 | 
				
			|||||||
} from "@fortaine/fetch-event-source";
 | 
					} from "@fortaine/fetch-event-source";
 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
import { getMessageTextContent } from "@/app/utils";
 | 
					import { getMessageTextContent, getTimeoutMSByModel } from "@/app/utils";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface OpenAIListModelResponse {
 | 
					export interface OpenAIListModelResponse {
 | 
				
			||||||
  object: string;
 | 
					  object: string;
 | 
				
			||||||
@@ -75,6 +72,10 @@ export class ErnieApi implements LLMApi {
 | 
				
			|||||||
    return [baseUrl, path].join("/");
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
    const messages = options.messages.map((v) => ({
 | 
					    const messages = options.messages.map((v) => ({
 | 
				
			||||||
      // "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function",
 | 
					      // "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function",
 | 
				
			||||||
@@ -149,13 +150,14 @@ export class ErnieApi implements LLMApi {
 | 
				
			|||||||
      // make a fetch request
 | 
					      // make a fetch request
 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
        () => controller.abort(),
 | 
					        () => controller.abort(),
 | 
				
			||||||
        REQUEST_TIMEOUT_MS,
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (shouldStream) {
 | 
					      if (shouldStream) {
 | 
				
			||||||
        let responseText = "";
 | 
					        let responseText = "";
 | 
				
			||||||
        let remainText = "";
 | 
					        let remainText = "";
 | 
				
			||||||
        let finished = false;
 | 
					        let finished = false;
 | 
				
			||||||
 | 
					        let responseRes: Response;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // animate response to make it looks smooth
 | 
					        // animate response to make it looks smooth
 | 
				
			||||||
        function animateResponseText() {
 | 
					        function animateResponseText() {
 | 
				
			||||||
@@ -185,19 +187,20 @@ export class ErnieApi implements LLMApi {
 | 
				
			|||||||
        const finish = () => {
 | 
					        const finish = () => {
 | 
				
			||||||
          if (!finished) {
 | 
					          if (!finished) {
 | 
				
			||||||
            finished = true;
 | 
					            finished = true;
 | 
				
			||||||
            options.onFinish(responseText + remainText);
 | 
					            options.onFinish(responseText + remainText, responseRes);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        controller.signal.onabort = finish;
 | 
					        controller.signal.onabort = finish;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fetchEventSource(chatPath, {
 | 
					        fetchEventSource(chatPath, {
 | 
				
			||||||
 | 
					          fetch: fetch as any,
 | 
				
			||||||
          ...chatPayload,
 | 
					          ...chatPayload,
 | 
				
			||||||
          async onopen(res) {
 | 
					          async onopen(res) {
 | 
				
			||||||
            clearTimeout(requestTimeoutId);
 | 
					            clearTimeout(requestTimeoutId);
 | 
				
			||||||
            const contentType = res.headers.get("content-type");
 | 
					            const contentType = res.headers.get("content-type");
 | 
				
			||||||
            console.log("[Baidu] request response content type: ", contentType);
 | 
					            console.log("[Baidu] request response content type: ", contentType);
 | 
				
			||||||
 | 
					            responseRes = res;
 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					            if (contentType?.startsWith("text/plain")) {
 | 
				
			||||||
              responseText = await res.clone().text();
 | 
					              responseText = await res.clone().text();
 | 
				
			||||||
              return finish();
 | 
					              return finish();
 | 
				
			||||||
@@ -260,7 +263,7 @@ export class ErnieApi implements LLMApi {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        const resJson = await res.json();
 | 
					        const resJson = await res.json();
 | 
				
			||||||
        const message = resJson?.result;
 | 
					        const message = resJson?.result;
 | 
				
			||||||
        options.onFinish(message);
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,12 @@
 | 
				
			|||||||
"use client";
 | 
					"use client";
 | 
				
			||||||
 | 
					import { ApiPath, ByteDance, BYTEDANCE_BASE_URL } from "@/app/constant";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ApiPath,
 | 
					  useAccessStore,
 | 
				
			||||||
  ByteDance,
 | 
					  useAppConfig,
 | 
				
			||||||
  BYTEDANCE_BASE_URL,
 | 
					  useChatStore,
 | 
				
			||||||
  REQUEST_TIMEOUT_MS,
 | 
					  ChatMessageTool,
 | 
				
			||||||
} from "@/app/constant";
 | 
					  usePluginStore,
 | 
				
			||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ChatOptions,
 | 
					  ChatOptions,
 | 
				
			||||||
@@ -13,15 +14,17 @@ import {
 | 
				
			|||||||
  LLMApi,
 | 
					  LLMApi,
 | 
				
			||||||
  LLMModel,
 | 
					  LLMModel,
 | 
				
			||||||
  MultimodalContent,
 | 
					  MultimodalContent,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
} from "../api";
 | 
					} from "../api";
 | 
				
			||||||
import Locale from "../../locales";
 | 
					
 | 
				
			||||||
import {
 | 
					import { streamWithThink } from "@/app/utils/chat";
 | 
				
			||||||
  EventStreamContentType,
 | 
					 | 
				
			||||||
  fetchEventSource,
 | 
					 | 
				
			||||||
} from "@fortaine/fetch-event-source";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
import { getMessageTextContent } from "@/app/utils";
 | 
					import { preProcessImageContent } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContentWithoutThinking,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface OpenAIListModelResponse {
 | 
					export interface OpenAIListModelResponse {
 | 
				
			||||||
  object: string;
 | 
					  object: string;
 | 
				
			||||||
@@ -32,7 +35,7 @@ export interface OpenAIListModelResponse {
 | 
				
			|||||||
  }>;
 | 
					  }>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface RequestPayload {
 | 
					interface RequestPayloadForByteDance {
 | 
				
			||||||
  messages: {
 | 
					  messages: {
 | 
				
			||||||
    role: "system" | "user" | "assistant";
 | 
					    role: "system" | "user" | "assistant";
 | 
				
			||||||
    content: string | MultimodalContent[];
 | 
					    content: string | MultimodalContent[];
 | 
				
			||||||
@@ -77,11 +80,19 @@ export class DoubaoApi implements LLMApi {
 | 
				
			|||||||
    return res.choices?.at(0)?.message?.content ?? "";
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
    const messages = options.messages.map((v) => ({
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
      role: v.role,
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
      content: getMessageTextContent(v),
 | 
					      const content =
 | 
				
			||||||
    }));
 | 
					        v.role === "assistant"
 | 
				
			||||||
 | 
					          ? getMessageTextContentWithoutThinking(v)
 | 
				
			||||||
 | 
					          : await preProcessImageContent(v.content);
 | 
				
			||||||
 | 
					      messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const modelConfig = {
 | 
					    const modelConfig = {
 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
@@ -92,7 +103,7 @@ export class DoubaoApi implements LLMApi {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const shouldStream = !!options.config.stream;
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
    const requestPayload: RequestPayload = {
 | 
					    const requestPayload: RequestPayloadForByteDance = {
 | 
				
			||||||
      messages,
 | 
					      messages,
 | 
				
			||||||
      stream: shouldStream,
 | 
					      stream: shouldStream,
 | 
				
			||||||
      model: modelConfig.model,
 | 
					      model: modelConfig.model,
 | 
				
			||||||
@@ -117,124 +128,108 @@ export class DoubaoApi implements LLMApi {
 | 
				
			|||||||
      // make a fetch request
 | 
					      // make a fetch request
 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
        () => controller.abort(),
 | 
					        () => controller.abort(),
 | 
				
			||||||
        REQUEST_TIMEOUT_MS,
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (shouldStream) {
 | 
					      if (shouldStream) {
 | 
				
			||||||
        let responseText = "";
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
        let remainText = "";
 | 
					          .getState()
 | 
				
			||||||
        let finished = false;
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return streamWithThink(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string | null;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					                reasoning_content: string | null;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // animate response to make it looks smooth
 | 
					            if (!choices?.length) return { isThinking: false, content: "" };
 | 
				
			||||||
        function animateResponseText() {
 | 
					
 | 
				
			||||||
          if (finished || controller.signal.aborted) {
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
            responseText += remainText;
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
            console.log("[Response Animation] finished");
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
            if (responseText?.length === 0) {
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
              options.onError?.(new Error("empty response from server"));
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
            }
 | 
					              if (id) {
 | 
				
			||||||
            return;
 | 
					                runTools.push({
 | 
				
			||||||
          }
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
          if (remainText.length > 0) {
 | 
					                  function: {
 | 
				
			||||||
            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
            const fetchText = remainText.slice(0, fetchCount);
 | 
					                    arguments: args,
 | 
				
			||||||
            responseText += fetchText;
 | 
					                  },
 | 
				
			||||||
            remainText = remainText.slice(fetchCount);
 | 
					                });
 | 
				
			||||||
            options.onUpdate?.(responseText, fetchText);
 | 
					              } else {
 | 
				
			||||||
          }
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
          requestAnimationFrame(animateResponseText);
 | 
					              }
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // start animaion
 | 
					 | 
				
			||||||
        animateResponseText();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const finish = () => {
 | 
					 | 
				
			||||||
          if (!finished) {
 | 
					 | 
				
			||||||
            finished = true;
 | 
					 | 
				
			||||||
            options.onFinish(responseText + remainText);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        controller.signal.onabort = finish;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        fetchEventSource(chatPath, {
 | 
					 | 
				
			||||||
          ...chatPayload,
 | 
					 | 
				
			||||||
          async onopen(res) {
 | 
					 | 
				
			||||||
            clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
            const contentType = res.headers.get("content-type");
 | 
					 | 
				
			||||||
            console.log(
 | 
					 | 
				
			||||||
              "[ByteDance] request response content type: ",
 | 
					 | 
				
			||||||
              contentType,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					 | 
				
			||||||
              responseText = await res.clone().text();
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            const reasoning = choices[0]?.delta?.reasoning_content;
 | 
				
			||||||
 | 
					            const content = choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Skip if both content and reasoning_content are empty or null
 | 
				
			||||||
            if (
 | 
					            if (
 | 
				
			||||||
              !res.ok ||
 | 
					              (!reasoning || reasoning.length === 0) &&
 | 
				
			||||||
              !res.headers
 | 
					              (!content || content.length === 0)
 | 
				
			||||||
                .get("content-type")
 | 
					 | 
				
			||||||
                ?.startsWith(EventStreamContentType) ||
 | 
					 | 
				
			||||||
              res.status !== 200
 | 
					 | 
				
			||||||
            ) {
 | 
					            ) {
 | 
				
			||||||
              const responseTexts = [responseText];
 | 
					              return {
 | 
				
			||||||
              let extraInfo = await res.clone().text();
 | 
					                isThinking: false,
 | 
				
			||||||
              try {
 | 
					                content: "",
 | 
				
			||||||
                const resJson = await res.clone().json();
 | 
					              };
 | 
				
			||||||
                extraInfo = prettyObject(resJson);
 | 
					 | 
				
			||||||
              } catch {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (res.status === 401) {
 | 
					 | 
				
			||||||
                responseTexts.push(Locale.Error.Unauthorized);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (extraInfo) {
 | 
					 | 
				
			||||||
                responseTexts.push(extraInfo);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              responseText = responseTexts.join("\n\n");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          },
 | 
					
 | 
				
			||||||
          onmessage(msg) {
 | 
					            if (reasoning && reasoning.length > 0) {
 | 
				
			||||||
            if (msg.data === "[DONE]" || finished) {
 | 
					              return {
 | 
				
			||||||
              return finish();
 | 
					                isThinking: true,
 | 
				
			||||||
            }
 | 
					                content: reasoning,
 | 
				
			||||||
            const text = msg.data;
 | 
					              };
 | 
				
			||||||
            try {
 | 
					            } else if (content && content.length > 0) {
 | 
				
			||||||
              const json = JSON.parse(text);
 | 
					              return {
 | 
				
			||||||
              const choices = json.choices as Array<{
 | 
					                isThinking: false,
 | 
				
			||||||
                delta: { content: string };
 | 
					                content: content,
 | 
				
			||||||
              }>;
 | 
					              };
 | 
				
			||||||
              const delta = choices[0]?.delta?.content;
 | 
					 | 
				
			||||||
              if (delta) {
 | 
					 | 
				
			||||||
                remainText += delta;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            } catch (e) {
 | 
					 | 
				
			||||||
              console.error("[Request] parse error", text, msg);
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              isThinking: false,
 | 
				
			||||||
 | 
					              content: "",
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          onclose() {
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
            finish();
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayloadForByteDance,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          onerror(e) {
 | 
					          options,
 | 
				
			||||||
            options.onError?.(e);
 | 
					        );
 | 
				
			||||||
            throw e;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          openWhenHidden: true,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const resJson = await res.json();
 | 
					        const resJson = await res.json();
 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
        options.onFinish(message);
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										253
									
								
								app/client/platforms/deepseek.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,253 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					// azure and openai, using same models. so using same LLMApi.
 | 
				
			||||||
 | 
					import { ApiPath, DEEPSEEK_BASE_URL, DeepSeek } from "@/app/constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { streamWithThink } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  getMessageTextContentWithoutThinking,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class DeepSeekApi implements LLMApi {
 | 
				
			||||||
 | 
					  private disableListModels = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.deepseekUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      const apiPath = ApiPath.DeepSeek;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? DEEPSEEK_BASE_URL : apiPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.DeepSeek)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      if (v.role === "assistant") {
 | 
				
			||||||
 | 
					        const content = getMessageTextContentWithoutThinking(v);
 | 
				
			||||||
 | 
					        messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const content = getMessageTextContent(v);
 | 
				
			||||||
 | 
					        messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 检测并修复消息顺序,确保除system外的第一个消息是user
 | 
				
			||||||
 | 
					    const filteredMessages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    let hasFoundFirstUser = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const msg of messages) {
 | 
				
			||||||
 | 
					      if (msg.role === "system") {
 | 
				
			||||||
 | 
					        // Keep all system messages
 | 
				
			||||||
 | 
					        filteredMessages.push(msg);
 | 
				
			||||||
 | 
					      } else if (msg.role === "user") {
 | 
				
			||||||
 | 
					        // User message directly added
 | 
				
			||||||
 | 
					        filteredMessages.push(msg);
 | 
				
			||||||
 | 
					        hasFoundFirstUser = true;
 | 
				
			||||||
 | 
					      } else if (hasFoundFirstUser) {
 | 
				
			||||||
 | 
					        // After finding the first user message, all subsequent non-system messages are retained.
 | 
				
			||||||
 | 
					        filteredMessages.push(msg);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // If hasFoundFirstUser is false and it is not a system message, it will be skipped.
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					        providerName: options.config.providerName,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const requestPayload: RequestPayload = {
 | 
				
			||||||
 | 
					      messages: filteredMessages,
 | 
				
			||||||
 | 
					      stream: options.config.stream,
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      presence_penalty: modelConfig.presence_penalty,
 | 
				
			||||||
 | 
					      frequency_penalty: modelConfig.frequency_penalty,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					      // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
				
			||||||
 | 
					      // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] openai payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPath = this.path(DeepSeek.ChatPath);
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return streamWithThink(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string | null;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					                reasoning_content: string | null;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const reasoning = choices[0]?.delta?.reasoning_content;
 | 
				
			||||||
 | 
					            const content = choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Skip if both content and reasoning_content are empty or null
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					              (!reasoning || reasoning.length === 0) &&
 | 
				
			||||||
 | 
					              (!content || content.length === 0)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: "",
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (reasoning && reasoning.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: true,
 | 
				
			||||||
 | 
					                content: reasoning,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            } else if (content && content.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: content,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              isThinking: false,
 | 
				
			||||||
 | 
					              content: "",
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										292
									
								
								app/client/platforms/glm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,292 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					import { ApiPath, CHATGLM_BASE_URL, ChatGLM } from "@/app/constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { stream } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  isVisionModel,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					import { preProcessImageContent } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface BasePayload {
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ChatPayload extends BasePayload {
 | 
				
			||||||
 | 
					  messages: ChatOptions["messages"];
 | 
				
			||||||
 | 
					  stream?: boolean;
 | 
				
			||||||
 | 
					  temperature?: number;
 | 
				
			||||||
 | 
					  presence_penalty?: number;
 | 
				
			||||||
 | 
					  frequency_penalty?: number;
 | 
				
			||||||
 | 
					  top_p?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ImageGenerationPayload extends BasePayload {
 | 
				
			||||||
 | 
					  prompt: string;
 | 
				
			||||||
 | 
					  size?: string;
 | 
				
			||||||
 | 
					  user_id?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface VideoGenerationPayload extends BasePayload {
 | 
				
			||||||
 | 
					  prompt: string;
 | 
				
			||||||
 | 
					  duration?: number;
 | 
				
			||||||
 | 
					  resolution?: string;
 | 
				
			||||||
 | 
					  user_id?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ModelType = "chat" | "image" | "video";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class ChatGLMApi implements LLMApi {
 | 
				
			||||||
 | 
					  private disableListModels = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private getModelType(model: string): ModelType {
 | 
				
			||||||
 | 
					    if (model.startsWith("cogview-")) return "image";
 | 
				
			||||||
 | 
					    if (model.startsWith("cogvideo-")) return "video";
 | 
				
			||||||
 | 
					    return "chat";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private getModelPath(type: ModelType): string {
 | 
				
			||||||
 | 
					    switch (type) {
 | 
				
			||||||
 | 
					      case "image":
 | 
				
			||||||
 | 
					        return ChatGLM.ImagePath;
 | 
				
			||||||
 | 
					      case "video":
 | 
				
			||||||
 | 
					        return ChatGLM.VideoPath;
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return ChatGLM.ChatPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private createPayload(
 | 
				
			||||||
 | 
					    messages: ChatOptions["messages"],
 | 
				
			||||||
 | 
					    modelConfig: any,
 | 
				
			||||||
 | 
					    options: ChatOptions,
 | 
				
			||||||
 | 
					  ): BasePayload {
 | 
				
			||||||
 | 
					    const modelType = this.getModelType(modelConfig.model);
 | 
				
			||||||
 | 
					    const lastMessage = messages[messages.length - 1];
 | 
				
			||||||
 | 
					    const prompt =
 | 
				
			||||||
 | 
					      typeof lastMessage.content === "string"
 | 
				
			||||||
 | 
					        ? lastMessage.content
 | 
				
			||||||
 | 
					        : lastMessage.content.map((c) => c.text).join("\n");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch (modelType) {
 | 
				
			||||||
 | 
					      case "image":
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          model: modelConfig.model,
 | 
				
			||||||
 | 
					          prompt,
 | 
				
			||||||
 | 
					          size: options.config.size,
 | 
				
			||||||
 | 
					        } as ImageGenerationPayload;
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          messages,
 | 
				
			||||||
 | 
					          stream: options.config.stream,
 | 
				
			||||||
 | 
					          model: modelConfig.model,
 | 
				
			||||||
 | 
					          temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					          presence_penalty: modelConfig.presence_penalty,
 | 
				
			||||||
 | 
					          frequency_penalty: modelConfig.frequency_penalty,
 | 
				
			||||||
 | 
					          top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					        } as ChatPayload;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private parseResponse(modelType: ModelType, json: any): string {
 | 
				
			||||||
 | 
					    switch (modelType) {
 | 
				
			||||||
 | 
					      case "image": {
 | 
				
			||||||
 | 
					        const imageUrl = json.data?.[0]?.url;
 | 
				
			||||||
 | 
					        return imageUrl ? `` : "";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case "video": {
 | 
				
			||||||
 | 
					        const videoUrl = json.data?.[0]?.url;
 | 
				
			||||||
 | 
					        return videoUrl ? `<video controls src="${videoUrl}"></video>` : "";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return this.extractMessage(json);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.chatglmUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      const apiPath = ApiPath.ChatGLM;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? CHATGLM_BASE_URL : apiPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ChatGLM)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const visionModel = isVisionModel(options.config.model);
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      const content = visionModel
 | 
				
			||||||
 | 
					        ? await preProcessImageContent(v.content)
 | 
				
			||||||
 | 
					        : getMessageTextContent(v);
 | 
				
			||||||
 | 
					      messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					        providerName: options.config.providerName,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const modelType = this.getModelType(modelConfig.model);
 | 
				
			||||||
 | 
					    const requestPayload = this.createPayload(messages, modelConfig, options);
 | 
				
			||||||
 | 
					    const path = this.path(this.getModelPath(modelType));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log(`[Request] glm ${modelType} payload: `, requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (modelType === "image" || modelType === "video") {
 | 
				
			||||||
 | 
					        const res = await fetch(path, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        console.log(`[Response] glm ${modelType}:`, resJson);
 | 
				
			||||||
 | 
					        const message = this.parseResponse(modelType, resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return stream(
 | 
				
			||||||
 | 
					          path,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(path, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,23 +1,36 @@
 | 
				
			|||||||
import { ApiPath, Google, REQUEST_TIMEOUT_MS } from "@/app/constant";
 | 
					import { ApiPath, Google } from "@/app/constant";
 | 
				
			||||||
import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api";
 | 
					 | 
				
			||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					 | 
				
			||||||
import { DEFAULT_API_HOST } from "@/app/constant";
 | 
					 | 
				
			||||||
import Locale from "../../locales";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  EventStreamContentType,
 | 
					  ChatOptions,
 | 
				
			||||||
  fetchEventSource,
 | 
					  getHeaders,
 | 
				
			||||||
} from "@fortaine/fetch-event-source";
 | 
					  LLMApi,
 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  LLMUsage,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { stream } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import { GEMINI_BASE_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getMessageTextContent,
 | 
					  getMessageTextContent,
 | 
				
			||||||
  getMessageImages,
 | 
					  getMessageImages,
 | 
				
			||||||
  isVisionModel,
 | 
					  isVisionModel,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
} from "@/app/utils";
 | 
					} from "@/app/utils";
 | 
				
			||||||
import { preProcessImageContent } from "@/app/utils/chat";
 | 
					import { preProcessImageContent } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import { nanoid } from "nanoid";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class GeminiProApi implements LLMApi {
 | 
					export class GeminiProApi implements LLMApi {
 | 
				
			||||||
  path(path: string): string {
 | 
					  path(path: string, shouldStream = false): string {
 | 
				
			||||||
    const accessStore = useAccessStore.getState();
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let baseUrl = "";
 | 
					    let baseUrl = "";
 | 
				
			||||||
@@ -27,7 +40,7 @@ export class GeminiProApi implements LLMApi {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const isApp = !!getClientConfig()?.isApp;
 | 
					    const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
      baseUrl = isApp ? DEFAULT_API_HOST + `/api/proxy/google` : ApiPath.Google;
 | 
					      baseUrl = isApp ? GEMINI_BASE_URL : ApiPath.Google;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
@@ -39,23 +52,42 @@ export class GeminiProApi implements LLMApi {
 | 
				
			|||||||
    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let chatPath = [baseUrl, path].join("/");
 | 
					    let chatPath = [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					    if (shouldStream) {
 | 
				
			||||||
    chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
 | 
					      chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
 | 
				
			||||||
    // if chatPath.startsWith('http') then add key in query string
 | 
					 | 
				
			||||||
    if (chatPath.startsWith("http") && accessStore.googleApiKey) {
 | 
					 | 
				
			||||||
      chatPath += `&key=${accessStore.googleApiKey}`;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return chatPath;
 | 
					    return chatPath;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  extractMessage(res: any) {
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
    console.log("[Response] gemini-pro response: ", res);
 | 
					    console.log("[Response] gemini-pro response: ", res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getTextFromParts = (parts: any[]) => {
 | 
				
			||||||
 | 
					      if (!Array.isArray(parts)) return "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return parts
 | 
				
			||||||
 | 
					        .map((part) => part?.text || "")
 | 
				
			||||||
 | 
					        .filter((text) => text.trim() !== "")
 | 
				
			||||||
 | 
					        .join("\n\n");
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let content = "";
 | 
				
			||||||
 | 
					    if (Array.isArray(res)) {
 | 
				
			||||||
 | 
					      res.map((item) => {
 | 
				
			||||||
 | 
					        content += getTextFromParts(item?.candidates?.at(0)?.content?.parts);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      res?.candidates?.at(0)?.content?.parts.at(0)?.text ||
 | 
					      getTextFromParts(res?.candidates?.at(0)?.content?.parts) ||
 | 
				
			||||||
 | 
					      content || //getTextFromParts(res?.at(0)?.candidates?.at(0)?.content?.parts) ||
 | 
				
			||||||
      res?.error?.message ||
 | 
					      res?.error?.message ||
 | 
				
			||||||
      ""
 | 
					      ""
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async chat(options: ChatOptions): Promise<void> {
 | 
					  async chat(options: ChatOptions): Promise<void> {
 | 
				
			||||||
    const apiClient = this;
 | 
					    const apiClient = this;
 | 
				
			||||||
    let multimodal = false;
 | 
					    let multimodal = false;
 | 
				
			||||||
@@ -154,7 +186,10 @@ export class GeminiProApi implements LLMApi {
 | 
				
			|||||||
    options.onController?.(controller);
 | 
					    options.onController?.(controller);
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      // https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb
 | 
					      // https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb
 | 
				
			||||||
      const chatPath = this.path(Google.ChatPath(modelConfig.model));
 | 
					      const chatPath = this.path(
 | 
				
			||||||
 | 
					        Google.ChatPath(modelConfig.model),
 | 
				
			||||||
 | 
					        shouldStream,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const chatPayload = {
 | 
					      const chatPayload = {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
@@ -163,121 +198,95 @@ export class GeminiProApi implements LLMApi {
 | 
				
			|||||||
        headers: getHeaders(),
 | 
					        headers: getHeaders(),
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const isThinking = options.config.model.includes("-thinking");
 | 
				
			||||||
      // make a fetch request
 | 
					      // make a fetch request
 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
        () => controller.abort(),
 | 
					        () => controller.abort(),
 | 
				
			||||||
        REQUEST_TIMEOUT_MS,
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (shouldStream) {
 | 
					      if (shouldStream) {
 | 
				
			||||||
        let responseText = "";
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
        let remainText = "";
 | 
					          .getState()
 | 
				
			||||||
        let finished = false;
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return stream(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          // @ts-ignore
 | 
				
			||||||
 | 
					          tools.length > 0
 | 
				
			||||||
 | 
					            ? // @ts-ignore
 | 
				
			||||||
 | 
					              [{ functionDeclarations: tools.map((tool) => tool.function) }]
 | 
				
			||||||
 | 
					            : [],
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const chunkJson = JSON.parse(text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const finish = () => {
 | 
					            const functionCall = chunkJson?.candidates
 | 
				
			||||||
          if (!finished) {
 | 
					              ?.at(0)
 | 
				
			||||||
            finished = true;
 | 
					              ?.content.parts.at(0)?.functionCall;
 | 
				
			||||||
            options.onFinish(responseText + remainText);
 | 
					            if (functionCall) {
 | 
				
			||||||
          }
 | 
					              const { name, args } = functionCall;
 | 
				
			||||||
        };
 | 
					              runTools.push({
 | 
				
			||||||
 | 
					                id: nanoid(),
 | 
				
			||||||
        // animate response to make it looks smooth
 | 
					                type: "function",
 | 
				
			||||||
        function animateResponseText() {
 | 
					                function: {
 | 
				
			||||||
          if (finished || controller.signal.aborted) {
 | 
					                  name,
 | 
				
			||||||
            responseText += remainText;
 | 
					                  arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
 | 
				
			||||||
            finish();
 | 
					                },
 | 
				
			||||||
            return;
 | 
					              });
 | 
				
			||||||
          }
 | 
					            }
 | 
				
			||||||
 | 
					            return chunkJson?.candidates
 | 
				
			||||||
          if (remainText.length > 0) {
 | 
					              ?.at(0)
 | 
				
			||||||
            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
					              ?.content.parts?.map((part: { text: string }) => part.text)
 | 
				
			||||||
            const fetchText = remainText.slice(0, fetchCount);
 | 
					              .join("\n\n");
 | 
				
			||||||
            responseText += fetchText;
 | 
					          },
 | 
				
			||||||
            remainText = remainText.slice(fetchCount);
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
            options.onUpdate?.(responseText, fetchText);
 | 
					          (
 | 
				
			||||||
          }
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
          requestAnimationFrame(animateResponseText);
 | 
					            toolCallResult: any[],
 | 
				
			||||||
        }
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
        // start animaion
 | 
					            requestPayload?.contents?.splice(
 | 
				
			||||||
        animateResponseText();
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.contents?.length,
 | 
				
			||||||
        controller.signal.onabort = finish;
 | 
					              0,
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
        fetchEventSource(chatPath, {
 | 
					                role: "model",
 | 
				
			||||||
          ...chatPayload,
 | 
					                parts: toolCallMessage.tool_calls.map(
 | 
				
			||||||
          async onopen(res) {
 | 
					                  (tool: ChatMessageTool) => ({
 | 
				
			||||||
            clearTimeout(requestTimeoutId);
 | 
					                    functionCall: {
 | 
				
			||||||
            const contentType = res.headers.get("content-type");
 | 
					                      name: tool?.function?.name,
 | 
				
			||||||
            console.log(
 | 
					                      args: JSON.parse(tool?.function?.arguments as string),
 | 
				
			||||||
              "[Gemini] request response content type: ",
 | 
					                    },
 | 
				
			||||||
              contentType,
 | 
					                  }),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              ...toolCallResult.map((result) => ({
 | 
				
			||||||
 | 
					                role: "function",
 | 
				
			||||||
 | 
					                parts: [
 | 
				
			||||||
 | 
					                  {
 | 
				
			||||||
 | 
					                    functionResponse: {
 | 
				
			||||||
 | 
					                      name: result.name,
 | 
				
			||||||
 | 
					                      response: {
 | 
				
			||||||
 | 
					                        name: result.name,
 | 
				
			||||||
 | 
					                        content: result.content, // TODO just text content...
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              })),
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					 | 
				
			||||||
              responseText = await res.clone().text();
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              !res.ok ||
 | 
					 | 
				
			||||||
              !res.headers
 | 
					 | 
				
			||||||
                .get("content-type")
 | 
					 | 
				
			||||||
                ?.startsWith(EventStreamContentType) ||
 | 
					 | 
				
			||||||
              res.status !== 200
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              const responseTexts = [responseText];
 | 
					 | 
				
			||||||
              let extraInfo = await res.clone().text();
 | 
					 | 
				
			||||||
              try {
 | 
					 | 
				
			||||||
                const resJson = await res.clone().json();
 | 
					 | 
				
			||||||
                extraInfo = prettyObject(resJson);
 | 
					 | 
				
			||||||
              } catch {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (res.status === 401) {
 | 
					 | 
				
			||||||
                responseTexts.push(Locale.Error.Unauthorized);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (extraInfo) {
 | 
					 | 
				
			||||||
                responseTexts.push(extraInfo);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              responseText = responseTexts.join("\n\n");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          onmessage(msg) {
 | 
					          options,
 | 
				
			||||||
            if (msg.data === "[DONE]" || finished) {
 | 
					        );
 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            const text = msg.data;
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
              const json = JSON.parse(text);
 | 
					 | 
				
			||||||
              const delta = apiClient.extractMessage(json);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (delta) {
 | 
					 | 
				
			||||||
                remainText += delta;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              const blockReason = json?.promptFeedback?.blockReason;
 | 
					 | 
				
			||||||
              if (blockReason) {
 | 
					 | 
				
			||||||
                // being blocked
 | 
					 | 
				
			||||||
                console.log(`[Google] [Safety Ratings] result:`, blockReason);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            } catch (e) {
 | 
					 | 
				
			||||||
              console.error("[Request] parse error", text, msg);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onclose() {
 | 
					 | 
				
			||||||
            finish();
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onerror(e) {
 | 
					 | 
				
			||||||
            options.onError?.(e);
 | 
					 | 
				
			||||||
            throw e;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          openWhenHidden: true,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
@@ -292,7 +301,7 @@ export class GeminiProApi implements LLMApi {
 | 
				
			|||||||
          );
 | 
					          );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        const message = apiClient.extractMessage(resJson);
 | 
					        const message = apiClient.extractMessage(resJson);
 | 
				
			||||||
        options.onFinish(message);
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,19 @@
 | 
				
			|||||||
"use client";
 | 
					"use client";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ApiPath,
 | 
					  ApiPath,
 | 
				
			||||||
  DEFAULT_API_HOST,
 | 
					  IFLYTEK_BASE_URL,
 | 
				
			||||||
  Iflytek,
 | 
					  Iflytek,
 | 
				
			||||||
  REQUEST_TIMEOUT_MS,
 | 
					  REQUEST_TIMEOUT_MS,
 | 
				
			||||||
} from "@/app/constant";
 | 
					} from "@/app/constant";
 | 
				
			||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
					import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { ChatOptions, getHeaders, LLMApi, LLMModel } from "../api";
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
import Locale from "../../locales";
 | 
					import Locale from "../../locales";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  EventStreamContentType,
 | 
					  EventStreamContentType,
 | 
				
			||||||
@@ -16,8 +22,9 @@ import {
 | 
				
			|||||||
import { prettyObject } from "@/app/utils/format";
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
import { getMessageTextContent } from "@/app/utils";
 | 
					import { getMessageTextContent } from "@/app/utils";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { OpenAIListModelResponse, RequestPayload } from "./openai";
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class SparkApi implements LLMApi {
 | 
					export class SparkApi implements LLMApi {
 | 
				
			||||||
  private disableListModels = true;
 | 
					  private disableListModels = true;
 | 
				
			||||||
@@ -34,7 +41,7 @@ export class SparkApi implements LLMApi {
 | 
				
			|||||||
    if (baseUrl.length === 0) {
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
      const apiPath = ApiPath.Iflytek;
 | 
					      const apiPath = ApiPath.Iflytek;
 | 
				
			||||||
      baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
 | 
					      baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
@@ -53,6 +60,10 @@ export class SparkApi implements LLMApi {
 | 
				
			|||||||
    return res.choices?.at(0)?.message?.content ?? "";
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
    const messages: ChatOptions["messages"] = [];
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
    for (const v of options.messages) {
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
@@ -106,6 +117,7 @@ export class SparkApi implements LLMApi {
 | 
				
			|||||||
        let responseText = "";
 | 
					        let responseText = "";
 | 
				
			||||||
        let remainText = "";
 | 
					        let remainText = "";
 | 
				
			||||||
        let finished = false;
 | 
					        let finished = false;
 | 
				
			||||||
 | 
					        let responseRes: Response;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Animate response text to make it look smooth
 | 
					        // Animate response text to make it look smooth
 | 
				
			||||||
        function animateResponseText() {
 | 
					        function animateResponseText() {
 | 
				
			||||||
@@ -132,19 +144,20 @@ export class SparkApi implements LLMApi {
 | 
				
			|||||||
        const finish = () => {
 | 
					        const finish = () => {
 | 
				
			||||||
          if (!finished) {
 | 
					          if (!finished) {
 | 
				
			||||||
            finished = true;
 | 
					            finished = true;
 | 
				
			||||||
            options.onFinish(responseText + remainText);
 | 
					            options.onFinish(responseText + remainText, responseRes);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        controller.signal.onabort = finish;
 | 
					        controller.signal.onabort = finish;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fetchEventSource(chatPath, {
 | 
					        fetchEventSource(chatPath, {
 | 
				
			||||||
 | 
					          fetch: fetch as any,
 | 
				
			||||||
          ...chatPayload,
 | 
					          ...chatPayload,
 | 
				
			||||||
          async onopen(res) {
 | 
					          async onopen(res) {
 | 
				
			||||||
            clearTimeout(requestTimeoutId);
 | 
					            clearTimeout(requestTimeoutId);
 | 
				
			||||||
            const contentType = res.headers.get("content-type");
 | 
					            const contentType = res.headers.get("content-type");
 | 
				
			||||||
            console.log("[Spark] request response content type: ", contentType);
 | 
					            console.log("[Spark] request response content type: ", contentType);
 | 
				
			||||||
 | 
					            responseRes = res;
 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					            if (contentType?.startsWith("text/plain")) {
 | 
				
			||||||
              responseText = await res.clone().text();
 | 
					              responseText = await res.clone().text();
 | 
				
			||||||
              return finish();
 | 
					              return finish();
 | 
				
			||||||
@@ -219,7 +232,7 @@ export class SparkApi implements LLMApi {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        const resJson = await res.json();
 | 
					        const resJson = await res.json();
 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
        options.onFinish(message);
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,35 +2,29 @@
 | 
				
			|||||||
// azure and openai, using same models. so using same LLMApi.
 | 
					// azure and openai, using same models. so using same LLMApi.
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ApiPath,
 | 
					  ApiPath,
 | 
				
			||||||
  DEFAULT_API_HOST,
 | 
					  MOONSHOT_BASE_URL,
 | 
				
			||||||
  DEFAULT_MODELS,
 | 
					 | 
				
			||||||
  Moonshot,
 | 
					  Moonshot,
 | 
				
			||||||
  REQUEST_TIMEOUT_MS,
 | 
					  REQUEST_TIMEOUT_MS,
 | 
				
			||||||
  ServiceProvider,
 | 
					 | 
				
			||||||
} from "@/app/constant";
 | 
					} from "@/app/constant";
 | 
				
			||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
					import {
 | 
				
			||||||
import { collectModelsWithDefaultModel } from "@/app/utils/model";
 | 
					  useAccessStore,
 | 
				
			||||||
import { preProcessImageContent } from "@/app/utils/chat";
 | 
					  useAppConfig,
 | 
				
			||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { stream } from "@/app/utils/chat";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ChatOptions,
 | 
					  ChatOptions,
 | 
				
			||||||
  getHeaders,
 | 
					  getHeaders,
 | 
				
			||||||
  LLMApi,
 | 
					  LLMApi,
 | 
				
			||||||
  LLMModel,
 | 
					  LLMModel,
 | 
				
			||||||
  LLMUsage,
 | 
					  SpeechOptions,
 | 
				
			||||||
  MultimodalContent,
 | 
					 | 
				
			||||||
} from "../api";
 | 
					} from "../api";
 | 
				
			||||||
import Locale from "../../locales";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  EventStreamContentType,
 | 
					 | 
				
			||||||
  fetchEventSource,
 | 
					 | 
				
			||||||
} from "@fortaine/fetch-event-source";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
import { getMessageTextContent } from "@/app/utils";
 | 
					import { getMessageTextContent } from "@/app/utils";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
import { OpenAIListModelResponse, RequestPayload } from "./openai";
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class MoonshotApi implements LLMApi {
 | 
					export class MoonshotApi implements LLMApi {
 | 
				
			||||||
  private disableListModels = true;
 | 
					  private disableListModels = true;
 | 
				
			||||||
@@ -47,7 +41,7 @@ export class MoonshotApi implements LLMApi {
 | 
				
			|||||||
    if (baseUrl.length === 0) {
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
      const apiPath = ApiPath.Moonshot;
 | 
					      const apiPath = ApiPath.Moonshot;
 | 
				
			||||||
      baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
 | 
					      baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
@@ -66,6 +60,10 @@ export class MoonshotApi implements LLMApi {
 | 
				
			|||||||
    return res.choices?.at(0)?.message?.content ?? "";
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
    const messages: ChatOptions["messages"] = [];
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
    for (const v of options.messages) {
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
@@ -116,122 +114,73 @@ export class MoonshotApi implements LLMApi {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (shouldStream) {
 | 
					      if (shouldStream) {
 | 
				
			||||||
        let responseText = "";
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
        let remainText = "";
 | 
					          .getState()
 | 
				
			||||||
        let finished = false;
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
        // animate response to make it looks smooth
 | 
					          );
 | 
				
			||||||
        function animateResponseText() {
 | 
					        return stream(
 | 
				
			||||||
          if (finished || controller.signal.aborted) {
 | 
					          chatPath,
 | 
				
			||||||
            responseText += remainText;
 | 
					          requestPayload,
 | 
				
			||||||
            console.log("[Response Animation] finished");
 | 
					          getHeaders(),
 | 
				
			||||||
            if (responseText?.length === 0) {
 | 
					          tools as any,
 | 
				
			||||||
              options.onError?.(new Error("empty response from server"));
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return;
 | 
					            return choices[0]?.delta?.content;
 | 
				
			||||||
          }
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
          if (remainText.length > 0) {
 | 
					          (
 | 
				
			||||||
            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
            const fetchText = remainText.slice(0, fetchCount);
 | 
					            toolCallMessage: any,
 | 
				
			||||||
            responseText += fetchText;
 | 
					            toolCallResult: any[],
 | 
				
			||||||
            remainText = remainText.slice(fetchCount);
 | 
					          ) => {
 | 
				
			||||||
            options.onUpdate?.(responseText, fetchText);
 | 
					            // @ts-ignore
 | 
				
			||||||
          }
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
          requestAnimationFrame(animateResponseText);
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
        }
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
        // start animaion
 | 
					              ...toolCallResult,
 | 
				
			||||||
        animateResponseText();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const finish = () => {
 | 
					 | 
				
			||||||
          if (!finished) {
 | 
					 | 
				
			||||||
            finished = true;
 | 
					 | 
				
			||||||
            options.onFinish(responseText + remainText);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        controller.signal.onabort = finish;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        fetchEventSource(chatPath, {
 | 
					 | 
				
			||||||
          ...chatPayload,
 | 
					 | 
				
			||||||
          async onopen(res) {
 | 
					 | 
				
			||||||
            clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
            const contentType = res.headers.get("content-type");
 | 
					 | 
				
			||||||
            console.log(
 | 
					 | 
				
			||||||
              "[OpenAI] request response content type: ",
 | 
					 | 
				
			||||||
              contentType,
 | 
					 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					 | 
				
			||||||
              responseText = await res.clone().text();
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              !res.ok ||
 | 
					 | 
				
			||||||
              !res.headers
 | 
					 | 
				
			||||||
                .get("content-type")
 | 
					 | 
				
			||||||
                ?.startsWith(EventStreamContentType) ||
 | 
					 | 
				
			||||||
              res.status !== 200
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              const responseTexts = [responseText];
 | 
					 | 
				
			||||||
              let extraInfo = await res.clone().text();
 | 
					 | 
				
			||||||
              try {
 | 
					 | 
				
			||||||
                const resJson = await res.clone().json();
 | 
					 | 
				
			||||||
                extraInfo = prettyObject(resJson);
 | 
					 | 
				
			||||||
              } catch {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (res.status === 401) {
 | 
					 | 
				
			||||||
                responseTexts.push(Locale.Error.Unauthorized);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (extraInfo) {
 | 
					 | 
				
			||||||
                responseTexts.push(extraInfo);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              responseText = responseTexts.join("\n\n");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          onmessage(msg) {
 | 
					          options,
 | 
				
			||||||
            if (msg.data === "[DONE]" || finished) {
 | 
					        );
 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            const text = msg.data;
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
              const json = JSON.parse(text);
 | 
					 | 
				
			||||||
              const choices = json.choices as Array<{
 | 
					 | 
				
			||||||
                delta: { content: string };
 | 
					 | 
				
			||||||
              }>;
 | 
					 | 
				
			||||||
              const delta = choices[0]?.delta?.content;
 | 
					 | 
				
			||||||
              const textmoderation = json?.prompt_filter_results;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (delta) {
 | 
					 | 
				
			||||||
                remainText += delta;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            } catch (e) {
 | 
					 | 
				
			||||||
              console.error("[Request] parse error", text, msg);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onclose() {
 | 
					 | 
				
			||||||
            finish();
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onerror(e) {
 | 
					 | 
				
			||||||
            options.onError?.(e);
 | 
					 | 
				
			||||||
            throw e;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          openWhenHidden: true,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const resJson = await res.json();
 | 
					        const resJson = await res.json();
 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
        options.onFinish(message);
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,22 +2,29 @@
 | 
				
			|||||||
// azure and openai, using same models. so using same LLMApi.
 | 
					// azure and openai, using same models. so using same LLMApi.
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ApiPath,
 | 
					  ApiPath,
 | 
				
			||||||
  DEFAULT_API_HOST,
 | 
					  OPENAI_BASE_URL,
 | 
				
			||||||
  DEFAULT_MODELS,
 | 
					  DEFAULT_MODELS,
 | 
				
			||||||
  OpenaiPath,
 | 
					  OpenaiPath,
 | 
				
			||||||
  Azure,
 | 
					  Azure,
 | 
				
			||||||
  REQUEST_TIMEOUT_MS,
 | 
					  REQUEST_TIMEOUT_MS,
 | 
				
			||||||
  ServiceProvider,
 | 
					  ServiceProvider,
 | 
				
			||||||
} from "@/app/constant";
 | 
					} from "@/app/constant";
 | 
				
			||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
					import {
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
import { collectModelsWithDefaultModel } from "@/app/utils/model";
 | 
					import { collectModelsWithDefaultModel } from "@/app/utils/model";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  preProcessImageContent,
 | 
					  preProcessImageContent,
 | 
				
			||||||
  uploadImage,
 | 
					  uploadImage,
 | 
				
			||||||
  base64Image2Blob,
 | 
					  base64Image2Blob,
 | 
				
			||||||
 | 
					  streamWithThink,
 | 
				
			||||||
} from "@/app/utils/chat";
 | 
					} from "@/app/utils/chat";
 | 
				
			||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
					import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
				
			||||||
import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing";
 | 
					import { ModelSize, DalleQuality, DalleStyle } from "@/app/typing";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ChatOptions,
 | 
					  ChatOptions,
 | 
				
			||||||
@@ -26,20 +33,17 @@ import {
 | 
				
			|||||||
  LLMModel,
 | 
					  LLMModel,
 | 
				
			||||||
  LLMUsage,
 | 
					  LLMUsage,
 | 
				
			||||||
  MultimodalContent,
 | 
					  MultimodalContent,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
} from "../api";
 | 
					} from "../api";
 | 
				
			||||||
import Locale from "../../locales";
 | 
					import Locale from "../../locales";
 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  EventStreamContentType,
 | 
					 | 
				
			||||||
  fetchEventSource,
 | 
					 | 
				
			||||||
} from "@fortaine/fetch-event-source";
 | 
					 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getMessageTextContent,
 | 
					  getMessageTextContent,
 | 
				
			||||||
  getMessageImages,
 | 
					 | 
				
			||||||
  isVisionModel,
 | 
					  isVisionModel,
 | 
				
			||||||
  isDalle3 as _isDalle3,
 | 
					  isDalle3 as _isDalle3,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
} from "@/app/utils";
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface OpenAIListModelResponse {
 | 
					export interface OpenAIListModelResponse {
 | 
				
			||||||
  object: string;
 | 
					  object: string;
 | 
				
			||||||
@@ -62,6 +66,7 @@ export interface RequestPayload {
 | 
				
			|||||||
  frequency_penalty: number;
 | 
					  frequency_penalty: number;
 | 
				
			||||||
  top_p: number;
 | 
					  top_p: number;
 | 
				
			||||||
  max_tokens?: number;
 | 
					  max_tokens?: number;
 | 
				
			||||||
 | 
					  max_completion_tokens?: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface DalleRequestPayload {
 | 
					export interface DalleRequestPayload {
 | 
				
			||||||
@@ -69,7 +74,7 @@ export interface DalleRequestPayload {
 | 
				
			|||||||
  prompt: string;
 | 
					  prompt: string;
 | 
				
			||||||
  response_format: "url" | "b64_json";
 | 
					  response_format: "url" | "b64_json";
 | 
				
			||||||
  n: number;
 | 
					  n: number;
 | 
				
			||||||
  size: DalleSize;
 | 
					  size: ModelSize;
 | 
				
			||||||
  quality: DalleQuality;
 | 
					  quality: DalleQuality;
 | 
				
			||||||
  style: DalleStyle;
 | 
					  style: DalleStyle;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -96,7 +101,7 @@ export class ChatGPTApi implements LLMApi {
 | 
				
			|||||||
    if (baseUrl.length === 0) {
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
      const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
 | 
					      const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
 | 
				
			||||||
      baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
 | 
					      baseUrl = isApp ? OPENAI_BASE_URL : apiPath;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
@@ -140,6 +145,44 @@ export class ChatGPTApi implements LLMApi {
 | 
				
			|||||||
    return res.choices?.at(0)?.message?.content ?? res;
 | 
					    return res.choices?.at(0)?.message?.content ?? res;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    const requestPayload = {
 | 
				
			||||||
 | 
					      model: options.model,
 | 
				
			||||||
 | 
					      input: options.input,
 | 
				
			||||||
 | 
					      voice: options.voice,
 | 
				
			||||||
 | 
					      response_format: options.response_format,
 | 
				
			||||||
 | 
					      speed: options.speed,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] openai speech payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const speechPath = this.path(OpenaiPath.SpeechPath);
 | 
				
			||||||
 | 
					      const speechPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        REQUEST_TIMEOUT_MS,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const res = await fetch(speechPath, speechPayload);
 | 
				
			||||||
 | 
					      clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					      return await res.arrayBuffer();
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a speech request", e);
 | 
				
			||||||
 | 
					      throw e;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
    const modelConfig = {
 | 
					    const modelConfig = {
 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
@@ -153,6 +196,10 @@ export class ChatGPTApi implements LLMApi {
 | 
				
			|||||||
    let requestPayload: RequestPayload | DalleRequestPayload;
 | 
					    let requestPayload: RequestPayload | DalleRequestPayload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const isDalle3 = _isDalle3(options.config.model);
 | 
					    const isDalle3 = _isDalle3(options.config.model);
 | 
				
			||||||
 | 
					    const isO1OrO3 =
 | 
				
			||||||
 | 
					      options.config.model.startsWith("o1") ||
 | 
				
			||||||
 | 
					      options.config.model.startsWith("o3") ||
 | 
				
			||||||
 | 
					      options.config.model.startsWith("o4-mini");
 | 
				
			||||||
    if (isDalle3) {
 | 
					    if (isDalle3) {
 | 
				
			||||||
      const prompt = getMessageTextContent(
 | 
					      const prompt = getMessageTextContent(
 | 
				
			||||||
        options.messages.slice(-1)?.pop() as any,
 | 
					        options.messages.slice(-1)?.pop() as any,
 | 
				
			||||||
@@ -174,23 +221,30 @@ export class ChatGPTApi implements LLMApi {
 | 
				
			|||||||
        const content = visionModel
 | 
					        const content = visionModel
 | 
				
			||||||
          ? await preProcessImageContent(v.content)
 | 
					          ? await preProcessImageContent(v.content)
 | 
				
			||||||
          : getMessageTextContent(v);
 | 
					          : getMessageTextContent(v);
 | 
				
			||||||
        messages.push({ role: v.role, content });
 | 
					        if (!(isO1OrO3 && v.role === "system"))
 | 
				
			||||||
 | 
					          messages.push({ role: v.role, content });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
 | 
				
			||||||
      requestPayload = {
 | 
					      requestPayload = {
 | 
				
			||||||
        messages,
 | 
					        messages,
 | 
				
			||||||
        stream: options.config.stream,
 | 
					        stream: options.config.stream,
 | 
				
			||||||
        model: modelConfig.model,
 | 
					        model: modelConfig.model,
 | 
				
			||||||
        temperature: modelConfig.temperature,
 | 
					        temperature: !isO1OrO3 ? modelConfig.temperature : 1,
 | 
				
			||||||
        presence_penalty: modelConfig.presence_penalty,
 | 
					        presence_penalty: !isO1OrO3 ? modelConfig.presence_penalty : 0,
 | 
				
			||||||
        frequency_penalty: modelConfig.frequency_penalty,
 | 
					        frequency_penalty: !isO1OrO3 ? modelConfig.frequency_penalty : 0,
 | 
				
			||||||
        top_p: modelConfig.top_p,
 | 
					        top_p: !isO1OrO3 ? modelConfig.top_p : 1,
 | 
				
			||||||
        // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
					        // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
				
			||||||
        // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
					        // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // O1 使用 max_completion_tokens 控制token数 (https://platform.openai.com/docs/guides/reasoning#controlling-costs)
 | 
				
			||||||
 | 
					      if (isO1OrO3) {
 | 
				
			||||||
 | 
					        requestPayload["max_completion_tokens"] = modelConfig.max_tokens;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // add max_tokens to vision model
 | 
					      // add max_tokens to vision model
 | 
				
			||||||
      if (visionModel && modelConfig.model.includes("preview")) {
 | 
					      if (visionModel && !isO1OrO3) {
 | 
				
			||||||
        requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
 | 
					        requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -233,149 +287,125 @@ export class ChatGPTApi implements LLMApi {
 | 
				
			|||||||
          isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
 | 
					          isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const chatPayload = {
 | 
					 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
        body: JSON.stringify(requestPayload),
 | 
					 | 
				
			||||||
        signal: controller.signal,
 | 
					 | 
				
			||||||
        headers: getHeaders(),
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // make a fetch request
 | 
					 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					 | 
				
			||||||
        () => controller.abort(),
 | 
					 | 
				
			||||||
        isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (shouldStream) {
 | 
					      if (shouldStream) {
 | 
				
			||||||
        let responseText = "";
 | 
					        let index = -1;
 | 
				
			||||||
        let remainText = "";
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
        let finished = false;
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        // console.log("getAsTools", tools, funcs);
 | 
				
			||||||
 | 
					        streamWithThink(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					                reasoning_content: string | null;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // animate response to make it looks smooth
 | 
					            if (!choices?.length) return { isThinking: false, content: "" };
 | 
				
			||||||
        function animateResponseText() {
 | 
					
 | 
				
			||||||
          if (finished || controller.signal.aborted) {
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
            responseText += remainText;
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
            console.log("[Response Animation] finished");
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
            if (responseText?.length === 0) {
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
              options.onError?.(new Error("empty response from server"));
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                index += 1;
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (remainText.length > 0) {
 | 
					            const reasoning = choices[0]?.delta?.reasoning_content;
 | 
				
			||||||
            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
					            const content = choices[0]?.delta?.content;
 | 
				
			||||||
            const fetchText = remainText.slice(0, fetchCount);
 | 
					 | 
				
			||||||
            responseText += fetchText;
 | 
					 | 
				
			||||||
            remainText = remainText.slice(fetchCount);
 | 
					 | 
				
			||||||
            options.onUpdate?.(responseText, fetchText);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          requestAnimationFrame(animateResponseText);
 | 
					            // Skip if both content and reasoning_content are empty or null
 | 
				
			||||||
        }
 | 
					            if (
 | 
				
			||||||
 | 
					              (!reasoning || reasoning.length === 0) &&
 | 
				
			||||||
 | 
					              (!content || content.length === 0)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: "",
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // start animaion
 | 
					            if (reasoning && reasoning.length > 0) {
 | 
				
			||||||
        animateResponseText();
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: true,
 | 
				
			||||||
 | 
					                content: reasoning,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            } else if (content && content.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: content,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const finish = () => {
 | 
					            return {
 | 
				
			||||||
          if (!finished) {
 | 
					              isThinking: false,
 | 
				
			||||||
            finished = true;
 | 
					              content: "",
 | 
				
			||||||
            options.onFinish(responseText + remainText);
 | 
					            };
 | 
				
			||||||
          }
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // reset index value
 | 
				
			||||||
 | 
					            index = -1;
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const chatPayload = {
 | 
				
			||||||
 | 
					          method: "POST",
 | 
				
			||||||
 | 
					          body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					          signal: controller.signal,
 | 
				
			||||||
 | 
					          headers: getHeaders(),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        controller.signal.onabort = finish;
 | 
					        // make a fetch request
 | 
				
			||||||
 | 
					        const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					          () => controller.abort(),
 | 
				
			||||||
 | 
					          getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fetchEventSource(chatPath, {
 | 
					 | 
				
			||||||
          ...chatPayload,
 | 
					 | 
				
			||||||
          async onopen(res) {
 | 
					 | 
				
			||||||
            clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
            const contentType = res.headers.get("content-type");
 | 
					 | 
				
			||||||
            console.log(
 | 
					 | 
				
			||||||
              "[OpenAI] request response content type: ",
 | 
					 | 
				
			||||||
              contentType,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					 | 
				
			||||||
              responseText = await res.clone().text();
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              !res.ok ||
 | 
					 | 
				
			||||||
              !res.headers
 | 
					 | 
				
			||||||
                .get("content-type")
 | 
					 | 
				
			||||||
                ?.startsWith(EventStreamContentType) ||
 | 
					 | 
				
			||||||
              res.status !== 200
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              const responseTexts = [responseText];
 | 
					 | 
				
			||||||
              let extraInfo = await res.clone().text();
 | 
					 | 
				
			||||||
              try {
 | 
					 | 
				
			||||||
                const resJson = await res.clone().json();
 | 
					 | 
				
			||||||
                extraInfo = prettyObject(resJson);
 | 
					 | 
				
			||||||
              } catch {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (res.status === 401) {
 | 
					 | 
				
			||||||
                responseTexts.push(Locale.Error.Unauthorized);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (extraInfo) {
 | 
					 | 
				
			||||||
                responseTexts.push(extraInfo);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              responseText = responseTexts.join("\n\n");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onmessage(msg) {
 | 
					 | 
				
			||||||
            if (msg.data === "[DONE]" || finished) {
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            const text = msg.data;
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
              const json = JSON.parse(text);
 | 
					 | 
				
			||||||
              const choices = json.choices as Array<{
 | 
					 | 
				
			||||||
                delta: { content: string };
 | 
					 | 
				
			||||||
              }>;
 | 
					 | 
				
			||||||
              const delta = choices[0]?.delta?.content;
 | 
					 | 
				
			||||||
              const textmoderation = json?.prompt_filter_results;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (delta) {
 | 
					 | 
				
			||||||
                remainText += delta;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              if (
 | 
					 | 
				
			||||||
                textmoderation &&
 | 
					 | 
				
			||||||
                textmoderation.length > 0 &&
 | 
					 | 
				
			||||||
                ServiceProvider.Azure
 | 
					 | 
				
			||||||
              ) {
 | 
					 | 
				
			||||||
                const contentFilterResults =
 | 
					 | 
				
			||||||
                  textmoderation[0]?.content_filter_results;
 | 
					 | 
				
			||||||
                console.log(
 | 
					 | 
				
			||||||
                  `[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`,
 | 
					 | 
				
			||||||
                  contentFilterResults,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            } catch (e) {
 | 
					 | 
				
			||||||
              console.error("[Request] parse error", text, msg);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onclose() {
 | 
					 | 
				
			||||||
            finish();
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onerror(e) {
 | 
					 | 
				
			||||||
            options.onError?.(e);
 | 
					 | 
				
			||||||
            throw e;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          openWhenHidden: true,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const resJson = await res.json();
 | 
					        const resJson = await res.json();
 | 
				
			||||||
        const message = await this.extractMessage(resJson);
 | 
					        const message = await this.extractMessage(resJson);
 | 
				
			||||||
        options.onFinish(message);
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
@@ -461,7 +491,9 @@ export class ChatGPTApi implements LLMApi {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const resJson = (await res.json()) as OpenAIListModelResponse;
 | 
					    const resJson = (await res.json()) as OpenAIListModelResponse;
 | 
				
			||||||
    const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-"));
 | 
					    const chatModels = resJson.data?.filter(
 | 
				
			||||||
 | 
					      (m) => m.id.startsWith("gpt-") || m.id.startsWith("chatgpt-"),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    console.log("[Models]", chatModels);
 | 
					    console.log("[Models]", chatModels);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!chatModels) {
 | 
					    if (!chatModels) {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										287
									
								
								app/client/platforms/siliconflow.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,287 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					// azure and openai, using same models. so using same LLMApi.
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  SILICONFLOW_BASE_URL,
 | 
				
			||||||
 | 
					  SiliconFlow,
 | 
				
			||||||
 | 
					  DEFAULT_MODELS,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { preProcessImageContent, streamWithThink } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  getMessageTextContentWithoutThinking,
 | 
				
			||||||
 | 
					  isVisionModel,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					export interface SiliconFlowListModelResponse {
 | 
				
			||||||
 | 
					  object: string;
 | 
				
			||||||
 | 
					  data: Array<{
 | 
				
			||||||
 | 
					    id: string;
 | 
				
			||||||
 | 
					    object: string;
 | 
				
			||||||
 | 
					    root: string;
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SiliconflowApi implements LLMApi {
 | 
				
			||||||
 | 
					  private disableListModels = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.siliconflowUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      const apiPath = ApiPath.SiliconFlow;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? SILICONFLOW_BASE_URL : apiPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      !baseUrl.startsWith("http") &&
 | 
				
			||||||
 | 
					      !baseUrl.startsWith(ApiPath.SiliconFlow)
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const visionModel = isVisionModel(options.config.model);
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      if (v.role === "assistant") {
 | 
				
			||||||
 | 
					        const content = getMessageTextContentWithoutThinking(v);
 | 
				
			||||||
 | 
					        messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const content = visionModel
 | 
				
			||||||
 | 
					          ? await preProcessImageContent(v.content)
 | 
				
			||||||
 | 
					          : getMessageTextContent(v);
 | 
				
			||||||
 | 
					        messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					        providerName: options.config.providerName,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const requestPayload: RequestPayload = {
 | 
				
			||||||
 | 
					      messages,
 | 
				
			||||||
 | 
					      stream: options.config.stream,
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      presence_penalty: modelConfig.presence_penalty,
 | 
				
			||||||
 | 
					      frequency_penalty: modelConfig.frequency_penalty,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					      // max_tokens: Math.max(modelConfig.max_tokens, 1024),
 | 
				
			||||||
 | 
					      // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] openai payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPath = this.path(SiliconFlow.ChatPath);
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // console.log(chatPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Use extended timeout for thinking models as they typically require more processing time
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return streamWithThink(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string | null;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					                reasoning_content: string | null;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const reasoning = choices[0]?.delta?.reasoning_content;
 | 
				
			||||||
 | 
					            const content = choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Skip if both content and reasoning_content are empty or null
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					              (!reasoning || reasoning.length === 0) &&
 | 
				
			||||||
 | 
					              (!content || content.length === 0)
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: "",
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (reasoning && reasoning.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: true,
 | 
				
			||||||
 | 
					                content: reasoning,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            } else if (content && content.length > 0) {
 | 
				
			||||||
 | 
					              return {
 | 
				
			||||||
 | 
					                isThinking: false,
 | 
				
			||||||
 | 
					                content: content,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              isThinking: false,
 | 
				
			||||||
 | 
					              content: "",
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    if (this.disableListModels) {
 | 
				
			||||||
 | 
					      return DEFAULT_MODELS.slice();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const res = await fetch(this.path(SiliconFlow.ListModelPath), {
 | 
				
			||||||
 | 
					      method: "GET",
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        ...getHeaders(),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const resJson = (await res.json()) as SiliconFlowListModelResponse;
 | 
				
			||||||
 | 
					    const chatModels = resJson.data;
 | 
				
			||||||
 | 
					    console.log("[Models]", chatModels);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!chatModels) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let seq = 1000; //同 Constant.ts 中的排序保持一致
 | 
				
			||||||
 | 
					    return chatModels.map((m) => ({
 | 
				
			||||||
 | 
					      name: m.id,
 | 
				
			||||||
 | 
					      available: true,
 | 
				
			||||||
 | 
					      sorted: seq++,
 | 
				
			||||||
 | 
					      provider: {
 | 
				
			||||||
 | 
					        id: "siliconflow",
 | 
				
			||||||
 | 
					        providerName: "SiliconFlow",
 | 
				
			||||||
 | 
					        providerType: "siliconflow",
 | 
				
			||||||
 | 
					        sorted: 14,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
"use client";
 | 
					"use client";
 | 
				
			||||||
import { ApiPath, DEFAULT_API_HOST, REQUEST_TIMEOUT_MS } from "@/app/constant";
 | 
					import { ApiPath, TENCENT_BASE_URL } from "@/app/constant";
 | 
				
			||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
					import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@@ -8,6 +8,7 @@ import {
 | 
				
			|||||||
  LLMApi,
 | 
					  LLMApi,
 | 
				
			||||||
  LLMModel,
 | 
					  LLMModel,
 | 
				
			||||||
  MultimodalContent,
 | 
					  MultimodalContent,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
} from "../api";
 | 
					} from "../api";
 | 
				
			||||||
import Locale from "../../locales";
 | 
					import Locale from "../../locales";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@@ -16,11 +17,16 @@ import {
 | 
				
			|||||||
} from "@fortaine/fetch-event-source";
 | 
					} from "@fortaine/fetch-event-source";
 | 
				
			||||||
import { prettyObject } from "@/app/utils/format";
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
import { getClientConfig } from "@/app/config/client";
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
 | 
					import {
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  isVisionModel,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
import mapKeys from "lodash-es/mapKeys";
 | 
					import mapKeys from "lodash-es/mapKeys";
 | 
				
			||||||
import mapValues from "lodash-es/mapValues";
 | 
					import mapValues from "lodash-es/mapValues";
 | 
				
			||||||
import isArray from "lodash-es/isArray";
 | 
					import isArray from "lodash-es/isArray";
 | 
				
			||||||
import isObject from "lodash-es/isObject";
 | 
					import isObject from "lodash-es/isObject";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface OpenAIListModelResponse {
 | 
					export interface OpenAIListModelResponse {
 | 
				
			||||||
  object: string;
 | 
					  object: string;
 | 
				
			||||||
@@ -69,9 +75,7 @@ export class HunyuanApi implements LLMApi {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
      const isApp = !!getClientConfig()?.isApp;
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
      baseUrl = isApp
 | 
					      baseUrl = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
 | 
				
			||||||
        ? DEFAULT_API_HOST + "/api/proxy/tencent"
 | 
					 | 
				
			||||||
        : ApiPath.Tencent;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
@@ -89,6 +93,10 @@ export class HunyuanApi implements LLMApi {
 | 
				
			|||||||
    return res.Choices?.at(0)?.Message?.Content ?? "";
 | 
					    return res.Choices?.at(0)?.Message?.Content ?? "";
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async chat(options: ChatOptions) {
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
    const visionModel = isVisionModel(options.config.model);
 | 
					    const visionModel = isVisionModel(options.config.model);
 | 
				
			||||||
    const messages = options.messages.map((v, index) => ({
 | 
					    const messages = options.messages.map((v, index) => ({
 | 
				
			||||||
@@ -131,13 +139,14 @@ export class HunyuanApi implements LLMApi {
 | 
				
			|||||||
      // make a fetch request
 | 
					      // make a fetch request
 | 
				
			||||||
      const requestTimeoutId = setTimeout(
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
        () => controller.abort(),
 | 
					        () => controller.abort(),
 | 
				
			||||||
        REQUEST_TIMEOUT_MS,
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (shouldStream) {
 | 
					      if (shouldStream) {
 | 
				
			||||||
        let responseText = "";
 | 
					        let responseText = "";
 | 
				
			||||||
        let remainText = "";
 | 
					        let remainText = "";
 | 
				
			||||||
        let finished = false;
 | 
					        let finished = false;
 | 
				
			||||||
 | 
					        let responseRes: Response;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // animate response to make it looks smooth
 | 
					        // animate response to make it looks smooth
 | 
				
			||||||
        function animateResponseText() {
 | 
					        function animateResponseText() {
 | 
				
			||||||
@@ -167,13 +176,14 @@ export class HunyuanApi implements LLMApi {
 | 
				
			|||||||
        const finish = () => {
 | 
					        const finish = () => {
 | 
				
			||||||
          if (!finished) {
 | 
					          if (!finished) {
 | 
				
			||||||
            finished = true;
 | 
					            finished = true;
 | 
				
			||||||
            options.onFinish(responseText + remainText);
 | 
					            options.onFinish(responseText + remainText, responseRes);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        controller.signal.onabort = finish;
 | 
					        controller.signal.onabort = finish;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fetchEventSource(chatPath, {
 | 
					        fetchEventSource(chatPath, {
 | 
				
			||||||
 | 
					          fetch: fetch as any,
 | 
				
			||||||
          ...chatPayload,
 | 
					          ...chatPayload,
 | 
				
			||||||
          async onopen(res) {
 | 
					          async onopen(res) {
 | 
				
			||||||
            clearTimeout(requestTimeoutId);
 | 
					            clearTimeout(requestTimeoutId);
 | 
				
			||||||
@@ -182,7 +192,7 @@ export class HunyuanApi implements LLMApi {
 | 
				
			|||||||
              "[Tencent] request response content type: ",
 | 
					              "[Tencent] request response content type: ",
 | 
				
			||||||
              contentType,
 | 
					              contentType,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					            responseRes = res;
 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					            if (contentType?.startsWith("text/plain")) {
 | 
				
			||||||
              responseText = await res.clone().text();
 | 
					              responseText = await res.clone().text();
 | 
				
			||||||
              return finish();
 | 
					              return finish();
 | 
				
			||||||
@@ -248,7 +258,7 @@ export class HunyuanApi implements LLMApi {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        const resJson = await res.json();
 | 
					        const resJson = await res.json();
 | 
				
			||||||
        const message = this.extractMessage(resJson);
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
        options.onFinish(message);
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      console.log("[Request] failed to make a chat request", e);
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										194
									
								
								app/client/platforms/xai.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,194 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					// azure and openai, using same models. so using same LLMApi.
 | 
				
			||||||
 | 
					import { ApiPath, XAI_BASE_URL, XAI } from "@/app/constant";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useAppConfig,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  usePluginStore,
 | 
				
			||||||
 | 
					} from "@/app/store";
 | 
				
			||||||
 | 
					import { stream } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} from "../api";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import { getTimeoutMSByModel } from "@/app/utils";
 | 
				
			||||||
 | 
					import { preProcessImageContent } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class XAIApi implements LLMApi {
 | 
				
			||||||
 | 
					  private disableListModels = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.xaiUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      const apiPath = ApiPath.XAI;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? XAI_BASE_URL : apiPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.XAI)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl, path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return [baseUrl, path].join("/");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  extractMessage(res: any) {
 | 
				
			||||||
 | 
					    return res.choices?.at(0)?.message?.content ?? "";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  speech(options: SpeechOptions): Promise<ArrayBuffer> {
 | 
				
			||||||
 | 
					    throw new Error("Method not implemented.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async chat(options: ChatOptions) {
 | 
				
			||||||
 | 
					    const messages: ChatOptions["messages"] = [];
 | 
				
			||||||
 | 
					    for (const v of options.messages) {
 | 
				
			||||||
 | 
					      const content = await preProcessImageContent(v.content);
 | 
				
			||||||
 | 
					      messages.push({ role: v.role, content });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					        providerName: options.config.providerName,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const requestPayload: RequestPayload = {
 | 
				
			||||||
 | 
					      messages,
 | 
				
			||||||
 | 
					      stream: options.config.stream,
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      presence_penalty: modelConfig.presence_penalty,
 | 
				
			||||||
 | 
					      frequency_penalty: modelConfig.frequency_penalty,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] xai payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPath = this.path(XAI.ChatPath);
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        getTimeoutMSByModel(options.config.model),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        return stream(
 | 
				
			||||||
 | 
					          chatPath,
 | 
				
			||||||
 | 
					          requestPayload,
 | 
				
			||||||
 | 
					          getHeaders(),
 | 
				
			||||||
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
 | 
					          controller,
 | 
				
			||||||
 | 
					          // parseSSE
 | 
				
			||||||
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					            const json = JSON.parse(text);
 | 
				
			||||||
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const index = tool_calls[0]?.index;
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              if (id) {
 | 
				
			||||||
 | 
					                runTools.push({
 | 
				
			||||||
 | 
					                  id,
 | 
				
			||||||
 | 
					                  type: tool_calls[0]?.type,
 | 
				
			||||||
 | 
					                  function: {
 | 
				
			||||||
 | 
					                    name: tool_calls[0]?.function?.name as string,
 | 
				
			||||||
 | 
					                    arguments: args,
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return choices[0]?.delta?.content;
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const resJson = await res.json();
 | 
				
			||||||
 | 
					        const message = this.extractMessage(resJson);
 | 
				
			||||||
 | 
					        options.onFinish(message, res);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.log("[Request] failed to make a chat request", e);
 | 
				
			||||||
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  async usage() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      used: 0,
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async models(): Promise<LLMModel[]> {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -38,6 +38,7 @@ interface ChatCommands {
 | 
				
			|||||||
  next?: Command;
 | 
					  next?: Command;
 | 
				
			||||||
  prev?: Command;
 | 
					  prev?: Command;
 | 
				
			||||||
  clear?: Command;
 | 
					  clear?: Command;
 | 
				
			||||||
 | 
					  fork?: Command;
 | 
				
			||||||
  del?: Command;
 | 
					  del?: Command;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,12 @@
 | 
				
			|||||||
import { useEffect, useState, useRef, useMemo } from "react";
 | 
					import {
 | 
				
			||||||
 | 
					  useEffect,
 | 
				
			||||||
 | 
					  useState,
 | 
				
			||||||
 | 
					  useRef,
 | 
				
			||||||
 | 
					  useMemo,
 | 
				
			||||||
 | 
					  forwardRef,
 | 
				
			||||||
 | 
					  useImperativeHandle,
 | 
				
			||||||
 | 
					} from "react";
 | 
				
			||||||
import { useParams } from "react-router";
 | 
					import { useParams } from "react-router";
 | 
				
			||||||
import { useWindowSize } from "@/app/utils";
 | 
					 | 
				
			||||||
import { IconButton } from "./button";
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
import { nanoid } from "nanoid";
 | 
					import { nanoid } from "nanoid";
 | 
				
			||||||
import ExportIcon from "../icons/share.svg";
 | 
					import ExportIcon from "../icons/share.svg";
 | 
				
			||||||
@@ -8,6 +14,7 @@ import CopyIcon from "../icons/copy.svg";
 | 
				
			|||||||
import DownloadIcon from "../icons/download.svg";
 | 
					import DownloadIcon from "../icons/download.svg";
 | 
				
			||||||
import GithubIcon from "../icons/github.svg";
 | 
					import GithubIcon from "../icons/github.svg";
 | 
				
			||||||
import LoadingButtonIcon from "../icons/loading.svg";
 | 
					import LoadingButtonIcon from "../icons/loading.svg";
 | 
				
			||||||
 | 
					import ReloadButtonIcon from "../icons/reload.svg";
 | 
				
			||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
import { Modal, showToast } from "./ui-lib";
 | 
					import { Modal, showToast } from "./ui-lib";
 | 
				
			||||||
import { copyToClipboard, downloadAs } from "../utils";
 | 
					import { copyToClipboard, downloadAs } from "../utils";
 | 
				
			||||||
@@ -15,73 +22,89 @@ import { Path, ApiPath, REPO_URL } from "@/app/constant";
 | 
				
			|||||||
import { Loading } from "./home";
 | 
					import { Loading } from "./home";
 | 
				
			||||||
import styles from "./artifacts.module.scss";
 | 
					import styles from "./artifacts.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function HTMLPreview(props: {
 | 
					type HTMLPreviewProps = {
 | 
				
			||||||
  code: string;
 | 
					  code: string;
 | 
				
			||||||
  autoHeight?: boolean;
 | 
					  autoHeight?: boolean;
 | 
				
			||||||
  height?: number | string;
 | 
					  height?: number | string;
 | 
				
			||||||
  onLoad?: (title?: string) => void;
 | 
					  onLoad?: (title?: string) => void;
 | 
				
			||||||
}) {
 | 
					};
 | 
				
			||||||
  const ref = useRef<HTMLIFrameElement>(null);
 | 
					 | 
				
			||||||
  const frameId = useRef<string>(nanoid());
 | 
					 | 
				
			||||||
  const [iframeHeight, setIframeHeight] = useState(600);
 | 
					 | 
				
			||||||
  const [title, setTitle] = useState("");
 | 
					 | 
				
			||||||
  /*
 | 
					 | 
				
			||||||
   * https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
 | 
					 | 
				
			||||||
   * 1. using srcdoc
 | 
					 | 
				
			||||||
   * 2. using src with dataurl:
 | 
					 | 
				
			||||||
   *    easy to share
 | 
					 | 
				
			||||||
   *    length limit (Data URIs cannot be larger than 32,768 characters.)
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					export type HTMLPreviewHander = {
 | 
				
			||||||
    const handleMessage = (e: any) => {
 | 
					  reload: () => void;
 | 
				
			||||||
      const { id, height, title } = e.data;
 | 
					};
 | 
				
			||||||
      setTitle(title);
 | 
					
 | 
				
			||||||
      if (id == frameId.current) {
 | 
					export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
 | 
				
			||||||
        setIframeHeight(height);
 | 
					  function HTMLPreview(props, ref) {
 | 
				
			||||||
 | 
					    const iframeRef = useRef<HTMLIFrameElement>(null);
 | 
				
			||||||
 | 
					    const [frameId, setFrameId] = useState<string>(nanoid());
 | 
				
			||||||
 | 
					    const [iframeHeight, setIframeHeight] = useState(600);
 | 
				
			||||||
 | 
					    const [title, setTitle] = useState("");
 | 
				
			||||||
 | 
					    /*
 | 
				
			||||||
 | 
					     * https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
 | 
				
			||||||
 | 
					     * 1. using srcdoc
 | 
				
			||||||
 | 
					     * 2. using src with dataurl:
 | 
				
			||||||
 | 
					     *    easy to share
 | 
				
			||||||
 | 
					     *    length limit (Data URIs cannot be larger than 32,768 characters.)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					      const handleMessage = (e: any) => {
 | 
				
			||||||
 | 
					        const { id, height, title } = e.data;
 | 
				
			||||||
 | 
					        setTitle(title);
 | 
				
			||||||
 | 
					        if (id == frameId) {
 | 
				
			||||||
 | 
					          setIframeHeight(height);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      window.addEventListener("message", handleMessage);
 | 
				
			||||||
 | 
					      return () => {
 | 
				
			||||||
 | 
					        window.removeEventListener("message", handleMessage);
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }, [frameId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useImperativeHandle(ref, () => ({
 | 
				
			||||||
 | 
					      reload: () => {
 | 
				
			||||||
 | 
					        setFrameId(nanoid());
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const height = useMemo(() => {
 | 
				
			||||||
 | 
					      if (!props.autoHeight) return props.height || 600;
 | 
				
			||||||
 | 
					      if (typeof props.height === "string") {
 | 
				
			||||||
 | 
					        return props.height;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const parentHeight = props.height || 600;
 | 
				
			||||||
 | 
					      return iframeHeight + 40 > parentHeight
 | 
				
			||||||
 | 
					        ? parentHeight
 | 
				
			||||||
 | 
					        : iframeHeight + 40;
 | 
				
			||||||
 | 
					    }, [props.autoHeight, props.height, iframeHeight]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const srcDoc = useMemo(() => {
 | 
				
			||||||
 | 
					      const script = `<script>window.addEventListener("DOMContentLoaded", () => new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body))</script>`;
 | 
				
			||||||
 | 
					      if (props.code.includes("<!DOCTYPE html>")) {
 | 
				
			||||||
 | 
					        props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return script + props.code;
 | 
				
			||||||
 | 
					    }, [props.code, frameId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleOnLoad = () => {
 | 
				
			||||||
 | 
					      if (props?.onLoad) {
 | 
				
			||||||
 | 
					        props.onLoad(title);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    window.addEventListener("message", handleMessage);
 | 
					 | 
				
			||||||
    return () => {
 | 
					 | 
				
			||||||
      window.removeEventListener("message", handleMessage);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }, []);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const height = useMemo(() => {
 | 
					    return (
 | 
				
			||||||
    if (!props.autoHeight) return props.height || 600;
 | 
					      <iframe
 | 
				
			||||||
    if (typeof props.height === "string") {
 | 
					        className={styles["artifacts-iframe"]}
 | 
				
			||||||
      return props.height;
 | 
					        key={frameId}
 | 
				
			||||||
    }
 | 
					        ref={iframeRef}
 | 
				
			||||||
    const parentHeight = props.height || 600;
 | 
					        sandbox="allow-forms allow-modals allow-scripts"
 | 
				
			||||||
    return iframeHeight + 40 > parentHeight ? parentHeight : iframeHeight + 40;
 | 
					        style={{ height }}
 | 
				
			||||||
  }, [props.autoHeight, props.height, iframeHeight]);
 | 
					        srcDoc={srcDoc}
 | 
				
			||||||
 | 
					        onLoad={handleOnLoad}
 | 
				
			||||||
  const srcDoc = useMemo(() => {
 | 
					      />
 | 
				
			||||||
    const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId.current}', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>`;
 | 
					    );
 | 
				
			||||||
    if (props.code.includes("</head>")) {
 | 
					  },
 | 
				
			||||||
      props.code.replace("</head>", "</head>" + script);
 | 
					);
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return props.code + script;
 | 
					 | 
				
			||||||
  }, [props.code]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleOnLoad = () => {
 | 
					 | 
				
			||||||
    if (props?.onLoad) {
 | 
					 | 
				
			||||||
      props.onLoad(title);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <iframe
 | 
					 | 
				
			||||||
      className={styles["artifacts-iframe"]}
 | 
					 | 
				
			||||||
      id={frameId.current}
 | 
					 | 
				
			||||||
      ref={ref}
 | 
					 | 
				
			||||||
      sandbox="allow-forms allow-modals allow-scripts"
 | 
					 | 
				
			||||||
      style={{ height }}
 | 
					 | 
				
			||||||
      srcDoc={srcDoc}
 | 
					 | 
				
			||||||
      onLoad={handleOnLoad}
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ArtifactsShareButton({
 | 
					export function ArtifactsShareButton({
 | 
				
			||||||
  getCode,
 | 
					  getCode,
 | 
				
			||||||
@@ -184,6 +207,7 @@ export function Artifacts() {
 | 
				
			|||||||
  const [code, setCode] = useState("");
 | 
					  const [code, setCode] = useState("");
 | 
				
			||||||
  const [loading, setLoading] = useState(true);
 | 
					  const [loading, setLoading] = useState(true);
 | 
				
			||||||
  const [fileName, setFileName] = useState("");
 | 
					  const [fileName, setFileName] = useState("");
 | 
				
			||||||
 | 
					  const previewRef = useRef<HTMLPreviewHander>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (id) {
 | 
					    if (id) {
 | 
				
			||||||
@@ -208,6 +232,13 @@ export function Artifacts() {
 | 
				
			|||||||
        <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
 | 
					        <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
 | 
				
			||||||
          <IconButton bordered icon={<GithubIcon />} shadow />
 | 
					          <IconButton bordered icon={<GithubIcon />} shadow />
 | 
				
			||||||
        </a>
 | 
					        </a>
 | 
				
			||||||
 | 
					        <IconButton
 | 
				
			||||||
 | 
					          bordered
 | 
				
			||||||
 | 
					          style={{ marginLeft: 20 }}
 | 
				
			||||||
 | 
					          icon={<ReloadButtonIcon />}
 | 
				
			||||||
 | 
					          shadow
 | 
				
			||||||
 | 
					          onClick={() => previewRef.current?.reload()}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
        <div className={styles["artifacts-title"]}>NextChat Artifacts</div>
 | 
					        <div className={styles["artifacts-title"]}>NextChat Artifacts</div>
 | 
				
			||||||
        <ArtifactsShareButton
 | 
					        <ArtifactsShareButton
 | 
				
			||||||
          id={id}
 | 
					          id={id}
 | 
				
			||||||
@@ -220,6 +251,7 @@ export function Artifacts() {
 | 
				
			|||||||
        {code && (
 | 
					        {code && (
 | 
				
			||||||
          <HTMLPreview
 | 
					          <HTMLPreview
 | 
				
			||||||
            code={code}
 | 
					            code={code}
 | 
				
			||||||
 | 
					            ref={previewRef}
 | 
				
			||||||
            autoHeight={false}
 | 
					            autoHeight={false}
 | 
				
			||||||
            height={"100%"}
 | 
					            height={"100%"}
 | 
				
			||||||
            onLoad={(title) => {
 | 
					            onLoad={(title) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,70 @@
 | 
				
			|||||||
.auth-page {
 | 
					.auth-page {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: flex-start;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  height: 100%;
 | 
					  height: 100%;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  .top-banner {
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    padding: 12px 64px;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    background: var(--second);
 | 
				
			||||||
 | 
					    .top-banner-inner {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      font-size: 14px;
 | 
				
			||||||
 | 
					      line-height: 150%;
 | 
				
			||||||
 | 
					      span {
 | 
				
			||||||
 | 
					        gap: 8px;
 | 
				
			||||||
 | 
					        a {
 | 
				
			||||||
 | 
					          display: inline-flex;
 | 
				
			||||||
 | 
					          align-items: center;
 | 
				
			||||||
 | 
					          text-decoration: none;
 | 
				
			||||||
 | 
					          margin-left: 8px;
 | 
				
			||||||
 | 
					          color: var(--primary);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .top-banner-close {
 | 
				
			||||||
 | 
					      cursor: pointer;
 | 
				
			||||||
 | 
					      position: absolute;
 | 
				
			||||||
 | 
					      top: 50%;
 | 
				
			||||||
 | 
					      right: 48px;
 | 
				
			||||||
 | 
					      transform: translateY(-50%);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (max-width: 600px) {
 | 
				
			||||||
 | 
					    .top-banner {
 | 
				
			||||||
 | 
					      padding: 12px 24px 12px 12px;
 | 
				
			||||||
 | 
					      .top-banner-close {
 | 
				
			||||||
 | 
					        right: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .top-banner-inner {
 | 
				
			||||||
 | 
					        .top-banner-logo {
 | 
				
			||||||
 | 
					          margin-right: 8px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .auth-header {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    padding: 10px;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    animation: slide-in-from-top ease 0.3s;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .auth-logo {
 | 
					  .auth-logo {
 | 
				
			||||||
 | 
					    margin-top: 10vh;
 | 
				
			||||||
    transform: scale(1.4);
 | 
					    transform: scale(1.4);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -14,6 +72,7 @@
 | 
				
			|||||||
    font-size: 24px;
 | 
					    font-size: 24px;
 | 
				
			||||||
    font-weight: bold;
 | 
					    font-weight: bold;
 | 
				
			||||||
    line-height: 2;
 | 
					    line-height: 2;
 | 
				
			||||||
 | 
					    margin-bottom: 1vh;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .auth-tips {
 | 
					  .auth-tips {
 | 
				
			||||||
@@ -24,6 +83,10 @@
 | 
				
			|||||||
    margin: 3vh 0;
 | 
					    margin: 3vh 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .auth-input-second {
 | 
				
			||||||
 | 
					    margin: 0 0 3vh 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .auth-actions {
 | 
					  .auth-actions {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    justify-content: center;
 | 
					    justify-content: center;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,21 +1,37 @@
 | 
				
			|||||||
import styles from "./auth.module.scss";
 | 
					import styles from "./auth.module.scss";
 | 
				
			||||||
import { IconButton } from "./button";
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
 | 
					import { useState, useEffect } from "react";
 | 
				
			||||||
import { useNavigate } from "react-router-dom";
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
import { Path } from "../constant";
 | 
					import { Path, SAAS_CHAT_URL } from "../constant";
 | 
				
			||||||
import { useAccessStore } from "../store";
 | 
					import { useAccessStore } from "../store";
 | 
				
			||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
 | 
					import Delete from "../icons/close.svg";
 | 
				
			||||||
 | 
					import Arrow from "../icons/arrow.svg";
 | 
				
			||||||
 | 
					import Logo from "../icons/logo.svg";
 | 
				
			||||||
 | 
					import { useMobileScreen } from "@/app/utils";
 | 
				
			||||||
import BotIcon from "../icons/bot.svg";
 | 
					import BotIcon from "../icons/bot.svg";
 | 
				
			||||||
import { useEffect } from "react";
 | 
					 | 
				
			||||||
import { getClientConfig } from "../config/client";
 | 
					import { getClientConfig } from "../config/client";
 | 
				
			||||||
 | 
					import { PasswordInput } from "./ui-lib";
 | 
				
			||||||
 | 
					import LeftIcon from "@/app/icons/left.svg";
 | 
				
			||||||
 | 
					import { safeLocalStorage } from "@/app/utils";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  trackSettingsPageGuideToCPaymentClick,
 | 
				
			||||||
 | 
					  trackAuthorizationPageButtonToCPaymentClick,
 | 
				
			||||||
 | 
					} from "../utils/auth-settings-events";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const storage = safeLocalStorage();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function AuthPage() {
 | 
					export function AuthPage() {
 | 
				
			||||||
  const navigate = useNavigate();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  const accessStore = useAccessStore();
 | 
					  const accessStore = useAccessStore();
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const goHome = () => navigate(Path.Home);
 | 
					  const goHome = () => navigate(Path.Home);
 | 
				
			||||||
  const goChat = () => navigate(Path.Chat);
 | 
					  const goChat = () => navigate(Path.Chat);
 | 
				
			||||||
 | 
					  const goSaas = () => {
 | 
				
			||||||
 | 
					    trackAuthorizationPageButtonToCPaymentClick();
 | 
				
			||||||
 | 
					    window.location.href = SAAS_CHAT_URL;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const resetAccessCode = () => {
 | 
					  const resetAccessCode = () => {
 | 
				
			||||||
    accessStore.update((access) => {
 | 
					    accessStore.update((access) => {
 | 
				
			||||||
      access.openaiApiKey = "";
 | 
					      access.openaiApiKey = "";
 | 
				
			||||||
@@ -32,43 +48,58 @@ export function AuthPage() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={styles["auth-page"]}>
 | 
					    <div className={styles["auth-page"]}>
 | 
				
			||||||
      <div className={`no-dark ${styles["auth-logo"]}`}>
 | 
					      <TopBanner></TopBanner>
 | 
				
			||||||
 | 
					      <div className={styles["auth-header"]}>
 | 
				
			||||||
 | 
					        <IconButton
 | 
				
			||||||
 | 
					          icon={<LeftIcon />}
 | 
				
			||||||
 | 
					          text={Locale.Auth.Return}
 | 
				
			||||||
 | 
					          onClick={() => navigate(Path.Home)}
 | 
				
			||||||
 | 
					        ></IconButton>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div className={clsx("no-dark", styles["auth-logo"])}>
 | 
				
			||||||
        <BotIcon />
 | 
					        <BotIcon />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
 | 
					      <div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
 | 
				
			||||||
      <div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
 | 
					      <div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <input
 | 
					      <PasswordInput
 | 
				
			||||||
        className={styles["auth-input"]}
 | 
					        style={{ marginTop: "3vh", marginBottom: "3vh" }}
 | 
				
			||||||
        type="password"
 | 
					        aria={Locale.Settings.ShowPassword}
 | 
				
			||||||
        placeholder={Locale.Auth.Input}
 | 
					        aria-label={Locale.Auth.Input}
 | 
				
			||||||
        value={accessStore.accessCode}
 | 
					        value={accessStore.accessCode}
 | 
				
			||||||
 | 
					        type="text"
 | 
				
			||||||
 | 
					        placeholder={Locale.Auth.Input}
 | 
				
			||||||
        onChange={(e) => {
 | 
					        onChange={(e) => {
 | 
				
			||||||
          accessStore.update(
 | 
					          accessStore.update(
 | 
				
			||||||
            (access) => (access.accessCode = e.currentTarget.value),
 | 
					            (access) => (access.accessCode = e.currentTarget.value),
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {!accessStore.hideUserApiKey ? (
 | 
					      {!accessStore.hideUserApiKey ? (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
          <div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
 | 
					          <div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
 | 
				
			||||||
          <input
 | 
					          <PasswordInput
 | 
				
			||||||
            className={styles["auth-input"]}
 | 
					            style={{ marginTop: "3vh", marginBottom: "3vh" }}
 | 
				
			||||||
            type="password"
 | 
					            aria={Locale.Settings.ShowPassword}
 | 
				
			||||||
            placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
 | 
					            aria-label={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
 | 
				
			||||||
            value={accessStore.openaiApiKey}
 | 
					            value={accessStore.openaiApiKey}
 | 
				
			||||||
 | 
					            type="text"
 | 
				
			||||||
 | 
					            placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
 | 
				
			||||||
            onChange={(e) => {
 | 
					            onChange={(e) => {
 | 
				
			||||||
              accessStore.update(
 | 
					              accessStore.update(
 | 
				
			||||||
                (access) => (access.openaiApiKey = e.currentTarget.value),
 | 
					                (access) => (access.openaiApiKey = e.currentTarget.value),
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
            }}
 | 
					            }}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
          <input
 | 
					          <PasswordInput
 | 
				
			||||||
            className={styles["auth-input"]}
 | 
					            style={{ marginTop: "3vh", marginBottom: "3vh" }}
 | 
				
			||||||
            type="password"
 | 
					            aria={Locale.Settings.ShowPassword}
 | 
				
			||||||
            placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
 | 
					            aria-label={Locale.Settings.Access.Google.ApiKey.Placeholder}
 | 
				
			||||||
            value={accessStore.googleApiKey}
 | 
					            value={accessStore.googleApiKey}
 | 
				
			||||||
 | 
					            type="text"
 | 
				
			||||||
 | 
					            placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
 | 
				
			||||||
            onChange={(e) => {
 | 
					            onChange={(e) => {
 | 
				
			||||||
              accessStore.update(
 | 
					              accessStore.update(
 | 
				
			||||||
                (access) => (access.googleApiKey = e.currentTarget.value),
 | 
					                (access) => (access.googleApiKey = e.currentTarget.value),
 | 
				
			||||||
@@ -85,13 +116,74 @@ export function AuthPage() {
 | 
				
			|||||||
          onClick={goChat}
 | 
					          onClick={goChat}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <IconButton
 | 
					        <IconButton
 | 
				
			||||||
          text={Locale.Auth.Later}
 | 
					          text={Locale.Auth.SaasTips}
 | 
				
			||||||
          onClick={() => {
 | 
					          onClick={() => {
 | 
				
			||||||
            resetAccessCode();
 | 
					            goSaas();
 | 
				
			||||||
            goHome();
 | 
					 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function TopBanner() {
 | 
				
			||||||
 | 
					  const [isHovered, setIsHovered] = useState(false);
 | 
				
			||||||
 | 
					  const [isVisible, setIsVisible] = useState(true);
 | 
				
			||||||
 | 
					  const isMobile = useMobileScreen();
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    // 检查 localStorage 中是否有标记
 | 
				
			||||||
 | 
					    const bannerDismissed = storage.getItem("bannerDismissed");
 | 
				
			||||||
 | 
					    // 如果标记不存在,存储默认值并显示横幅
 | 
				
			||||||
 | 
					    if (!bannerDismissed) {
 | 
				
			||||||
 | 
					      storage.setItem("bannerDismissed", "false");
 | 
				
			||||||
 | 
					      setIsVisible(true); // 显示横幅
 | 
				
			||||||
 | 
					    } else if (bannerDismissed === "true") {
 | 
				
			||||||
 | 
					      // 如果标记为 "true",则隐藏横幅
 | 
				
			||||||
 | 
					      setIsVisible(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleMouseEnter = () => {
 | 
				
			||||||
 | 
					    setIsHovered(true);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleMouseLeave = () => {
 | 
				
			||||||
 | 
					    setIsHovered(false);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleClose = () => {
 | 
				
			||||||
 | 
					    setIsVisible(false);
 | 
				
			||||||
 | 
					    storage.setItem("bannerDismissed", "true");
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!isVisible) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={styles["top-banner"]}
 | 
				
			||||||
 | 
					      onMouseEnter={handleMouseEnter}
 | 
				
			||||||
 | 
					      onMouseLeave={handleMouseLeave}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div className={clsx(styles["top-banner-inner"], "no-dark")}>
 | 
				
			||||||
 | 
					        <Logo className={styles["top-banner-logo"]}></Logo>
 | 
				
			||||||
 | 
					        <span>
 | 
				
			||||||
 | 
					          {Locale.Auth.TopTips}
 | 
				
			||||||
 | 
					          <a
 | 
				
			||||||
 | 
					            href={SAAS_CHAT_URL}
 | 
				
			||||||
 | 
					            rel="stylesheet"
 | 
				
			||||||
 | 
					            onClick={() => {
 | 
				
			||||||
 | 
					              trackSettingsPageGuideToCPaymentClick();
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {Locale.Settings.Access.SaasStart.ChatNow}
 | 
				
			||||||
 | 
					            <Arrow style={{ marginLeft: "4px" }} />
 | 
				
			||||||
 | 
					          </a>
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      {(isHovered || isMobile) && (
 | 
				
			||||||
 | 
					        <Delete className={styles["top-banner-close"]} onClick={handleClose} />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,6 @@
 | 
				
			|||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  padding: 10px;
 | 
					  padding: 10px;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  transition: all 0.3s ease;
 | 
					  transition: all 0.3s ease;
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import * as React from "react";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import styles from "./button.module.scss";
 | 
					import styles from "./button.module.scss";
 | 
				
			||||||
import { CSSProperties } from "react";
 | 
					import { CSSProperties } from "react";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ButtonType = "primary" | "danger" | null;
 | 
					export type ButtonType = "primary" | "danger" | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -22,12 +23,16 @@ export function IconButton(props: {
 | 
				
			|||||||
}) {
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <button
 | 
					    <button
 | 
				
			||||||
      className={
 | 
					      className={clsx(
 | 
				
			||||||
        styles["icon-button"] +
 | 
					        "clickable",
 | 
				
			||||||
        ` ${props.bordered && styles.border} ${props.shadow && styles.shadow} ${
 | 
					        styles["icon-button"],
 | 
				
			||||||
          props.className ?? ""
 | 
					        {
 | 
				
			||||||
        } clickable ${styles[props.type ?? ""]}`
 | 
					          [styles.border]: props.bordered,
 | 
				
			||||||
      }
 | 
					          [styles.shadow]: props.shadow,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        styles[props.type ?? ""],
 | 
				
			||||||
 | 
					        props.className,
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
      onClick={props.onClick}
 | 
					      onClick={props.onClick}
 | 
				
			||||||
      title={props.title}
 | 
					      title={props.title}
 | 
				
			||||||
      disabled={props.disabled}
 | 
					      disabled={props.disabled}
 | 
				
			||||||
@@ -40,10 +45,9 @@ export function IconButton(props: {
 | 
				
			|||||||
      {props.icon && (
 | 
					      {props.icon && (
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
          aria-label={props.text || props.title}
 | 
					          aria-label={props.text || props.title}
 | 
				
			||||||
          className={
 | 
					          className={clsx(styles["icon-button-icon"], {
 | 
				
			||||||
            styles["icon-button-icon"] +
 | 
					            "no-dark": props.type === "primary",
 | 
				
			||||||
            ` ${props.type === "primary" && "no-dark"}`
 | 
					          })}
 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          {props.icon}
 | 
					          {props.icon}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
import DeleteIcon from "../icons/delete.svg";
 | 
					import DeleteIcon from "../icons/delete.svg";
 | 
				
			||||||
import BotIcon from "../icons/bot.svg";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import styles from "./home.module.scss";
 | 
					import styles from "./home.module.scss";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@@ -12,13 +11,14 @@ import {
 | 
				
			|||||||
import { useChatStore } from "../store";
 | 
					import { useChatStore } from "../store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
 | 
					import { useLocation, useNavigate } from "react-router-dom";
 | 
				
			||||||
import { Path } from "../constant";
 | 
					import { Path } from "../constant";
 | 
				
			||||||
import { MaskAvatar } from "./mask";
 | 
					import { MaskAvatar } from "./mask";
 | 
				
			||||||
import { Mask } from "../store/mask";
 | 
					import { Mask } from "../store/mask";
 | 
				
			||||||
import { useRef, useEffect } from "react";
 | 
					import { useRef, useEffect } from "react";
 | 
				
			||||||
import { showConfirm } from "./ui-lib";
 | 
					import { showConfirm } from "./ui-lib";
 | 
				
			||||||
import { useMobileScreen } from "../utils";
 | 
					import { useMobileScreen } from "../utils";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ChatItem(props: {
 | 
					export function ChatItem(props: {
 | 
				
			||||||
  onClick?: () => void;
 | 
					  onClick?: () => void;
 | 
				
			||||||
@@ -46,11 +46,11 @@ export function ChatItem(props: {
 | 
				
			|||||||
    <Draggable draggableId={`${props.id}`} index={props.index}>
 | 
					    <Draggable draggableId={`${props.id}`} index={props.index}>
 | 
				
			||||||
      {(provided) => (
 | 
					      {(provided) => (
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
          className={`${styles["chat-item"]} ${
 | 
					          className={clsx(styles["chat-item"], {
 | 
				
			||||||
            props.selected &&
 | 
					            [styles["chat-item-selected"]]:
 | 
				
			||||||
            (currentPath === Path.Chat || currentPath === Path.Home) &&
 | 
					              props.selected &&
 | 
				
			||||||
            styles["chat-item-selected"]
 | 
					              (currentPath === Path.Chat || currentPath === Path.Home),
 | 
				
			||||||
          }`}
 | 
					          })}
 | 
				
			||||||
          onClick={props.onClick}
 | 
					          onClick={props.onClick}
 | 
				
			||||||
          ref={(ele) => {
 | 
					          ref={(ele) => {
 | 
				
			||||||
            draggableRef.current = ele;
 | 
					            draggableRef.current = ele;
 | 
				
			||||||
@@ -64,7 +64,7 @@ export function ChatItem(props: {
 | 
				
			|||||||
        >
 | 
					        >
 | 
				
			||||||
          {props.narrow ? (
 | 
					          {props.narrow ? (
 | 
				
			||||||
            <div className={styles["chat-item-narrow"]}>
 | 
					            <div className={styles["chat-item-narrow"]}>
 | 
				
			||||||
              <div className={styles["chat-item-avatar"] + " no-dark"}>
 | 
					              <div className={clsx(styles["chat-item-avatar"], "no-dark")}>
 | 
				
			||||||
                <MaskAvatar
 | 
					                <MaskAvatar
 | 
				
			||||||
                  avatar={props.mask.avatar}
 | 
					                  avatar={props.mask.avatar}
 | 
				
			||||||
                  model={props.mask.modelConfig.model}
 | 
					                  model={props.mask.modelConfig.model}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -45,6 +45,14 @@
 | 
				
			|||||||
.chat-input-actions {
 | 
					.chat-input-actions {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-wrap: wrap;
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  gap: 5px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &-end {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    margin-left: auto;
 | 
				
			||||||
 | 
					    gap: 5px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .chat-input-action {
 | 
					  .chat-input-action {
 | 
				
			||||||
    display: inline-flex;
 | 
					    display: inline-flex;
 | 
				
			||||||
@@ -62,10 +70,6 @@
 | 
				
			|||||||
    width: var(--icon-width);
 | 
					    width: var(--icon-width);
 | 
				
			||||||
    overflow: hidden;
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &:not(:last-child) {
 | 
					 | 
				
			||||||
      margin-right: 5px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .text {
 | 
					    .text {
 | 
				
			||||||
      white-space: nowrap;
 | 
					      white-space: nowrap;
 | 
				
			||||||
      padding-left: 5px;
 | 
					      padding-left: 5px;
 | 
				
			||||||
@@ -231,10 +235,12 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  animation: slide-in ease 0.3s;
 | 
					  animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $linear: linear-gradient(to right,
 | 
					  $linear: linear-gradient(
 | 
				
			||||||
      rgba(0, 0, 0, 0),
 | 
					    to right,
 | 
				
			||||||
      rgba(0, 0, 0, 1),
 | 
					    rgba(0, 0, 0, 0),
 | 
				
			||||||
      rgba(0, 0, 0, 0));
 | 
					    rgba(0, 0, 0, 1),
 | 
				
			||||||
 | 
					    rgba(0, 0, 0, 0)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
  mask-image: $linear;
 | 
					  mask-image: $linear;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @mixin show {
 | 
					  @mixin show {
 | 
				
			||||||
@@ -373,7 +379,7 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-message-user>.chat-message-container {
 | 
					.chat-message-user > .chat-message-container {
 | 
				
			||||||
  align-items: flex-end;
 | 
					  align-items: flex-end;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -413,6 +419,21 @@
 | 
				
			|||||||
  margin-top: 5px;
 | 
					  margin-top: 5px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-tools {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: #aaa;
 | 
				
			||||||
 | 
					  line-height: 1.5;
 | 
				
			||||||
 | 
					  margin-top: 5px;
 | 
				
			||||||
 | 
					  .chat-message-tool {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: end;
 | 
				
			||||||
 | 
					    svg {
 | 
				
			||||||
 | 
					      margin-left: 5px;
 | 
				
			||||||
 | 
					      margin-right: 5px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-message-item {
 | 
					.chat-message-item {
 | 
				
			||||||
  box-sizing: border-box;
 | 
					  box-sizing: border-box;
 | 
				
			||||||
  max-width: 100%;
 | 
					  max-width: 100%;
 | 
				
			||||||
@@ -428,6 +449,25 @@
 | 
				
			|||||||
  transition: all ease 0.3s;
 | 
					  transition: all ease 0.3s;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-message-audio {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  border-radius: 10px;
 | 
				
			||||||
 | 
					  background-color: rgba(0, 0, 0, 0.05);
 | 
				
			||||||
 | 
					  border: var(--border-in-light);
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  transition: all ease 0.3s;
 | 
				
			||||||
 | 
					  margin-top: 10px;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  user-select: text;
 | 
				
			||||||
 | 
					  word-break: break-word;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  audio {
 | 
				
			||||||
 | 
					    height: 30px; /* 调整高度 */
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-message-item-image {
 | 
					.chat-message-item-image {
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  margin-top: 10px;
 | 
					  margin-top: 10px;
 | 
				
			||||||
@@ -456,9 +496,8 @@
 | 
				
			|||||||
  border: rgba($color: #888, $alpha: 0.2) 1px solid;
 | 
					  border: rgba($color: #888, $alpha: 0.2) 1px solid;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
@media only screen and (max-width: 600px) {
 | 
					@media only screen and (max-width: 600px) {
 | 
				
			||||||
  $calc-image-width: calc(100vw/3*2/var(--image-count));
 | 
					  $calc-image-width: calc(100vw / 3 * 2 / var(--image-count));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .chat-message-item-image-multi {
 | 
					  .chat-message-item-image-multi {
 | 
				
			||||||
    width: $calc-image-width;
 | 
					    width: $calc-image-width;
 | 
				
			||||||
@@ -466,13 +505,18 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .chat-message-item-image {
 | 
					  .chat-message-item-image {
 | 
				
			||||||
    max-width: calc(100vw/3*2);
 | 
					    max-width: calc(100vw / 3 * 2);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media screen and (min-width: 600px) {
 | 
					@media screen and (min-width: 600px) {
 | 
				
			||||||
  $max-image-width: calc(calc(1200px - var(--sidebar-width))/3*2/var(--image-count));
 | 
					  $max-image-width: calc(
 | 
				
			||||||
  $image-width: calc(calc(var(--window-width) - var(--sidebar-width))/3*2/var(--image-count));
 | 
					    calc(1200px - var(--sidebar-width)) / 3 * 2 / var(--image-count)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  $image-width: calc(
 | 
				
			||||||
 | 
					    calc(var(--window-width) - var(--sidebar-width)) / 3 * 2 /
 | 
				
			||||||
 | 
					      var(--image-count)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .chat-message-item-image-multi {
 | 
					  .chat-message-item-image-multi {
 | 
				
			||||||
    width: $image-width;
 | 
					    width: $image-width;
 | 
				
			||||||
@@ -482,7 +526,7 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .chat-message-item-image {
 | 
					  .chat-message-item-image {
 | 
				
			||||||
    max-width: calc(calc(1200px - var(--sidebar-width))/3*2);
 | 
					    max-width: calc(calc(1200px - var(--sidebar-width)) / 3 * 2);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -500,7 +544,7 @@
 | 
				
			|||||||
  z-index: 1;
 | 
					  z-index: 1;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-message-user>.chat-message-container>.chat-message-item {
 | 
					.chat-message-user > .chat-message-container > .chat-message-item {
 | 
				
			||||||
  background-color: var(--second);
 | 
					  background-color: var(--second);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover {
 | 
					  &:hover {
 | 
				
			||||||
@@ -611,7 +655,8 @@
 | 
				
			|||||||
  min-height: 68px;
 | 
					  min-height: 68px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-input:focus {}
 | 
					.chat-input:focus {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-input-send {
 | 
					.chat-input-send {
 | 
				
			||||||
  background-color: var(--primary);
 | 
					  background-color: var(--primary);
 | 
				
			||||||
@@ -631,3 +676,78 @@
 | 
				
			|||||||
    bottom: 30px;
 | 
					    bottom: 30px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.shortcut-key-container {
 | 
				
			||||||
 | 
					  padding: 10px;
 | 
				
			||||||
 | 
					  overflow-y: auto;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.shortcut-key-grid {
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
 | 
				
			||||||
 | 
					  gap: 16px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.shortcut-key-item {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  padding: 10px;
 | 
				
			||||||
 | 
					  background-color: var(--white);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.shortcut-key-title {
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  color: var(--black);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.shortcut-key-keys {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  gap: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.shortcut-key {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  border: var(--border-in-light);
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  padding: 4px;
 | 
				
			||||||
 | 
					  background-color: var(--gray);
 | 
				
			||||||
 | 
					  min-width: 32px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.shortcut-key span {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: var(--black);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chat-main {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  .chat-body-container {
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    flex: 1;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .chat-side-panel {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    inset: 0;
 | 
				
			||||||
 | 
					    background: var(--white);
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					    z-index: 10;
 | 
				
			||||||
 | 
					    transform: translateX(100%);
 | 
				
			||||||
 | 
					    transition: all ease 0.3s;
 | 
				
			||||||
 | 
					    &-show {
 | 
				
			||||||
 | 
					      transform: translateX(0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,8 +6,21 @@ import EmojiPicker, {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { ModelType } from "../store";
 | 
					import { ModelType } from "../store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import BotIcon from "../icons/bot.svg";
 | 
					import BotIconDefault from "../icons/llm-icons/default.svg";
 | 
				
			||||||
import BlackBotIcon from "../icons/black-bot.svg";
 | 
					import BotIconOpenAI from "../icons/llm-icons/openai.svg";
 | 
				
			||||||
 | 
					import BotIconGemini from "../icons/llm-icons/gemini.svg";
 | 
				
			||||||
 | 
					import BotIconGemma from "../icons/llm-icons/gemma.svg";
 | 
				
			||||||
 | 
					import BotIconClaude from "../icons/llm-icons/claude.svg";
 | 
				
			||||||
 | 
					import BotIconMeta from "../icons/llm-icons/meta.svg";
 | 
				
			||||||
 | 
					import BotIconMistral from "../icons/llm-icons/mistral.svg";
 | 
				
			||||||
 | 
					import BotIconDeepseek from "../icons/llm-icons/deepseek.svg";
 | 
				
			||||||
 | 
					import BotIconMoonshot from "../icons/llm-icons/moonshot.svg";
 | 
				
			||||||
 | 
					import BotIconQwen from "../icons/llm-icons/qwen.svg";
 | 
				
			||||||
 | 
					import BotIconWenxin from "../icons/llm-icons/wenxin.svg";
 | 
				
			||||||
 | 
					import BotIconGrok from "../icons/llm-icons/grok.svg";
 | 
				
			||||||
 | 
					import BotIconHunyuan from "../icons/llm-icons/hunyuan.svg";
 | 
				
			||||||
 | 
					import BotIconDoubao from "../icons/llm-icons/doubao.svg";
 | 
				
			||||||
 | 
					import BotIconChatglm from "../icons/llm-icons/chatglm.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getEmojiUrl(unified: string, style: EmojiStyle) {
 | 
					export function getEmojiUrl(unified: string, style: EmojiStyle) {
 | 
				
			||||||
  // Whoever owns this Content Delivery Network (CDN), I am using your CDN to serve emojis
 | 
					  // Whoever owns this Content Delivery Network (CDN), I am using your CDN to serve emojis
 | 
				
			||||||
@@ -33,14 +46,55 @@ export function AvatarPicker(props: {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Avatar(props: { model?: ModelType; avatar?: string }) {
 | 
					export function Avatar(props: { model?: ModelType; avatar?: string }) {
 | 
				
			||||||
 | 
					  let LlmIcon = BotIconDefault;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (props.model) {
 | 
					  if (props.model) {
 | 
				
			||||||
 | 
					    const modelName = props.model.toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      modelName.startsWith("gpt") ||
 | 
				
			||||||
 | 
					      modelName.startsWith("chatgpt") ||
 | 
				
			||||||
 | 
					      modelName.startsWith("dall-e") ||
 | 
				
			||||||
 | 
					      modelName.startsWith("dalle") ||
 | 
				
			||||||
 | 
					      modelName.startsWith("o1") ||
 | 
				
			||||||
 | 
					      modelName.startsWith("o3")
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconOpenAI;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("gemini")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconGemini;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("gemma")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconGemma;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("claude")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconClaude;
 | 
				
			||||||
 | 
					    } else if (modelName.includes("llama")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconMeta;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("mixtral") || modelName.startsWith("codestral")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconMistral;
 | 
				
			||||||
 | 
					    } else if (modelName.includes("deepseek")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconDeepseek;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("moonshot")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconMoonshot;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("qwen")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconQwen;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("ernie")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconWenxin;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("grok")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconGrok;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("hunyuan")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconHunyuan;
 | 
				
			||||||
 | 
					    } else if (modelName.startsWith("doubao") || modelName.startsWith("ep-")) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconDoubao;
 | 
				
			||||||
 | 
					    } else if (
 | 
				
			||||||
 | 
					      modelName.includes("glm") ||
 | 
				
			||||||
 | 
					      modelName.startsWith("cogview-") ||
 | 
				
			||||||
 | 
					      modelName.startsWith("cogvideox-")
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      LlmIcon = BotIconChatglm;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className="no-dark">
 | 
					      <div className="no-dark">
 | 
				
			||||||
        {props.model?.startsWith("gpt-4") ? (
 | 
					        <LlmIcon className="user-avatar" width={30} height={30} />
 | 
				
			||||||
          <BlackBotIcon className="user-avatar" />
 | 
					 | 
				
			||||||
        ) : (
 | 
					 | 
				
			||||||
          <BotIcon className="user-avatar" />
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ import { ISSUE_URL } from "../constant";
 | 
				
			|||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
import { showConfirm } from "./ui-lib";
 | 
					import { showConfirm } from "./ui-lib";
 | 
				
			||||||
import { useSyncStore } from "../store/sync";
 | 
					import { useSyncStore } from "../store/sync";
 | 
				
			||||||
 | 
					import { useChatStore } from "../store/chat";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IErrorBoundaryState {
 | 
					interface IErrorBoundaryState {
 | 
				
			||||||
  hasError: boolean;
 | 
					  hasError: boolean;
 | 
				
			||||||
@@ -30,8 +31,7 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
      useSyncStore.getState().export();
 | 
					      useSyncStore.getState().export();
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      localStorage.clear();
 | 
					      useChatStore.getState().clearAllData();
 | 
				
			||||||
      location.reload();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
/* eslint-disable @next/next/no-img-element */
 | 
					/* eslint-disable @next/next/no-img-element */
 | 
				
			||||||
import { ChatMessage, ModelType, useAppConfig, useChatStore } from "../store";
 | 
					import { ChatMessage, useAppConfig, useChatStore } from "../store";
 | 
				
			||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
import styles from "./exporter.module.scss";
 | 
					import styles from "./exporter.module.scss";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@@ -23,7 +23,6 @@ import CopyIcon from "../icons/copy.svg";
 | 
				
			|||||||
import LoadingIcon from "../icons/three-dots.svg";
 | 
					import LoadingIcon from "../icons/three-dots.svg";
 | 
				
			||||||
import ChatGptIcon from "../icons/chatgpt.png";
 | 
					import ChatGptIcon from "../icons/chatgpt.png";
 | 
				
			||||||
import ShareIcon from "../icons/share.svg";
 | 
					import ShareIcon from "../icons/share.svg";
 | 
				
			||||||
import BotIcon from "../icons/bot.png";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import DownloadIcon from "../icons/download.svg";
 | 
					import DownloadIcon from "../icons/download.svg";
 | 
				
			||||||
import { useEffect, useMemo, useRef, useState } from "react";
 | 
					import { useEffect, useMemo, useRef, useState } from "react";
 | 
				
			||||||
@@ -33,13 +32,14 @@ import dynamic from "next/dynamic";
 | 
				
			|||||||
import NextImage from "next/image";
 | 
					import NextImage from "next/image";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { toBlob, toPng } from "html-to-image";
 | 
					import { toBlob, toPng } from "html-to-image";
 | 
				
			||||||
import { DEFAULT_MASK_AVATAR } from "../store/mask";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { prettyObject } from "../utils/format";
 | 
					import { prettyObject } from "../utils/format";
 | 
				
			||||||
import { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
 | 
					import { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
 | 
				
			||||||
import { getClientConfig } from "../config/client";
 | 
					import { getClientConfig } from "../config/client";
 | 
				
			||||||
import { type ClientApi, getClientApi } from "../client/api";
 | 
					import { type ClientApi, getClientApi } from "../client/api";
 | 
				
			||||||
import { getMessageTextContent } from "../utils";
 | 
					import { getMessageTextContent } from "../utils";
 | 
				
			||||||
 | 
					import { MaskAvatar } from "./mask";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
 | 
					const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
 | 
				
			||||||
  loading: () => <LoadingIcon />,
 | 
					  loading: () => <LoadingIcon />,
 | 
				
			||||||
@@ -118,9 +118,10 @@ function Steps<
 | 
				
			|||||||
          return (
 | 
					          return (
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
              key={i}
 | 
					              key={i}
 | 
				
			||||||
              className={`${styles["step"]} ${
 | 
					              className={clsx("clickable", styles["step"], {
 | 
				
			||||||
                styles[i <= props.index ? "step-finished" : ""]
 | 
					                [styles["step-finished"]]: i <= props.index,
 | 
				
			||||||
              } ${i === props.index && styles["step-current"]} clickable`}
 | 
					                [styles["step-current"]]: i === props.index,
 | 
				
			||||||
 | 
					              })}
 | 
				
			||||||
              onClick={() => {
 | 
					              onClick={() => {
 | 
				
			||||||
                props.onStepChange?.(i);
 | 
					                props.onStepChange?.(i);
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
@@ -405,22 +406,6 @@ export function PreviewActions(props: {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function ExportAvatar(props: { avatar: string }) {
 | 
					 | 
				
			||||||
  if (props.avatar === DEFAULT_MASK_AVATAR) {
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <img
 | 
					 | 
				
			||||||
        src={BotIcon.src}
 | 
					 | 
				
			||||||
        width={30}
 | 
					 | 
				
			||||||
        height={30}
 | 
					 | 
				
			||||||
        alt="bot"
 | 
					 | 
				
			||||||
        className="user-avatar"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return <Avatar avatar={props.avatar} />;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function ImagePreviewer(props: {
 | 
					export function ImagePreviewer(props: {
 | 
				
			||||||
  messages: ChatMessage[];
 | 
					  messages: ChatMessage[];
 | 
				
			||||||
  topic: string;
 | 
					  topic: string;
 | 
				
			||||||
@@ -525,11 +510,11 @@ export function ImagePreviewer(props: {
 | 
				
			|||||||
        messages={props.messages}
 | 
					        messages={props.messages}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
        className={`${styles["preview-body"]} ${styles["default-theme"]}`}
 | 
					        className={clsx(styles["preview-body"], styles["default-theme"])}
 | 
				
			||||||
        ref={previewRef}
 | 
					        ref={previewRef}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <div className={styles["chat-info"]}>
 | 
					        <div className={styles["chat-info"]}>
 | 
				
			||||||
          <div className={styles["logo"] + " no-dark"}>
 | 
					          <div className={clsx(styles["logo"], "no-dark")}>
 | 
				
			||||||
            <NextImage
 | 
					            <NextImage
 | 
				
			||||||
              src={ChatGptIcon.src}
 | 
					              src={ChatGptIcon.src}
 | 
				
			||||||
              alt="logo"
 | 
					              alt="logo"
 | 
				
			||||||
@@ -544,9 +529,12 @@ export function ImagePreviewer(props: {
 | 
				
			|||||||
              github.com/ChatGPTNextWeb/ChatGPT-Next-Web
 | 
					              github.com/ChatGPTNextWeb/ChatGPT-Next-Web
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div className={styles["icons"]}>
 | 
					            <div className={styles["icons"]}>
 | 
				
			||||||
              <ExportAvatar avatar={config.avatar} />
 | 
					              <MaskAvatar avatar={config.avatar} />
 | 
				
			||||||
              <span className={styles["icon-space"]}>&</span>
 | 
					              <span className={styles["icon-space"]}>&</span>
 | 
				
			||||||
              <ExportAvatar avatar={mask.avatar} />
 | 
					              <MaskAvatar
 | 
				
			||||||
 | 
					                avatar={mask.avatar}
 | 
				
			||||||
 | 
					                model={session.mask.modelConfig.model}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
@@ -570,13 +558,18 @@ export function ImagePreviewer(props: {
 | 
				
			|||||||
        {props.messages.map((m, i) => {
 | 
					        {props.messages.map((m, i) => {
 | 
				
			||||||
          return (
 | 
					          return (
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
              className={styles["message"] + " " + styles["message-" + m.role]}
 | 
					              className={clsx(styles["message"], styles["message-" + m.role])}
 | 
				
			||||||
              key={i}
 | 
					              key={i}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <div className={styles["avatar"]}>
 | 
					              <div className={styles["avatar"]}>
 | 
				
			||||||
                <ExportAvatar
 | 
					                {m.role === "user" ? (
 | 
				
			||||||
                  avatar={m.role === "user" ? config.avatar : mask.avatar}
 | 
					                  <Avatar avatar={config.avatar}></Avatar>
 | 
				
			||||||
                />
 | 
					                ) : (
 | 
				
			||||||
 | 
					                  <MaskAvatar
 | 
				
			||||||
 | 
					                    avatar={session.mask.avatar}
 | 
				
			||||||
 | 
					                    model={m.model || session.mask.modelConfig.model}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <div className={styles["body"]}>
 | 
					              <div className={styles["body"]}>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -140,6 +140,9 @@
 | 
				
			|||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  justify-content: space-between;
 | 
					  justify-content: space-between;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  &-narrow {
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.sidebar-logo {
 | 
					.sidebar-logo {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
require("../polyfill");
 | 
					require("../polyfill");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useState, useEffect } from "react";
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					 | 
				
			||||||
import styles from "./home.module.scss";
 | 
					import styles from "./home.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import BotIcon from "../icons/bot.svg";
 | 
					import BotIcon from "../icons/bot.svg";
 | 
				
			||||||
@@ -19,8 +18,8 @@ import { getISOLang, getLang } from "../locales";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  HashRouter as Router,
 | 
					  HashRouter as Router,
 | 
				
			||||||
  Routes,
 | 
					 | 
				
			||||||
  Route,
 | 
					  Route,
 | 
				
			||||||
 | 
					  Routes,
 | 
				
			||||||
  useLocation,
 | 
					  useLocation,
 | 
				
			||||||
} from "react-router-dom";
 | 
					} from "react-router-dom";
 | 
				
			||||||
import { SideBar } from "./sidebar";
 | 
					import { SideBar } from "./sidebar";
 | 
				
			||||||
@@ -29,10 +28,12 @@ import { AuthPage } from "./auth";
 | 
				
			|||||||
import { getClientConfig } from "../config/client";
 | 
					import { getClientConfig } from "../config/client";
 | 
				
			||||||
import { type ClientApi, getClientApi } from "../client/api";
 | 
					import { type ClientApi, getClientApi } from "../client/api";
 | 
				
			||||||
import { useAccessStore } from "../store";
 | 
					import { useAccessStore } from "../store";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					import { initializeMcpSystem, isMcpEnabled } from "../mcp/actions";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Loading(props: { noLogo?: boolean }) {
 | 
					export function Loading(props: { noLogo?: boolean }) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={styles["loading-content"] + " no-dark"}>
 | 
					    <div className={clsx("no-dark", styles["loading-content"])}>
 | 
				
			||||||
      {!props.noLogo && <BotIcon />}
 | 
					      {!props.noLogo && <BotIcon />}
 | 
				
			||||||
      <LoadingIcon />
 | 
					      <LoadingIcon />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
@@ -59,10 +60,28 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
 | 
				
			|||||||
  loading: () => <Loading noLogo />,
 | 
					  loading: () => <Loading noLogo />,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PluginPage = dynamic(async () => (await import("./plugin")).PluginPage, {
 | 
				
			||||||
 | 
					  loading: () => <Loading noLogo />,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SearchChat = dynamic(
 | 
				
			||||||
 | 
					  async () => (await import("./search-chat")).SearchChatPage,
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    loading: () => <Loading noLogo />,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Sd = dynamic(async () => (await import("./sd")).Sd, {
 | 
					const Sd = dynamic(async () => (await import("./sd")).Sd, {
 | 
				
			||||||
  loading: () => <Loading noLogo />,
 | 
					  loading: () => <Loading noLogo />,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const McpMarketPage = dynamic(
 | 
				
			||||||
 | 
					  async () => (await import("./mcp-market")).McpMarketPage,
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    loading: () => <Loading noLogo />,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useSwitchTheme() {
 | 
					export function useSwitchTheme() {
 | 
				
			||||||
  const config = useAppConfig();
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -168,14 +187,21 @@ function Screen() {
 | 
				
			|||||||
    if (isSdNew) return <Sd />;
 | 
					    if (isSdNew) return <Sd />;
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <>
 | 
					      <>
 | 
				
			||||||
        <SideBar className={isHome ? styles["sidebar-show"] : ""} />
 | 
					        <SideBar
 | 
				
			||||||
 | 
					          className={clsx({
 | 
				
			||||||
 | 
					            [styles["sidebar-show"]]: isHome,
 | 
				
			||||||
 | 
					          })}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
        <WindowContent>
 | 
					        <WindowContent>
 | 
				
			||||||
          <Routes>
 | 
					          <Routes>
 | 
				
			||||||
            <Route path={Path.Home} element={<Chat />} />
 | 
					            <Route path={Path.Home} element={<Chat />} />
 | 
				
			||||||
            <Route path={Path.NewChat} element={<NewChat />} />
 | 
					            <Route path={Path.NewChat} element={<NewChat />} />
 | 
				
			||||||
            <Route path={Path.Masks} element={<MaskPage />} />
 | 
					            <Route path={Path.Masks} element={<MaskPage />} />
 | 
				
			||||||
 | 
					            <Route path={Path.Plugins} element={<PluginPage />} />
 | 
				
			||||||
 | 
					            <Route path={Path.SearchChat} element={<SearchChat />} />
 | 
				
			||||||
            <Route path={Path.Chat} element={<Chat />} />
 | 
					            <Route path={Path.Chat} element={<Chat />} />
 | 
				
			||||||
            <Route path={Path.Settings} element={<Settings />} />
 | 
					            <Route path={Path.Settings} element={<Settings />} />
 | 
				
			||||||
 | 
					            <Route path={Path.McpMarket} element={<McpMarketPage />} />
 | 
				
			||||||
          </Routes>
 | 
					          </Routes>
 | 
				
			||||||
        </WindowContent>
 | 
					        </WindowContent>
 | 
				
			||||||
      </>
 | 
					      </>
 | 
				
			||||||
@@ -184,9 +210,10 @@ function Screen() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      className={`${styles.container} ${
 | 
					      className={clsx(styles.container, {
 | 
				
			||||||
        shouldTightBorder ? styles["tight-container"] : styles.container
 | 
					        [styles["tight-container"]]: shouldTightBorder,
 | 
				
			||||||
      } ${getLang() === "ar" ? styles["rtl-screen"] : ""}`}
 | 
					        [styles["rtl-screen"]]: getLang() === "ar",
 | 
				
			||||||
 | 
					      })}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      {renderContent()}
 | 
					      {renderContent()}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
@@ -215,6 +242,20 @@ export function Home() {
 | 
				
			|||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    console.log("[Config] got config from build time", getClientConfig());
 | 
					    console.log("[Config] got config from build time", getClientConfig());
 | 
				
			||||||
    useAccessStore.getState().fetch();
 | 
					    useAccessStore.getState().fetch();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const initMcp = async () => {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const enabled = await isMcpEnabled();
 | 
				
			||||||
 | 
					        if (enabled) {
 | 
				
			||||||
 | 
					          console.log("[MCP] initializing...");
 | 
				
			||||||
 | 
					          await initializeMcpSystem();
 | 
				
			||||||
 | 
					          console.log("[MCP] initialized");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (err) {
 | 
				
			||||||
 | 
					        console.error("[MCP] failed to initialize:", err);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    initMcp();
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!useHasHydrated()) {
 | 
					  if (!useHasHydrated()) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import * as React from "react";
 | 
					import * as React from "react";
 | 
				
			||||||
import styles from "./input-range.module.scss";
 | 
					import styles from "./input-range.module.scss";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface InputRangeProps {
 | 
					interface InputRangeProps {
 | 
				
			||||||
  onChange: React.ChangeEventHandler<HTMLInputElement>;
 | 
					  onChange: React.ChangeEventHandler<HTMLInputElement>;
 | 
				
			||||||
@@ -23,7 +24,7 @@ export function InputRange({
 | 
				
			|||||||
  aria,
 | 
					  aria,
 | 
				
			||||||
}: InputRangeProps) {
 | 
					}: InputRangeProps) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={styles["input-range"] + ` ${className ?? ""}`}>
 | 
					    <div className={clsx(styles["input-range"], className)}>
 | 
				
			||||||
      {title || value}
 | 
					      {title || value}
 | 
				
			||||||
      <input
 | 
					      <input
 | 
				
			||||||
        aria-label={aria}
 | 
					        aria-label={aria}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,14 +8,23 @@ import RehypeHighlight from "rehype-highlight";
 | 
				
			|||||||
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
 | 
					import { useRef, useState, RefObject, useEffect, useMemo } from "react";
 | 
				
			||||||
import { copyToClipboard, useWindowSize } from "../utils";
 | 
					import { copyToClipboard, useWindowSize } from "../utils";
 | 
				
			||||||
import mermaid from "mermaid";
 | 
					import mermaid from "mermaid";
 | 
				
			||||||
 | 
					import Locale from "../locales";
 | 
				
			||||||
import LoadingIcon from "../icons/three-dots.svg";
 | 
					import LoadingIcon from "../icons/three-dots.svg";
 | 
				
			||||||
 | 
					import ReloadButtonIcon from "../icons/reload.svg";
 | 
				
			||||||
import React from "react";
 | 
					import React from "react";
 | 
				
			||||||
import { useDebouncedCallback } from "use-debounce";
 | 
					import { useDebouncedCallback } from "use-debounce";
 | 
				
			||||||
import { showImageModal, FullScreen } from "./ui-lib";
 | 
					import { showImageModal, FullScreen } from "./ui-lib";
 | 
				
			||||||
import { ArtifactsShareButton, HTMLPreview } from "./artifacts";
 | 
					import {
 | 
				
			||||||
import { Plugin } from "../constant";
 | 
					  ArtifactsShareButton,
 | 
				
			||||||
 | 
					  HTMLPreview,
 | 
				
			||||||
 | 
					  HTMLPreviewHander,
 | 
				
			||||||
 | 
					} from "./artifacts";
 | 
				
			||||||
import { useChatStore } from "../store";
 | 
					import { useChatStore } from "../store";
 | 
				
			||||||
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useAppConfig } from "../store/config";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Mermaid(props: { code: string }) {
 | 
					export function Mermaid(props: { code: string }) {
 | 
				
			||||||
  const ref = useRef<HTMLDivElement>(null);
 | 
					  const ref = useRef<HTMLDivElement>(null);
 | 
				
			||||||
  const [hasError, setHasError] = useState(false);
 | 
					  const [hasError, setHasError] = useState(false);
 | 
				
			||||||
@@ -49,7 +58,7 @@ export function Mermaid(props: { code: string }) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      className="no-dark mermaid"
 | 
					      className={clsx("no-dark", "mermaid")}
 | 
				
			||||||
      style={{
 | 
					      style={{
 | 
				
			||||||
        cursor: "pointer",
 | 
					        cursor: "pointer",
 | 
				
			||||||
        overflow: "auto",
 | 
					        overflow: "auto",
 | 
				
			||||||
@@ -64,13 +73,12 @@ export function Mermaid(props: { code: string }) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export function PreCode(props: { children: any }) {
 | 
					export function PreCode(props: { children: any }) {
 | 
				
			||||||
  const ref = useRef<HTMLPreElement>(null);
 | 
					  const ref = useRef<HTMLPreElement>(null);
 | 
				
			||||||
  const refText = ref.current?.innerText;
 | 
					  const previewRef = useRef<HTMLPreviewHander>(null);
 | 
				
			||||||
  const [mermaidCode, setMermaidCode] = useState("");
 | 
					  const [mermaidCode, setMermaidCode] = useState("");
 | 
				
			||||||
  const [htmlCode, setHtmlCode] = useState("");
 | 
					  const [htmlCode, setHtmlCode] = useState("");
 | 
				
			||||||
  const { height } = useWindowSize();
 | 
					  const { height } = useWindowSize();
 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
  const session = chatStore.currentSession();
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
  const plugins = session.mask?.plugin;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const renderArtifacts = useDebouncedCallback(() => {
 | 
					  const renderArtifacts = useDebouncedCallback(() => {
 | 
				
			||||||
    if (!ref.current) return;
 | 
					    if (!ref.current) return;
 | 
				
			||||||
@@ -79,22 +87,21 @@ export function PreCode(props: { children: any }) {
 | 
				
			|||||||
      setMermaidCode((mermaidDom as HTMLElement).innerText);
 | 
					      setMermaidCode((mermaidDom as HTMLElement).innerText);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const htmlDom = ref.current.querySelector("code.language-html");
 | 
					    const htmlDom = ref.current.querySelector("code.language-html");
 | 
				
			||||||
 | 
					    const refText = ref.current.querySelector("code")?.innerText;
 | 
				
			||||||
    if (htmlDom) {
 | 
					    if (htmlDom) {
 | 
				
			||||||
      setHtmlCode((htmlDom as HTMLElement).innerText);
 | 
					      setHtmlCode((htmlDom as HTMLElement).innerText);
 | 
				
			||||||
    } else if (refText?.startsWith("<!DOCTYPE")) {
 | 
					    } else if (
 | 
				
			||||||
 | 
					      refText?.startsWith("<!DOCTYPE") ||
 | 
				
			||||||
 | 
					      refText?.startsWith("<svg") ||
 | 
				
			||||||
 | 
					      refText?.startsWith("<?xml")
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
      setHtmlCode(refText);
 | 
					      setHtmlCode(refText);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, 600);
 | 
					  }, 600);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  const config = useAppConfig();
 | 
				
			||||||
    setTimeout(renderArtifacts, 1);
 | 
					  const enableArtifacts =
 | 
				
			||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					    session.mask?.enableArtifacts !== false && config.enableArtifacts;
 | 
				
			||||||
  }, [refText]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const enableArtifacts = useMemo(
 | 
					 | 
				
			||||||
    () => plugins?.includes(Plugin.Artifacts),
 | 
					 | 
				
			||||||
    [plugins],
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  //Wrap the paragraph for plain-text
 | 
					  //Wrap the paragraph for plain-text
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
@@ -119,6 +126,7 @@ export function PreCode(props: { children: any }) {
 | 
				
			|||||||
          codeElement.style.whiteSpace = "pre-wrap";
 | 
					          codeElement.style.whiteSpace = "pre-wrap";
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					      setTimeout(renderArtifacts, 1);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -129,8 +137,9 @@ export function PreCode(props: { children: any }) {
 | 
				
			|||||||
          className="copy-code-button"
 | 
					          className="copy-code-button"
 | 
				
			||||||
          onClick={() => {
 | 
					          onClick={() => {
 | 
				
			||||||
            if (ref.current) {
 | 
					            if (ref.current) {
 | 
				
			||||||
              const code = ref.current.innerText;
 | 
					              copyToClipboard(
 | 
				
			||||||
              copyToClipboard(code);
 | 
					                ref.current.querySelector("code")?.innerText ?? "",
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        ></span>
 | 
					        ></span>
 | 
				
			||||||
@@ -145,7 +154,15 @@ export function PreCode(props: { children: any }) {
 | 
				
			|||||||
            style={{ position: "absolute", right: 20, top: 10 }}
 | 
					            style={{ position: "absolute", right: 20, top: 10 }}
 | 
				
			||||||
            getCode={() => htmlCode}
 | 
					            getCode={() => htmlCode}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            style={{ position: "absolute", right: 120, top: 10 }}
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					            icon={<ReloadButtonIcon />}
 | 
				
			||||||
 | 
					            shadow
 | 
				
			||||||
 | 
					            onClick={() => previewRef.current?.reload()}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
          <HTMLPreview
 | 
					          <HTMLPreview
 | 
				
			||||||
 | 
					            ref={previewRef}
 | 
				
			||||||
            code={htmlCode}
 | 
					            code={htmlCode}
 | 
				
			||||||
            autoHeight={!document.fullscreenElement}
 | 
					            autoHeight={!document.fullscreenElement}
 | 
				
			||||||
            height={!document.fullscreenElement ? 600 : height}
 | 
					            height={!document.fullscreenElement ? 600 : height}
 | 
				
			||||||
@@ -156,7 +173,13 @@ export function PreCode(props: { children: any }) {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function CustomCode(props: { children: any }) {
 | 
					function CustomCode(props: { children: any; className?: string }) {
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const enableCodeFold =
 | 
				
			||||||
 | 
					    session.mask?.enableCodeFold !== false && config.enableCodeFold;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ref = useRef<HTMLPreElement>(null);
 | 
					  const ref = useRef<HTMLPreElement>(null);
 | 
				
			||||||
  const [collapsed, setCollapsed] = useState(true);
 | 
					  const [collapsed, setCollapsed] = useState(true);
 | 
				
			||||||
  const [showToggle, setShowToggle] = useState(false);
 | 
					  const [showToggle, setShowToggle] = useState(false);
 | 
				
			||||||
@@ -172,47 +195,39 @@ function CustomCode(props: { children: any }) {
 | 
				
			|||||||
  const toggleCollapsed = () => {
 | 
					  const toggleCollapsed = () => {
 | 
				
			||||||
    setCollapsed((collapsed) => !collapsed);
 | 
					    setCollapsed((collapsed) => !collapsed);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					  const renderShowMoreButton = () => {
 | 
				
			||||||
 | 
					    if (showToggle && enableCodeFold && collapsed) {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={clsx("show-hide-button", {
 | 
				
			||||||
 | 
					            collapsed,
 | 
				
			||||||
 | 
					            expanded: !collapsed,
 | 
				
			||||||
 | 
					          })}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <code
 | 
					      <code
 | 
				
			||||||
 | 
					        className={clsx(props?.className)}
 | 
				
			||||||
        ref={ref}
 | 
					        ref={ref}
 | 
				
			||||||
        style={{
 | 
					        style={{
 | 
				
			||||||
          maxHeight: collapsed ? "400px" : "none",
 | 
					          maxHeight: enableCodeFold && collapsed ? "400px" : "none",
 | 
				
			||||||
          overflowY: "hidden",
 | 
					          overflowY: "hidden",
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        {props.children}
 | 
					        {props.children}
 | 
				
			||||||
        {showToggle && collapsed && (
 | 
					 | 
				
			||||||
          <div
 | 
					 | 
				
			||||||
            className={`show-hide-button ${
 | 
					 | 
				
			||||||
              collapsed ? "collapsed" : "expanded"
 | 
					 | 
				
			||||||
            }`}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <button onClick={toggleCollapsed}>查看全部</button>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      </code>
 | 
					      </code>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {renderShowMoreButton()}
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function escapeDollarNumber(text: string) {
 | 
					 | 
				
			||||||
  let escapedText = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  for (let i = 0; i < text.length; i += 1) {
 | 
					 | 
				
			||||||
    let char = text[i];
 | 
					 | 
				
			||||||
    const nextChar = text[i + 1] || " ";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (char === "$" && nextChar >= "0" && nextChar <= "9") {
 | 
					 | 
				
			||||||
      char = "\\$";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    escapedText += char;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return escapedText;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function escapeBrackets(text: string) {
 | 
					function escapeBrackets(text: string) {
 | 
				
			||||||
  const pattern =
 | 
					  const pattern =
 | 
				
			||||||
    /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
 | 
					    /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
 | 
				
			||||||
@@ -231,9 +246,30 @@ function escapeBrackets(text: string) {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function tryWrapHtmlCode(text: string) {
 | 
				
			||||||
 | 
					  // try add wrap html code (fixed: html codeblock include 2 newline)
 | 
				
			||||||
 | 
					  // ignore embed codeblock
 | 
				
			||||||
 | 
					  if (text.includes("```")) {
 | 
				
			||||||
 | 
					    return text;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return text
 | 
				
			||||||
 | 
					    .replace(
 | 
				
			||||||
 | 
					      /([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
 | 
				
			||||||
 | 
					      (match, quoteStart, lang, newLine, doctype) => {
 | 
				
			||||||
 | 
					        return !quoteStart ? "\n```html\n" + doctype : match;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .replace(
 | 
				
			||||||
 | 
					      /(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
 | 
				
			||||||
 | 
					      (match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
 | 
				
			||||||
 | 
					        return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function _MarkDownContent(props: { content: string }) {
 | 
					function _MarkDownContent(props: { content: string }) {
 | 
				
			||||||
  const escapedContent = useMemo(() => {
 | 
					  const escapedContent = useMemo(() => {
 | 
				
			||||||
    return escapeBrackets(escapeDollarNumber(props.content));
 | 
					    return tryWrapHtmlCode(escapeBrackets(props.content));
 | 
				
			||||||
  }, [props.content]);
 | 
					  }, [props.content]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@@ -255,6 +291,20 @@ function _MarkDownContent(props: { content: string }) {
 | 
				
			|||||||
        p: (pProps) => <p {...pProps} dir="auto" />,
 | 
					        p: (pProps) => <p {...pProps} dir="auto" />,
 | 
				
			||||||
        a: (aProps) => {
 | 
					        a: (aProps) => {
 | 
				
			||||||
          const href = aProps.href || "";
 | 
					          const href = aProps.href || "";
 | 
				
			||||||
 | 
					          if (/\.(aac|mp3|opus|wav)$/.test(href)) {
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              <figure>
 | 
				
			||||||
 | 
					                <audio controls src={href}></audio>
 | 
				
			||||||
 | 
					              </figure>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) {
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              <video controls width="99.9%">
 | 
				
			||||||
 | 
					                <source src={href} />
 | 
				
			||||||
 | 
					              </video>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
          const isInternal = /^\/#/i.test(href);
 | 
					          const isInternal = /^\/#/i.test(href);
 | 
				
			||||||
          const target = isInternal ? "_self" : aProps.target ?? "_blank";
 | 
					          const target = isInternal ? "_self" : aProps.target ?? "_blank";
 | 
				
			||||||
          return <a {...aProps} target={target} />;
 | 
					          return <a {...aProps} target={target} />;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,7 +37,7 @@ import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales";
 | 
				
			|||||||
import { useNavigate } from "react-router-dom";
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import chatStyle from "./chat.module.scss";
 | 
					import chatStyle from "./chat.module.scss";
 | 
				
			||||||
import { useEffect, useState } from "react";
 | 
					import { useState } from "react";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  copyToClipboard,
 | 
					  copyToClipboard,
 | 
				
			||||||
  downloadAs,
 | 
					  downloadAs,
 | 
				
			||||||
@@ -48,7 +48,6 @@ import { Updater } from "../typing";
 | 
				
			|||||||
import { ModelConfigList } from "./model-config";
 | 
					import { ModelConfigList } from "./model-config";
 | 
				
			||||||
import { FileName, Path } from "../constant";
 | 
					import { FileName, Path } from "../constant";
 | 
				
			||||||
import { BUILTIN_MASK_STORE } from "../masks";
 | 
					import { BUILTIN_MASK_STORE } from "../masks";
 | 
				
			||||||
import { nanoid } from "nanoid";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  DragDropContext,
 | 
					  DragDropContext,
 | 
				
			||||||
  Droppable,
 | 
					  Droppable,
 | 
				
			||||||
@@ -56,6 +55,7 @@ import {
 | 
				
			|||||||
  OnDragEndResponder,
 | 
					  OnDragEndResponder,
 | 
				
			||||||
} from "@hello-pangea/dnd";
 | 
					} from "@hello-pangea/dnd";
 | 
				
			||||||
import { getMessageTextContent } from "../utils";
 | 
					import { getMessageTextContent } from "../utils";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// drag and drop helper function
 | 
					// drag and drop helper function
 | 
				
			||||||
function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
 | 
					function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
 | 
				
			||||||
@@ -167,6 +167,41 @@ export function MaskConfig(props: {
 | 
				
			|||||||
          ></input>
 | 
					          ></input>
 | 
				
			||||||
        </ListItem>
 | 
					        </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {globalConfig.enableArtifacts && (
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Mask.Config.Artifacts.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Mask.Config.Artifacts.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              aria-label={Locale.Mask.Config.Artifacts.Title}
 | 
				
			||||||
 | 
					              type="checkbox"
 | 
				
			||||||
 | 
					              checked={props.mask.enableArtifacts !== false}
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                props.updateMask((mask) => {
 | 
				
			||||||
 | 
					                  mask.enableArtifacts = e.currentTarget.checked;
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            ></input>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {globalConfig.enableCodeFold && (
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Mask.Config.CodeFold.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Mask.Config.CodeFold.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              aria-label={Locale.Mask.Config.CodeFold.Title}
 | 
				
			||||||
 | 
					              type="checkbox"
 | 
				
			||||||
 | 
					              checked={props.mask.enableCodeFold !== false}
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                props.updateMask((mask) => {
 | 
				
			||||||
 | 
					                  mask.enableCodeFold = e.currentTarget.checked;
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            ></input>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {!props.shouldSyncFromGlobal ? (
 | 
					        {!props.shouldSyncFromGlobal ? (
 | 
				
			||||||
          <ListItem
 | 
					          <ListItem
 | 
				
			||||||
            title={Locale.Mask.Config.Share.Title}
 | 
					            title={Locale.Mask.Config.Share.Title}
 | 
				
			||||||
@@ -410,16 +445,7 @@ export function MaskPage() {
 | 
				
			|||||||
  const maskStore = useMaskStore();
 | 
					  const maskStore = useMaskStore();
 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [filterLang, setFilterLang] = useState<Lang | undefined>(
 | 
					  const filterLang = maskStore.language;
 | 
				
			||||||
    () => localStorage.getItem("Mask-language") as Lang | undefined,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    if (filterLang) {
 | 
					 | 
				
			||||||
      localStorage.setItem("Mask-language", filterLang);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      localStorage.removeItem("Mask-language");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [filterLang]);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const allMasks = maskStore
 | 
					  const allMasks = maskStore
 | 
				
			||||||
    .getAll()
 | 
					    .getAll()
 | 
				
			||||||
@@ -526,9 +552,9 @@ export function MaskPage() {
 | 
				
			|||||||
              onChange={(e) => {
 | 
					              onChange={(e) => {
 | 
				
			||||||
                const value = e.currentTarget.value;
 | 
					                const value = e.currentTarget.value;
 | 
				
			||||||
                if (value === Locale.Settings.Lang.All) {
 | 
					                if (value === Locale.Settings.Lang.All) {
 | 
				
			||||||
                  setFilterLang(undefined);
 | 
					                  maskStore.setLanguage(undefined);
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                  setFilterLang(value as Lang);
 | 
					                  maskStore.setLanguage(value as Lang);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
@@ -563,7 +589,7 @@ export function MaskPage() {
 | 
				
			|||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <div className={styles["mask-title"]}>
 | 
					                  <div className={styles["mask-title"]}>
 | 
				
			||||||
                    <div className={styles["mask-name"]}>{m.name}</div>
 | 
					                    <div className={styles["mask-name"]}>{m.name}</div>
 | 
				
			||||||
                    <div className={styles["mask-info"] + " one-line"}>
 | 
					                    <div className={clsx(styles["mask-info"], "one-line")}>
 | 
				
			||||||
                      {`${Locale.Mask.Item.Info(m.context.length)} / ${
 | 
					                      {`${Locale.Mask.Item.Info(m.context.length)} / ${
 | 
				
			||||||
                        ALL_LANG_OPTIONS[m.lang]
 | 
					                        ALL_LANG_OPTIONS[m.lang]
 | 
				
			||||||
                      } / ${m.modelConfig.model}`}
 | 
					                      } / ${m.modelConfig.model}`}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										657
									
								
								app/components/mcp-market.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,657 @@
 | 
				
			|||||||
 | 
					@import "../styles/animation.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mcp-market-page {
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .loading-indicator {
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					    color: var(--primary);
 | 
				
			||||||
 | 
					    margin-left: 8px;
 | 
				
			||||||
 | 
					    font-weight: normal;
 | 
				
			||||||
 | 
					    opacity: 0.8;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .mcp-market-page-body {
 | 
				
			||||||
 | 
					    padding: 20px;
 | 
				
			||||||
 | 
					    overflow-y: auto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .loading-container,
 | 
				
			||||||
 | 
					    .empty-container {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      min-height: 200px;
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      background-color: var(--white);
 | 
				
			||||||
 | 
					      border: var(--border-in-light);
 | 
				
			||||||
 | 
					      border-radius: 10px;
 | 
				
			||||||
 | 
					      animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .loading-text,
 | 
				
			||||||
 | 
					    .empty-text {
 | 
				
			||||||
 | 
					      font-size: 14px;
 | 
				
			||||||
 | 
					      color: var(--black);
 | 
				
			||||||
 | 
					      opacity: 0.5;
 | 
				
			||||||
 | 
					      text-align: center;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .mcp-market-filter {
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      max-width: 100%;
 | 
				
			||||||
 | 
					      margin-bottom: 20px;
 | 
				
			||||||
 | 
					      animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					      height: 40px;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .search-bar {
 | 
				
			||||||
 | 
					        flex-grow: 1;
 | 
				
			||||||
 | 
					        max-width: 100%;
 | 
				
			||||||
 | 
					        min-width: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .server-list {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					      gap: 1px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .mcp-market-item {
 | 
				
			||||||
 | 
					      padding: 20px;
 | 
				
			||||||
 | 
					      border: var(--border-in-light);
 | 
				
			||||||
 | 
					      animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					      background-color: var(--white);
 | 
				
			||||||
 | 
					      transition: all 0.3s ease;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &.disabled {
 | 
				
			||||||
 | 
					        opacity: 0.7;
 | 
				
			||||||
 | 
					        pointer-events: none;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:not(:last-child) {
 | 
				
			||||||
 | 
					        border-bottom: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:first-child {
 | 
				
			||||||
 | 
					        border-top-left-radius: 10px;
 | 
				
			||||||
 | 
					        border-top-right-radius: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:last-child {
 | 
				
			||||||
 | 
					        border-bottom-left-radius: 10px;
 | 
				
			||||||
 | 
					        border-bottom-right-radius: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &.loading {
 | 
				
			||||||
 | 
					        position: relative;
 | 
				
			||||||
 | 
					        &::after {
 | 
				
			||||||
 | 
					          content: "";
 | 
				
			||||||
 | 
					          position: absolute;
 | 
				
			||||||
 | 
					          top: 0;
 | 
				
			||||||
 | 
					          left: 0;
 | 
				
			||||||
 | 
					          right: 0;
 | 
				
			||||||
 | 
					          bottom: 0;
 | 
				
			||||||
 | 
					          background: linear-gradient(
 | 
				
			||||||
 | 
					            90deg,
 | 
				
			||||||
 | 
					            transparent,
 | 
				
			||||||
 | 
					            rgba(255, 255, 255, 0.2),
 | 
				
			||||||
 | 
					            transparent
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          background-size: 200% 100%;
 | 
				
			||||||
 | 
					          animation: loading-pulse 1.5s infinite;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .operation-status {
 | 
				
			||||||
 | 
					        display: inline-flex;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					        margin-left: 10px;
 | 
				
			||||||
 | 
					        padding: 2px 8px;
 | 
				
			||||||
 | 
					        border-radius: 4px;
 | 
				
			||||||
 | 
					        font-size: 12px;
 | 
				
			||||||
 | 
					        background-color: #16a34a;
 | 
				
			||||||
 | 
					        color: #fff;
 | 
				
			||||||
 | 
					        animation: pulse 1.5s infinite;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &[data-status="stopping"] {
 | 
				
			||||||
 | 
					          background-color: #9ca3af;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &[data-status="starting"] {
 | 
				
			||||||
 | 
					          background-color: #4ade80;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &[data-status="error"] {
 | 
				
			||||||
 | 
					          background-color: #f87171;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .mcp-market-header {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        justify-content: space-between;
 | 
				
			||||||
 | 
					        align-items: flex-start;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .mcp-market-title {
 | 
				
			||||||
 | 
					          flex-grow: 1;
 | 
				
			||||||
 | 
					          margin-right: 20px;
 | 
				
			||||||
 | 
					          max-width: calc(100% - 300px);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .mcp-market-name {
 | 
				
			||||||
 | 
					          font-size: 14px;
 | 
				
			||||||
 | 
					          font-weight: bold;
 | 
				
			||||||
 | 
					          display: flex;
 | 
				
			||||||
 | 
					          align-items: center;
 | 
				
			||||||
 | 
					          gap: 8px;
 | 
				
			||||||
 | 
					          margin-bottom: 8px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          .server-status {
 | 
				
			||||||
 | 
					            display: inline-flex;
 | 
				
			||||||
 | 
					            align-items: center;
 | 
				
			||||||
 | 
					            margin-left: 10px;
 | 
				
			||||||
 | 
					            padding: 2px 8px;
 | 
				
			||||||
 | 
					            border-radius: 4px;
 | 
				
			||||||
 | 
					            font-size: 12px;
 | 
				
			||||||
 | 
					            background-color: #22c55e;
 | 
				
			||||||
 | 
					            color: #fff;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            &.error {
 | 
				
			||||||
 | 
					              background-color: #ef4444;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            &.stopped {
 | 
				
			||||||
 | 
					              background-color: #6b7280;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            &.initializing {
 | 
				
			||||||
 | 
					              background-color: #f59e0b;
 | 
				
			||||||
 | 
					              animation: pulse 1.5s infinite;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .error-message {
 | 
				
			||||||
 | 
					              margin-left: 4px;
 | 
				
			||||||
 | 
					              font-size: 12px;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .repo-link {
 | 
				
			||||||
 | 
					          color: var(--primary);
 | 
				
			||||||
 | 
					          font-size: 12px;
 | 
				
			||||||
 | 
					          display: inline-flex;
 | 
				
			||||||
 | 
					          align-items: center;
 | 
				
			||||||
 | 
					          gap: 4px;
 | 
				
			||||||
 | 
					          text-decoration: none;
 | 
				
			||||||
 | 
					          opacity: 0.8;
 | 
				
			||||||
 | 
					          transition: opacity 0.2s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          &:hover {
 | 
				
			||||||
 | 
					            opacity: 1;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          svg {
 | 
				
			||||||
 | 
					            width: 14px;
 | 
				
			||||||
 | 
					            height: 14px;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .tags-container {
 | 
				
			||||||
 | 
					          display: flex;
 | 
				
			||||||
 | 
					          gap: 4px;
 | 
				
			||||||
 | 
					          flex-wrap: wrap;
 | 
				
			||||||
 | 
					          margin-bottom: 8px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .tag {
 | 
				
			||||||
 | 
					          background: var(--gray);
 | 
				
			||||||
 | 
					          color: var(--black);
 | 
				
			||||||
 | 
					          padding: 2px 6px;
 | 
				
			||||||
 | 
					          border-radius: 4px;
 | 
				
			||||||
 | 
					          font-size: 10px;
 | 
				
			||||||
 | 
					          opacity: 0.8;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .mcp-market-info {
 | 
				
			||||||
 | 
					          color: var(--black);
 | 
				
			||||||
 | 
					          font-size: 12px;
 | 
				
			||||||
 | 
					          overflow: hidden;
 | 
				
			||||||
 | 
					          text-overflow: ellipsis;
 | 
				
			||||||
 | 
					          white-space: nowrap;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .mcp-market-actions {
 | 
				
			||||||
 | 
					          display: flex;
 | 
				
			||||||
 | 
					          gap: 12px;
 | 
				
			||||||
 | 
					          align-items: flex-start;
 | 
				
			||||||
 | 
					          flex-shrink: 0;
 | 
				
			||||||
 | 
					          min-width: 180px;
 | 
				
			||||||
 | 
					          justify-content: flex-end;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .array-input {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 12px;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    padding: 16px;
 | 
				
			||||||
 | 
					    border: 1px solid var(--gray-200);
 | 
				
			||||||
 | 
					    border-radius: 10px;
 | 
				
			||||||
 | 
					    background-color: var(--white);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .array-input-item {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      gap: 8px;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      padding: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      input {
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        padding: 8px 12px;
 | 
				
			||||||
 | 
					        background-color: var(--gray-50);
 | 
				
			||||||
 | 
					        border-radius: 6px;
 | 
				
			||||||
 | 
					        transition: all 0.3s ease;
 | 
				
			||||||
 | 
					        font-size: 13px;
 | 
				
			||||||
 | 
					        border: 1px solid var(--gray-200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover {
 | 
				
			||||||
 | 
					          background-color: var(--gray-100);
 | 
				
			||||||
 | 
					          border-color: var(--gray-300);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:focus {
 | 
				
			||||||
 | 
					          background-color: var(--white);
 | 
				
			||||||
 | 
					          border-color: var(--primary);
 | 
				
			||||||
 | 
					          outline: none;
 | 
				
			||||||
 | 
					          box-shadow: 0 0 0 2px var(--primary-10);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &::placeholder {
 | 
				
			||||||
 | 
					          color: var(--gray-300);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :global(.icon-button.add-path-button) {
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      background-color: var(--primary);
 | 
				
			||||||
 | 
					      color: white;
 | 
				
			||||||
 | 
					      padding: 8px 12px;
 | 
				
			||||||
 | 
					      border-radius: 6px;
 | 
				
			||||||
 | 
					      transition: all 0.3s ease;
 | 
				
			||||||
 | 
					      margin-top: 8px;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					      border: none;
 | 
				
			||||||
 | 
					      height: 36px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:hover {
 | 
				
			||||||
 | 
					        background-color: var(--primary-dark);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      svg {
 | 
				
			||||||
 | 
					        width: 16px;
 | 
				
			||||||
 | 
					        height: 16px;
 | 
				
			||||||
 | 
					        margin-right: 4px;
 | 
				
			||||||
 | 
					        filter: brightness(2);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .path-list {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .path-item {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      gap: 10px;
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      input {
 | 
				
			||||||
 | 
					        flex: 1;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        max-width: 100%;
 | 
				
			||||||
 | 
					        padding: 10px;
 | 
				
			||||||
 | 
					        border: var(--border-in-light);
 | 
				
			||||||
 | 
					        border-radius: 10px;
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        font-size: 14px;
 | 
				
			||||||
 | 
					        background-color: var(--white);
 | 
				
			||||||
 | 
					        color: var(--black);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover {
 | 
				
			||||||
 | 
					          border-color: var(--gray-300);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:focus {
 | 
				
			||||||
 | 
					          border-color: var(--primary);
 | 
				
			||||||
 | 
					          outline: none;
 | 
				
			||||||
 | 
					          box-shadow: 0 0 0 2px var(--primary-10);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .browse-button {
 | 
				
			||||||
 | 
					        padding: 8px;
 | 
				
			||||||
 | 
					        border: var(--border-in-light);
 | 
				
			||||||
 | 
					        border-radius: 10px;
 | 
				
			||||||
 | 
					        background-color: transparent;
 | 
				
			||||||
 | 
					        color: var(--black-50);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover {
 | 
				
			||||||
 | 
					          border-color: var(--primary);
 | 
				
			||||||
 | 
					          color: var(--primary);
 | 
				
			||||||
 | 
					          background-color: transparent;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        svg {
 | 
				
			||||||
 | 
					          width: 16px;
 | 
				
			||||||
 | 
					          height: 16px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .delete-button {
 | 
				
			||||||
 | 
					        padding: 8px;
 | 
				
			||||||
 | 
					        border: var(--border-in-light);
 | 
				
			||||||
 | 
					        border-radius: 10px;
 | 
				
			||||||
 | 
					        background-color: transparent;
 | 
				
			||||||
 | 
					        color: var(--black-50);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover {
 | 
				
			||||||
 | 
					          border-color: var(--danger);
 | 
				
			||||||
 | 
					          color: var(--danger);
 | 
				
			||||||
 | 
					          background-color: transparent;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        svg {
 | 
				
			||||||
 | 
					          width: 16px;
 | 
				
			||||||
 | 
					          height: 16px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .file-input {
 | 
				
			||||||
 | 
					        display: none;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .add-button {
 | 
				
			||||||
 | 
					      align-self: flex-start;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      gap: 5px;
 | 
				
			||||||
 | 
					      padding: 8px 12px;
 | 
				
			||||||
 | 
					      background-color: transparent;
 | 
				
			||||||
 | 
					      border: var(--border-in-light);
 | 
				
			||||||
 | 
					      border-radius: 10px;
 | 
				
			||||||
 | 
					      color: var(--black);
 | 
				
			||||||
 | 
					      font-size: 12px;
 | 
				
			||||||
 | 
					      margin-top: 5px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:hover {
 | 
				
			||||||
 | 
					        border-color: var(--primary);
 | 
				
			||||||
 | 
					        color: var(--primary);
 | 
				
			||||||
 | 
					        background-color: transparent;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      svg {
 | 
				
			||||||
 | 
					        width: 16px;
 | 
				
			||||||
 | 
					        height: 16px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .config-section {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .config-header {
 | 
				
			||||||
 | 
					      margin-bottom: 12px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .config-title {
 | 
				
			||||||
 | 
					        font-size: 14px;
 | 
				
			||||||
 | 
					        font-weight: 600;
 | 
				
			||||||
 | 
					        color: var(--black);
 | 
				
			||||||
 | 
					        text-transform: capitalize;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .config-description {
 | 
				
			||||||
 | 
					        font-size: 12px;
 | 
				
			||||||
 | 
					        color: var(--gray-500);
 | 
				
			||||||
 | 
					        margin-top: 4px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .array-input {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					      gap: 12px;
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      padding: 16px;
 | 
				
			||||||
 | 
					      border: 1px solid var(--gray-200);
 | 
				
			||||||
 | 
					      border-radius: 10px;
 | 
				
			||||||
 | 
					      background-color: var(--white);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .array-input-item {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        gap: 8px;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        padding: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        input {
 | 
				
			||||||
 | 
					          width: 100%;
 | 
				
			||||||
 | 
					          padding: 8px 12px;
 | 
				
			||||||
 | 
					          background-color: var(--gray-50);
 | 
				
			||||||
 | 
					          border-radius: 6px;
 | 
				
			||||||
 | 
					          transition: all 0.3s ease;
 | 
				
			||||||
 | 
					          font-size: 13px;
 | 
				
			||||||
 | 
					          border: 1px solid var(--gray-200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          &:hover {
 | 
				
			||||||
 | 
					            background-color: var(--gray-100);
 | 
				
			||||||
 | 
					            border-color: var(--gray-300);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          &:focus {
 | 
				
			||||||
 | 
					            background-color: var(--white);
 | 
				
			||||||
 | 
					            border-color: var(--primary);
 | 
				
			||||||
 | 
					            outline: none;
 | 
				
			||||||
 | 
					            box-shadow: 0 0 0 2px var(--primary-10);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          &::placeholder {
 | 
				
			||||||
 | 
					            color: var(--gray-300);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :global(.icon-button) {
 | 
				
			||||||
 | 
					          width: 32px;
 | 
				
			||||||
 | 
					          height: 32px;
 | 
				
			||||||
 | 
					          padding: 0;
 | 
				
			||||||
 | 
					          border-radius: 6px;
 | 
				
			||||||
 | 
					          background-color: transparent;
 | 
				
			||||||
 | 
					          border: 1px solid var(--gray-200);
 | 
				
			||||||
 | 
					          flex-shrink: 0;
 | 
				
			||||||
 | 
					          display: flex;
 | 
				
			||||||
 | 
					          align-items: center;
 | 
				
			||||||
 | 
					          justify-content: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          &:hover {
 | 
				
			||||||
 | 
					            background-color: var(--gray-100);
 | 
				
			||||||
 | 
					            border-color: var(--gray-300);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          svg {
 | 
				
			||||||
 | 
					            width: 16px;
 | 
				
			||||||
 | 
					            height: 16px;
 | 
				
			||||||
 | 
					            opacity: 0.7;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      :global(.icon-button.add-path-button) {
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        background-color: var(--primary);
 | 
				
			||||||
 | 
					        color: white;
 | 
				
			||||||
 | 
					        padding: 8px 12px;
 | 
				
			||||||
 | 
					        border-radius: 6px;
 | 
				
			||||||
 | 
					        transition: all 0.3s ease;
 | 
				
			||||||
 | 
					        margin-top: 8px;
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					        justify-content: center;
 | 
				
			||||||
 | 
					        border: none;
 | 
				
			||||||
 | 
					        height: 36px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover {
 | 
				
			||||||
 | 
					          background-color: var(--primary-dark);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        svg {
 | 
				
			||||||
 | 
					          width: 16px;
 | 
				
			||||||
 | 
					          height: 16px;
 | 
				
			||||||
 | 
					          margin-right: 4px;
 | 
				
			||||||
 | 
					          filter: brightness(2);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .input-item {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    input {
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      padding: 10px;
 | 
				
			||||||
 | 
					      border: var(--border-in-light);
 | 
				
			||||||
 | 
					      border-radius: 10px;
 | 
				
			||||||
 | 
					      box-sizing: border-box;
 | 
				
			||||||
 | 
					      font-size: 14px;
 | 
				
			||||||
 | 
					      background-color: var(--white);
 | 
				
			||||||
 | 
					      color: var(--black);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:hover {
 | 
				
			||||||
 | 
					        border-color: var(--gray-300);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:focus {
 | 
				
			||||||
 | 
					        border-color: var(--primary);
 | 
				
			||||||
 | 
					        outline: none;
 | 
				
			||||||
 | 
					        box-shadow: 0 0 0 2px var(--primary-10);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &::placeholder {
 | 
				
			||||||
 | 
					        color: var(--gray-300) !important;
 | 
				
			||||||
 | 
					        opacity: 1;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .tools-list {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 16px;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    padding: 20px;
 | 
				
			||||||
 | 
					    max-width: 100%;
 | 
				
			||||||
 | 
					    overflow-x: hidden;
 | 
				
			||||||
 | 
					    word-break: break-word;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .tool-item {
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      box-sizing: border-box;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .tool-name {
 | 
				
			||||||
 | 
					        font-size: 14px;
 | 
				
			||||||
 | 
					        font-weight: 600;
 | 
				
			||||||
 | 
					        color: var(--black);
 | 
				
			||||||
 | 
					        margin-bottom: 8px;
 | 
				
			||||||
 | 
					        padding-left: 12px;
 | 
				
			||||||
 | 
					        border-left: 3px solid var(--primary);
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .tool-description {
 | 
				
			||||||
 | 
					        font-size: 13px;
 | 
				
			||||||
 | 
					        color: var(--gray-500);
 | 
				
			||||||
 | 
					        line-height: 1.6;
 | 
				
			||||||
 | 
					        padding-left: 15px;
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  :global {
 | 
				
			||||||
 | 
					    .modal-content {
 | 
				
			||||||
 | 
					      margin-top: 20px;
 | 
				
			||||||
 | 
					      max-width: 100%;
 | 
				
			||||||
 | 
					      overflow-x: hidden;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .list {
 | 
				
			||||||
 | 
					      padding: 10px;
 | 
				
			||||||
 | 
					      margin-bottom: 10px;
 | 
				
			||||||
 | 
					      background-color: var(--white);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .list-item {
 | 
				
			||||||
 | 
					      border: none;
 | 
				
			||||||
 | 
					      background-color: transparent;
 | 
				
			||||||
 | 
					      border-radius: 10px;
 | 
				
			||||||
 | 
					      padding: 10px;
 | 
				
			||||||
 | 
					      margin-bottom: 10px;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					      gap: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .list-header {
 | 
				
			||||||
 | 
					        margin-bottom: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .list-title {
 | 
				
			||||||
 | 
					          font-size: 14px;
 | 
				
			||||||
 | 
					          font-weight: bold;
 | 
				
			||||||
 | 
					          text-transform: capitalize;
 | 
				
			||||||
 | 
					          color: var(--black);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .list-sub-title {
 | 
				
			||||||
 | 
					          font-size: 12px;
 | 
				
			||||||
 | 
					          color: var(--gray-500);
 | 
				
			||||||
 | 
					          margin-top: 4px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes loading-pulse {
 | 
				
			||||||
 | 
					  0% {
 | 
				
			||||||
 | 
					    background-position: 200% 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  100% {
 | 
				
			||||||
 | 
					    background-position: -200% 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes pulse {
 | 
				
			||||||
 | 
					  0% {
 | 
				
			||||||
 | 
					    opacity: 0.6;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  50% {
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  100% {
 | 
				
			||||||
 | 
					    opacity: 0.6;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										755
									
								
								app/components/mcp-market.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,755 @@
 | 
				
			|||||||
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
 | 
					import { ErrorBoundary } from "./error";
 | 
				
			||||||
 | 
					import styles from "./mcp-market.module.scss";
 | 
				
			||||||
 | 
					import EditIcon from "../icons/edit.svg";
 | 
				
			||||||
 | 
					import AddIcon from "../icons/add.svg";
 | 
				
			||||||
 | 
					import CloseIcon from "../icons/close.svg";
 | 
				
			||||||
 | 
					import DeleteIcon from "../icons/delete.svg";
 | 
				
			||||||
 | 
					import RestartIcon from "../icons/reload.svg";
 | 
				
			||||||
 | 
					import EyeIcon from "../icons/eye.svg";
 | 
				
			||||||
 | 
					import GithubIcon from "../icons/github.svg";
 | 
				
			||||||
 | 
					import { List, ListItem, Modal, showToast } from "./ui-lib";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  addMcpServer,
 | 
				
			||||||
 | 
					  getClientsStatus,
 | 
				
			||||||
 | 
					  getClientTools,
 | 
				
			||||||
 | 
					  getMcpConfigFromFile,
 | 
				
			||||||
 | 
					  isMcpEnabled,
 | 
				
			||||||
 | 
					  pauseMcpServer,
 | 
				
			||||||
 | 
					  restartAllClients,
 | 
				
			||||||
 | 
					  resumeMcpServer,
 | 
				
			||||||
 | 
					} from "../mcp/actions";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ListToolsResponse,
 | 
				
			||||||
 | 
					  McpConfigData,
 | 
				
			||||||
 | 
					  PresetServer,
 | 
				
			||||||
 | 
					  ServerConfig,
 | 
				
			||||||
 | 
					  ServerStatusResponse,
 | 
				
			||||||
 | 
					} from "../mcp/types";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					import PlayIcon from "../icons/play.svg";
 | 
				
			||||||
 | 
					import StopIcon from "../icons/pause.svg";
 | 
				
			||||||
 | 
					import { Path } from "../constant";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ConfigProperty {
 | 
				
			||||||
 | 
					  type: string;
 | 
				
			||||||
 | 
					  description?: string;
 | 
				
			||||||
 | 
					  required?: boolean;
 | 
				
			||||||
 | 
					  minItems?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function McpMarketPage() {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const [mcpEnabled, setMcpEnabled] = useState(false);
 | 
				
			||||||
 | 
					  const [searchText, setSearchText] = useState("");
 | 
				
			||||||
 | 
					  const [userConfig, setUserConfig] = useState<Record<string, any>>({});
 | 
				
			||||||
 | 
					  const [editingServerId, setEditingServerId] = useState<string | undefined>();
 | 
				
			||||||
 | 
					  const [tools, setTools] = useState<ListToolsResponse["tools"] | null>(null);
 | 
				
			||||||
 | 
					  const [viewingServerId, setViewingServerId] = useState<string | undefined>();
 | 
				
			||||||
 | 
					  const [isLoading, setIsLoading] = useState(false);
 | 
				
			||||||
 | 
					  const [config, setConfig] = useState<McpConfigData>();
 | 
				
			||||||
 | 
					  const [clientStatuses, setClientStatuses] = useState<
 | 
				
			||||||
 | 
					    Record<string, ServerStatusResponse>
 | 
				
			||||||
 | 
					  >({});
 | 
				
			||||||
 | 
					  const [loadingPresets, setLoadingPresets] = useState(true);
 | 
				
			||||||
 | 
					  const [presetServers, setPresetServers] = useState<PresetServer[]>([]);
 | 
				
			||||||
 | 
					  const [loadingStates, setLoadingStates] = useState<Record<string, string>>(
 | 
				
			||||||
 | 
					    {},
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 检查 MCP 是否启用
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const checkMcpStatus = async () => {
 | 
				
			||||||
 | 
					      const enabled = await isMcpEnabled();
 | 
				
			||||||
 | 
					      setMcpEnabled(enabled);
 | 
				
			||||||
 | 
					      if (!enabled) {
 | 
				
			||||||
 | 
					        navigate(Path.Home);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    checkMcpStatus();
 | 
				
			||||||
 | 
					  }, [navigate]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 添加状态轮询
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!mcpEnabled || !config) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const updateStatuses = async () => {
 | 
				
			||||||
 | 
					      const statuses = await getClientsStatus();
 | 
				
			||||||
 | 
					      setClientStatuses(statuses);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 立即执行一次
 | 
				
			||||||
 | 
					    updateStatuses();
 | 
				
			||||||
 | 
					    // 每 1000ms 轮询一次
 | 
				
			||||||
 | 
					    const timer = setInterval(updateStatuses, 1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return () => clearInterval(timer);
 | 
				
			||||||
 | 
					  }, [mcpEnabled, config]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 加载预设服务器
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const loadPresetServers = async () => {
 | 
				
			||||||
 | 
					      if (!mcpEnabled) return;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        setLoadingPresets(true);
 | 
				
			||||||
 | 
					        const response = await fetch("https://nextchat.club/mcp/list");
 | 
				
			||||||
 | 
					        if (!response.ok) {
 | 
				
			||||||
 | 
					          throw new Error("Failed to load preset servers");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const data = await response.json();
 | 
				
			||||||
 | 
					        setPresetServers(data?.data ?? []);
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error("Failed to load preset servers:", error);
 | 
				
			||||||
 | 
					        showToast("Failed to load preset servers");
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        setLoadingPresets(false);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    loadPresetServers();
 | 
				
			||||||
 | 
					  }, [mcpEnabled]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 加载初始状态
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const loadInitialState = async () => {
 | 
				
			||||||
 | 
					      if (!mcpEnabled) return;
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        setIsLoading(true);
 | 
				
			||||||
 | 
					        const config = await getMcpConfigFromFile();
 | 
				
			||||||
 | 
					        setConfig(config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 获取所有客户端的状态
 | 
				
			||||||
 | 
					        const statuses = await getClientsStatus();
 | 
				
			||||||
 | 
					        setClientStatuses(statuses);
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error("Failed to load initial state:", error);
 | 
				
			||||||
 | 
					        showToast("Failed to load initial state");
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        setIsLoading(false);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    loadInitialState();
 | 
				
			||||||
 | 
					  }, [mcpEnabled]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 加载当前编辑服务器的配置
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!editingServerId || !config) return;
 | 
				
			||||||
 | 
					    const currentConfig = config.mcpServers[editingServerId];
 | 
				
			||||||
 | 
					    if (currentConfig) {
 | 
				
			||||||
 | 
					      // 从当前配置中提取用户配置
 | 
				
			||||||
 | 
					      const preset = presetServers.find((s) => s.id === editingServerId);
 | 
				
			||||||
 | 
					      if (preset?.configSchema) {
 | 
				
			||||||
 | 
					        const userConfig: Record<string, any> = {};
 | 
				
			||||||
 | 
					        Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => {
 | 
				
			||||||
 | 
					          if (mapping.type === "spread") {
 | 
				
			||||||
 | 
					            // For spread types, extract the array from args.
 | 
				
			||||||
 | 
					            const startPos = mapping.position ?? 0;
 | 
				
			||||||
 | 
					            userConfig[key] = currentConfig.args.slice(startPos);
 | 
				
			||||||
 | 
					          } else if (mapping.type === "single") {
 | 
				
			||||||
 | 
					            // For single types, get a single value
 | 
				
			||||||
 | 
					            userConfig[key] = currentConfig.args[mapping.position ?? 0];
 | 
				
			||||||
 | 
					          } else if (
 | 
				
			||||||
 | 
					            mapping.type === "env" &&
 | 
				
			||||||
 | 
					            mapping.key &&
 | 
				
			||||||
 | 
					            currentConfig.env
 | 
				
			||||||
 | 
					          ) {
 | 
				
			||||||
 | 
					            // For env types, get values from environment variables
 | 
				
			||||||
 | 
					            userConfig[key] = currentConfig.env[mapping.key];
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        setUserConfig(userConfig);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      setUserConfig({});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [editingServerId, config, presetServers]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!mcpEnabled) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 检查服务器是否已添加
 | 
				
			||||||
 | 
					  const isServerAdded = (id: string) => {
 | 
				
			||||||
 | 
					    return id in (config?.mcpServers ?? {});
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 保存服务器配置
 | 
				
			||||||
 | 
					  const saveServerConfig = async () => {
 | 
				
			||||||
 | 
					    const preset = presetServers.find((s) => s.id === editingServerId);
 | 
				
			||||||
 | 
					    if (!preset || !preset.configSchema || !editingServerId) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const savingServerId = editingServerId;
 | 
				
			||||||
 | 
					    setEditingServerId(undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      updateLoadingState(savingServerId, "Updating configuration...");
 | 
				
			||||||
 | 
					      // 构建服务器配置
 | 
				
			||||||
 | 
					      const args = [...preset.baseArgs];
 | 
				
			||||||
 | 
					      const env: Record<string, string> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => {
 | 
				
			||||||
 | 
					        const value = userConfig[key];
 | 
				
			||||||
 | 
					        if (mapping.type === "spread" && Array.isArray(value)) {
 | 
				
			||||||
 | 
					          const pos = mapping.position ?? 0;
 | 
				
			||||||
 | 
					          args.splice(pos, 0, ...value);
 | 
				
			||||||
 | 
					        } else if (
 | 
				
			||||||
 | 
					          mapping.type === "single" &&
 | 
				
			||||||
 | 
					          mapping.position !== undefined
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					          args[mapping.position] = value;
 | 
				
			||||||
 | 
					        } else if (
 | 
				
			||||||
 | 
					          mapping.type === "env" &&
 | 
				
			||||||
 | 
					          mapping.key &&
 | 
				
			||||||
 | 
					          typeof value === "string"
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					          env[mapping.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const serverConfig: ServerConfig = {
 | 
				
			||||||
 | 
					        command: preset.command,
 | 
				
			||||||
 | 
					        args,
 | 
				
			||||||
 | 
					        ...(Object.keys(env).length > 0 ? { env } : {}),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const newConfig = await addMcpServer(savingServerId, serverConfig);
 | 
				
			||||||
 | 
					      setConfig(newConfig);
 | 
				
			||||||
 | 
					      showToast("Server configuration updated successfully");
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      showToast(
 | 
				
			||||||
 | 
					        error instanceof Error ? error.message : "Failed to save configuration",
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      updateLoadingState(savingServerId, null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 获取服务器支持的 Tools
 | 
				
			||||||
 | 
					  const loadTools = async (id: string) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const result = await getClientTools(id);
 | 
				
			||||||
 | 
					      if (result) {
 | 
				
			||||||
 | 
					        setTools(result);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        throw new Error("Failed to load tools");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      showToast("Failed to load tools");
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					      setTools(null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 更新加载状态的辅助函数
 | 
				
			||||||
 | 
					  const updateLoadingState = (id: string, message: string | null) => {
 | 
				
			||||||
 | 
					    setLoadingStates((prev) => {
 | 
				
			||||||
 | 
					      if (message === null) {
 | 
				
			||||||
 | 
					        const { [id]: _, ...rest } = prev;
 | 
				
			||||||
 | 
					        return rest;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return { ...prev, [id]: message };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 修改添加服务器函数
 | 
				
			||||||
 | 
					  const addServer = async (preset: PresetServer) => {
 | 
				
			||||||
 | 
					    if (!preset.configurable) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const serverId = preset.id;
 | 
				
			||||||
 | 
					        updateLoadingState(serverId, "Creating MCP client...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const serverConfig: ServerConfig = {
 | 
				
			||||||
 | 
					          command: preset.command,
 | 
				
			||||||
 | 
					          args: [...preset.baseArgs],
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        const newConfig = await addMcpServer(preset.id, serverConfig);
 | 
				
			||||||
 | 
					        setConfig(newConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 更新状态
 | 
				
			||||||
 | 
					        const statuses = await getClientsStatus();
 | 
				
			||||||
 | 
					        setClientStatuses(statuses);
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        updateLoadingState(preset.id, null);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // 如果需要配置,打开配置对话框
 | 
				
			||||||
 | 
					      setEditingServerId(preset.id);
 | 
				
			||||||
 | 
					      setUserConfig({});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 修改暂停服务器函数
 | 
				
			||||||
 | 
					  const pauseServer = async (id: string) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      updateLoadingState(id, "Stopping server...");
 | 
				
			||||||
 | 
					      const newConfig = await pauseMcpServer(id);
 | 
				
			||||||
 | 
					      setConfig(newConfig);
 | 
				
			||||||
 | 
					      showToast("Server stopped successfully");
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      showToast("Failed to stop server");
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      updateLoadingState(id, null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Restart server
 | 
				
			||||||
 | 
					  const restartServer = async (id: string) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      updateLoadingState(id, "Starting server...");
 | 
				
			||||||
 | 
					      await resumeMcpServer(id);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      showToast(
 | 
				
			||||||
 | 
					        error instanceof Error
 | 
				
			||||||
 | 
					          ? error.message
 | 
				
			||||||
 | 
					          : "Failed to start server, please check logs",
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      updateLoadingState(id, null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Restart all clients
 | 
				
			||||||
 | 
					  const handleRestartAll = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      updateLoadingState("all", "Restarting all servers...");
 | 
				
			||||||
 | 
					      const newConfig = await restartAllClients();
 | 
				
			||||||
 | 
					      setConfig(newConfig);
 | 
				
			||||||
 | 
					      showToast("Restarting all clients");
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      showToast("Failed to restart clients");
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      updateLoadingState("all", null);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Render configuration form
 | 
				
			||||||
 | 
					  const renderConfigForm = () => {
 | 
				
			||||||
 | 
					    const preset = presetServers.find((s) => s.id === editingServerId);
 | 
				
			||||||
 | 
					    if (!preset?.configSchema) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Object.entries(preset.configSchema.properties).map(
 | 
				
			||||||
 | 
					      ([key, prop]: [string, ConfigProperty]) => {
 | 
				
			||||||
 | 
					        if (prop.type === "array") {
 | 
				
			||||||
 | 
					          const currentValue = userConfig[key as keyof typeof userConfig] || [];
 | 
				
			||||||
 | 
					          const itemLabel = (prop as any).itemLabel || key;
 | 
				
			||||||
 | 
					          const addButtonText =
 | 
				
			||||||
 | 
					            (prop as any).addButtonText || `Add ${itemLabel}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return (
 | 
				
			||||||
 | 
					            <ListItem
 | 
				
			||||||
 | 
					              key={key}
 | 
				
			||||||
 | 
					              title={key}
 | 
				
			||||||
 | 
					              subTitle={prop.description}
 | 
				
			||||||
 | 
					              vertical
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div className={styles["path-list"]}>
 | 
				
			||||||
 | 
					                {(currentValue as string[]).map(
 | 
				
			||||||
 | 
					                  (value: string, index: number) => (
 | 
				
			||||||
 | 
					                    <div key={index} className={styles["path-item"]}>
 | 
				
			||||||
 | 
					                      <input
 | 
				
			||||||
 | 
					                        type="text"
 | 
				
			||||||
 | 
					                        value={value}
 | 
				
			||||||
 | 
					                        placeholder={`${itemLabel} ${index + 1}`}
 | 
				
			||||||
 | 
					                        onChange={(e) => {
 | 
				
			||||||
 | 
					                          const newValue = [...currentValue] as string[];
 | 
				
			||||||
 | 
					                          newValue[index] = e.target.value;
 | 
				
			||||||
 | 
					                          setUserConfig({ ...userConfig, [key]: newValue });
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                      <IconButton
 | 
				
			||||||
 | 
					                        icon={<DeleteIcon />}
 | 
				
			||||||
 | 
					                        className={styles["delete-button"]}
 | 
				
			||||||
 | 
					                        onClick={() => {
 | 
				
			||||||
 | 
					                          const newValue = [...currentValue] as string[];
 | 
				
			||||||
 | 
					                          newValue.splice(index, 1);
 | 
				
			||||||
 | 
					                          setUserConfig({ ...userConfig, [key]: newValue });
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  icon={<AddIcon />}
 | 
				
			||||||
 | 
					                  text={addButtonText}
 | 
				
			||||||
 | 
					                  className={styles["add-button"]}
 | 
				
			||||||
 | 
					                  bordered
 | 
				
			||||||
 | 
					                  onClick={() => {
 | 
				
			||||||
 | 
					                    const newValue = [...currentValue, ""] as string[];
 | 
				
			||||||
 | 
					                    setUserConfig({ ...userConfig, [key]: newValue });
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </ListItem>
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        } else if (prop.type === "string") {
 | 
				
			||||||
 | 
					          const currentValue = userConfig[key as keyof typeof userConfig] || "";
 | 
				
			||||||
 | 
					          return (
 | 
				
			||||||
 | 
					            <ListItem key={key} title={key} subTitle={prop.description}>
 | 
				
			||||||
 | 
					              <input
 | 
				
			||||||
 | 
					                aria-label={key}
 | 
				
			||||||
 | 
					                type="text"
 | 
				
			||||||
 | 
					                value={currentValue}
 | 
				
			||||||
 | 
					                placeholder={`Enter ${key}`}
 | 
				
			||||||
 | 
					                onChange={(e) => {
 | 
				
			||||||
 | 
					                  setUserConfig({ ...userConfig, [key]: e.target.value });
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </ListItem>
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const checkServerStatus = (clientId: string) => {
 | 
				
			||||||
 | 
					    return clientStatuses[clientId] || { status: "undefined", errorMsg: null };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getServerStatusDisplay = (clientId: string) => {
 | 
				
			||||||
 | 
					    const status = checkServerStatus(clientId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const statusMap = {
 | 
				
			||||||
 | 
					      undefined: null, // 未配置/未找到不显示
 | 
				
			||||||
 | 
					      // 添加初始化状态
 | 
				
			||||||
 | 
					      initializing: (
 | 
				
			||||||
 | 
					        <span className={clsx(styles["server-status"], styles["initializing"])}>
 | 
				
			||||||
 | 
					          Initializing
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      paused: (
 | 
				
			||||||
 | 
					        <span className={clsx(styles["server-status"], styles["stopped"])}>
 | 
				
			||||||
 | 
					          Stopped
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      active: <span className={styles["server-status"]}>Running</span>,
 | 
				
			||||||
 | 
					      error: (
 | 
				
			||||||
 | 
					        <span className={clsx(styles["server-status"], styles["error"])}>
 | 
				
			||||||
 | 
					          Error
 | 
				
			||||||
 | 
					          <span className={styles["error-message"]}>: {status.errorMsg}</span>
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return statusMap[status.status];
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get the type of operation status
 | 
				
			||||||
 | 
					  const getOperationStatusType = (message: string) => {
 | 
				
			||||||
 | 
					    if (message.toLowerCase().includes("stopping")) return "stopping";
 | 
				
			||||||
 | 
					    if (message.toLowerCase().includes("starting")) return "starting";
 | 
				
			||||||
 | 
					    if (message.toLowerCase().includes("error")) return "error";
 | 
				
			||||||
 | 
					    return "default";
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 渲染服务器列表
 | 
				
			||||||
 | 
					  const renderServerList = () => {
 | 
				
			||||||
 | 
					    if (loadingPresets) {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <div className={styles["loading-container"]}>
 | 
				
			||||||
 | 
					          <div className={styles["loading-text"]}>
 | 
				
			||||||
 | 
					            Loading preset server list...
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!Array.isArray(presetServers) || presetServers.length === 0) {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <div className={styles["empty-container"]}>
 | 
				
			||||||
 | 
					          <div className={styles["empty-text"]}>No servers available</div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return presetServers
 | 
				
			||||||
 | 
					      .filter((server) => {
 | 
				
			||||||
 | 
					        if (searchText.length === 0) return true;
 | 
				
			||||||
 | 
					        const searchLower = searchText.toLowerCase();
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          server.name.toLowerCase().includes(searchLower) ||
 | 
				
			||||||
 | 
					          server.description.toLowerCase().includes(searchLower) ||
 | 
				
			||||||
 | 
					          server.tags.some((tag) => tag.toLowerCase().includes(searchLower))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .sort((a, b) => {
 | 
				
			||||||
 | 
					        const aStatus = checkServerStatus(a.id).status;
 | 
				
			||||||
 | 
					        const bStatus = checkServerStatus(b.id).status;
 | 
				
			||||||
 | 
					        const aLoading = loadingStates[a.id];
 | 
				
			||||||
 | 
					        const bLoading = loadingStates[b.id];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 定义状态优先级
 | 
				
			||||||
 | 
					        const statusPriority: Record<string, number> = {
 | 
				
			||||||
 | 
					          error: 0, // Highest priority for error status
 | 
				
			||||||
 | 
					          active: 1, // Second for active
 | 
				
			||||||
 | 
					          initializing: 2, // Initializing
 | 
				
			||||||
 | 
					          starting: 3, // Starting
 | 
				
			||||||
 | 
					          stopping: 4, // Stopping
 | 
				
			||||||
 | 
					          paused: 5, // Paused
 | 
				
			||||||
 | 
					          undefined: 6, // Lowest priority for undefined
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get actual status (including loading status)
 | 
				
			||||||
 | 
					        const getEffectiveStatus = (status: string, loading?: string) => {
 | 
				
			||||||
 | 
					          if (loading) {
 | 
				
			||||||
 | 
					            const operationType = getOperationStatusType(loading);
 | 
				
			||||||
 | 
					            return operationType === "default" ? status : operationType;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (status === "initializing" && !loading) {
 | 
				
			||||||
 | 
					            return "active";
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return status;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const aEffectiveStatus = getEffectiveStatus(aStatus, aLoading);
 | 
				
			||||||
 | 
					        const bEffectiveStatus = getEffectiveStatus(bStatus, bLoading);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 首先按状态排序
 | 
				
			||||||
 | 
					        if (aEffectiveStatus !== bEffectiveStatus) {
 | 
				
			||||||
 | 
					          return (
 | 
				
			||||||
 | 
					            (statusPriority[aEffectiveStatus] ?? 6) -
 | 
				
			||||||
 | 
					            (statusPriority[bEffectiveStatus] ?? 6)
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Sort by name when statuses are the same
 | 
				
			||||||
 | 
					        return a.name.localeCompare(b.name);
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .map((server) => (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={clsx(styles["mcp-market-item"], {
 | 
				
			||||||
 | 
					            [styles["loading"]]: loadingStates[server.id],
 | 
				
			||||||
 | 
					          })}
 | 
				
			||||||
 | 
					          key={server.id}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className={styles["mcp-market-header"]}>
 | 
				
			||||||
 | 
					            <div className={styles["mcp-market-title"]}>
 | 
				
			||||||
 | 
					              <div className={styles["mcp-market-name"]}>
 | 
				
			||||||
 | 
					                {server.name}
 | 
				
			||||||
 | 
					                {loadingStates[server.id] && (
 | 
				
			||||||
 | 
					                  <span
 | 
				
			||||||
 | 
					                    className={styles["operation-status"]}
 | 
				
			||||||
 | 
					                    data-status={getOperationStatusType(
 | 
				
			||||||
 | 
					                      loadingStates[server.id],
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    {loadingStates[server.id]}
 | 
				
			||||||
 | 
					                  </span>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					                {!loadingStates[server.id] && getServerStatusDisplay(server.id)}
 | 
				
			||||||
 | 
					                {server.repo && (
 | 
				
			||||||
 | 
					                  <a
 | 
				
			||||||
 | 
					                    href={server.repo}
 | 
				
			||||||
 | 
					                    target="_blank"
 | 
				
			||||||
 | 
					                    rel="noopener noreferrer"
 | 
				
			||||||
 | 
					                    className={styles["repo-link"]}
 | 
				
			||||||
 | 
					                    title="Open repository"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <GithubIcon />
 | 
				
			||||||
 | 
					                  </a>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div className={styles["tags-container"]}>
 | 
				
			||||||
 | 
					                {server.tags.map((tag, index) => (
 | 
				
			||||||
 | 
					                  <span key={index} className={styles["tag"]}>
 | 
				
			||||||
 | 
					                    {tag}
 | 
				
			||||||
 | 
					                  </span>
 | 
				
			||||||
 | 
					                ))}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={clsx(styles["mcp-market-info"], "one-line")}
 | 
				
			||||||
 | 
					                title={server.description}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {server.description}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className={styles["mcp-market-actions"]}>
 | 
				
			||||||
 | 
					              {isServerAdded(server.id) ? (
 | 
				
			||||||
 | 
					                <>
 | 
				
			||||||
 | 
					                  {server.configurable && (
 | 
				
			||||||
 | 
					                    <IconButton
 | 
				
			||||||
 | 
					                      icon={<EditIcon />}
 | 
				
			||||||
 | 
					                      text="Configure"
 | 
				
			||||||
 | 
					                      onClick={() => setEditingServerId(server.id)}
 | 
				
			||||||
 | 
					                      disabled={isLoading}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                  {checkServerStatus(server.id).status === "paused" ? (
 | 
				
			||||||
 | 
					                    <>
 | 
				
			||||||
 | 
					                      <IconButton
 | 
				
			||||||
 | 
					                        icon={<PlayIcon />}
 | 
				
			||||||
 | 
					                        text="Start"
 | 
				
			||||||
 | 
					                        onClick={() => restartServer(server.id)}
 | 
				
			||||||
 | 
					                        disabled={isLoading}
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                      {/* <IconButton
 | 
				
			||||||
 | 
					                        icon={<DeleteIcon />}
 | 
				
			||||||
 | 
					                        text="Remove"
 | 
				
			||||||
 | 
					                        onClick={() => removeServer(server.id)}
 | 
				
			||||||
 | 
					                        disabled={isLoading}
 | 
				
			||||||
 | 
					                      /> */}
 | 
				
			||||||
 | 
					                    </>
 | 
				
			||||||
 | 
					                  ) : (
 | 
				
			||||||
 | 
					                    <>
 | 
				
			||||||
 | 
					                      <IconButton
 | 
				
			||||||
 | 
					                        icon={<EyeIcon />}
 | 
				
			||||||
 | 
					                        text="Tools"
 | 
				
			||||||
 | 
					                        onClick={async () => {
 | 
				
			||||||
 | 
					                          setViewingServerId(server.id);
 | 
				
			||||||
 | 
					                          await loadTools(server.id);
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                        disabled={
 | 
				
			||||||
 | 
					                          isLoading ||
 | 
				
			||||||
 | 
					                          checkServerStatus(server.id).status === "error"
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                      <IconButton
 | 
				
			||||||
 | 
					                        icon={<StopIcon />}
 | 
				
			||||||
 | 
					                        text="Stop"
 | 
				
			||||||
 | 
					                        onClick={() => pauseServer(server.id)}
 | 
				
			||||||
 | 
					                        disabled={isLoading}
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                    </>
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                </>
 | 
				
			||||||
 | 
					              ) : (
 | 
				
			||||||
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  icon={<AddIcon />}
 | 
				
			||||||
 | 
					                  text="Add"
 | 
				
			||||||
 | 
					                  onClick={() => addServer(server)}
 | 
				
			||||||
 | 
					                  disabled={isLoading}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      ));
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ErrorBoundary>
 | 
				
			||||||
 | 
					      <div className={styles["mcp-market-page"]}>
 | 
				
			||||||
 | 
					        <div className="window-header">
 | 
				
			||||||
 | 
					          <div className="window-header-title">
 | 
				
			||||||
 | 
					            <div className="window-header-main-title">
 | 
				
			||||||
 | 
					              MCP Market
 | 
				
			||||||
 | 
					              {loadingStates["all"] && (
 | 
				
			||||||
 | 
					                <span className={styles["loading-indicator"]}>
 | 
				
			||||||
 | 
					                  {loadingStates["all"]}
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className="window-header-sub-title">
 | 
				
			||||||
 | 
					              {Object.keys(config?.mcpServers ?? {}).length} servers configured
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className="window-actions">
 | 
				
			||||||
 | 
					            <div className="window-action-button">
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                icon={<RestartIcon />}
 | 
				
			||||||
 | 
					                bordered
 | 
				
			||||||
 | 
					                onClick={handleRestartAll}
 | 
				
			||||||
 | 
					                text="Restart All"
 | 
				
			||||||
 | 
					                disabled={isLoading}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className="window-action-button">
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                icon={<CloseIcon />}
 | 
				
			||||||
 | 
					                bordered
 | 
				
			||||||
 | 
					                onClick={() => navigate(-1)}
 | 
				
			||||||
 | 
					                disabled={isLoading}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className={styles["mcp-market-page-body"]}>
 | 
				
			||||||
 | 
					          <div className={styles["mcp-market-filter"]}>
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              type="text"
 | 
				
			||||||
 | 
					              className={styles["search-bar"]}
 | 
				
			||||||
 | 
					              placeholder={"Search MCP Server"}
 | 
				
			||||||
 | 
					              autoFocus
 | 
				
			||||||
 | 
					              onInput={(e) => setSearchText(e.currentTarget.value)}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className={styles["server-list"]}>{renderServerList()}</div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {/*编辑服务器配置*/}
 | 
				
			||||||
 | 
					        {editingServerId && (
 | 
				
			||||||
 | 
					          <div className="modal-mask">
 | 
				
			||||||
 | 
					            <Modal
 | 
				
			||||||
 | 
					              title={`Configure Server - ${editingServerId}`}
 | 
				
			||||||
 | 
					              onClose={() => !isLoading && setEditingServerId(undefined)}
 | 
				
			||||||
 | 
					              actions={[
 | 
				
			||||||
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  key="cancel"
 | 
				
			||||||
 | 
					                  text="Cancel"
 | 
				
			||||||
 | 
					                  onClick={() => setEditingServerId(undefined)}
 | 
				
			||||||
 | 
					                  bordered
 | 
				
			||||||
 | 
					                  disabled={isLoading}
 | 
				
			||||||
 | 
					                />,
 | 
				
			||||||
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  key="confirm"
 | 
				
			||||||
 | 
					                  text="Save"
 | 
				
			||||||
 | 
					                  type="primary"
 | 
				
			||||||
 | 
					                  onClick={saveServerConfig}
 | 
				
			||||||
 | 
					                  bordered
 | 
				
			||||||
 | 
					                  disabled={isLoading}
 | 
				
			||||||
 | 
					                />,
 | 
				
			||||||
 | 
					              ]}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <List>{renderConfigForm()}</List>
 | 
				
			||||||
 | 
					            </Modal>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {viewingServerId && (
 | 
				
			||||||
 | 
					          <div className="modal-mask">
 | 
				
			||||||
 | 
					            <Modal
 | 
				
			||||||
 | 
					              title={`Server Details - ${viewingServerId}`}
 | 
				
			||||||
 | 
					              onClose={() => setViewingServerId(undefined)}
 | 
				
			||||||
 | 
					              actions={[
 | 
				
			||||||
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  key="close"
 | 
				
			||||||
 | 
					                  text="Close"
 | 
				
			||||||
 | 
					                  onClick={() => setViewingServerId(undefined)}
 | 
				
			||||||
 | 
					                  bordered
 | 
				
			||||||
 | 
					                />,
 | 
				
			||||||
 | 
					              ]}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div className={styles["tools-list"]}>
 | 
				
			||||||
 | 
					                {isLoading ? (
 | 
				
			||||||
 | 
					                  <div>Loading...</div>
 | 
				
			||||||
 | 
					                ) : tools?.tools ? (
 | 
				
			||||||
 | 
					                  tools.tools.map(
 | 
				
			||||||
 | 
					                    (tool: ListToolsResponse["tools"], index: number) => (
 | 
				
			||||||
 | 
					                      <div key={index} className={styles["tool-item"]}>
 | 
				
			||||||
 | 
					                        <div className={styles["tool-name"]}>{tool.name}</div>
 | 
				
			||||||
 | 
					                        <div className={styles["tool-description"]}>
 | 
				
			||||||
 | 
					                          {tool.description}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                ) : (
 | 
				
			||||||
 | 
					                  <div>No tools available</div>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </Modal>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </ErrorBoundary>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -8,6 +8,7 @@ import Locale from "../locales";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import styles from "./message-selector.module.scss";
 | 
					import styles from "./message-selector.module.scss";
 | 
				
			||||||
import { getMessageTextContent } from "../utils";
 | 
					import { getMessageTextContent } from "../utils";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function useShiftRange() {
 | 
					function useShiftRange() {
 | 
				
			||||||
  const [startIndex, setStartIndex] = useState<number>();
 | 
					  const [startIndex, setStartIndex] = useState<number>();
 | 
				
			||||||
@@ -71,6 +72,7 @@ export function MessageSelector(props: {
 | 
				
			|||||||
  defaultSelectAll?: boolean;
 | 
					  defaultSelectAll?: boolean;
 | 
				
			||||||
  onSelected?: (messages: ChatMessage[]) => void;
 | 
					  onSelected?: (messages: ChatMessage[]) => void;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
 | 
					  const LATEST_COUNT = 4;
 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
  const session = chatStore.currentSession();
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
  const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
 | 
					  const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
 | 
				
			||||||
@@ -141,15 +143,13 @@ export function MessageSelector(props: {
 | 
				
			|||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
  }, [startIndex, endIndex]);
 | 
					  }, [startIndex, endIndex]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const LATEST_COUNT = 4;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={styles["message-selector"]}>
 | 
					    <div className={styles["message-selector"]}>
 | 
				
			||||||
      <div className={styles["message-filter"]}>
 | 
					      <div className={styles["message-filter"]}>
 | 
				
			||||||
        <input
 | 
					        <input
 | 
				
			||||||
          type="text"
 | 
					          type="text"
 | 
				
			||||||
          placeholder={Locale.Select.Search}
 | 
					          placeholder={Locale.Select.Search}
 | 
				
			||||||
          className={styles["filter-item"] + " " + styles["search-bar"]}
 | 
					          className={clsx(styles["filter-item"], styles["search-bar"])}
 | 
				
			||||||
          value={searchInput}
 | 
					          value={searchInput}
 | 
				
			||||||
          onInput={(e) => {
 | 
					          onInput={(e) => {
 | 
				
			||||||
            setSearchInput(e.currentTarget.value);
 | 
					            setSearchInput(e.currentTarget.value);
 | 
				
			||||||
@@ -196,9 +196,9 @@ export function MessageSelector(props: {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          return (
 | 
					          return (
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
              className={`${styles["message"]} ${
 | 
					              className={clsx(styles["message"], {
 | 
				
			||||||
                props.selection.has(m.id!) && styles["message-selected"]
 | 
					                [styles["message-selected"]]: props.selection.has(m.id!),
 | 
				
			||||||
              }`}
 | 
					              })}
 | 
				
			||||||
              key={i}
 | 
					              key={i}
 | 
				
			||||||
              onClick={() => {
 | 
					              onClick={() => {
 | 
				
			||||||
                props.updateSelection((selection) => {
 | 
					                props.updateSelection((selection) => {
 | 
				
			||||||
@@ -221,7 +221,7 @@ export function MessageSelector(props: {
 | 
				
			|||||||
                <div className={styles["date"]}>
 | 
					                <div className={styles["date"]}>
 | 
				
			||||||
                  {new Date(m.date).toLocaleString()}
 | 
					                  {new Date(m.date).toLocaleString()}
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div className={`${styles["content"]} one-line`}>
 | 
					                <div className={clsx(styles["content"], "one-line")}>
 | 
				
			||||||
                  {getMessageTextContent(m)}
 | 
					                  {getMessageTextContent(m)}
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										7
									
								
								app/components/model-config.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					.select-compress-model {
 | 
				
			||||||
 | 
					  width: 60%;
 | 
				
			||||||
 | 
					  select {
 | 
				
			||||||
 | 
					    max-width: 100%;
 | 
				
			||||||
 | 
					    white-space: normal;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -5,13 +5,21 @@ import Locale from "../locales";
 | 
				
			|||||||
import { InputRange } from "./input-range";
 | 
					import { InputRange } from "./input-range";
 | 
				
			||||||
import { ListItem, Select } from "./ui-lib";
 | 
					import { ListItem, Select } from "./ui-lib";
 | 
				
			||||||
import { useAllModels } from "../utils/hooks";
 | 
					import { useAllModels } from "../utils/hooks";
 | 
				
			||||||
 | 
					import { groupBy } from "lodash-es";
 | 
				
			||||||
 | 
					import styles from "./model-config.module.scss";
 | 
				
			||||||
 | 
					import { getModelProvider } from "../utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ModelConfigList(props: {
 | 
					export function ModelConfigList(props: {
 | 
				
			||||||
  modelConfig: ModelConfig;
 | 
					  modelConfig: ModelConfig;
 | 
				
			||||||
  updateConfig: (updater: (config: ModelConfig) => void) => void;
 | 
					  updateConfig: (updater: (config: ModelConfig) => void) => void;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const allModels = useAllModels();
 | 
					  const allModels = useAllModels();
 | 
				
			||||||
 | 
					  const groupModels = groupBy(
 | 
				
			||||||
 | 
					    allModels.filter((v) => v.available),
 | 
				
			||||||
 | 
					    "provider.providerName",
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
  const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`;
 | 
					  const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`;
 | 
				
			||||||
 | 
					  const compressModelValue = `${props.modelConfig.compressModel}@${props.modelConfig?.compressProviderName}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
@@ -19,21 +27,26 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
        <Select
 | 
					        <Select
 | 
				
			||||||
          aria-label={Locale.Settings.Model}
 | 
					          aria-label={Locale.Settings.Model}
 | 
				
			||||||
          value={value}
 | 
					          value={value}
 | 
				
			||||||
 | 
					          align="left"
 | 
				
			||||||
          onChange={(e) => {
 | 
					          onChange={(e) => {
 | 
				
			||||||
            const [model, providerName] = e.currentTarget.value.split("@");
 | 
					            const [model, providerName] = getModelProvider(
 | 
				
			||||||
 | 
					              e.currentTarget.value,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
            props.updateConfig((config) => {
 | 
					            props.updateConfig((config) => {
 | 
				
			||||||
              config.model = ModalConfigValidator.model(model);
 | 
					              config.model = ModalConfigValidator.model(model);
 | 
				
			||||||
              config.providerName = providerName as ServiceProvider;
 | 
					              config.providerName = providerName as ServiceProvider;
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          {allModels
 | 
					          {Object.keys(groupModels).map((providerName, index) => (
 | 
				
			||||||
            .filter((v) => v.available)
 | 
					            <optgroup label={providerName} key={index}>
 | 
				
			||||||
            .map((v, i) => (
 | 
					              {groupModels[providerName].map((v, i) => (
 | 
				
			||||||
              <option value={`${v.name}@${v.provider?.providerName}`} key={i}>
 | 
					                <option value={`${v.name}@${v.provider?.providerName}`} key={i}>
 | 
				
			||||||
                {v.displayName}({v.provider?.providerName})
 | 
					                  {v.displayName}
 | 
				
			||||||
              </option>
 | 
					                </option>
 | 
				
			||||||
            ))}
 | 
					              ))}
 | 
				
			||||||
 | 
					            </optgroup>
 | 
				
			||||||
 | 
					          ))}
 | 
				
			||||||
        </Select>
 | 
					        </Select>
 | 
				
			||||||
      </ListItem>
 | 
					      </ListItem>
 | 
				
			||||||
      <ListItem
 | 
					      <ListItem
 | 
				
			||||||
@@ -228,6 +241,33 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        ></input>
 | 
					        ></input>
 | 
				
			||||||
      </ListItem>
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.CompressModel.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.CompressModel.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Select
 | 
				
			||||||
 | 
					          className={styles["select-compress-model"]}
 | 
				
			||||||
 | 
					          aria-label={Locale.Settings.CompressModel.Title}
 | 
				
			||||||
 | 
					          value={compressModelValue}
 | 
				
			||||||
 | 
					          onChange={(e) => {
 | 
				
			||||||
 | 
					            const [model, providerName] = getModelProvider(
 | 
				
			||||||
 | 
					              e.currentTarget.value,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            props.updateConfig((config) => {
 | 
				
			||||||
 | 
					              config.compressModel = ModalConfigValidator.model(model);
 | 
				
			||||||
 | 
					              config.compressProviderName = providerName as ServiceProvider;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {allModels
 | 
				
			||||||
 | 
					            .filter((v) => v.available)
 | 
				
			||||||
 | 
					            .map((v, i) => (
 | 
				
			||||||
 | 
					              <option value={`${v.name}@${v.provider?.providerName}`} key={i}>
 | 
				
			||||||
 | 
					                {v.displayName}({v.provider?.providerName})
 | 
				
			||||||
 | 
					              </option>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					        </Select>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,7 @@ import { MaskAvatar } from "./mask";
 | 
				
			|||||||
import { useCommand } from "../command";
 | 
					import { useCommand } from "../command";
 | 
				
			||||||
import { showConfirm } from "./ui-lib";
 | 
					import { showConfirm } from "./ui-lib";
 | 
				
			||||||
import { BUILTIN_MASK_STORE } from "../masks";
 | 
					import { BUILTIN_MASK_STORE } from "../masks";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
 | 
					function MaskItem(props: { mask: Mask; onClick?: () => void }) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@@ -24,7 +25,9 @@ function MaskItem(props: { mask: Mask; onClick?: () => void }) {
 | 
				
			|||||||
        avatar={props.mask.avatar}
 | 
					        avatar={props.mask.avatar}
 | 
				
			||||||
        model={props.mask.modelConfig.model}
 | 
					        model={props.mask.modelConfig.model}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
 | 
					      <div className={clsx(styles["mask-name"], "one-line")}>
 | 
				
			||||||
 | 
					        {props.mask.name}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										38
									
								
								app/components/plugin.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					.plugin-title {
 | 
				
			||||||
 | 
					  font-weight: bolder;
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					  margin: 10px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.plugin-content {
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  font-family: inherit;
 | 
				
			||||||
 | 
					  pre code {
 | 
				
			||||||
 | 
					    max-height: 240px;
 | 
				
			||||||
 | 
					    overflow-y: auto;
 | 
				
			||||||
 | 
					    white-space: pre-wrap;
 | 
				
			||||||
 | 
					    min-width: 280px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.plugin-schema {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: flex-end;
 | 
				
			||||||
 | 
					  flex-direction: row;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  input {
 | 
				
			||||||
 | 
					    margin-right: 20px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @media screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					        margin-right: 0px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 5px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    button {
 | 
				
			||||||
 | 
					      padding: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										370
									
								
								app/components/plugin.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,370 @@
 | 
				
			|||||||
 | 
					import { useDebouncedCallback } from "use-debounce";
 | 
				
			||||||
 | 
					import OpenAPIClientAxios from "openapi-client-axios";
 | 
				
			||||||
 | 
					import yaml from "js-yaml";
 | 
				
			||||||
 | 
					import { PLUGINS_REPO_URL } from "../constant";
 | 
				
			||||||
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
 | 
					import { ErrorBoundary } from "./error";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import styles from "./mask.module.scss";
 | 
				
			||||||
 | 
					import pluginStyles from "./plugin.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import EditIcon from "../icons/edit.svg";
 | 
				
			||||||
 | 
					import AddIcon from "../icons/add.svg";
 | 
				
			||||||
 | 
					import CloseIcon from "../icons/close.svg";
 | 
				
			||||||
 | 
					import DeleteIcon from "../icons/delete.svg";
 | 
				
			||||||
 | 
					import ConfirmIcon from "../icons/confirm.svg";
 | 
				
			||||||
 | 
					import ReloadIcon from "../icons/reload.svg";
 | 
				
			||||||
 | 
					import GithubIcon from "../icons/github.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  PasswordInput,
 | 
				
			||||||
 | 
					  List,
 | 
				
			||||||
 | 
					  ListItem,
 | 
				
			||||||
 | 
					  Modal,
 | 
				
			||||||
 | 
					  showConfirm,
 | 
				
			||||||
 | 
					  showToast,
 | 
				
			||||||
 | 
					} from "./ui-lib";
 | 
				
			||||||
 | 
					import Locale from "../locales";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function PluginPage() {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const pluginStore = usePluginStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const allPlugins = pluginStore.getAll();
 | 
				
			||||||
 | 
					  const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]);
 | 
				
			||||||
 | 
					  const [searchText, setSearchText] = useState("");
 | 
				
			||||||
 | 
					  const plugins = searchText.length > 0 ? searchPlugins : allPlugins;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // refactored already, now it accurate
 | 
				
			||||||
 | 
					  const onSearch = (text: string) => {
 | 
				
			||||||
 | 
					    setSearchText(text);
 | 
				
			||||||
 | 
					    if (text.length > 0) {
 | 
				
			||||||
 | 
					      const result = allPlugins.filter(
 | 
				
			||||||
 | 
					        (m) => m?.title.toLowerCase().includes(text.toLowerCase()),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      setSearchPlugins(result);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      setSearchPlugins(allPlugins);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [editingPluginId, setEditingPluginId] = useState<string | undefined>();
 | 
				
			||||||
 | 
					  const editingPlugin = pluginStore.get(editingPluginId);
 | 
				
			||||||
 | 
					  const editingPluginTool = FunctionToolService.get(editingPlugin?.id);
 | 
				
			||||||
 | 
					  const closePluginModal = () => setEditingPluginId(undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onChangePlugin = useDebouncedCallback((editingPlugin, e) => {
 | 
				
			||||||
 | 
					    const content = e.target.innerText;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const api = new OpenAPIClientAxios({
 | 
				
			||||||
 | 
					        definition: yaml.load(content) as any,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      api
 | 
				
			||||||
 | 
					        .init()
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					          if (content != editingPlugin.content) {
 | 
				
			||||||
 | 
					            pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
				
			||||||
 | 
					              plugin.content = content;
 | 
				
			||||||
 | 
					              const tool = FunctionToolService.add(plugin, true);
 | 
				
			||||||
 | 
					              plugin.title = tool.api.definition.info.title;
 | 
				
			||||||
 | 
					              plugin.version = tool.api.definition.info.version;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch((e) => {
 | 
				
			||||||
 | 
					          console.error(e);
 | 
				
			||||||
 | 
					          showToast(Locale.Plugin.EditModal.Error);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(e);
 | 
				
			||||||
 | 
					      showToast(Locale.Plugin.EditModal.Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, 100).bind(null, editingPlugin);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [loadUrl, setLoadUrl] = useState<string>("");
 | 
				
			||||||
 | 
					  const loadFromUrl = (loadUrl: string) =>
 | 
				
			||||||
 | 
					    fetch(loadUrl)
 | 
				
			||||||
 | 
					      .catch((e) => {
 | 
				
			||||||
 | 
					        const p = new URL(loadUrl);
 | 
				
			||||||
 | 
					        return fetch(`/api/proxy/${p.pathname}?${p.search}`, {
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            "X-Base-URL": p.origin,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .then((res) => res.text())
 | 
				
			||||||
 | 
					      .then((content) => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          return JSON.stringify(JSON.parse(content), null, "  ");
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          return content;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .then((content) => {
 | 
				
			||||||
 | 
					        pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
				
			||||||
 | 
					          plugin.content = content;
 | 
				
			||||||
 | 
					          const tool = FunctionToolService.add(plugin, true);
 | 
				
			||||||
 | 
					          plugin.title = tool.api.definition.info.title;
 | 
				
			||||||
 | 
					          plugin.version = tool.api.definition.info.version;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch((e) => {
 | 
				
			||||||
 | 
					        showToast(Locale.Plugin.EditModal.Error);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ErrorBoundary>
 | 
				
			||||||
 | 
					      <div className={styles["mask-page"]}>
 | 
				
			||||||
 | 
					        <div className="window-header">
 | 
				
			||||||
 | 
					          <div className="window-header-title">
 | 
				
			||||||
 | 
					            <div className="window-header-main-title">
 | 
				
			||||||
 | 
					              {Locale.Plugin.Page.Title}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className="window-header-submai-title">
 | 
				
			||||||
 | 
					              {Locale.Plugin.Page.SubTitle(plugins.length)}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className="window-actions">
 | 
				
			||||||
 | 
					            <div className="window-action-button">
 | 
				
			||||||
 | 
					              <a
 | 
				
			||||||
 | 
					                href={PLUGINS_REPO_URL}
 | 
				
			||||||
 | 
					                target="_blank"
 | 
				
			||||||
 | 
					                rel="noopener noreferrer"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <IconButton icon={<GithubIcon />} bordered />
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className="window-action-button">
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                icon={<CloseIcon />}
 | 
				
			||||||
 | 
					                bordered
 | 
				
			||||||
 | 
					                onClick={() => navigate(-1)}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className={styles["mask-page-body"]}>
 | 
				
			||||||
 | 
					          <div className={styles["mask-filter"]}>
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              type="text"
 | 
				
			||||||
 | 
					              className={styles["search-bar"]}
 | 
				
			||||||
 | 
					              placeholder={Locale.Plugin.Page.Search}
 | 
				
			||||||
 | 
					              autoFocus
 | 
				
			||||||
 | 
					              onInput={(e) => onSearch(e.currentTarget.value)}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <IconButton
 | 
				
			||||||
 | 
					              className={styles["mask-create"]}
 | 
				
			||||||
 | 
					              icon={<AddIcon />}
 | 
				
			||||||
 | 
					              text={Locale.Plugin.Page.Create}
 | 
				
			||||||
 | 
					              bordered
 | 
				
			||||||
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                const createdPlugin = pluginStore.create();
 | 
				
			||||||
 | 
					                setEditingPluginId(createdPlugin.id);
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div>
 | 
				
			||||||
 | 
					            {plugins.length == 0 && (
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                style={{
 | 
				
			||||||
 | 
					                  display: "flex",
 | 
				
			||||||
 | 
					                  margin: "60px auto",
 | 
				
			||||||
 | 
					                  alignItems: "center",
 | 
				
			||||||
 | 
					                  justifyContent: "center",
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {Locale.Plugin.Page.Find}
 | 
				
			||||||
 | 
					                <a
 | 
				
			||||||
 | 
					                  href={PLUGINS_REPO_URL}
 | 
				
			||||||
 | 
					                  target="_blank"
 | 
				
			||||||
 | 
					                  rel="noopener noreferrer"
 | 
				
			||||||
 | 
					                  style={{ marginLeft: 16 }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <IconButton icon={<GithubIcon />} bordered />
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {plugins.map((m) => (
 | 
				
			||||||
 | 
					              <div className={styles["mask-item"]} key={m.id}>
 | 
				
			||||||
 | 
					                <div className={styles["mask-header"]}>
 | 
				
			||||||
 | 
					                  <div className={styles["mask-icon"]}></div>
 | 
				
			||||||
 | 
					                  <div className={styles["mask-title"]}>
 | 
				
			||||||
 | 
					                    <div className={styles["mask-name"]}>
 | 
				
			||||||
 | 
					                      {m.title}@<small>{m.version}</small>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div className={clsx(styles["mask-info"], "one-line")}>
 | 
				
			||||||
 | 
					                      {Locale.Plugin.Item.Info(
 | 
				
			||||||
 | 
					                        FunctionToolService.add(m).length,
 | 
				
			||||||
 | 
					                      )}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div className={styles["mask-actions"]}>
 | 
				
			||||||
 | 
					                  <IconButton
 | 
				
			||||||
 | 
					                    icon={<EditIcon />}
 | 
				
			||||||
 | 
					                    text={Locale.Plugin.Item.Edit}
 | 
				
			||||||
 | 
					                    onClick={() => setEditingPluginId(m.id)}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                  {!m.builtin && (
 | 
				
			||||||
 | 
					                    <IconButton
 | 
				
			||||||
 | 
					                      icon={<DeleteIcon />}
 | 
				
			||||||
 | 
					                      text={Locale.Plugin.Item.Delete}
 | 
				
			||||||
 | 
					                      onClick={async () => {
 | 
				
			||||||
 | 
					                        if (
 | 
				
			||||||
 | 
					                          await showConfirm(Locale.Plugin.Item.DeleteConfirm)
 | 
				
			||||||
 | 
					                        ) {
 | 
				
			||||||
 | 
					                          pluginStore.delete(m.id);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {editingPlugin && (
 | 
				
			||||||
 | 
					        <div className="modal-mask">
 | 
				
			||||||
 | 
					          <Modal
 | 
				
			||||||
 | 
					            title={Locale.Plugin.EditModal.Title(editingPlugin?.builtin)}
 | 
				
			||||||
 | 
					            onClose={closePluginModal}
 | 
				
			||||||
 | 
					            actions={[
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                icon={<ConfirmIcon />}
 | 
				
			||||||
 | 
					                text={Locale.UI.Confirm}
 | 
				
			||||||
 | 
					                key="export"
 | 
				
			||||||
 | 
					                bordered
 | 
				
			||||||
 | 
					                onClick={() => setEditingPluginId("")}
 | 
				
			||||||
 | 
					              />,
 | 
				
			||||||
 | 
					            ]}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <List>
 | 
				
			||||||
 | 
					              <ListItem title={Locale.Plugin.EditModal.Auth}>
 | 
				
			||||||
 | 
					                <select
 | 
				
			||||||
 | 
					                  value={editingPlugin?.authType}
 | 
				
			||||||
 | 
					                  onChange={(e) => {
 | 
				
			||||||
 | 
					                    pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
				
			||||||
 | 
					                      plugin.authType = e.target.value;
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <option value="">{Locale.Plugin.Auth.None}</option>
 | 
				
			||||||
 | 
					                  <option value="bearer">{Locale.Plugin.Auth.Bearer}</option>
 | 
				
			||||||
 | 
					                  <option value="basic">{Locale.Plugin.Auth.Basic}</option>
 | 
				
			||||||
 | 
					                  <option value="custom">{Locale.Plugin.Auth.Custom}</option>
 | 
				
			||||||
 | 
					                </select>
 | 
				
			||||||
 | 
					              </ListItem>
 | 
				
			||||||
 | 
					              {["bearer", "basic", "custom"].includes(
 | 
				
			||||||
 | 
					                editingPlugin.authType as string,
 | 
				
			||||||
 | 
					              ) && (
 | 
				
			||||||
 | 
					                <ListItem title={Locale.Plugin.Auth.Location}>
 | 
				
			||||||
 | 
					                  <select
 | 
				
			||||||
 | 
					                    value={editingPlugin?.authLocation}
 | 
				
			||||||
 | 
					                    onChange={(e) => {
 | 
				
			||||||
 | 
					                      pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
				
			||||||
 | 
					                        plugin.authLocation = e.target.value;
 | 
				
			||||||
 | 
					                      });
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <option value="header">
 | 
				
			||||||
 | 
					                      {Locale.Plugin.Auth.LocationHeader}
 | 
				
			||||||
 | 
					                    </option>
 | 
				
			||||||
 | 
					                    <option value="query">
 | 
				
			||||||
 | 
					                      {Locale.Plugin.Auth.LocationQuery}
 | 
				
			||||||
 | 
					                    </option>
 | 
				
			||||||
 | 
					                    <option value="body">
 | 
				
			||||||
 | 
					                      {Locale.Plugin.Auth.LocationBody}
 | 
				
			||||||
 | 
					                    </option>
 | 
				
			||||||
 | 
					                  </select>
 | 
				
			||||||
 | 
					                </ListItem>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					              {editingPlugin.authType == "custom" && (
 | 
				
			||||||
 | 
					                <ListItem title={Locale.Plugin.Auth.CustomHeader}>
 | 
				
			||||||
 | 
					                  <input
 | 
				
			||||||
 | 
					                    type="text"
 | 
				
			||||||
 | 
					                    value={editingPlugin?.authHeader}
 | 
				
			||||||
 | 
					                    onChange={(e) => {
 | 
				
			||||||
 | 
					                      pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
				
			||||||
 | 
					                        plugin.authHeader = e.target.value;
 | 
				
			||||||
 | 
					                      });
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                  ></input>
 | 
				
			||||||
 | 
					                </ListItem>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					              {["bearer", "basic", "custom"].includes(
 | 
				
			||||||
 | 
					                editingPlugin.authType as string,
 | 
				
			||||||
 | 
					              ) && (
 | 
				
			||||||
 | 
					                <ListItem title={Locale.Plugin.Auth.Token}>
 | 
				
			||||||
 | 
					                  <PasswordInput
 | 
				
			||||||
 | 
					                    type="text"
 | 
				
			||||||
 | 
					                    value={editingPlugin?.authToken}
 | 
				
			||||||
 | 
					                    onChange={(e) => {
 | 
				
			||||||
 | 
					                      pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
 | 
				
			||||||
 | 
					                        plugin.authToken = e.currentTarget.value;
 | 
				
			||||||
 | 
					                      });
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                  ></PasswordInput>
 | 
				
			||||||
 | 
					                </ListItem>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </List>
 | 
				
			||||||
 | 
					            <List>
 | 
				
			||||||
 | 
					              <ListItem title={Locale.Plugin.EditModal.Content}>
 | 
				
			||||||
 | 
					                <div className={pluginStyles["plugin-schema"]}>
 | 
				
			||||||
 | 
					                  <input
 | 
				
			||||||
 | 
					                    type="text"
 | 
				
			||||||
 | 
					                    style={{ minWidth: 200 }}
 | 
				
			||||||
 | 
					                    onInput={(e) => setLoadUrl(e.currentTarget.value)}
 | 
				
			||||||
 | 
					                  ></input>
 | 
				
			||||||
 | 
					                  <IconButton
 | 
				
			||||||
 | 
					                    icon={<ReloadIcon />}
 | 
				
			||||||
 | 
					                    text={Locale.Plugin.EditModal.Load}
 | 
				
			||||||
 | 
					                    bordered
 | 
				
			||||||
 | 
					                    onClick={() => loadFromUrl(loadUrl)}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </ListItem>
 | 
				
			||||||
 | 
					              <ListItem
 | 
				
			||||||
 | 
					                subTitle={
 | 
				
			||||||
 | 
					                  <div
 | 
				
			||||||
 | 
					                    className={clsx(
 | 
				
			||||||
 | 
					                      "markdown-body",
 | 
				
			||||||
 | 
					                      pluginStyles["plugin-content"],
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                    dir="auto"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <pre>
 | 
				
			||||||
 | 
					                      <code
 | 
				
			||||||
 | 
					                        contentEditable={true}
 | 
				
			||||||
 | 
					                        dangerouslySetInnerHTML={{
 | 
				
			||||||
 | 
					                          __html: editingPlugin.content,
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                        onBlur={onChangePlugin}
 | 
				
			||||||
 | 
					                      ></code>
 | 
				
			||||||
 | 
					                    </pre>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              ></ListItem>
 | 
				
			||||||
 | 
					              {editingPluginTool?.tools.map((tool, index) => (
 | 
				
			||||||
 | 
					                <ListItem
 | 
				
			||||||
 | 
					                  key={index}
 | 
				
			||||||
 | 
					                  title={tool?.function?.name}
 | 
				
			||||||
 | 
					                  subTitle={tool?.function?.description}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              ))}
 | 
				
			||||||
 | 
					            </List>
 | 
				
			||||||
 | 
					          </Modal>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </ErrorBoundary>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								app/components/realtime-chat/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export * from "./realtime-chat";
 | 
				
			||||||
							
								
								
									
										74
									
								
								app/components/realtime-chat/realtime-chat.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					.realtime-chat {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  padding: 20px;
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  .circle-mic {
 | 
				
			||||||
 | 
					    width: 150px;
 | 
				
			||||||
 | 
					    height: 150px;
 | 
				
			||||||
 | 
					    border-radius: 50%;
 | 
				
			||||||
 | 
					    background: linear-gradient(to bottom right, #a0d8ef, #f0f8ff);
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .icon-center {
 | 
				
			||||||
 | 
					    font-size: 24px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .bottom-icons {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    bottom: 20px;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    padding: 0 20px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .icon-left,
 | 
				
			||||||
 | 
					  .icon-right {
 | 
				
			||||||
 | 
					    width: 46px;
 | 
				
			||||||
 | 
					    height: 46px;
 | 
				
			||||||
 | 
					    font-size: 36px;
 | 
				
			||||||
 | 
					    background: var(--second);
 | 
				
			||||||
 | 
					    border-radius: 50%;
 | 
				
			||||||
 | 
					    padding: 2px;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      opacity: 0.8;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &.mobile {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pulse {
 | 
				
			||||||
 | 
					  animation: pulse 1.5s infinite;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes pulse {
 | 
				
			||||||
 | 
					  0% {
 | 
				
			||||||
 | 
					    transform: scale(1);
 | 
				
			||||||
 | 
					    opacity: 0.7;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  50% {
 | 
				
			||||||
 | 
					    transform: scale(1.1);
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  100% {
 | 
				
			||||||
 | 
					    transform: scale(1);
 | 
				
			||||||
 | 
					    opacity: 0.7;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										359
									
								
								app/components/realtime-chat/realtime-chat.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,359 @@
 | 
				
			|||||||
 | 
					import VoiceIcon from "@/app/icons/voice.svg";
 | 
				
			||||||
 | 
					import VoiceOffIcon from "@/app/icons/voice-off.svg";
 | 
				
			||||||
 | 
					import PowerIcon from "@/app/icons/power.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import styles from "./realtime-chat.module.scss";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useState, useRef, useEffect } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useChatStore, createMessage, useAppConfig } from "@/app/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { IconButton } from "@/app/components/button";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Modality,
 | 
				
			||||||
 | 
					  RTClient,
 | 
				
			||||||
 | 
					  RTInputAudioItem,
 | 
				
			||||||
 | 
					  RTResponse,
 | 
				
			||||||
 | 
					  TurnDetection,
 | 
				
			||||||
 | 
					} from "rt-client";
 | 
				
			||||||
 | 
					import { AudioHandler } from "@/app/lib/audio";
 | 
				
			||||||
 | 
					import { uploadImage } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import { VoicePrint } from "@/app/components/voice-print";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RealtimeChatProps {
 | 
				
			||||||
 | 
					  onClose?: () => void;
 | 
				
			||||||
 | 
					  onStartVoice?: () => void;
 | 
				
			||||||
 | 
					  onPausedVoice?: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function RealtimeChat({
 | 
				
			||||||
 | 
					  onClose,
 | 
				
			||||||
 | 
					  onStartVoice,
 | 
				
			||||||
 | 
					  onPausedVoice,
 | 
				
			||||||
 | 
					}: RealtimeChatProps) {
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const [status, setStatus] = useState("");
 | 
				
			||||||
 | 
					  const [isRecording, setIsRecording] = useState(false);
 | 
				
			||||||
 | 
					  const [isConnected, setIsConnected] = useState(false);
 | 
				
			||||||
 | 
					  const [isConnecting, setIsConnecting] = useState(false);
 | 
				
			||||||
 | 
					  const [modality, setModality] = useState("audio");
 | 
				
			||||||
 | 
					  const [useVAD, setUseVAD] = useState(true);
 | 
				
			||||||
 | 
					  const [frequencies, setFrequencies] = useState<Uint8Array | undefined>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const clientRef = useRef<RTClient | null>(null);
 | 
				
			||||||
 | 
					  const audioHandlerRef = useRef<AudioHandler | null>(null);
 | 
				
			||||||
 | 
					  const initRef = useRef(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const temperature = config.realtimeConfig.temperature;
 | 
				
			||||||
 | 
					  const apiKey = config.realtimeConfig.apiKey;
 | 
				
			||||||
 | 
					  const model = config.realtimeConfig.model;
 | 
				
			||||||
 | 
					  const azure = config.realtimeConfig.provider === "Azure";
 | 
				
			||||||
 | 
					  const azureEndpoint = config.realtimeConfig.azure.endpoint;
 | 
				
			||||||
 | 
					  const azureDeployment = config.realtimeConfig.azure.deployment;
 | 
				
			||||||
 | 
					  const voice = config.realtimeConfig.voice;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleConnect = async () => {
 | 
				
			||||||
 | 
					    if (isConnecting) return;
 | 
				
			||||||
 | 
					    if (!isConnected) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        setIsConnecting(true);
 | 
				
			||||||
 | 
					        clientRef.current = azure
 | 
				
			||||||
 | 
					          ? new RTClient(
 | 
				
			||||||
 | 
					              new URL(azureEndpoint),
 | 
				
			||||||
 | 
					              { key: apiKey },
 | 
				
			||||||
 | 
					              { deployment: azureDeployment },
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          : new RTClient({ key: apiKey }, { model });
 | 
				
			||||||
 | 
					        const modalities: Modality[] =
 | 
				
			||||||
 | 
					          modality === "audio" ? ["text", "audio"] : ["text"];
 | 
				
			||||||
 | 
					        const turnDetection: TurnDetection = useVAD
 | 
				
			||||||
 | 
					          ? { type: "server_vad" }
 | 
				
			||||||
 | 
					          : null;
 | 
				
			||||||
 | 
					        await clientRef.current.configure({
 | 
				
			||||||
 | 
					          instructions: "",
 | 
				
			||||||
 | 
					          voice,
 | 
				
			||||||
 | 
					          input_audio_transcription: { model: "whisper-1" },
 | 
				
			||||||
 | 
					          turn_detection: turnDetection,
 | 
				
			||||||
 | 
					          tools: [],
 | 
				
			||||||
 | 
					          temperature,
 | 
				
			||||||
 | 
					          modalities,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        startResponseListener();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setIsConnected(true);
 | 
				
			||||||
 | 
					        // TODO
 | 
				
			||||||
 | 
					        // try {
 | 
				
			||||||
 | 
					        //   const recentMessages = chatStore.getMessagesWithMemory();
 | 
				
			||||||
 | 
					        //   for (const message of recentMessages) {
 | 
				
			||||||
 | 
					        //     const { role, content } = message;
 | 
				
			||||||
 | 
					        //     if (typeof content === "string") {
 | 
				
			||||||
 | 
					        //       await clientRef.current.sendItem({
 | 
				
			||||||
 | 
					        //         type: "message",
 | 
				
			||||||
 | 
					        //         role: role as any,
 | 
				
			||||||
 | 
					        //         content: [
 | 
				
			||||||
 | 
					        //           {
 | 
				
			||||||
 | 
					        //             type: (role === "assistant" ? "text" : "input_text") as any,
 | 
				
			||||||
 | 
					        //             text: content as string,
 | 
				
			||||||
 | 
					        //           },
 | 
				
			||||||
 | 
					        //         ],
 | 
				
			||||||
 | 
					        //       });
 | 
				
			||||||
 | 
					        //     }
 | 
				
			||||||
 | 
					        //   }
 | 
				
			||||||
 | 
					        //   // await clientRef.current.generateResponse();
 | 
				
			||||||
 | 
					        // } catch (error) {
 | 
				
			||||||
 | 
					        //   console.error("Set message failed:", error);
 | 
				
			||||||
 | 
					        // }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error("Connection failed:", error);
 | 
				
			||||||
 | 
					        setStatus("Connection failed");
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        setIsConnecting(false);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      await disconnect();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const disconnect = async () => {
 | 
				
			||||||
 | 
					    if (clientRef.current) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        await clientRef.current.close();
 | 
				
			||||||
 | 
					        clientRef.current = null;
 | 
				
			||||||
 | 
					        setIsConnected(false);
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error("Disconnect failed:", error);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const startResponseListener = async () => {
 | 
				
			||||||
 | 
					    if (!clientRef.current) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      for await (const serverEvent of clientRef.current.events()) {
 | 
				
			||||||
 | 
					        if (serverEvent.type === "response") {
 | 
				
			||||||
 | 
					          await handleResponse(serverEvent);
 | 
				
			||||||
 | 
					        } else if (serverEvent.type === "input_audio") {
 | 
				
			||||||
 | 
					          await handleInputAudio(serverEvent);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      if (clientRef.current) {
 | 
				
			||||||
 | 
					        console.error("Response iteration error:", error);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleResponse = async (response: RTResponse) => {
 | 
				
			||||||
 | 
					    for await (const item of response) {
 | 
				
			||||||
 | 
					      if (item.type === "message" && item.role === "assistant") {
 | 
				
			||||||
 | 
					        const botMessage = createMessage({
 | 
				
			||||||
 | 
					          role: item.role,
 | 
				
			||||||
 | 
					          content: "",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        // add bot message first
 | 
				
			||||||
 | 
					        chatStore.updateTargetSession(session, (session) => {
 | 
				
			||||||
 | 
					          session.messages = session.messages.concat([botMessage]);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        let hasAudio = false;
 | 
				
			||||||
 | 
					        for await (const content of item) {
 | 
				
			||||||
 | 
					          if (content.type === "text") {
 | 
				
			||||||
 | 
					            for await (const text of content.textChunks()) {
 | 
				
			||||||
 | 
					              botMessage.content += text;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          } else if (content.type === "audio") {
 | 
				
			||||||
 | 
					            const textTask = async () => {
 | 
				
			||||||
 | 
					              for await (const text of content.transcriptChunks()) {
 | 
				
			||||||
 | 
					                botMessage.content += text;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            const audioTask = async () => {
 | 
				
			||||||
 | 
					              audioHandlerRef.current?.startStreamingPlayback();
 | 
				
			||||||
 | 
					              for await (const audio of content.audioChunks()) {
 | 
				
			||||||
 | 
					                hasAudio = true;
 | 
				
			||||||
 | 
					                audioHandlerRef.current?.playChunk(audio);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            await Promise.all([textTask(), audioTask()]);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          // update message.content
 | 
				
			||||||
 | 
					          chatStore.updateTargetSession(session, (session) => {
 | 
				
			||||||
 | 
					            session.messages = session.messages.concat();
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (hasAudio) {
 | 
				
			||||||
 | 
					          // upload audio get audio_url
 | 
				
			||||||
 | 
					          const blob = audioHandlerRef.current?.savePlayFile();
 | 
				
			||||||
 | 
					          uploadImage(blob!).then((audio_url) => {
 | 
				
			||||||
 | 
					            botMessage.audio_url = audio_url;
 | 
				
			||||||
 | 
					            // update text and audio_url
 | 
				
			||||||
 | 
					            chatStore.updateTargetSession(session, (session) => {
 | 
				
			||||||
 | 
					              session.messages = session.messages.concat();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleInputAudio = async (item: RTInputAudioItem) => {
 | 
				
			||||||
 | 
					    await item.waitForCompletion();
 | 
				
			||||||
 | 
					    if (item.transcription) {
 | 
				
			||||||
 | 
					      const userMessage = createMessage({
 | 
				
			||||||
 | 
					        role: "user",
 | 
				
			||||||
 | 
					        content: item.transcription,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      chatStore.updateTargetSession(session, (session) => {
 | 
				
			||||||
 | 
					        session.messages = session.messages.concat([userMessage]);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      // save input audio_url, and update session
 | 
				
			||||||
 | 
					      const { audioStartMillis, audioEndMillis } = item;
 | 
				
			||||||
 | 
					      // upload audio get audio_url
 | 
				
			||||||
 | 
					      const blob = audioHandlerRef.current?.saveRecordFile(
 | 
				
			||||||
 | 
					        audioStartMillis,
 | 
				
			||||||
 | 
					        audioEndMillis,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      uploadImage(blob!).then((audio_url) => {
 | 
				
			||||||
 | 
					        userMessage.audio_url = audio_url;
 | 
				
			||||||
 | 
					        chatStore.updateTargetSession(session, (session) => {
 | 
				
			||||||
 | 
					          session.messages = session.messages.concat();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // stop streaming play after get input audio.
 | 
				
			||||||
 | 
					    audioHandlerRef.current?.stopStreamingPlayback();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const toggleRecording = async () => {
 | 
				
			||||||
 | 
					    if (!isRecording && clientRef.current) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        if (!audioHandlerRef.current) {
 | 
				
			||||||
 | 
					          audioHandlerRef.current = new AudioHandler();
 | 
				
			||||||
 | 
					          await audioHandlerRef.current.initialize();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        await audioHandlerRef.current.startRecording(async (chunk) => {
 | 
				
			||||||
 | 
					          await clientRef.current?.sendAudio(chunk);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        setIsRecording(true);
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error("Failed to start recording:", error);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else if (audioHandlerRef.current) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        audioHandlerRef.current.stopRecording();
 | 
				
			||||||
 | 
					        if (!useVAD) {
 | 
				
			||||||
 | 
					          const inputAudio = await clientRef.current?.commitAudio();
 | 
				
			||||||
 | 
					          await handleInputAudio(inputAudio!);
 | 
				
			||||||
 | 
					          await clientRef.current?.generateResponse();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        setIsRecording(false);
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error("Failed to stop recording:", error);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    // 防止重复初始化
 | 
				
			||||||
 | 
					    if (initRef.current) return;
 | 
				
			||||||
 | 
					    initRef.current = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const initAudioHandler = async () => {
 | 
				
			||||||
 | 
					      const handler = new AudioHandler();
 | 
				
			||||||
 | 
					      await handler.initialize();
 | 
				
			||||||
 | 
					      audioHandlerRef.current = handler;
 | 
				
			||||||
 | 
					      await handleConnect();
 | 
				
			||||||
 | 
					      await toggleRecording();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    initAudioHandler().catch((error) => {
 | 
				
			||||||
 | 
					      setStatus(error);
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      if (isRecording) {
 | 
				
			||||||
 | 
					        toggleRecording();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      audioHandlerRef.current?.close().catch(console.error);
 | 
				
			||||||
 | 
					      disconnect();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    let animationFrameId: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (isConnected && isRecording) {
 | 
				
			||||||
 | 
					      const animationFrame = () => {
 | 
				
			||||||
 | 
					        if (audioHandlerRef.current) {
 | 
				
			||||||
 | 
					          const freqData = audioHandlerRef.current.getByteFrequencyData();
 | 
				
			||||||
 | 
					          setFrequencies(freqData);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        animationFrameId = requestAnimationFrame(animationFrame);
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      animationFrameId = requestAnimationFrame(animationFrame);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      setFrequencies(undefined);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      if (animationFrameId) {
 | 
				
			||||||
 | 
					        cancelAnimationFrame(animationFrameId);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, [isConnected, isRecording]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // update session params
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    clientRef.current?.configure({ voice });
 | 
				
			||||||
 | 
					  }, [voice]);
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    clientRef.current?.configure({ temperature });
 | 
				
			||||||
 | 
					  }, [temperature]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleClose = async () => {
 | 
				
			||||||
 | 
					    onClose?.();
 | 
				
			||||||
 | 
					    if (isRecording) {
 | 
				
			||||||
 | 
					      await toggleRecording();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    disconnect().catch(console.error);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={styles["realtime-chat"]}>
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        className={clsx(styles["circle-mic"], {
 | 
				
			||||||
 | 
					          [styles["pulse"]]: isRecording,
 | 
				
			||||||
 | 
					        })}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <VoicePrint frequencies={frequencies} isActive={isRecording} />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className={styles["bottom-icons"]}>
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            icon={isRecording ? <VoiceIcon /> : <VoiceOffIcon />}
 | 
				
			||||||
 | 
					            onClick={toggleRecording}
 | 
				
			||||||
 | 
					            disabled={!isConnected}
 | 
				
			||||||
 | 
					            shadow
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div className={styles["icon-center"]}>{status}</div>
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            icon={<PowerIcon />}
 | 
				
			||||||
 | 
					            onClick={handleClose}
 | 
				
			||||||
 | 
					            shadow
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										173
									
								
								app/components/realtime-chat/realtime-config.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,173 @@
 | 
				
			|||||||
 | 
					import { RealtimeConfig } from "@/app/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { ListItem, Select, PasswordInput } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { InputRange } from "@/app/components/input-range";
 | 
				
			||||||
 | 
					import { Voice } from "rt-client";
 | 
				
			||||||
 | 
					import { ServiceProvider } from "@/app/constant";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const providers = [ServiceProvider.OpenAI, ServiceProvider.Azure];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const models = ["gpt-4o-realtime-preview-2024-10-01"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const voice = ["alloy", "shimmer", "echo"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function RealtimeConfigList(props: {
 | 
				
			||||||
 | 
					  realtimeConfig: RealtimeConfig;
 | 
				
			||||||
 | 
					  updateConfig: (updater: (config: RealtimeConfig) => void) => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const azureConfigComponent = props.realtimeConfig.provider ===
 | 
				
			||||||
 | 
					    ServiceProvider.Azure && (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Realtime.Azure.Endpoint.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.Realtime.Azure.Endpoint.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          value={props.realtimeConfig?.azure?.endpoint}
 | 
				
			||||||
 | 
					          type="text"
 | 
				
			||||||
 | 
					          placeholder={Locale.Settings.Realtime.Azure.Endpoint.Title}
 | 
				
			||||||
 | 
					          onChange={(e) => {
 | 
				
			||||||
 | 
					            props.updateConfig(
 | 
				
			||||||
 | 
					              (config) => (config.azure.endpoint = e.currentTarget.value),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Realtime.Azure.Deployment.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.Realtime.Azure.Deployment.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          value={props.realtimeConfig?.azure?.deployment}
 | 
				
			||||||
 | 
					          type="text"
 | 
				
			||||||
 | 
					          placeholder={Locale.Settings.Realtime.Azure.Deployment.Title}
 | 
				
			||||||
 | 
					          onChange={(e) => {
 | 
				
			||||||
 | 
					            props.updateConfig(
 | 
				
			||||||
 | 
					              (config) => (config.azure.deployment = e.currentTarget.value),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Realtime.Enable.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.Realtime.Enable.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          type="checkbox"
 | 
				
			||||||
 | 
					          checked={props.realtimeConfig.enable}
 | 
				
			||||||
 | 
					          onChange={(e) =>
 | 
				
			||||||
 | 
					            props.updateConfig(
 | 
				
			||||||
 | 
					              (config) => (config.enable = e.currentTarget.checked),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ></input>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {props.realtimeConfig.enable && (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.Realtime.Provider.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.Realtime.Provider.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Select
 | 
				
			||||||
 | 
					              aria-label={Locale.Settings.Realtime.Provider.Title}
 | 
				
			||||||
 | 
					              value={props.realtimeConfig.provider}
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                props.updateConfig(
 | 
				
			||||||
 | 
					                  (config) =>
 | 
				
			||||||
 | 
					                    (config.provider = e.target.value as ServiceProvider),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {providers.map((v, i) => (
 | 
				
			||||||
 | 
					                <option value={v} key={i}>
 | 
				
			||||||
 | 
					                  {v}
 | 
				
			||||||
 | 
					                </option>
 | 
				
			||||||
 | 
					              ))}
 | 
				
			||||||
 | 
					            </Select>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.Realtime.Model.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.Realtime.Model.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Select
 | 
				
			||||||
 | 
					              aria-label={Locale.Settings.Realtime.Model.Title}
 | 
				
			||||||
 | 
					              value={props.realtimeConfig.model}
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                props.updateConfig((config) => (config.model = e.target.value));
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {models.map((v, i) => (
 | 
				
			||||||
 | 
					                <option value={v} key={i}>
 | 
				
			||||||
 | 
					                  {v}
 | 
				
			||||||
 | 
					                </option>
 | 
				
			||||||
 | 
					              ))}
 | 
				
			||||||
 | 
					            </Select>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.Realtime.ApiKey.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.Realtime.ApiKey.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <PasswordInput
 | 
				
			||||||
 | 
					              aria={Locale.Settings.ShowPassword}
 | 
				
			||||||
 | 
					              aria-label={Locale.Settings.Realtime.ApiKey.Title}
 | 
				
			||||||
 | 
					              value={props.realtimeConfig.apiKey}
 | 
				
			||||||
 | 
					              type="text"
 | 
				
			||||||
 | 
					              placeholder={Locale.Settings.Realtime.ApiKey.Placeholder}
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                props.updateConfig(
 | 
				
			||||||
 | 
					                  (config) => (config.apiKey = e.currentTarget.value),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					          {azureConfigComponent}
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.TTS.Voice.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.TTS.Voice.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Select
 | 
				
			||||||
 | 
					              value={props.realtimeConfig.voice}
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                props.updateConfig(
 | 
				
			||||||
 | 
					                  (config) => (config.voice = e.currentTarget.value as Voice),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {voice.map((v, i) => (
 | 
				
			||||||
 | 
					                <option value={v} key={i}>
 | 
				
			||||||
 | 
					                  {v}
 | 
				
			||||||
 | 
					                </option>
 | 
				
			||||||
 | 
					              ))}
 | 
				
			||||||
 | 
					            </Select>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.Realtime.Temperature.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.Realtime.Temperature.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <InputRange
 | 
				
			||||||
 | 
					              aria={Locale.Settings.Temperature.Title}
 | 
				
			||||||
 | 
					              value={props.realtimeConfig?.temperature?.toFixed(1)}
 | 
				
			||||||
 | 
					              min="0.6"
 | 
				
			||||||
 | 
					              max="1"
 | 
				
			||||||
 | 
					              step="0.1"
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                props.updateConfig(
 | 
				
			||||||
 | 
					                  (config) =>
 | 
				
			||||||
 | 
					                    (config.temperature = e.currentTarget.valueAsNumber),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            ></InputRange>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -4,6 +4,7 @@ import { Select } from "@/app/components/ui-lib";
 | 
				
			|||||||
import { IconButton } from "@/app/components/button";
 | 
					import { IconButton } from "@/app/components/button";
 | 
				
			||||||
import Locale from "@/app/locales";
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
import { useSdStore } from "@/app/store/sd";
 | 
					import { useSdStore } from "@/app/store/sd";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const params = [
 | 
					export const params = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
@@ -136,7 +137,7 @@ export function ControlParamItem(props: {
 | 
				
			|||||||
  className?: string;
 | 
					  className?: string;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={styles["ctrl-param-item"] + ` ${props.className || ""}`}>
 | 
					    <div className={clsx(styles["ctrl-param-item"], props.className)}>
 | 
				
			||||||
      <div className={styles["ctrl-param-item-header"]}>
 | 
					      <div className={styles["ctrl-param-item-header"]}>
 | 
				
			||||||
        <div className={styles["ctrl-param-item-title"]}>
 | 
					        <div className={styles["ctrl-param-item-title"]}>
 | 
				
			||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,6 +36,7 @@ import { removeImage } from "@/app/utils/chat";
 | 
				
			|||||||
import { SideBar } from "./sd-sidebar";
 | 
					import { SideBar } from "./sd-sidebar";
 | 
				
			||||||
import { WindowContent } from "@/app/components/home";
 | 
					import { WindowContent } from "@/app/components/home";
 | 
				
			||||||
import { params } from "./sd-panel";
 | 
					import { params } from "./sd-panel";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getSdTaskStatus(item: any) {
 | 
					function getSdTaskStatus(item: any) {
 | 
				
			||||||
  let s: string;
 | 
					  let s: string;
 | 
				
			||||||
@@ -104,7 +105,7 @@ export function Sd() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <SideBar className={isSd ? homeStyles["sidebar-show"] : ""} />
 | 
					      <SideBar className={clsx({ [homeStyles["sidebar-show"]]: isSd })} />
 | 
				
			||||||
      <WindowContent>
 | 
					      <WindowContent>
 | 
				
			||||||
        <div className={chatStyles.chat} key={"1"}>
 | 
					        <div className={chatStyles.chat} key={"1"}>
 | 
				
			||||||
          <div className="window-header" data-tauri-drag-region>
 | 
					          <div className="window-header" data-tauri-drag-region>
 | 
				
			||||||
@@ -121,7 +122,10 @@ export function Sd() {
 | 
				
			|||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
              className={`window-header-title ${chatStyles["chat-body-title"]}`}
 | 
					              className={clsx(
 | 
				
			||||||
 | 
					                "window-header-title",
 | 
				
			||||||
 | 
					                chatStyles["chat-body-title"],
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <div className={`window-header-main-title`}>Stability AI</div>
 | 
					              <div className={`window-header-main-title`}>Stability AI</div>
 | 
				
			||||||
              <div className="window-header-sub-title">
 | 
					              <div className="window-header-sub-title">
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										167
									
								
								app/components/search-chat.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,167 @@
 | 
				
			|||||||
 | 
					import { useState, useEffect, useRef, useCallback } from "react";
 | 
				
			||||||
 | 
					import { ErrorBoundary } from "./error";
 | 
				
			||||||
 | 
					import styles from "./mask.module.scss";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
 | 
					import CloseIcon from "../icons/close.svg";
 | 
				
			||||||
 | 
					import EyeIcon from "../icons/eye.svg";
 | 
				
			||||||
 | 
					import Locale from "../locales";
 | 
				
			||||||
 | 
					import { Path } from "../constant";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useChatStore } from "../store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Item = {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  content: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function SearchChatPage() {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const sessions = chatStore.sessions;
 | 
				
			||||||
 | 
					  const selectSession = chatStore.selectSession;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [searchResults, setSearchResults] = useState<Item[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const previousValueRef = useRef<string>("");
 | 
				
			||||||
 | 
					  const searchInputRef = useRef<HTMLInputElement>(null);
 | 
				
			||||||
 | 
					  const doSearch = useCallback((text: string) => {
 | 
				
			||||||
 | 
					    const lowerCaseText = text.toLowerCase();
 | 
				
			||||||
 | 
					    const results: Item[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sessions.forEach((session, index) => {
 | 
				
			||||||
 | 
					      const fullTextContents: string[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      session.messages.forEach((message) => {
 | 
				
			||||||
 | 
					        const content = message.content as string;
 | 
				
			||||||
 | 
					        if (!content.toLowerCase || content === "") return;
 | 
				
			||||||
 | 
					        const lowerCaseContent = content.toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // full text search
 | 
				
			||||||
 | 
					        let pos = lowerCaseContent.indexOf(lowerCaseText);
 | 
				
			||||||
 | 
					        while (pos !== -1) {
 | 
				
			||||||
 | 
					          const start = Math.max(0, pos - 35);
 | 
				
			||||||
 | 
					          const end = Math.min(content.length, pos + lowerCaseText.length + 35);
 | 
				
			||||||
 | 
					          fullTextContents.push(content.substring(start, end));
 | 
				
			||||||
 | 
					          pos = lowerCaseContent.indexOf(
 | 
				
			||||||
 | 
					            lowerCaseText,
 | 
				
			||||||
 | 
					            pos + lowerCaseText.length,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (fullTextContents.length > 0) {
 | 
				
			||||||
 | 
					        results.push({
 | 
				
			||||||
 | 
					          id: index,
 | 
				
			||||||
 | 
					          name: session.topic,
 | 
				
			||||||
 | 
					          content: fullTextContents.join("... "), // concat content with...
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // sort by length of matching content
 | 
				
			||||||
 | 
					    results.sort((a, b) => b.content.length - a.content.length);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return results;
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const intervalId = setInterval(() => {
 | 
				
			||||||
 | 
					      if (searchInputRef.current) {
 | 
				
			||||||
 | 
					        const currentValue = searchInputRef.current.value;
 | 
				
			||||||
 | 
					        if (currentValue !== previousValueRef.current) {
 | 
				
			||||||
 | 
					          if (currentValue.length > 0) {
 | 
				
			||||||
 | 
					            const result = doSearch(currentValue);
 | 
				
			||||||
 | 
					            setSearchResults(result);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          previousValueRef.current = currentValue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }, 1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Cleanup the interval on component unmount
 | 
				
			||||||
 | 
					    return () => clearInterval(intervalId);
 | 
				
			||||||
 | 
					  }, [doSearch]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ErrorBoundary>
 | 
				
			||||||
 | 
					      <div className={styles["mask-page"]}>
 | 
				
			||||||
 | 
					        {/* header */}
 | 
				
			||||||
 | 
					        <div className="window-header">
 | 
				
			||||||
 | 
					          <div className="window-header-title">
 | 
				
			||||||
 | 
					            <div className="window-header-main-title">
 | 
				
			||||||
 | 
					              {Locale.SearchChat.Page.Title}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className="window-header-submai-title">
 | 
				
			||||||
 | 
					              {Locale.SearchChat.Page.SubTitle(searchResults.length)}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className="window-actions">
 | 
				
			||||||
 | 
					            <div className="window-action-button">
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                icon={<CloseIcon />}
 | 
				
			||||||
 | 
					                bordered
 | 
				
			||||||
 | 
					                onClick={() => navigate(-1)}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className={styles["mask-page-body"]}>
 | 
				
			||||||
 | 
					          <div className={styles["mask-filter"]}>
 | 
				
			||||||
 | 
					            {/**搜索输入框 */}
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              type="text"
 | 
				
			||||||
 | 
					              className={styles["search-bar"]}
 | 
				
			||||||
 | 
					              placeholder={Locale.SearchChat.Page.Search}
 | 
				
			||||||
 | 
					              autoFocus
 | 
				
			||||||
 | 
					              ref={searchInputRef}
 | 
				
			||||||
 | 
					              onKeyDown={(e) => {
 | 
				
			||||||
 | 
					                if (e.key === "Enter") {
 | 
				
			||||||
 | 
					                  e.preventDefault();
 | 
				
			||||||
 | 
					                  const searchText = e.currentTarget.value;
 | 
				
			||||||
 | 
					                  if (searchText.length > 0) {
 | 
				
			||||||
 | 
					                    const result = doSearch(searchText);
 | 
				
			||||||
 | 
					                    setSearchResults(result);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div>
 | 
				
			||||||
 | 
					            {searchResults.map((item) => (
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={styles["mask-item"]}
 | 
				
			||||||
 | 
					                key={item.id}
 | 
				
			||||||
 | 
					                onClick={() => {
 | 
				
			||||||
 | 
					                  navigate(Path.Chat);
 | 
				
			||||||
 | 
					                  selectSession(item.id);
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					                style={{ cursor: "pointer" }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {/** 搜索匹配的文本 */}
 | 
				
			||||||
 | 
					                <div className={styles["mask-header"]}>
 | 
				
			||||||
 | 
					                  <div className={styles["mask-title"]}>
 | 
				
			||||||
 | 
					                    <div className={styles["mask-name"]}>{item.name}</div>
 | 
				
			||||||
 | 
					                    {item.content.slice(0, 70)}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                {/** 操作按钮 */}
 | 
				
			||||||
 | 
					                <div className={styles["mask-actions"]}>
 | 
				
			||||||
 | 
					                  <IconButton
 | 
				
			||||||
 | 
					                    icon={<EyeIcon />}
 | 
				
			||||||
 | 
					                    text={Locale.SearchChat.Item.View}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </ErrorBoundary>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -72,3 +72,9 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.subtitle-button {
 | 
				
			||||||
 | 
					  button {
 | 
				
			||||||
 | 
					    overflow:visible ;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import CopyIcon from "../icons/copy.svg";
 | 
				
			|||||||
import ClearIcon from "../icons/clear.svg";
 | 
					import ClearIcon from "../icons/clear.svg";
 | 
				
			||||||
import LoadingIcon from "../icons/three-dots.svg";
 | 
					import LoadingIcon from "../icons/three-dots.svg";
 | 
				
			||||||
import EditIcon from "../icons/edit.svg";
 | 
					import EditIcon from "../icons/edit.svg";
 | 
				
			||||||
 | 
					import FireIcon from "../icons/fire.svg";
 | 
				
			||||||
import EyeIcon from "../icons/eye.svg";
 | 
					import EyeIcon from "../icons/eye.svg";
 | 
				
			||||||
import DownloadIcon from "../icons/download.svg";
 | 
					import DownloadIcon from "../icons/download.svg";
 | 
				
			||||||
import UploadIcon from "../icons/upload.svg";
 | 
					import UploadIcon from "../icons/upload.svg";
 | 
				
			||||||
@@ -18,7 +19,7 @@ import ConfirmIcon from "../icons/confirm.svg";
 | 
				
			|||||||
import ConnectionIcon from "../icons/connection.svg";
 | 
					import ConnectionIcon from "../icons/connection.svg";
 | 
				
			||||||
import CloudSuccessIcon from "../icons/cloud-success.svg";
 | 
					import CloudSuccessIcon from "../icons/cloud-success.svg";
 | 
				
			||||||
import CloudFailIcon from "../icons/cloud-fail.svg";
 | 
					import CloudFailIcon from "../icons/cloud-fail.svg";
 | 
				
			||||||
 | 
					import { trackSettingsPageGuideToCPaymentClick } from "../utils/auth-settings-events";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Input,
 | 
					  Input,
 | 
				
			||||||
  List,
 | 
					  List,
 | 
				
			||||||
@@ -48,7 +49,7 @@ import Locale, {
 | 
				
			|||||||
  changeLang,
 | 
					  changeLang,
 | 
				
			||||||
  getLang,
 | 
					  getLang,
 | 
				
			||||||
} from "../locales";
 | 
					} from "../locales";
 | 
				
			||||||
import { copyToClipboard } from "../utils";
 | 
					import { copyToClipboard, clientUpdate, semverCompare } from "../utils";
 | 
				
			||||||
import Link from "next/link";
 | 
					import Link from "next/link";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Anthropic,
 | 
					  Anthropic,
 | 
				
			||||||
@@ -58,6 +59,7 @@ import {
 | 
				
			|||||||
  ByteDance,
 | 
					  ByteDance,
 | 
				
			||||||
  Alibaba,
 | 
					  Alibaba,
 | 
				
			||||||
  Moonshot,
 | 
					  Moonshot,
 | 
				
			||||||
 | 
					  XAI,
 | 
				
			||||||
  Google,
 | 
					  Google,
 | 
				
			||||||
  GoogleSafetySettingsThreshold,
 | 
					  GoogleSafetySettingsThreshold,
 | 
				
			||||||
  OPENAI_BASE_URL,
 | 
					  OPENAI_BASE_URL,
 | 
				
			||||||
@@ -69,6 +71,10 @@ import {
 | 
				
			|||||||
  UPDATE_URL,
 | 
					  UPDATE_URL,
 | 
				
			||||||
  Stability,
 | 
					  Stability,
 | 
				
			||||||
  Iflytek,
 | 
					  Iflytek,
 | 
				
			||||||
 | 
					  SAAS_CHAT_URL,
 | 
				
			||||||
 | 
					  ChatGLM,
 | 
				
			||||||
 | 
					  DeepSeek,
 | 
				
			||||||
 | 
					  SiliconFlow,
 | 
				
			||||||
} from "../constant";
 | 
					} from "../constant";
 | 
				
			||||||
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
 | 
					import { Prompt, SearchService, usePromptStore } from "../store/prompt";
 | 
				
			||||||
import { ErrorBoundary } from "./error";
 | 
					import { ErrorBoundary } from "./error";
 | 
				
			||||||
@@ -80,6 +86,8 @@ import { useSyncStore } from "../store/sync";
 | 
				
			|||||||
import { nanoid } from "nanoid";
 | 
					import { nanoid } from "nanoid";
 | 
				
			||||||
import { useMaskStore } from "../store/mask";
 | 
					import { useMaskStore } from "../store/mask";
 | 
				
			||||||
import { ProviderType } from "../utils/cloud";
 | 
					import { ProviderType } from "../utils/cloud";
 | 
				
			||||||
 | 
					import { TTSConfigList } from "./tts-config";
 | 
				
			||||||
 | 
					import { RealtimeConfigList } from "./realtime-chat/realtime-config";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function EditPromptModal(props: { id: string; onClose: () => void }) {
 | 
					function EditPromptModal(props: { id: string; onClose: () => void }) {
 | 
				
			||||||
  const promptStore = usePromptStore();
 | 
					  const promptStore = usePromptStore();
 | 
				
			||||||
@@ -582,7 +590,7 @@ export function Settings() {
 | 
				
			|||||||
  const [checkingUpdate, setCheckingUpdate] = useState(false);
 | 
					  const [checkingUpdate, setCheckingUpdate] = useState(false);
 | 
				
			||||||
  const currentVersion = updateStore.formatVersion(updateStore.version);
 | 
					  const currentVersion = updateStore.formatVersion(updateStore.version);
 | 
				
			||||||
  const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
 | 
					  const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
 | 
				
			||||||
  const hasNewVersion = currentVersion !== remoteId;
 | 
					  const hasNewVersion = semverCompare(currentVersion, remoteId) === -1;
 | 
				
			||||||
  const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
 | 
					  const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function checkUpdate(force = false) {
 | 
					  function checkUpdate(force = false) {
 | 
				
			||||||
@@ -685,6 +693,31 @@ export function Settings() {
 | 
				
			|||||||
    </ListItem>
 | 
					    </ListItem>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const saasStartComponent = (
 | 
				
			||||||
 | 
					    <ListItem
 | 
				
			||||||
 | 
					      className={styles["subtitle-button"]}
 | 
				
			||||||
 | 
					      title={
 | 
				
			||||||
 | 
					        Locale.Settings.Access.SaasStart.Title +
 | 
				
			||||||
 | 
					        `${Locale.Settings.Access.SaasStart.Label}`
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      subTitle={Locale.Settings.Access.SaasStart.SubTitle}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <IconButton
 | 
				
			||||||
 | 
					        aria={
 | 
				
			||||||
 | 
					          Locale.Settings.Access.SaasStart.Title +
 | 
				
			||||||
 | 
					          Locale.Settings.Access.SaasStart.ChatNow
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        icon={<FireIcon />}
 | 
				
			||||||
 | 
					        type={"primary"}
 | 
				
			||||||
 | 
					        text={Locale.Settings.Access.SaasStart.ChatNow}
 | 
				
			||||||
 | 
					        onClick={() => {
 | 
				
			||||||
 | 
					          trackSettingsPageGuideToCPaymentClick();
 | 
				
			||||||
 | 
					          window.location.href = SAAS_CHAT_URL;
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </ListItem>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const useCustomConfigComponent = // Conditionally render the following ListItem based on clientConfig.isApp
 | 
					  const useCustomConfigComponent = // Conditionally render the following ListItem based on clientConfig.isApp
 | 
				
			||||||
    !clientConfig?.isApp && ( // only show if isApp is false
 | 
					    !clientConfig?.isApp && ( // only show if isApp is false
 | 
				
			||||||
      <ListItem
 | 
					      <ListItem
 | 
				
			||||||
@@ -1166,6 +1199,167 @@ export function Settings() {
 | 
				
			|||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const deepseekConfigComponent = accessStore.provider ===
 | 
				
			||||||
 | 
					    ServiceProvider.DeepSeek && (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Access.DeepSeek.Endpoint.Title}
 | 
				
			||||||
 | 
					        subTitle={
 | 
				
			||||||
 | 
					          Locale.Settings.Access.DeepSeek.Endpoint.SubTitle +
 | 
				
			||||||
 | 
					          DeepSeek.ExampleEndpoint
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          aria-label={Locale.Settings.Access.DeepSeek.Endpoint.Title}
 | 
				
			||||||
 | 
					          type="text"
 | 
				
			||||||
 | 
					          value={accessStore.deepseekUrl}
 | 
				
			||||||
 | 
					          placeholder={DeepSeek.ExampleEndpoint}
 | 
				
			||||||
 | 
					          onChange={(e) =>
 | 
				
			||||||
 | 
					            accessStore.update(
 | 
				
			||||||
 | 
					              (access) => (access.deepseekUrl = e.currentTarget.value),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ></input>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Access.DeepSeek.ApiKey.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.Access.DeepSeek.ApiKey.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <PasswordInput
 | 
				
			||||||
 | 
					          aria-label={Locale.Settings.Access.DeepSeek.ApiKey.Title}
 | 
				
			||||||
 | 
					          value={accessStore.deepseekApiKey}
 | 
				
			||||||
 | 
					          type="text"
 | 
				
			||||||
 | 
					          placeholder={Locale.Settings.Access.DeepSeek.ApiKey.Placeholder}
 | 
				
			||||||
 | 
					          onChange={(e) => {
 | 
				
			||||||
 | 
					            accessStore.update(
 | 
				
			||||||
 | 
					              (access) => (access.deepseekApiKey = e.currentTarget.value),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const XAIConfigComponent = accessStore.provider === ServiceProvider.XAI && (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Access.XAI.Endpoint.Title}
 | 
				
			||||||
 | 
					        subTitle={
 | 
				
			||||||
 | 
					          Locale.Settings.Access.XAI.Endpoint.SubTitle + XAI.ExampleEndpoint
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          aria-label={Locale.Settings.Access.XAI.Endpoint.Title}
 | 
				
			||||||
 | 
					          type="text"
 | 
				
			||||||
 | 
					          value={accessStore.xaiUrl}
 | 
				
			||||||
 | 
					          placeholder={XAI.ExampleEndpoint}
 | 
				
			||||||
 | 
					          onChange={(e) =>
 | 
				
			||||||
 | 
					            accessStore.update(
 | 
				
			||||||
 | 
					              (access) => (access.xaiUrl = e.currentTarget.value),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ></input>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Access.XAI.ApiKey.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.Access.XAI.ApiKey.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <PasswordInput
 | 
				
			||||||
 | 
					          aria-label={Locale.Settings.Access.XAI.ApiKey.Title}
 | 
				
			||||||
 | 
					          value={accessStore.xaiApiKey}
 | 
				
			||||||
 | 
					          type="text"
 | 
				
			||||||
 | 
					          placeholder={Locale.Settings.Access.XAI.ApiKey.Placeholder}
 | 
				
			||||||
 | 
					          onChange={(e) => {
 | 
				
			||||||
 | 
					            accessStore.update(
 | 
				
			||||||
 | 
					              (access) => (access.xaiApiKey = e.currentTarget.value),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const chatglmConfigComponent = accessStore.provider ===
 | 
				
			||||||
 | 
					    ServiceProvider.ChatGLM && (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Access.ChatGLM.Endpoint.Title}
 | 
				
			||||||
 | 
					        subTitle={
 | 
				
			||||||
 | 
					          Locale.Settings.Access.ChatGLM.Endpoint.SubTitle +
 | 
				
			||||||
 | 
					          ChatGLM.ExampleEndpoint
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          aria-label={Locale.Settings.Access.ChatGLM.Endpoint.Title}
 | 
				
			||||||
 | 
					          type="text"
 | 
				
			||||||
 | 
					          value={accessStore.chatglmUrl}
 | 
				
			||||||
 | 
					          placeholder={ChatGLM.ExampleEndpoint}
 | 
				
			||||||
 | 
					          onChange={(e) =>
 | 
				
			||||||
 | 
					            accessStore.update(
 | 
				
			||||||
 | 
					              (access) => (access.chatglmUrl = e.currentTarget.value),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ></input>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Access.ChatGLM.ApiKey.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.Access.ChatGLM.ApiKey.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <PasswordInput
 | 
				
			||||||
 | 
					          aria-label={Locale.Settings.Access.ChatGLM.ApiKey.Title}
 | 
				
			||||||
 | 
					          value={accessStore.chatglmApiKey}
 | 
				
			||||||
 | 
					          type="text"
 | 
				
			||||||
 | 
					          placeholder={Locale.Settings.Access.ChatGLM.ApiKey.Placeholder}
 | 
				
			||||||
 | 
					          onChange={(e) => {
 | 
				
			||||||
 | 
					            accessStore.update(
 | 
				
			||||||
 | 
					              (access) => (access.chatglmApiKey = e.currentTarget.value),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const siliconflowConfigComponent = accessStore.provider ===
 | 
				
			||||||
 | 
					    ServiceProvider.SiliconFlow && (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Access.SiliconFlow.Endpoint.Title}
 | 
				
			||||||
 | 
					        subTitle={
 | 
				
			||||||
 | 
					          Locale.Settings.Access.SiliconFlow.Endpoint.SubTitle +
 | 
				
			||||||
 | 
					          SiliconFlow.ExampleEndpoint
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          aria-label={Locale.Settings.Access.SiliconFlow.Endpoint.Title}
 | 
				
			||||||
 | 
					          type="text"
 | 
				
			||||||
 | 
					          value={accessStore.siliconflowUrl}
 | 
				
			||||||
 | 
					          placeholder={SiliconFlow.ExampleEndpoint}
 | 
				
			||||||
 | 
					          onChange={(e) =>
 | 
				
			||||||
 | 
					            accessStore.update(
 | 
				
			||||||
 | 
					              (access) => (access.siliconflowUrl = e.currentTarget.value),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ></input>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.Access.SiliconFlow.ApiKey.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.Access.SiliconFlow.ApiKey.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <PasswordInput
 | 
				
			||||||
 | 
					          aria-label={Locale.Settings.Access.SiliconFlow.ApiKey.Title}
 | 
				
			||||||
 | 
					          value={accessStore.siliconflowApiKey}
 | 
				
			||||||
 | 
					          type="text"
 | 
				
			||||||
 | 
					          placeholder={Locale.Settings.Access.SiliconFlow.ApiKey.Placeholder}
 | 
				
			||||||
 | 
					          onChange={(e) => {
 | 
				
			||||||
 | 
					            accessStore.update(
 | 
				
			||||||
 | 
					              (access) => (access.siliconflowApiKey = e.currentTarget.value),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const stabilityConfigComponent = accessStore.provider ===
 | 
					  const stabilityConfigComponent = accessStore.provider ===
 | 
				
			||||||
    ServiceProvider.Stability && (
 | 
					    ServiceProvider.Stability && (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
@@ -1329,9 +1523,17 @@ export function Settings() {
 | 
				
			|||||||
            {checkingUpdate ? (
 | 
					            {checkingUpdate ? (
 | 
				
			||||||
              <LoadingIcon />
 | 
					              <LoadingIcon />
 | 
				
			||||||
            ) : hasNewVersion ? (
 | 
					            ) : hasNewVersion ? (
 | 
				
			||||||
              <Link href={updateUrl} target="_blank" className="link">
 | 
					              clientConfig?.isApp ? (
 | 
				
			||||||
                {Locale.Settings.Update.GoToUpdate}
 | 
					                <IconButton
 | 
				
			||||||
              </Link>
 | 
					                  icon={<ResetIcon></ResetIcon>}
 | 
				
			||||||
 | 
					                  text={Locale.Settings.Update.GoToUpdate}
 | 
				
			||||||
 | 
					                  onClick={() => clientUpdate()}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              ) : (
 | 
				
			||||||
 | 
					                <Link href={updateUrl} target="_blank" className="link">
 | 
				
			||||||
 | 
					                  {Locale.Settings.Update.GoToUpdate}
 | 
				
			||||||
 | 
					                </Link>
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
            ) : (
 | 
					            ) : (
 | 
				
			||||||
              <IconButton
 | 
					              <IconButton
 | 
				
			||||||
                icon={<ResetIcon></ResetIcon>}
 | 
					                icon={<ResetIcon></ResetIcon>}
 | 
				
			||||||
@@ -1464,6 +1666,39 @@ export function Settings() {
 | 
				
			|||||||
              }
 | 
					              }
 | 
				
			||||||
            ></input>
 | 
					            ></input>
 | 
				
			||||||
          </ListItem>
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Mask.Config.Artifacts.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Mask.Config.Artifacts.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              aria-label={Locale.Mask.Config.Artifacts.Title}
 | 
				
			||||||
 | 
					              type="checkbox"
 | 
				
			||||||
 | 
					              checked={config.enableArtifacts}
 | 
				
			||||||
 | 
					              onChange={(e) =>
 | 
				
			||||||
 | 
					                updateConfig(
 | 
				
			||||||
 | 
					                  (config) =>
 | 
				
			||||||
 | 
					                    (config.enableArtifacts = e.currentTarget.checked),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            ></input>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Mask.Config.CodeFold.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Mask.Config.CodeFold.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <input
 | 
				
			||||||
 | 
					              aria-label={Locale.Mask.Config.CodeFold.Title}
 | 
				
			||||||
 | 
					              type="checkbox"
 | 
				
			||||||
 | 
					              checked={config.enableCodeFold}
 | 
				
			||||||
 | 
					              data-testid="enable-code-fold-checkbox"
 | 
				
			||||||
 | 
					              onChange={(e) =>
 | 
				
			||||||
 | 
					                updateConfig(
 | 
				
			||||||
 | 
					                  (config) => (config.enableCodeFold = e.currentTarget.checked),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            ></input>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
        </List>
 | 
					        </List>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <SyncItems />
 | 
					        <SyncItems />
 | 
				
			||||||
@@ -1540,6 +1775,7 @@ export function Settings() {
 | 
				
			|||||||
        </List>
 | 
					        </List>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <List id={SlotID.CustomModel}>
 | 
					        <List id={SlotID.CustomModel}>
 | 
				
			||||||
 | 
					          {saasStartComponent}
 | 
				
			||||||
          {accessCodeComponent}
 | 
					          {accessCodeComponent}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          {!accessStore.hideUserApiKey && (
 | 
					          {!accessStore.hideUserApiKey && (
 | 
				
			||||||
@@ -1580,8 +1816,12 @@ export function Settings() {
 | 
				
			|||||||
                  {alibabaConfigComponent}
 | 
					                  {alibabaConfigComponent}
 | 
				
			||||||
                  {tencentConfigComponent}
 | 
					                  {tencentConfigComponent}
 | 
				
			||||||
                  {moonshotConfigComponent}
 | 
					                  {moonshotConfigComponent}
 | 
				
			||||||
 | 
					                  {deepseekConfigComponent}
 | 
				
			||||||
                  {stabilityConfigComponent}
 | 
					                  {stabilityConfigComponent}
 | 
				
			||||||
                  {lflytekConfigComponent}
 | 
					                  {lflytekConfigComponent}
 | 
				
			||||||
 | 
					                  {XAIConfigComponent}
 | 
				
			||||||
 | 
					                  {chatglmConfigComponent}
 | 
				
			||||||
 | 
					                  {siliconflowConfigComponent}
 | 
				
			||||||
                </>
 | 
					                </>
 | 
				
			||||||
              )}
 | 
					              )}
 | 
				
			||||||
            </>
 | 
					            </>
 | 
				
			||||||
@@ -1616,9 +1856,11 @@ export function Settings() {
 | 
				
			|||||||
          <ListItem
 | 
					          <ListItem
 | 
				
			||||||
            title={Locale.Settings.Access.CustomModel.Title}
 | 
					            title={Locale.Settings.Access.CustomModel.Title}
 | 
				
			||||||
            subTitle={Locale.Settings.Access.CustomModel.SubTitle}
 | 
					            subTitle={Locale.Settings.Access.CustomModel.SubTitle}
 | 
				
			||||||
 | 
					            vertical={true}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <input
 | 
					            <input
 | 
				
			||||||
              aria-label={Locale.Settings.Access.CustomModel.Title}
 | 
					              aria-label={Locale.Settings.Access.CustomModel.Title}
 | 
				
			||||||
 | 
					              style={{ width: "100%", maxWidth: "unset", textAlign: "left" }}
 | 
				
			||||||
              type="text"
 | 
					              type="text"
 | 
				
			||||||
              value={config.customModels}
 | 
					              value={config.customModels}
 | 
				
			||||||
              placeholder="model1,model2,model3"
 | 
					              placeholder="model1,model2,model3"
 | 
				
			||||||
@@ -1645,6 +1887,28 @@ export function Settings() {
 | 
				
			|||||||
        {shouldShowPromptModal && (
 | 
					        {shouldShowPromptModal && (
 | 
				
			||||||
          <UserPromptModal onClose={() => setShowPromptModal(false)} />
 | 
					          <UserPromptModal onClose={() => setShowPromptModal(false)} />
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
 | 
					        <List>
 | 
				
			||||||
 | 
					          <RealtimeConfigList
 | 
				
			||||||
 | 
					            realtimeConfig={config.realtimeConfig}
 | 
				
			||||||
 | 
					            updateConfig={(updater) => {
 | 
				
			||||||
 | 
					              const realtimeConfig = { ...config.realtimeConfig };
 | 
				
			||||||
 | 
					              updater(realtimeConfig);
 | 
				
			||||||
 | 
					              config.update(
 | 
				
			||||||
 | 
					                (config) => (config.realtimeConfig = realtimeConfig),
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </List>
 | 
				
			||||||
 | 
					        <List>
 | 
				
			||||||
 | 
					          <TTSConfigList
 | 
				
			||||||
 | 
					            ttsConfig={config.ttsConfig}
 | 
				
			||||||
 | 
					            updateConfig={(updater) => {
 | 
				
			||||||
 | 
					              const ttsConfig = { ...config.ttsConfig };
 | 
				
			||||||
 | 
					              updater(ttsConfig);
 | 
				
			||||||
 | 
					              config.update((config) => (config.ttsConfig = ttsConfig));
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </List>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <DangerItems />
 | 
					        <DangerItems />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import React, { useEffect, useRef, useMemo, useState, Fragment } from "react";
 | 
					import React, { Fragment, useEffect, useMemo, useRef, useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import styles from "./home.module.scss";
 | 
					import styles from "./home.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -7,9 +7,9 @@ import SettingsIcon from "../icons/settings.svg";
 | 
				
			|||||||
import GithubIcon from "../icons/github.svg";
 | 
					import GithubIcon from "../icons/github.svg";
 | 
				
			||||||
import ChatGptIcon from "../icons/chatgpt.svg";
 | 
					import ChatGptIcon from "../icons/chatgpt.svg";
 | 
				
			||||||
import AddIcon from "../icons/add.svg";
 | 
					import AddIcon from "../icons/add.svg";
 | 
				
			||||||
import CloseIcon from "../icons/close.svg";
 | 
					 | 
				
			||||||
import DeleteIcon from "../icons/delete.svg";
 | 
					import DeleteIcon from "../icons/delete.svg";
 | 
				
			||||||
import MaskIcon from "../icons/mask.svg";
 | 
					import MaskIcon from "../icons/mask.svg";
 | 
				
			||||||
 | 
					import McpIcon from "../icons/mcp.svg";
 | 
				
			||||||
import DragIcon from "../icons/drag.svg";
 | 
					import DragIcon from "../icons/drag.svg";
 | 
				
			||||||
import DiscoveryIcon from "../icons/discovery.svg";
 | 
					import DiscoveryIcon from "../icons/discovery.svg";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -23,14 +23,21 @@ import {
 | 
				
			|||||||
  MIN_SIDEBAR_WIDTH,
 | 
					  MIN_SIDEBAR_WIDTH,
 | 
				
			||||||
  NARROW_SIDEBAR_WIDTH,
 | 
					  NARROW_SIDEBAR_WIDTH,
 | 
				
			||||||
  Path,
 | 
					  Path,
 | 
				
			||||||
  PLUGINS,
 | 
					 | 
				
			||||||
  REPO_URL,
 | 
					  REPO_URL,
 | 
				
			||||||
} from "../constant";
 | 
					} from "../constant";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Link, useNavigate } from "react-router-dom";
 | 
					import { Link, useNavigate } from "react-router-dom";
 | 
				
			||||||
import { isIOS, useMobileScreen } from "../utils";
 | 
					import { isIOS, useMobileScreen } from "../utils";
 | 
				
			||||||
import dynamic from "next/dynamic";
 | 
					import dynamic from "next/dynamic";
 | 
				
			||||||
import { showConfirm, Selector } from "./ui-lib";
 | 
					import { Selector, showConfirm } from "./ui-lib";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					import { isMcpEnabled } from "../mcp/actions";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DISCOVERY = [
 | 
				
			||||||
 | 
					  { name: Locale.Plugin.Name, path: Path.Plugins },
 | 
				
			||||||
 | 
					  { name: "Stable Diffusion", path: Path.Sd },
 | 
				
			||||||
 | 
					  { name: Locale.SearchChat.Page.Title, path: Path.SearchChat },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
 | 
					const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
 | 
				
			||||||
  loading: () => null,
 | 
					  loading: () => null,
 | 
				
			||||||
@@ -128,6 +135,7 @@ export function useDragSideBar() {
 | 
				
			|||||||
    shouldNarrow,
 | 
					    shouldNarrow,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function SideBarContainer(props: {
 | 
					export function SideBarContainer(props: {
 | 
				
			||||||
  children: React.ReactNode;
 | 
					  children: React.ReactNode;
 | 
				
			||||||
  onDragStart: (e: MouseEvent) => void;
 | 
					  onDragStart: (e: MouseEvent) => void;
 | 
				
			||||||
@@ -142,9 +150,9 @@ export function SideBarContainer(props: {
 | 
				
			|||||||
  const { children, className, onDragStart, shouldNarrow } = props;
 | 
					  const { children, className, onDragStart, shouldNarrow } = props;
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      className={`${styles.sidebar} ${className} ${
 | 
					      className={clsx(styles.sidebar, className, {
 | 
				
			||||||
        shouldNarrow && styles["narrow-sidebar"]
 | 
					        [styles["narrow-sidebar"]]: shouldNarrow,
 | 
				
			||||||
      }`}
 | 
					      })}
 | 
				
			||||||
      style={{
 | 
					      style={{
 | 
				
			||||||
        // #3016 disable transition on ios mobile screen
 | 
					        // #3016 disable transition on ios mobile screen
 | 
				
			||||||
        transition: isMobileScreen && isIOSMobile ? "none" : undefined,
 | 
					        transition: isMobileScreen && isIOSMobile ? "none" : undefined,
 | 
				
			||||||
@@ -166,18 +174,24 @@ export function SideBarHeader(props: {
 | 
				
			|||||||
  subTitle?: string | React.ReactNode;
 | 
					  subTitle?: string | React.ReactNode;
 | 
				
			||||||
  logo?: React.ReactNode;
 | 
					  logo?: React.ReactNode;
 | 
				
			||||||
  children?: React.ReactNode;
 | 
					  children?: React.ReactNode;
 | 
				
			||||||
 | 
					  shouldNarrow?: boolean;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const { title, subTitle, logo, children } = props;
 | 
					  const { title, subTitle, logo, children, shouldNarrow } = props;
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Fragment>
 | 
					    <Fragment>
 | 
				
			||||||
      <div className={styles["sidebar-header"]} data-tauri-drag-region>
 | 
					      <div
 | 
				
			||||||
 | 
					        className={clsx(styles["sidebar-header"], {
 | 
				
			||||||
 | 
					          [styles["sidebar-header-narrow"]]: shouldNarrow,
 | 
				
			||||||
 | 
					        })}
 | 
				
			||||||
 | 
					        data-tauri-drag-region
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        <div className={styles["sidebar-title-container"]}>
 | 
					        <div className={styles["sidebar-title-container"]}>
 | 
				
			||||||
          <div className={styles["sidebar-title"]} data-tauri-drag-region>
 | 
					          <div className={styles["sidebar-title"]} data-tauri-drag-region>
 | 
				
			||||||
            {title}
 | 
					            {title}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div className={styles["sidebar-sub-title"]}>{subTitle}</div>
 | 
					          <div className={styles["sidebar-sub-title"]}>{subTitle}</div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div className={styles["sidebar-logo"] + " no-dark"}>{logo}</div>
 | 
					        <div className={clsx(styles["sidebar-logo"], "no-dark")}>{logo}</div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      {children}
 | 
					      {children}
 | 
				
			||||||
    </Fragment>
 | 
					    </Fragment>
 | 
				
			||||||
@@ -213,10 +227,21 @@ export function SideBarTail(props: {
 | 
				
			|||||||
export function SideBar(props: { className?: string }) {
 | 
					export function SideBar(props: { className?: string }) {
 | 
				
			||||||
  useHotKey();
 | 
					  useHotKey();
 | 
				
			||||||
  const { onDragStart, shouldNarrow } = useDragSideBar();
 | 
					  const { onDragStart, shouldNarrow } = useDragSideBar();
 | 
				
			||||||
  const [showPluginSelector, setShowPluginSelector] = useState(false);
 | 
					  const [showDiscoverySelector, setshowDiscoverySelector] = useState(false);
 | 
				
			||||||
  const navigate = useNavigate();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  const config = useAppConfig();
 | 
					  const config = useAppConfig();
 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const [mcpEnabled, setMcpEnabled] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    // 检查 MCP 是否启用
 | 
				
			||||||
 | 
					    const checkMcpStatus = async () => {
 | 
				
			||||||
 | 
					      const enabled = await isMcpEnabled();
 | 
				
			||||||
 | 
					      setMcpEnabled(enabled);
 | 
				
			||||||
 | 
					      console.log("[SideBar] MCP enabled:", enabled);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    checkMcpStatus();
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <SideBarContainer
 | 
					    <SideBarContainer
 | 
				
			||||||
@@ -228,6 +253,7 @@ export function SideBar(props: { className?: string }) {
 | 
				
			|||||||
        title="NextChat"
 | 
					        title="NextChat"
 | 
				
			||||||
        subTitle="Build your own AI assistant."
 | 
					        subTitle="Build your own AI assistant."
 | 
				
			||||||
        logo={<ChatGptIcon />}
 | 
					        logo={<ChatGptIcon />}
 | 
				
			||||||
 | 
					        shouldNarrow={shouldNarrow}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <div className={styles["sidebar-header-bar"]}>
 | 
					        <div className={styles["sidebar-header-bar"]}>
 | 
				
			||||||
          <IconButton
 | 
					          <IconButton
 | 
				
			||||||
@@ -243,30 +269,36 @@ export function SideBar(props: { className?: string }) {
 | 
				
			|||||||
            }}
 | 
					            }}
 | 
				
			||||||
            shadow
 | 
					            shadow
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
 | 
					          {mcpEnabled && (
 | 
				
			||||||
 | 
					            <IconButton
 | 
				
			||||||
 | 
					              icon={<McpIcon />}
 | 
				
			||||||
 | 
					              text={shouldNarrow ? undefined : Locale.Mcp.Name}
 | 
				
			||||||
 | 
					              className={styles["sidebar-bar-button"]}
 | 
				
			||||||
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                navigate(Path.McpMarket, { state: { fromHome: true } });
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					              shadow
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
          <IconButton
 | 
					          <IconButton
 | 
				
			||||||
            icon={<DiscoveryIcon />}
 | 
					            icon={<DiscoveryIcon />}
 | 
				
			||||||
            text={shouldNarrow ? undefined : Locale.Discovery.Name}
 | 
					            text={shouldNarrow ? undefined : Locale.Discovery.Name}
 | 
				
			||||||
            className={styles["sidebar-bar-button"]}
 | 
					            className={styles["sidebar-bar-button"]}
 | 
				
			||||||
            onClick={() => setShowPluginSelector(true)}
 | 
					            onClick={() => setshowDiscoverySelector(true)}
 | 
				
			||||||
            shadow
 | 
					            shadow
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        {showPluginSelector && (
 | 
					        {showDiscoverySelector && (
 | 
				
			||||||
          <Selector
 | 
					          <Selector
 | 
				
			||||||
            items={[
 | 
					            items={[
 | 
				
			||||||
              {
 | 
					              ...DISCOVERY.map((item) => {
 | 
				
			||||||
                title: "👇 Please select the plugin you need to use",
 | 
					 | 
				
			||||||
                value: "-",
 | 
					 | 
				
			||||||
                disable: true,
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              ...PLUGINS.map((item) => {
 | 
					 | 
				
			||||||
                return {
 | 
					                return {
 | 
				
			||||||
                  title: item.name,
 | 
					                  title: item.name,
 | 
				
			||||||
                  value: item.path,
 | 
					                  value: item.path,
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
              }),
 | 
					              }),
 | 
				
			||||||
            ]}
 | 
					            ]}
 | 
				
			||||||
            onClose={() => setShowPluginSelector(false)}
 | 
					            onClose={() => setshowDiscoverySelector(false)}
 | 
				
			||||||
            onSelection={(s) => {
 | 
					            onSelection={(s) => {
 | 
				
			||||||
              navigate(s[0], { state: { fromHome: true } });
 | 
					              navigate(s[0], { state: { fromHome: true } });
 | 
				
			||||||
            }}
 | 
					            }}
 | 
				
			||||||
@@ -285,7 +317,7 @@ export function SideBar(props: { className?: string }) {
 | 
				
			|||||||
      <SideBarTail
 | 
					      <SideBarTail
 | 
				
			||||||
        primaryAction={
 | 
					        primaryAction={
 | 
				
			||||||
          <>
 | 
					          <>
 | 
				
			||||||
            <div className={styles["sidebar-action"] + " " + styles.mobile}>
 | 
					            <div className={clsx(styles["sidebar-action"], styles.mobile)}>
 | 
				
			||||||
              <IconButton
 | 
					              <IconButton
 | 
				
			||||||
                icon={<DeleteIcon />}
 | 
					                icon={<DeleteIcon />}
 | 
				
			||||||
                onClick={async () => {
 | 
					                onClick={async () => {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										133
									
								
								app/components/tts-config.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,133 @@
 | 
				
			|||||||
 | 
					import { TTSConfig, TTSConfigValidator } from "../store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Locale from "../locales";
 | 
				
			||||||
 | 
					import { ListItem, Select } from "./ui-lib";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DEFAULT_TTS_ENGINE,
 | 
				
			||||||
 | 
					  DEFAULT_TTS_ENGINES,
 | 
				
			||||||
 | 
					  DEFAULT_TTS_MODELS,
 | 
				
			||||||
 | 
					  DEFAULT_TTS_VOICES,
 | 
				
			||||||
 | 
					} from "../constant";
 | 
				
			||||||
 | 
					import { InputRange } from "./input-range";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function TTSConfigList(props: {
 | 
				
			||||||
 | 
					  ttsConfig: TTSConfig;
 | 
				
			||||||
 | 
					  updateConfig: (updater: (config: TTSConfig) => void) => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.TTS.Enable.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.TTS.Enable.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          type="checkbox"
 | 
				
			||||||
 | 
					          checked={props.ttsConfig.enable}
 | 
				
			||||||
 | 
					          onChange={(e) =>
 | 
				
			||||||
 | 
					            props.updateConfig(
 | 
				
			||||||
 | 
					              (config) => (config.enable = e.currentTarget.checked),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ></input>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      {/* <ListItem
 | 
				
			||||||
 | 
					        title={Locale.Settings.TTS.Autoplay.Title}
 | 
				
			||||||
 | 
					        subTitle={Locale.Settings.TTS.Autoplay.SubTitle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          type="checkbox"
 | 
				
			||||||
 | 
					          checked={props.ttsConfig.autoplay}
 | 
				
			||||||
 | 
					          onChange={(e) =>
 | 
				
			||||||
 | 
					            props.updateConfig(
 | 
				
			||||||
 | 
					              (config) => (config.autoplay = e.currentTarget.checked),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ></input>
 | 
				
			||||||
 | 
					      </ListItem> */}
 | 
				
			||||||
 | 
					      <ListItem title={Locale.Settings.TTS.Engine}>
 | 
				
			||||||
 | 
					        <Select
 | 
				
			||||||
 | 
					          value={props.ttsConfig.engine}
 | 
				
			||||||
 | 
					          onChange={(e) => {
 | 
				
			||||||
 | 
					            props.updateConfig(
 | 
				
			||||||
 | 
					              (config) =>
 | 
				
			||||||
 | 
					                (config.engine = TTSConfigValidator.engine(
 | 
				
			||||||
 | 
					                  e.currentTarget.value,
 | 
				
			||||||
 | 
					                )),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {DEFAULT_TTS_ENGINES.map((v, i) => (
 | 
				
			||||||
 | 
					            <option value={v} key={i}>
 | 
				
			||||||
 | 
					              {v}
 | 
				
			||||||
 | 
					            </option>
 | 
				
			||||||
 | 
					          ))}
 | 
				
			||||||
 | 
					        </Select>
 | 
				
			||||||
 | 
					      </ListItem>
 | 
				
			||||||
 | 
					      {props.ttsConfig.engine === DEFAULT_TTS_ENGINE && (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <ListItem title={Locale.Settings.TTS.Model}>
 | 
				
			||||||
 | 
					            <Select
 | 
				
			||||||
 | 
					              value={props.ttsConfig.model}
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                props.updateConfig(
 | 
				
			||||||
 | 
					                  (config) =>
 | 
				
			||||||
 | 
					                    (config.model = TTSConfigValidator.model(
 | 
				
			||||||
 | 
					                      e.currentTarget.value,
 | 
				
			||||||
 | 
					                    )),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {DEFAULT_TTS_MODELS.map((v, i) => (
 | 
				
			||||||
 | 
					                <option value={v} key={i}>
 | 
				
			||||||
 | 
					                  {v}
 | 
				
			||||||
 | 
					                </option>
 | 
				
			||||||
 | 
					              ))}
 | 
				
			||||||
 | 
					            </Select>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.TTS.Voice.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.TTS.Voice.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Select
 | 
				
			||||||
 | 
					              value={props.ttsConfig.voice}
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                props.updateConfig(
 | 
				
			||||||
 | 
					                  (config) =>
 | 
				
			||||||
 | 
					                    (config.voice = TTSConfigValidator.voice(
 | 
				
			||||||
 | 
					                      e.currentTarget.value,
 | 
				
			||||||
 | 
					                    )),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {DEFAULT_TTS_VOICES.map((v, i) => (
 | 
				
			||||||
 | 
					                <option value={v} key={i}>
 | 
				
			||||||
 | 
					                  {v}
 | 
				
			||||||
 | 
					                </option>
 | 
				
			||||||
 | 
					              ))}
 | 
				
			||||||
 | 
					            </Select>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					          <ListItem
 | 
				
			||||||
 | 
					            title={Locale.Settings.TTS.Speed.Title}
 | 
				
			||||||
 | 
					            subTitle={Locale.Settings.TTS.Speed.SubTitle}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <InputRange
 | 
				
			||||||
 | 
					              aria={Locale.Settings.TTS.Speed.Title}
 | 
				
			||||||
 | 
					              value={props.ttsConfig.speed?.toFixed(1)}
 | 
				
			||||||
 | 
					              min="0.3"
 | 
				
			||||||
 | 
					              max="4.0"
 | 
				
			||||||
 | 
					              step="0.1"
 | 
				
			||||||
 | 
					              onChange={(e) => {
 | 
				
			||||||
 | 
					                props.updateConfig(
 | 
				
			||||||
 | 
					                  (config) =>
 | 
				
			||||||
 | 
					                    (config.speed = TTSConfigValidator.speed(
 | 
				
			||||||
 | 
					                      e.currentTarget.valueAsNumber,
 | 
				
			||||||
 | 
					                    )),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            ></InputRange>
 | 
				
			||||||
 | 
					          </ListItem>
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										119
									
								
								app/components/tts.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					@import "../styles/animation.scss";
 | 
				
			||||||
 | 
					.plugin-page {
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .plugin-page-body {
 | 
				
			||||||
 | 
					    padding: 20px;
 | 
				
			||||||
 | 
					    overflow-y: auto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .plugin-filter {
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      max-width: 100%;
 | 
				
			||||||
 | 
					      margin-bottom: 20px;
 | 
				
			||||||
 | 
					      animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					      height: 40px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .search-bar {
 | 
				
			||||||
 | 
					        flex-grow: 1;
 | 
				
			||||||
 | 
					        max-width: 100%;
 | 
				
			||||||
 | 
					        min-width: 0;
 | 
				
			||||||
 | 
					        outline: none;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .search-bar:focus {
 | 
				
			||||||
 | 
					        border: 1px solid var(--primary);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .plugin-filter-lang {
 | 
				
			||||||
 | 
					        height: 100%;
 | 
				
			||||||
 | 
					        margin-left: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .plugin-create {
 | 
				
			||||||
 | 
					        height: 100%;
 | 
				
			||||||
 | 
					        margin-left: 10px;
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        min-width: 80px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .plugin-item {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      justify-content: space-between;
 | 
				
			||||||
 | 
					      padding: 20px;
 | 
				
			||||||
 | 
					      border: var(--border-in-light);
 | 
				
			||||||
 | 
					      animation: slide-in ease 0.3s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:not(:last-child) {
 | 
				
			||||||
 | 
					        border-bottom: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:first-child {
 | 
				
			||||||
 | 
					        border-top-left-radius: 10px;
 | 
				
			||||||
 | 
					        border-top-right-radius: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:last-child {
 | 
				
			||||||
 | 
					        border-bottom-left-radius: 10px;
 | 
				
			||||||
 | 
					        border-bottom-right-radius: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .plugin-header {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .plugin-icon {
 | 
				
			||||||
 | 
					          display: flex;
 | 
				
			||||||
 | 
					          align-items: center;
 | 
				
			||||||
 | 
					          justify-content: center;
 | 
				
			||||||
 | 
					          margin-right: 10px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .plugin-title {
 | 
				
			||||||
 | 
					          .plugin-name {
 | 
				
			||||||
 | 
					            font-size: 14px;
 | 
				
			||||||
 | 
					            font-weight: bold;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          .plugin-info {
 | 
				
			||||||
 | 
					            font-size: 12px;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          .plugin-runtime-warning {
 | 
				
			||||||
 | 
					            font-size: 12px;
 | 
				
			||||||
 | 
					            color: #f86c6c;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .plugin-actions {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        flex-wrap: nowrap;
 | 
				
			||||||
 | 
					        transition: all ease 0.3s;
 | 
				
			||||||
 | 
					        justify-content: center;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @media screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        flex-direction: column;
 | 
				
			||||||
 | 
					        padding-bottom: 10px;
 | 
				
			||||||
 | 
					        border-radius: 10px;
 | 
				
			||||||
 | 
					        margin-bottom: 20px;
 | 
				
			||||||
 | 
					        box-shadow: var(--card-shadow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:not(:last-child) {
 | 
				
			||||||
 | 
					          border-bottom: var(--border-in-light);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .plugin-actions {
 | 
				
			||||||
 | 
					          width: 100%;
 | 
				
			||||||
 | 
					          justify-content: space-between;
 | 
				
			||||||
 | 
					          padding-top: 10px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -62,14 +62,14 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.vertical{
 | 
					  &.vertical {
 | 
				
			||||||
    flex-direction: column;
 | 
					    flex-direction: column;
 | 
				
			||||||
    align-items: start;
 | 
					    align-items: start;
 | 
				
			||||||
    .list-header{
 | 
					    .list-header {
 | 
				
			||||||
      .list-item-title{
 | 
					      .list-item-title {
 | 
				
			||||||
        margin-bottom: 5px;
 | 
					        margin-bottom: 5px;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .list-item-sub-title{
 | 
					      .list-item-sub-title {
 | 
				
			||||||
        margin-bottom: 2px;
 | 
					        margin-bottom: 2px;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -252,6 +252,12 @@
 | 
				
			|||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  max-width: fit-content;
 | 
					  max-width: fit-content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &.left-align-option {
 | 
				
			||||||
 | 
					    option {
 | 
				
			||||||
 | 
					      text-align: left;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .select-with-icon-select {
 | 
					  .select-with-icon-select {
 | 
				
			||||||
    height: 100%;
 | 
					    height: 100%;
 | 
				
			||||||
    border: var(--border-in-light);
 | 
					    border: var(--border-in-light);
 | 
				
			||||||
@@ -304,7 +310,7 @@
 | 
				
			|||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  z-index: 999;
 | 
					  z-index: 999;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .selector-item-disabled{
 | 
					  .selector-item-disabled {
 | 
				
			||||||
    opacity: 0.6;
 | 
					    opacity: 0.6;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -330,3 +336,4 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,8 @@ import React, {
 | 
				
			|||||||
  useRef,
 | 
					  useRef,
 | 
				
			||||||
} from "react";
 | 
					} from "react";
 | 
				
			||||||
import { IconButton } from "./button";
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
 | 
					import { Avatar } from "./emoji";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Popover(props: {
 | 
					export function Popover(props: {
 | 
				
			||||||
  children: JSX.Element;
 | 
					  children: JSX.Element;
 | 
				
			||||||
@@ -45,13 +47,13 @@ export function Popover(props: {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export function Card(props: { children: JSX.Element[]; className?: string }) {
 | 
					export function Card(props: { children: JSX.Element[]; className?: string }) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={styles.card + " " + props.className}>{props.children}</div>
 | 
					    <div className={clsx(styles.card, props.className)}>{props.children}</div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ListItem(props: {
 | 
					export function ListItem(props: {
 | 
				
			||||||
  title: string;
 | 
					  title?: string;
 | 
				
			||||||
  subTitle?: string;
 | 
					  subTitle?: string | JSX.Element;
 | 
				
			||||||
  children?: JSX.Element | JSX.Element[];
 | 
					  children?: JSX.Element | JSX.Element[];
 | 
				
			||||||
  icon?: JSX.Element;
 | 
					  icon?: JSX.Element;
 | 
				
			||||||
  className?: string;
 | 
					  className?: string;
 | 
				
			||||||
@@ -60,11 +62,13 @@ export function ListItem(props: {
 | 
				
			|||||||
}) {
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      className={
 | 
					      className={clsx(
 | 
				
			||||||
        styles["list-item"] +
 | 
					        styles["list-item"],
 | 
				
			||||||
        ` ${props.vertical ? styles["vertical"] : ""} ` +
 | 
					        {
 | 
				
			||||||
        ` ${props.className || ""}`
 | 
					          [styles["vertical"]]: props.vertical,
 | 
				
			||||||
      }
 | 
					        },
 | 
				
			||||||
 | 
					        props.className,
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
      onClick={props.onClick}
 | 
					      onClick={props.onClick}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <div className={styles["list-header"]}>
 | 
					      <div className={styles["list-header"]}>
 | 
				
			||||||
@@ -135,9 +139,9 @@ export function Modal(props: ModalProps) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      className={
 | 
					      className={clsx(styles["modal-container"], {
 | 
				
			||||||
        styles["modal-container"] + ` ${isMax && styles["modal-container-max"]}`
 | 
					        [styles["modal-container-max"]]: isMax,
 | 
				
			||||||
      }
 | 
					      })}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <div className={styles["modal-header"]}>
 | 
					      <div className={styles["modal-header"]}>
 | 
				
			||||||
        <div className={styles["modal-title"]}>{props.title}</div>
 | 
					        <div className={styles["modal-title"]}>{props.title}</div>
 | 
				
			||||||
@@ -260,7 +264,7 @@ export function Input(props: InputProps) {
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <textarea
 | 
					    <textarea
 | 
				
			||||||
      {...props}
 | 
					      {...props}
 | 
				
			||||||
      className={`${styles["input"]} ${props.className}`}
 | 
					      className={clsx(styles["input"], props.className)}
 | 
				
			||||||
    ></textarea>
 | 
					    ></textarea>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -292,13 +296,23 @@ export function PasswordInput(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export function Select(
 | 
					export function Select(
 | 
				
			||||||
  props: React.DetailedHTMLProps<
 | 
					  props: React.DetailedHTMLProps<
 | 
				
			||||||
    React.SelectHTMLAttributes<HTMLSelectElement>,
 | 
					    React.SelectHTMLAttributes<HTMLSelectElement> & {
 | 
				
			||||||
 | 
					      align?: "left" | "center";
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    HTMLSelectElement
 | 
					    HTMLSelectElement
 | 
				
			||||||
  >,
 | 
					  >,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  const { className, children, ...otherProps } = props;
 | 
					  const { className, children, align, ...otherProps } = props;
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={`${styles["select-with-icon"]} ${className}`}>
 | 
					    <div
 | 
				
			||||||
 | 
					      className={clsx(
 | 
				
			||||||
 | 
					        styles["select-with-icon"],
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          [styles["left-align-option"]]: align === "left",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        className,
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
      <select className={styles["select-with-icon-select"]} {...otherProps}>
 | 
					      <select className={styles["select-with-icon-select"]} {...otherProps}>
 | 
				
			||||||
        {children}
 | 
					        {children}
 | 
				
			||||||
      </select>
 | 
					      </select>
 | 
				
			||||||
@@ -503,12 +517,13 @@ export function Selector<T>(props: {
 | 
				
			|||||||
            const selected = selectedValues.includes(item.value);
 | 
					            const selected = selectedValues.includes(item.value);
 | 
				
			||||||
            return (
 | 
					            return (
 | 
				
			||||||
              <ListItem
 | 
					              <ListItem
 | 
				
			||||||
                className={`${styles["selector-item"]} ${
 | 
					                className={clsx(styles["selector-item"], {
 | 
				
			||||||
                  item.disable && styles["selector-item-disabled"]
 | 
					                  [styles["selector-item-disabled"]]: item.disable,
 | 
				
			||||||
                }`}
 | 
					                })}
 | 
				
			||||||
                key={i}
 | 
					                key={i}
 | 
				
			||||||
                title={item.title}
 | 
					                title={item.title}
 | 
				
			||||||
                subTitle={item.subTitle}
 | 
					                subTitle={item.subTitle}
 | 
				
			||||||
 | 
					                icon={<Avatar model={item.value as string} />}
 | 
				
			||||||
                onClick={(e) => {
 | 
					                onClick={(e) => {
 | 
				
			||||||
                  if (item.disable) {
 | 
					                  if (item.disable) {
 | 
				
			||||||
                    e.stopPropagation();
 | 
					                    e.stopPropagation();
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								app/components/voice-print/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export * from "./voice-print";
 | 
				
			||||||
							
								
								
									
										11
									
								
								app/components/voice-print/voice-print.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					.voice-print {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 60px;
 | 
				
			||||||
 | 
					  margin: 20px 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  canvas {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    filter: brightness(1.2); // 增加整体亮度
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										180
									
								
								app/components/voice-print/voice-print.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,180 @@
 | 
				
			|||||||
 | 
					import { useEffect, useRef, useCallback } from "react";
 | 
				
			||||||
 | 
					import styles from "./voice-print.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface VoicePrintProps {
 | 
				
			||||||
 | 
					  frequencies?: Uint8Array;
 | 
				
			||||||
 | 
					  isActive?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
 | 
				
			||||||
 | 
					  // Canvas引用,用于获取绘图上下文
 | 
				
			||||||
 | 
					  const canvasRef = useRef<HTMLCanvasElement>(null);
 | 
				
			||||||
 | 
					  // 存储历史频率数据,用于平滑处理
 | 
				
			||||||
 | 
					  const historyRef = useRef<number[][]>([]);
 | 
				
			||||||
 | 
					  // 控制保留的历史数据帧数,影响平滑度
 | 
				
			||||||
 | 
					  const historyLengthRef = useRef(10);
 | 
				
			||||||
 | 
					  // 存储动画帧ID,用于清理
 | 
				
			||||||
 | 
					  const animationFrameRef = useRef<number>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 更新频率历史数据
 | 
				
			||||||
 | 
					   * 使用FIFO队列维护固定长度的历史记录
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  const updateHistory = useCallback((freqArray: number[]) => {
 | 
				
			||||||
 | 
					    historyRef.current.push(freqArray);
 | 
				
			||||||
 | 
					    if (historyRef.current.length > historyLengthRef.current) {
 | 
				
			||||||
 | 
					      historyRef.current.shift();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const canvas = canvasRef.current;
 | 
				
			||||||
 | 
					    if (!canvas) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const ctx = canvas.getContext("2d");
 | 
				
			||||||
 | 
					    if (!ctx) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 处理高DPI屏幕显示
 | 
				
			||||||
 | 
					     * 根据设备像素比例调整canvas实际渲染分辨率
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    const dpr = window.devicePixelRatio || 1;
 | 
				
			||||||
 | 
					    canvas.width = canvas.offsetWidth * dpr;
 | 
				
			||||||
 | 
					    canvas.height = canvas.offsetHeight * dpr;
 | 
				
			||||||
 | 
					    ctx.scale(dpr, dpr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 主要绘制函数
 | 
				
			||||||
 | 
					     * 使用requestAnimationFrame实现平滑动画
 | 
				
			||||||
 | 
					     * 包含以下步骤:
 | 
				
			||||||
 | 
					     * 1. 清空画布
 | 
				
			||||||
 | 
					     * 2. 更新历史数据
 | 
				
			||||||
 | 
					     * 3. 计算波形点
 | 
				
			||||||
 | 
					     * 4. 绘制上下对称的声纹
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    const draw = () => {
 | 
				
			||||||
 | 
					      // 清空画布
 | 
				
			||||||
 | 
					      ctx.clearRect(0, 0, canvas.width, canvas.height);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!frequencies || !isActive) {
 | 
				
			||||||
 | 
					        historyRef.current = [];
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const freqArray = Array.from(frequencies);
 | 
				
			||||||
 | 
					      updateHistory(freqArray);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 绘制声纹
 | 
				
			||||||
 | 
					      const points: [number, number][] = [];
 | 
				
			||||||
 | 
					      const centerY = canvas.height / 2;
 | 
				
			||||||
 | 
					      const width = canvas.width;
 | 
				
			||||||
 | 
					      const sliceWidth = width / (frequencies.length - 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 绘制主波形
 | 
				
			||||||
 | 
					      ctx.beginPath();
 | 
				
			||||||
 | 
					      ctx.moveTo(0, centerY);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      /**
 | 
				
			||||||
 | 
					       * 声纹绘制算法:
 | 
				
			||||||
 | 
					       * 1. 使用历史数据平均值实现平滑过渡
 | 
				
			||||||
 | 
					       * 2. 通过正弦函数添加自然波动
 | 
				
			||||||
 | 
					       * 3. 使用贝塞尔曲线连接点,使曲线更平滑
 | 
				
			||||||
 | 
					       * 4. 绘制对称部分形成完整声纹
 | 
				
			||||||
 | 
					       */
 | 
				
			||||||
 | 
					      for (let i = 0; i < frequencies.length; i++) {
 | 
				
			||||||
 | 
					        const x = i * sliceWidth;
 | 
				
			||||||
 | 
					        let avgFrequency = frequencies[i];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 波形平滑处理:
 | 
				
			||||||
 | 
					         * 1. 收集历史数据中对应位置的频率值
 | 
				
			||||||
 | 
					         * 2. 计算当前值与历史值的加权平均
 | 
				
			||||||
 | 
					         * 3. 根据平均值计算实际显示高度
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        if (historyRef.current.length > 0) {
 | 
				
			||||||
 | 
					          const historicalValues = historyRef.current.map((h) => h[i] || 0);
 | 
				
			||||||
 | 
					          avgFrequency =
 | 
				
			||||||
 | 
					            (avgFrequency + historicalValues.reduce((a, b) => a + b, 0)) /
 | 
				
			||||||
 | 
					            (historyRef.current.length + 1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 波形变换:
 | 
				
			||||||
 | 
					         * 1. 归一化频率值到0-1范围
 | 
				
			||||||
 | 
					         * 2. 添加时间相关的正弦变换
 | 
				
			||||||
 | 
					         * 3. 使用贝塞尔曲线平滑连接点
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        const normalized = avgFrequency / 255.0;
 | 
				
			||||||
 | 
					        const height = normalized * (canvas.height / 2);
 | 
				
			||||||
 | 
					        const y = centerY + height * Math.sin(i * 0.2 + Date.now() * 0.002);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        points.push([x, y]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (i === 0) {
 | 
				
			||||||
 | 
					          ctx.moveTo(x, y);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // 使用贝塞尔曲线使波形更平滑
 | 
				
			||||||
 | 
					          const prevPoint = points[i - 1];
 | 
				
			||||||
 | 
					          const midX = (prevPoint[0] + x) / 2;
 | 
				
			||||||
 | 
					          ctx.quadraticCurveTo(
 | 
				
			||||||
 | 
					            prevPoint[0],
 | 
				
			||||||
 | 
					            prevPoint[1],
 | 
				
			||||||
 | 
					            midX,
 | 
				
			||||||
 | 
					            (prevPoint[1] + y) / 2,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 绘制对称的下半部分
 | 
				
			||||||
 | 
					      for (let i = points.length - 1; i >= 0; i--) {
 | 
				
			||||||
 | 
					        const [x, y] = points[i];
 | 
				
			||||||
 | 
					        const symmetricY = centerY - (y - centerY);
 | 
				
			||||||
 | 
					        if (i === points.length - 1) {
 | 
				
			||||||
 | 
					          ctx.lineTo(x, symmetricY);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          const nextPoint = points[i + 1];
 | 
				
			||||||
 | 
					          const midX = (nextPoint[0] + x) / 2;
 | 
				
			||||||
 | 
					          ctx.quadraticCurveTo(
 | 
				
			||||||
 | 
					            nextPoint[0],
 | 
				
			||||||
 | 
					            centerY - (nextPoint[1] - centerY),
 | 
				
			||||||
 | 
					            midX,
 | 
				
			||||||
 | 
					            centerY - ((nextPoint[1] + y) / 2 - centerY),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      ctx.closePath();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      /**
 | 
				
			||||||
 | 
					       * 渐变效果:
 | 
				
			||||||
 | 
					       * 从左到右应用三色渐变,带透明度
 | 
				
			||||||
 | 
					       * 使用蓝色系配色提升视觉效果
 | 
				
			||||||
 | 
					       */
 | 
				
			||||||
 | 
					      const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
 | 
				
			||||||
 | 
					      gradient.addColorStop(0, "rgba(100, 180, 255, 0.95)");
 | 
				
			||||||
 | 
					      gradient.addColorStop(0.5, "rgba(140, 200, 255, 0.9)");
 | 
				
			||||||
 | 
					      gradient.addColorStop(1, "rgba(180, 220, 255, 0.95)");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      ctx.fillStyle = gradient;
 | 
				
			||||||
 | 
					      ctx.fill();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      animationFrameRef.current = requestAnimationFrame(draw);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 启动动画循环
 | 
				
			||||||
 | 
					    draw();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 清理函数:在组件卸载时取消动画
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      if (animationFrameRef.current) {
 | 
				
			||||||
 | 
					        cancelAnimationFrame(animationFrameRef.current);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, [frequencies, isActive, updateHistory]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={styles["voice-print"]}>
 | 
				
			||||||
 | 
					      <canvas ref={canvasRef} />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import md5 from "spark-md5";
 | 
					import md5 from "spark-md5";
 | 
				
			||||||
import { DEFAULT_MODELS, DEFAULT_GA_ID } from "../constant";
 | 
					import { DEFAULT_MODELS, DEFAULT_GA_ID } from "../constant";
 | 
				
			||||||
 | 
					import { isGPT4Model } from "../utils/model";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
  namespace NodeJS {
 | 
					  namespace NodeJS {
 | 
				
			||||||
@@ -22,6 +23,7 @@ declare global {
 | 
				
			|||||||
      DISABLE_FAST_LINK?: string; // disallow parse settings from url or not
 | 
					      DISABLE_FAST_LINK?: string; // disallow parse settings from url or not
 | 
				
			||||||
      CUSTOM_MODELS?: string; // to control custom models
 | 
					      CUSTOM_MODELS?: string; // to control custom models
 | 
				
			||||||
      DEFAULT_MODEL?: string; // to control default model in every new chat window
 | 
					      DEFAULT_MODEL?: string; // to control default model in every new chat window
 | 
				
			||||||
 | 
					      VISION_MODELS?: string; // to control vision models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // stability only
 | 
					      // stability only
 | 
				
			||||||
      STABILITY_URL?: string;
 | 
					      STABILITY_URL?: string;
 | 
				
			||||||
@@ -71,8 +73,25 @@ declare global {
 | 
				
			|||||||
      IFLYTEK_API_KEY?: string;
 | 
					      IFLYTEK_API_KEY?: string;
 | 
				
			||||||
      IFLYTEK_API_SECRET?: string;
 | 
					      IFLYTEK_API_SECRET?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      DEEPSEEK_URL?: string;
 | 
				
			||||||
 | 
					      DEEPSEEK_API_KEY?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // xai only
 | 
				
			||||||
 | 
					      XAI_URL?: string;
 | 
				
			||||||
 | 
					      XAI_API_KEY?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // chatglm only
 | 
				
			||||||
 | 
					      CHATGLM_URL?: string;
 | 
				
			||||||
 | 
					      CHATGLM_API_KEY?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // siliconflow only
 | 
				
			||||||
 | 
					      SILICONFLOW_URL?: string;
 | 
				
			||||||
 | 
					      SILICONFLOW_API_KEY?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // custom template for preprocessing user input
 | 
					      // custom template for preprocessing user input
 | 
				
			||||||
      DEFAULT_INPUT_TEMPLATE?: string;
 | 
					      DEFAULT_INPUT_TEMPLATE?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      ENABLE_MCP?: string; // enable mcp functionality
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -116,19 +135,16 @@ export const getServerSideConfig = () => {
 | 
				
			|||||||
  const disableGPT4 = !!process.env.DISABLE_GPT4;
 | 
					  const disableGPT4 = !!process.env.DISABLE_GPT4;
 | 
				
			||||||
  let customModels = process.env.CUSTOM_MODELS ?? "";
 | 
					  let customModels = process.env.CUSTOM_MODELS ?? "";
 | 
				
			||||||
  let defaultModel = process.env.DEFAULT_MODEL ?? "";
 | 
					  let defaultModel = process.env.DEFAULT_MODEL ?? "";
 | 
				
			||||||
 | 
					  let visionModels = process.env.VISION_MODELS ?? "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (disableGPT4) {
 | 
					  if (disableGPT4) {
 | 
				
			||||||
    if (customModels) customModels += ",";
 | 
					    if (customModels) customModels += ",";
 | 
				
			||||||
    customModels += DEFAULT_MODELS.filter(
 | 
					    customModels += DEFAULT_MODELS.filter((m) => isGPT4Model(m.name))
 | 
				
			||||||
      (m) => m.name.startsWith("gpt-4") && !m.name.startsWith("gpt-4o-mini"),
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
      .map((m) => "-" + m.name)
 | 
					      .map((m) => "-" + m.name)
 | 
				
			||||||
      .join(",");
 | 
					      .join(",");
 | 
				
			||||||
    if (
 | 
					    if (defaultModel && isGPT4Model(defaultModel)) {
 | 
				
			||||||
      defaultModel.startsWith("gpt-4") &&
 | 
					 | 
				
			||||||
      !defaultModel.startsWith("gpt-4o-mini")
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
      defaultModel = "";
 | 
					      defaultModel = "";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const isStability = !!process.env.STABILITY_API_KEY;
 | 
					  const isStability = !!process.env.STABILITY_API_KEY;
 | 
				
			||||||
@@ -143,6 +159,10 @@ export const getServerSideConfig = () => {
 | 
				
			|||||||
  const isAlibaba = !!process.env.ALIBABA_API_KEY;
 | 
					  const isAlibaba = !!process.env.ALIBABA_API_KEY;
 | 
				
			||||||
  const isMoonshot = !!process.env.MOONSHOT_API_KEY;
 | 
					  const isMoonshot = !!process.env.MOONSHOT_API_KEY;
 | 
				
			||||||
  const isIflytek = !!process.env.IFLYTEK_API_KEY;
 | 
					  const isIflytek = !!process.env.IFLYTEK_API_KEY;
 | 
				
			||||||
 | 
					  const isDeepSeek = !!process.env.DEEPSEEK_API_KEY;
 | 
				
			||||||
 | 
					  const isXAI = !!process.env.XAI_API_KEY;
 | 
				
			||||||
 | 
					  const isChatGLM = !!process.env.CHATGLM_API_KEY;
 | 
				
			||||||
 | 
					  const isSiliconFlow = !!process.env.SILICONFLOW_API_KEY;
 | 
				
			||||||
  // const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
 | 
					  // const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
 | 
				
			||||||
  // const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
 | 
					  // const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
 | 
				
			||||||
  // const randomIndex = Math.floor(Math.random() * apiKeys.length);
 | 
					  // const randomIndex = Math.floor(Math.random() * apiKeys.length);
 | 
				
			||||||
@@ -151,8 +171,8 @@ export const getServerSideConfig = () => {
 | 
				
			|||||||
  //   `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
 | 
					  //   `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
 | 
				
			||||||
  // );
 | 
					  // );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const allowedWebDevEndpoints = (
 | 
					  const allowedWebDavEndpoints = (
 | 
				
			||||||
    process.env.WHITE_WEBDEV_ENDPOINTS ?? ""
 | 
					    process.env.WHITE_WEBDAV_ENDPOINTS ?? ""
 | 
				
			||||||
  ).split(",");
 | 
					  ).split(",");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
@@ -205,11 +225,27 @@ export const getServerSideConfig = () => {
 | 
				
			|||||||
    iflytekApiKey: process.env.IFLYTEK_API_KEY,
 | 
					    iflytekApiKey: process.env.IFLYTEK_API_KEY,
 | 
				
			||||||
    iflytekApiSecret: process.env.IFLYTEK_API_SECRET,
 | 
					    iflytekApiSecret: process.env.IFLYTEK_API_SECRET,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isDeepSeek,
 | 
				
			||||||
 | 
					    deepseekUrl: process.env.DEEPSEEK_URL,
 | 
				
			||||||
 | 
					    deepseekApiKey: getApiKey(process.env.DEEPSEEK_API_KEY),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isXAI,
 | 
				
			||||||
 | 
					    xaiUrl: process.env.XAI_URL,
 | 
				
			||||||
 | 
					    xaiApiKey: getApiKey(process.env.XAI_API_KEY),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isChatGLM,
 | 
				
			||||||
 | 
					    chatglmUrl: process.env.CHATGLM_URL,
 | 
				
			||||||
 | 
					    chatglmApiKey: getApiKey(process.env.CHATGLM_API_KEY),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
 | 
					    cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
 | 
				
			||||||
    cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
 | 
					    cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
 | 
				
			||||||
    cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
 | 
					    cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
 | 
				
			||||||
    cloudflareKVTTL: process.env.CLOUDFLARE_KV_TTL,
 | 
					    cloudflareKVTTL: process.env.CLOUDFLARE_KV_TTL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isSiliconFlow,
 | 
				
			||||||
 | 
					    siliconFlowUrl: process.env.SILICONFLOW_URL,
 | 
				
			||||||
 | 
					    siliconFlowApiKey: getApiKey(process.env.SILICONFLOW_API_KEY),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    gtmId: process.env.GTM_ID,
 | 
					    gtmId: process.env.GTM_ID,
 | 
				
			||||||
    gaId: process.env.GA_ID || DEFAULT_GA_ID,
 | 
					    gaId: process.env.GA_ID || DEFAULT_GA_ID,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -226,6 +262,8 @@ export const getServerSideConfig = () => {
 | 
				
			|||||||
    disableFastLink: !!process.env.DISABLE_FAST_LINK,
 | 
					    disableFastLink: !!process.env.DISABLE_FAST_LINK,
 | 
				
			||||||
    customModels,
 | 
					    customModels,
 | 
				
			||||||
    defaultModel,
 | 
					    defaultModel,
 | 
				
			||||||
    allowedWebDevEndpoints,
 | 
					    visionModels,
 | 
				
			||||||
 | 
					    allowedWebDavEndpoints,
 | 
				
			||||||
 | 
					    enableMcp: process.env.ENABLE_MCP === "true",
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										391
									
								
								app/constant.ts
									
									
									
									
									
								
							
							
						
						@@ -1,6 +1,7 @@
 | 
				
			|||||||
export const OWNER = "ChatGPTNextWeb";
 | 
					export const OWNER = "ChatGPTNextWeb";
 | 
				
			||||||
export const REPO = "ChatGPT-Next-Web";
 | 
					export const REPO = "ChatGPT-Next-Web";
 | 
				
			||||||
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
 | 
					export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
 | 
				
			||||||
 | 
					export const PLUGINS_REPO_URL = `https://github.com/${OWNER}/NextChat-Awesome-Plugins`;
 | 
				
			||||||
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
 | 
					export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
 | 
				
			||||||
export const UPDATE_URL = `${REPO_URL}#keep-updated`;
 | 
					export const UPDATE_URL = `${REPO_URL}#keep-updated`;
 | 
				
			||||||
export const RELEASE_URL = `${REPO_URL}/releases`;
 | 
					export const RELEASE_URL = `${REPO_URL}/releases`;
 | 
				
			||||||
@@ -10,7 +11,6 @@ export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const STABILITY_BASE_URL = "https://api.stability.ai";
 | 
					export const STABILITY_BASE_URL = "https://api.stability.ai";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DEFAULT_API_HOST = "https://api.nextchat.dev";
 | 
					 | 
				
			||||||
export const OPENAI_BASE_URL = "https://api.openai.com";
 | 
					export const OPENAI_BASE_URL = "https://api.openai.com";
 | 
				
			||||||
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
 | 
					export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -28,6 +28,14 @@ export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
 | 
				
			|||||||
export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
 | 
					export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
 | 
				
			||||||
export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
 | 
					export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DEEPSEEK_BASE_URL = "https://api.deepseek.com";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const XAI_BASE_URL = "https://api.x.ai";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const CHATGLM_BASE_URL = "https://open.bigmodel.cn";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SILICONFLOW_BASE_URL = "https://api.siliconflow.cn";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const CACHE_URL_PREFIX = "/api/cache";
 | 
					export const CACHE_URL_PREFIX = "/api/cache";
 | 
				
			||||||
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
 | 
					export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -37,10 +45,13 @@ export enum Path {
 | 
				
			|||||||
  Settings = "/settings",
 | 
					  Settings = "/settings",
 | 
				
			||||||
  NewChat = "/new-chat",
 | 
					  NewChat = "/new-chat",
 | 
				
			||||||
  Masks = "/masks",
 | 
					  Masks = "/masks",
 | 
				
			||||||
 | 
					  Plugins = "/plugins",
 | 
				
			||||||
  Auth = "/auth",
 | 
					  Auth = "/auth",
 | 
				
			||||||
  Sd = "/sd",
 | 
					  Sd = "/sd",
 | 
				
			||||||
  SdNew = "/sd-new",
 | 
					  SdNew = "/sd-new",
 | 
				
			||||||
  Artifacts = "/artifacts",
 | 
					  Artifacts = "/artifacts",
 | 
				
			||||||
 | 
					  SearchChat = "/search-chat",
 | 
				
			||||||
 | 
					  McpMarket = "/mcp-market",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum ApiPath {
 | 
					export enum ApiPath {
 | 
				
			||||||
@@ -57,6 +68,10 @@ export enum ApiPath {
 | 
				
			|||||||
  Iflytek = "/api/iflytek",
 | 
					  Iflytek = "/api/iflytek",
 | 
				
			||||||
  Stability = "/api/stability",
 | 
					  Stability = "/api/stability",
 | 
				
			||||||
  Artifacts = "/api/artifacts",
 | 
					  Artifacts = "/api/artifacts",
 | 
				
			||||||
 | 
					  XAI = "/api/xai",
 | 
				
			||||||
 | 
					  ChatGLM = "/api/chatglm",
 | 
				
			||||||
 | 
					  DeepSeek = "/api/deepseek",
 | 
				
			||||||
 | 
					  SiliconFlow = "/api/siliconflow",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum SlotID {
 | 
					export enum SlotID {
 | 
				
			||||||
@@ -69,12 +84,9 @@ export enum FileName {
 | 
				
			|||||||
  Prompts = "prompts.json",
 | 
					  Prompts = "prompts.json",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum Plugin {
 | 
					 | 
				
			||||||
  Artifacts = "artifacts",
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export enum StoreKey {
 | 
					export enum StoreKey {
 | 
				
			||||||
  Chat = "chat-next-web-store",
 | 
					  Chat = "chat-next-web-store",
 | 
				
			||||||
 | 
					  Plugin = "chat-next-web-plugin",
 | 
				
			||||||
  Access = "access-control",
 | 
					  Access = "access-control",
 | 
				
			||||||
  Config = "app-config",
 | 
					  Config = "app-config",
 | 
				
			||||||
  Mask = "mask-store",
 | 
					  Mask = "mask-store",
 | 
				
			||||||
@@ -82,6 +94,7 @@ export enum StoreKey {
 | 
				
			|||||||
  Update = "chat-update",
 | 
					  Update = "chat-update",
 | 
				
			||||||
  Sync = "sync",
 | 
					  Sync = "sync",
 | 
				
			||||||
  SdList = "sd-list",
 | 
					  SdList = "sd-list",
 | 
				
			||||||
 | 
					  Mcp = "mcp-store",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DEFAULT_SIDEBAR_WIDTH = 300;
 | 
					export const DEFAULT_SIDEBAR_WIDTH = 300;
 | 
				
			||||||
@@ -97,6 +110,7 @@ export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id;
 | 
				
			|||||||
export const STORAGE_KEY = "chatgpt-next-web";
 | 
					export const STORAGE_KEY = "chatgpt-next-web";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const REQUEST_TIMEOUT_MS = 60000;
 | 
					export const REQUEST_TIMEOUT_MS = 60000;
 | 
				
			||||||
 | 
					export const REQUEST_TIMEOUT_MS_FOR_THINKING = REQUEST_TIMEOUT_MS * 5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
 | 
					export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -112,6 +126,10 @@ export enum ServiceProvider {
 | 
				
			|||||||
  Moonshot = "Moonshot",
 | 
					  Moonshot = "Moonshot",
 | 
				
			||||||
  Stability = "Stability",
 | 
					  Stability = "Stability",
 | 
				
			||||||
  Iflytek = "Iflytek",
 | 
					  Iflytek = "Iflytek",
 | 
				
			||||||
 | 
					  XAI = "XAI",
 | 
				
			||||||
 | 
					  ChatGLM = "ChatGLM",
 | 
				
			||||||
 | 
					  DeepSeek = "DeepSeek",
 | 
				
			||||||
 | 
					  SiliconFlow = "SiliconFlow",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
 | 
					// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
 | 
				
			||||||
@@ -134,6 +152,10 @@ export enum ModelProvider {
 | 
				
			|||||||
  Hunyuan = "Hunyuan",
 | 
					  Hunyuan = "Hunyuan",
 | 
				
			||||||
  Moonshot = "Moonshot",
 | 
					  Moonshot = "Moonshot",
 | 
				
			||||||
  Iflytek = "Iflytek",
 | 
					  Iflytek = "Iflytek",
 | 
				
			||||||
 | 
					  XAI = "XAI",
 | 
				
			||||||
 | 
					  ChatGLM = "ChatGLM",
 | 
				
			||||||
 | 
					  DeepSeek = "DeepSeek",
 | 
				
			||||||
 | 
					  SiliconFlow = "SiliconFlow",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Stability = {
 | 
					export const Stability = {
 | 
				
			||||||
@@ -150,6 +172,7 @@ export const Anthropic = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const OpenaiPath = {
 | 
					export const OpenaiPath = {
 | 
				
			||||||
  ChatPath: "v1/chat/completions",
 | 
					  ChatPath: "v1/chat/completions",
 | 
				
			||||||
 | 
					  SpeechPath: "v1/audio/speech",
 | 
				
			||||||
  ImagePath: "v1/images/generations",
 | 
					  ImagePath: "v1/images/generations",
 | 
				
			||||||
  UsagePath: "dashboard/billing/usage",
 | 
					  UsagePath: "dashboard/billing/usage",
 | 
				
			||||||
  SubsPath: "dashboard/billing/subscription",
 | 
					  SubsPath: "dashboard/billing/subscription",
 | 
				
			||||||
@@ -198,7 +221,12 @@ export const ByteDance = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const Alibaba = {
 | 
					export const Alibaba = {
 | 
				
			||||||
  ExampleEndpoint: ALIBABA_BASE_URL,
 | 
					  ExampleEndpoint: ALIBABA_BASE_URL,
 | 
				
			||||||
  ChatPath: "v1/services/aigc/text-generation/generation",
 | 
					  ChatPath: (modelName: string) => {
 | 
				
			||||||
 | 
					    if (modelName.includes("vl") || modelName.includes("omni")) {
 | 
				
			||||||
 | 
					      return "v1/services/aigc/multimodal-generation/generation";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return `v1/services/aigc/text-generation/generation`;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Tencent = {
 | 
					export const Tencent = {
 | 
				
			||||||
@@ -215,6 +243,29 @@ export const Iflytek = {
 | 
				
			|||||||
  ChatPath: "v1/chat/completions",
 | 
					  ChatPath: "v1/chat/completions",
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DeepSeek = {
 | 
				
			||||||
 | 
					  ExampleEndpoint: DEEPSEEK_BASE_URL,
 | 
				
			||||||
 | 
					  ChatPath: "chat/completions",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const XAI = {
 | 
				
			||||||
 | 
					  ExampleEndpoint: XAI_BASE_URL,
 | 
				
			||||||
 | 
					  ChatPath: "v1/chat/completions",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ChatGLM = {
 | 
				
			||||||
 | 
					  ExampleEndpoint: CHATGLM_BASE_URL,
 | 
				
			||||||
 | 
					  ChatPath: "api/paas/v4/chat/completions",
 | 
				
			||||||
 | 
					  ImagePath: "api/paas/v4/images/generations",
 | 
				
			||||||
 | 
					  VideoPath: "api/paas/v4/videos/generations",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SiliconFlow = {
 | 
				
			||||||
 | 
					  ExampleEndpoint: SILICONFLOW_BASE_URL,
 | 
				
			||||||
 | 
					  ChatPath: "v1/chat/completions",
 | 
				
			||||||
 | 
					  ListModelPath: "v1/models?&sub_type=chat",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
 | 
					export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
 | 
				
			||||||
// export const DEFAULT_SYSTEM_TEMPLATE = `
 | 
					// export const DEFAULT_SYSTEM_TEMPLATE = `
 | 
				
			||||||
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
 | 
					// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
 | 
				
			||||||
@@ -233,27 +284,209 @@ Latex inline: \\(x^2\\)
 | 
				
			|||||||
Latex block: $$e=mc^2$$
 | 
					Latex block: $$e=mc^2$$
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MCP_TOOLS_TEMPLATE = `
 | 
				
			||||||
 | 
					[clientId]
 | 
				
			||||||
 | 
					{{ clientId }}
 | 
				
			||||||
 | 
					[tools]
 | 
				
			||||||
 | 
					{{ tools }}
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MCP_SYSTEM_TEMPLATE = `
 | 
				
			||||||
 | 
					You are an AI assistant with access to system tools. Your role is to help users by combining natural language understanding with tool operations when needed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. AVAILABLE TOOLS:
 | 
				
			||||||
 | 
					{{ MCP_TOOLS }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. WHEN TO USE TOOLS:
 | 
				
			||||||
 | 
					   - ALWAYS USE TOOLS when they can help answer user questions
 | 
				
			||||||
 | 
					   - DO NOT just describe what you could do - TAKE ACTION immediately
 | 
				
			||||||
 | 
					   - If you're not sure whether to use a tool, USE IT
 | 
				
			||||||
 | 
					   - Common triggers for tool use:
 | 
				
			||||||
 | 
					     * Questions about files or directories
 | 
				
			||||||
 | 
					     * Requests to check, list, or manipulate system resources
 | 
				
			||||||
 | 
					     * Any query that can be answered with available tools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					3. HOW TO USE TOOLS:
 | 
				
			||||||
 | 
					   A. Tool Call Format:
 | 
				
			||||||
 | 
					      - Use markdown code blocks with format: \`\`\`json:mcp:{clientId}\`\`\`
 | 
				
			||||||
 | 
					      - Always include:
 | 
				
			||||||
 | 
					        * method: "tools/call"(Only this method is supported)
 | 
				
			||||||
 | 
					        * params: 
 | 
				
			||||||
 | 
					          - name: must match an available primitive name
 | 
				
			||||||
 | 
					          - arguments: required parameters for the primitive
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   B. Response Format:
 | 
				
			||||||
 | 
					      - Tool responses will come as user messages
 | 
				
			||||||
 | 
					      - Format: \`\`\`json:mcp-response:{clientId}\`\`\`
 | 
				
			||||||
 | 
					      - Wait for response before making another tool call
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   C. Important Rules:
 | 
				
			||||||
 | 
					      - Only use tools/call method
 | 
				
			||||||
 | 
					      - Only ONE tool call per message
 | 
				
			||||||
 | 
					      - ALWAYS TAKE ACTION instead of just describing what you could do
 | 
				
			||||||
 | 
					      - Include the correct clientId in code block language tag
 | 
				
			||||||
 | 
					      - Verify arguments match the primitive's requirements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					4. INTERACTION FLOW:
 | 
				
			||||||
 | 
					   A. When user makes a request:
 | 
				
			||||||
 | 
					      - IMMEDIATELY use appropriate tool if available
 | 
				
			||||||
 | 
					      - DO NOT ask if user wants you to use the tool
 | 
				
			||||||
 | 
					      - DO NOT just describe what you could do
 | 
				
			||||||
 | 
					   B. After receiving tool response:
 | 
				
			||||||
 | 
					      - Explain results clearly
 | 
				
			||||||
 | 
					      - Take next appropriate action if needed
 | 
				
			||||||
 | 
					   C. If tools fail:
 | 
				
			||||||
 | 
					      - Explain the error
 | 
				
			||||||
 | 
					      - Try alternative approach immediately
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					5. EXAMPLE INTERACTION:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  good example:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   \`\`\`json:mcp:filesystem
 | 
				
			||||||
 | 
					   {
 | 
				
			||||||
 | 
					     "method": "tools/call",
 | 
				
			||||||
 | 
					     "params": {
 | 
				
			||||||
 | 
					       "name": "list_allowed_directories",
 | 
				
			||||||
 | 
					       "arguments": {}
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					   }
 | 
				
			||||||
 | 
					   \`\`\`"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  \`\`\`json:mcp-response:filesystem
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					  "method": "tools/call",
 | 
				
			||||||
 | 
					  "params": {
 | 
				
			||||||
 | 
					    "name": "write_file",
 | 
				
			||||||
 | 
					    "arguments": {
 | 
				
			||||||
 | 
					      "path": "/Users/river/dev/nextchat/test/joke.txt",
 | 
				
			||||||
 | 
					      "content": "为什么数学书总是感到忧伤?因为它有太多的问题。"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					\`\`\`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   follwing is the wrong! mcp json example:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   \`\`\`json:mcp:filesystem
 | 
				
			||||||
 | 
					   {
 | 
				
			||||||
 | 
					      "method": "write_file",
 | 
				
			||||||
 | 
					      "params": {
 | 
				
			||||||
 | 
					        "path": "NextChat_Information.txt",
 | 
				
			||||||
 | 
					        "content": "1"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					   }
 | 
				
			||||||
 | 
					   \`\`\`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   This is wrong because the method is not tools/call.
 | 
				
			||||||
 | 
					   
 | 
				
			||||||
 | 
					   \`\`\`{
 | 
				
			||||||
 | 
					  "method": "search_repositories",
 | 
				
			||||||
 | 
					  "params": {
 | 
				
			||||||
 | 
					    "query": "2oeee"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					   \`\`\`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   This is wrong because the method is not tools/call.!!!!!!!!!!!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   the right format is:
 | 
				
			||||||
 | 
					   \`\`\`json:mcp:filesystem
 | 
				
			||||||
 | 
					   {
 | 
				
			||||||
 | 
					     "method": "tools/call",
 | 
				
			||||||
 | 
					     "params": {
 | 
				
			||||||
 | 
					       "name": "search_repositories",
 | 
				
			||||||
 | 
					       "arguments": {
 | 
				
			||||||
 | 
					         "query": "2oeee"
 | 
				
			||||||
 | 
					       }
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					   }
 | 
				
			||||||
 | 
					   \`\`\`
 | 
				
			||||||
 | 
					   
 | 
				
			||||||
 | 
					   please follow the format strictly ONLY use tools/call method!!!!!!!!!!!
 | 
				
			||||||
 | 
					   
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SUMMARIZE_MODEL = "gpt-4o-mini";
 | 
					export const SUMMARIZE_MODEL = "gpt-4o-mini";
 | 
				
			||||||
export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
 | 
					export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
 | 
				
			||||||
 | 
					export const DEEPSEEK_SUMMARIZE_MODEL = "deepseek-chat";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const KnowledgeCutOffDate: Record<string, string> = {
 | 
					export const KnowledgeCutOffDate: Record<string, string> = {
 | 
				
			||||||
  default: "2021-09",
 | 
					  default: "2021-09",
 | 
				
			||||||
  "gpt-4-turbo": "2023-12",
 | 
					  "gpt-4-turbo": "2023-12",
 | 
				
			||||||
  "gpt-4-turbo-2024-04-09": "2023-12",
 | 
					  "gpt-4-turbo-2024-04-09": "2023-12",
 | 
				
			||||||
  "gpt-4-turbo-preview": "2023-12",
 | 
					  "gpt-4-turbo-preview": "2023-12",
 | 
				
			||||||
 | 
					  "gpt-4.1": "2024-06",
 | 
				
			||||||
 | 
					  "gpt-4.1-2025-04-14": "2024-06",
 | 
				
			||||||
 | 
					  "gpt-4.1-mini": "2024-06",
 | 
				
			||||||
 | 
					  "gpt-4.1-mini-2025-04-14": "2024-06",
 | 
				
			||||||
 | 
					  "gpt-4.1-nano": "2024-06",
 | 
				
			||||||
 | 
					  "gpt-4.1-nano-2025-04-14": "2024-06",
 | 
				
			||||||
 | 
					  "gpt-4.5-preview": "2023-10",
 | 
				
			||||||
 | 
					  "gpt-4.5-preview-2025-02-27": "2023-10",
 | 
				
			||||||
  "gpt-4o": "2023-10",
 | 
					  "gpt-4o": "2023-10",
 | 
				
			||||||
  "gpt-4o-2024-05-13": "2023-10",
 | 
					  "gpt-4o-2024-05-13": "2023-10",
 | 
				
			||||||
  "gpt-4o-2024-08-06": "2023-10",
 | 
					  "gpt-4o-2024-08-06": "2023-10",
 | 
				
			||||||
 | 
					  "gpt-4o-2024-11-20": "2023-10",
 | 
				
			||||||
 | 
					  "chatgpt-4o-latest": "2023-10",
 | 
				
			||||||
  "gpt-4o-mini": "2023-10",
 | 
					  "gpt-4o-mini": "2023-10",
 | 
				
			||||||
  "gpt-4o-mini-2024-07-18": "2023-10",
 | 
					  "gpt-4o-mini-2024-07-18": "2023-10",
 | 
				
			||||||
  "gpt-4-vision-preview": "2023-04",
 | 
					  "gpt-4-vision-preview": "2023-04",
 | 
				
			||||||
 | 
					  "o1-mini-2024-09-12": "2023-10",
 | 
				
			||||||
 | 
					  "o1-mini": "2023-10",
 | 
				
			||||||
 | 
					  "o1-preview-2024-09-12": "2023-10",
 | 
				
			||||||
 | 
					  "o1-preview": "2023-10",
 | 
				
			||||||
 | 
					  "o1-2024-12-17": "2023-10",
 | 
				
			||||||
 | 
					  o1: "2023-10",
 | 
				
			||||||
 | 
					  "o3-mini-2025-01-31": "2023-10",
 | 
				
			||||||
 | 
					  "o3-mini": "2023-10",
 | 
				
			||||||
  // After improvements,
 | 
					  // After improvements,
 | 
				
			||||||
  // it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
 | 
					  // it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
 | 
				
			||||||
  "gemini-pro": "2023-12",
 | 
					  "gemini-pro": "2023-12",
 | 
				
			||||||
  "gemini-pro-vision": "2023-12",
 | 
					  "gemini-pro-vision": "2023-12",
 | 
				
			||||||
 | 
					  "deepseek-chat": "2024-07",
 | 
				
			||||||
 | 
					  "deepseek-coder": "2024-07",
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DEFAULT_TTS_ENGINE = "OpenAI-TTS";
 | 
				
			||||||
 | 
					export const DEFAULT_TTS_ENGINES = ["OpenAI-TTS", "Edge-TTS"];
 | 
				
			||||||
 | 
					export const DEFAULT_TTS_MODEL = "tts-1";
 | 
				
			||||||
 | 
					export const DEFAULT_TTS_VOICE = "alloy";
 | 
				
			||||||
 | 
					export const DEFAULT_TTS_MODELS = ["tts-1", "tts-1-hd"];
 | 
				
			||||||
 | 
					export const DEFAULT_TTS_VOICES = [
 | 
				
			||||||
 | 
					  "alloy",
 | 
				
			||||||
 | 
					  "echo",
 | 
				
			||||||
 | 
					  "fable",
 | 
				
			||||||
 | 
					  "onyx",
 | 
				
			||||||
 | 
					  "nova",
 | 
				
			||||||
 | 
					  "shimmer",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const VISION_MODEL_REGEXES = [
 | 
				
			||||||
 | 
					  /vision/,
 | 
				
			||||||
 | 
					  /gpt-4o/,
 | 
				
			||||||
 | 
					  /gpt-4\.1/,
 | 
				
			||||||
 | 
					  /claude-3/,
 | 
				
			||||||
 | 
					  /gemini-1\.5/,
 | 
				
			||||||
 | 
					  /gemini-exp/,
 | 
				
			||||||
 | 
					  /gemini-2\.0/,
 | 
				
			||||||
 | 
					  /learnlm/,
 | 
				
			||||||
 | 
					  /qwen-vl/,
 | 
				
			||||||
 | 
					  /qwen2-vl/,
 | 
				
			||||||
 | 
					  /gpt-4-turbo(?!.*preview)/, // Matches "gpt-4-turbo" but not "gpt-4-turbo-preview"
 | 
				
			||||||
 | 
					  /^dall-e-3$/, // Matches exactly "dall-e-3"
 | 
				
			||||||
 | 
					  /glm-4v/,
 | 
				
			||||||
 | 
					  /vl/i,
 | 
				
			||||||
 | 
					  /o3/,
 | 
				
			||||||
 | 
					  /o4-mini/,
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const EXCLUDE_VISION_MODEL_REGEXES = [/claude-3-5-haiku-20241022/];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const openaiModels = [
 | 
					const openaiModels = [
 | 
				
			||||||
 | 
					  // As of July 2024, gpt-4o-mini should be used in place of gpt-3.5-turbo,
 | 
				
			||||||
 | 
					  // as it is cheaper, more capable, multimodal, and just as fast. gpt-3.5-turbo is still available for use in the API.
 | 
				
			||||||
  "gpt-3.5-turbo",
 | 
					  "gpt-3.5-turbo",
 | 
				
			||||||
  "gpt-3.5-turbo-1106",
 | 
					  "gpt-3.5-turbo-1106",
 | 
				
			||||||
  "gpt-3.5-turbo-0125",
 | 
					  "gpt-3.5-turbo-0125",
 | 
				
			||||||
@@ -263,22 +496,56 @@ const openaiModels = [
 | 
				
			|||||||
  "gpt-4-32k-0613",
 | 
					  "gpt-4-32k-0613",
 | 
				
			||||||
  "gpt-4-turbo",
 | 
					  "gpt-4-turbo",
 | 
				
			||||||
  "gpt-4-turbo-preview",
 | 
					  "gpt-4-turbo-preview",
 | 
				
			||||||
 | 
					  "gpt-4.1",
 | 
				
			||||||
 | 
					  "gpt-4.1-2025-04-14",
 | 
				
			||||||
 | 
					  "gpt-4.1-mini",
 | 
				
			||||||
 | 
					  "gpt-4.1-mini-2025-04-14",
 | 
				
			||||||
 | 
					  "gpt-4.1-nano",
 | 
				
			||||||
 | 
					  "gpt-4.1-nano-2025-04-14",
 | 
				
			||||||
 | 
					  "gpt-4.5-preview",
 | 
				
			||||||
 | 
					  "gpt-4.5-preview-2025-02-27",
 | 
				
			||||||
  "gpt-4o",
 | 
					  "gpt-4o",
 | 
				
			||||||
  "gpt-4o-2024-05-13",
 | 
					  "gpt-4o-2024-05-13",
 | 
				
			||||||
  "gpt-4o-2024-08-06",
 | 
					  "gpt-4o-2024-08-06",
 | 
				
			||||||
 | 
					  "gpt-4o-2024-11-20",
 | 
				
			||||||
 | 
					  "chatgpt-4o-latest",
 | 
				
			||||||
  "gpt-4o-mini",
 | 
					  "gpt-4o-mini",
 | 
				
			||||||
  "gpt-4o-mini-2024-07-18",
 | 
					  "gpt-4o-mini-2024-07-18",
 | 
				
			||||||
  "gpt-4-vision-preview",
 | 
					  "gpt-4-vision-preview",
 | 
				
			||||||
  "gpt-4-turbo-2024-04-09",
 | 
					  "gpt-4-turbo-2024-04-09",
 | 
				
			||||||
  "gpt-4-1106-preview",
 | 
					  "gpt-4-1106-preview",
 | 
				
			||||||
  "dall-e-3",
 | 
					  "dall-e-3",
 | 
				
			||||||
 | 
					  "o1-mini",
 | 
				
			||||||
 | 
					  "o1-preview",
 | 
				
			||||||
 | 
					  "o3-mini",
 | 
				
			||||||
 | 
					  "o3",
 | 
				
			||||||
 | 
					  "o4-mini",
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const googleModels = [
 | 
					const googleModels = [
 | 
				
			||||||
  "gemini-1.0-pro",
 | 
					  "gemini-1.0-pro", // Deprecated on 2/15/2025
 | 
				
			||||||
  "gemini-1.5-pro-latest",
 | 
					  "gemini-1.5-pro-latest",
 | 
				
			||||||
 | 
					  "gemini-1.5-pro",
 | 
				
			||||||
 | 
					  "gemini-1.5-pro-002",
 | 
				
			||||||
 | 
					  "gemini-1.5-pro-exp-0827",
 | 
				
			||||||
  "gemini-1.5-flash-latest",
 | 
					  "gemini-1.5-flash-latest",
 | 
				
			||||||
  "gemini-pro-vision",
 | 
					  "gemini-1.5-flash-8b-latest",
 | 
				
			||||||
 | 
					  "gemini-1.5-flash",
 | 
				
			||||||
 | 
					  "gemini-1.5-flash-8b",
 | 
				
			||||||
 | 
					  "gemini-1.5-flash-002",
 | 
				
			||||||
 | 
					  "gemini-1.5-flash-exp-0827",
 | 
				
			||||||
 | 
					  "learnlm-1.5-pro-experimental",
 | 
				
			||||||
 | 
					  "gemini-exp-1114",
 | 
				
			||||||
 | 
					  "gemini-exp-1121",
 | 
				
			||||||
 | 
					  "gemini-exp-1206",
 | 
				
			||||||
 | 
					  "gemini-2.0-flash",
 | 
				
			||||||
 | 
					  "gemini-2.0-flash-exp",
 | 
				
			||||||
 | 
					  "gemini-2.0-flash-lite-preview-02-05",
 | 
				
			||||||
 | 
					  "gemini-2.0-flash-thinking-exp",
 | 
				
			||||||
 | 
					  "gemini-2.0-flash-thinking-exp-1219",
 | 
				
			||||||
 | 
					  "gemini-2.0-flash-thinking-exp-01-21",
 | 
				
			||||||
 | 
					  "gemini-2.0-pro-exp",
 | 
				
			||||||
 | 
					  "gemini-2.0-pro-exp-02-05",
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const anthropicModels = [
 | 
					const anthropicModels = [
 | 
				
			||||||
@@ -287,8 +554,15 @@ const anthropicModels = [
 | 
				
			|||||||
  "claude-2.1",
 | 
					  "claude-2.1",
 | 
				
			||||||
  "claude-3-sonnet-20240229",
 | 
					  "claude-3-sonnet-20240229",
 | 
				
			||||||
  "claude-3-opus-20240229",
 | 
					  "claude-3-opus-20240229",
 | 
				
			||||||
 | 
					  "claude-3-opus-latest",
 | 
				
			||||||
  "claude-3-haiku-20240307",
 | 
					  "claude-3-haiku-20240307",
 | 
				
			||||||
 | 
					  "claude-3-5-haiku-20241022",
 | 
				
			||||||
 | 
					  "claude-3-5-haiku-latest",
 | 
				
			||||||
  "claude-3-5-sonnet-20240620",
 | 
					  "claude-3-5-sonnet-20240620",
 | 
				
			||||||
 | 
					  "claude-3-5-sonnet-20241022",
 | 
				
			||||||
 | 
					  "claude-3-5-sonnet-latest",
 | 
				
			||||||
 | 
					  "claude-3-7-sonnet-20250219",
 | 
				
			||||||
 | 
					  "claude-3-7-sonnet-latest",
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const baiduModels = [
 | 
					const baiduModels = [
 | 
				
			||||||
@@ -322,6 +596,9 @@ const alibabaModes = [
 | 
				
			|||||||
  "qwen-max-0403",
 | 
					  "qwen-max-0403",
 | 
				
			||||||
  "qwen-max-0107",
 | 
					  "qwen-max-0107",
 | 
				
			||||||
  "qwen-max-longcontext",
 | 
					  "qwen-max-longcontext",
 | 
				
			||||||
 | 
					  "qwen-omni-turbo",
 | 
				
			||||||
 | 
					  "qwen-vl-plus",
 | 
				
			||||||
 | 
					  "qwen-vl-max",
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const tencentModels = [
 | 
					const tencentModels = [
 | 
				
			||||||
@@ -344,6 +621,56 @@ const iflytekModels = [
 | 
				
			|||||||
  "4.0Ultra",
 | 
					  "4.0Ultra",
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const deepseekModels = ["deepseek-chat", "deepseek-coder", "deepseek-reasoner"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const xAIModes = [
 | 
				
			||||||
 | 
					  "grok-beta",
 | 
				
			||||||
 | 
					  "grok-2",
 | 
				
			||||||
 | 
					  "grok-2-1212",
 | 
				
			||||||
 | 
					  "grok-2-latest",
 | 
				
			||||||
 | 
					  "grok-vision-beta",
 | 
				
			||||||
 | 
					  "grok-2-vision-1212",
 | 
				
			||||||
 | 
					  "grok-2-vision",
 | 
				
			||||||
 | 
					  "grok-2-vision-latest",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const chatglmModels = [
 | 
				
			||||||
 | 
					  "glm-4-plus",
 | 
				
			||||||
 | 
					  "glm-4-0520",
 | 
				
			||||||
 | 
					  "glm-4",
 | 
				
			||||||
 | 
					  "glm-4-air",
 | 
				
			||||||
 | 
					  "glm-4-airx",
 | 
				
			||||||
 | 
					  "glm-4-long",
 | 
				
			||||||
 | 
					  "glm-4-flashx",
 | 
				
			||||||
 | 
					  "glm-4-flash",
 | 
				
			||||||
 | 
					  "glm-4v-plus",
 | 
				
			||||||
 | 
					  "glm-4v",
 | 
				
			||||||
 | 
					  "glm-4v-flash", // free
 | 
				
			||||||
 | 
					  "cogview-3-plus",
 | 
				
			||||||
 | 
					  "cogview-3",
 | 
				
			||||||
 | 
					  "cogview-3-flash", // free
 | 
				
			||||||
 | 
					  // 目前无法适配轮询任务
 | 
				
			||||||
 | 
					  //   "cogvideox",
 | 
				
			||||||
 | 
					  //   "cogvideox-flash", // free
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const siliconflowModels = [
 | 
				
			||||||
 | 
					  "Qwen/Qwen2.5-7B-Instruct",
 | 
				
			||||||
 | 
					  "Qwen/Qwen2.5-72B-Instruct",
 | 
				
			||||||
 | 
					  "deepseek-ai/DeepSeek-R1",
 | 
				
			||||||
 | 
					  "deepseek-ai/DeepSeek-R1-Distill-Llama-70B",
 | 
				
			||||||
 | 
					  "deepseek-ai/DeepSeek-R1-Distill-Llama-8B",
 | 
				
			||||||
 | 
					  "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B",
 | 
				
			||||||
 | 
					  "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B",
 | 
				
			||||||
 | 
					  "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
 | 
				
			||||||
 | 
					  "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
 | 
				
			||||||
 | 
					  "deepseek-ai/DeepSeek-V3",
 | 
				
			||||||
 | 
					  "meta-llama/Llama-3.3-70B-Instruct",
 | 
				
			||||||
 | 
					  "THUDM/glm-4-9b-chat",
 | 
				
			||||||
 | 
					  "Pro/deepseek-ai/DeepSeek-R1",
 | 
				
			||||||
 | 
					  "Pro/deepseek-ai/DeepSeek-V3",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let seq = 1000; // 内置的模型序号生成器从1000开始
 | 
					let seq = 1000; // 内置的模型序号生成器从1000开始
 | 
				
			||||||
export const DEFAULT_MODELS = [
 | 
					export const DEFAULT_MODELS = [
 | 
				
			||||||
  ...openaiModels.map((name) => ({
 | 
					  ...openaiModels.map((name) => ({
 | 
				
			||||||
@@ -456,6 +783,50 @@ export const DEFAULT_MODELS = [
 | 
				
			|||||||
      sorted: 10,
 | 
					      sorted: 10,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  })),
 | 
					  })),
 | 
				
			||||||
 | 
					  ...xAIModes.map((name) => ({
 | 
				
			||||||
 | 
					    name,
 | 
				
			||||||
 | 
					    available: true,
 | 
				
			||||||
 | 
					    sorted: seq++,
 | 
				
			||||||
 | 
					    provider: {
 | 
				
			||||||
 | 
					      id: "xai",
 | 
				
			||||||
 | 
					      providerName: "XAI",
 | 
				
			||||||
 | 
					      providerType: "xai",
 | 
				
			||||||
 | 
					      sorted: 11,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })),
 | 
				
			||||||
 | 
					  ...chatglmModels.map((name) => ({
 | 
				
			||||||
 | 
					    name,
 | 
				
			||||||
 | 
					    available: true,
 | 
				
			||||||
 | 
					    sorted: seq++,
 | 
				
			||||||
 | 
					    provider: {
 | 
				
			||||||
 | 
					      id: "chatglm",
 | 
				
			||||||
 | 
					      providerName: "ChatGLM",
 | 
				
			||||||
 | 
					      providerType: "chatglm",
 | 
				
			||||||
 | 
					      sorted: 12,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })),
 | 
				
			||||||
 | 
					  ...deepseekModels.map((name) => ({
 | 
				
			||||||
 | 
					    name,
 | 
				
			||||||
 | 
					    available: true,
 | 
				
			||||||
 | 
					    sorted: seq++,
 | 
				
			||||||
 | 
					    provider: {
 | 
				
			||||||
 | 
					      id: "deepseek",
 | 
				
			||||||
 | 
					      providerName: "DeepSeek",
 | 
				
			||||||
 | 
					      providerType: "deepseek",
 | 
				
			||||||
 | 
					      sorted: 13,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })),
 | 
				
			||||||
 | 
					  ...siliconflowModels.map((name) => ({
 | 
				
			||||||
 | 
					    name,
 | 
				
			||||||
 | 
					    available: true,
 | 
				
			||||||
 | 
					    sorted: seq++,
 | 
				
			||||||
 | 
					    provider: {
 | 
				
			||||||
 | 
					      id: "siliconflow",
 | 
				
			||||||
 | 
					      providerName: "SiliconFlow",
 | 
				
			||||||
 | 
					      providerType: "siliconflow",
 | 
				
			||||||
 | 
					      sorted: 14,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })),
 | 
				
			||||||
] as const;
 | 
					] as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const CHAT_PAGE_SIZE = 15;
 | 
					export const CHAT_PAGE_SIZE = 15;
 | 
				
			||||||
@@ -475,4 +846,6 @@ export const internalAllowedWebDavEndpoints = [
 | 
				
			|||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DEFAULT_GA_ID = "G-89WN60ZK2E";
 | 
					export const DEFAULT_GA_ID = "G-89WN60ZK2E";
 | 
				
			||||||
export const PLUGINS = [{ name: "Stable Diffusion", path: Path.Sd }];
 | 
					
 | 
				
			||||||
 | 
					export const SAAS_CHAT_URL = "https://nextchat.club";
 | 
				
			||||||
 | 
					export const SAAS_CHAT_UTM_URL = "https://nextchat.club?utm=github";
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								app/global.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -21,10 +21,23 @@ declare interface Window {
 | 
				
			|||||||
      writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
 | 
					      writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
 | 
				
			||||||
      writeTextFile(path: string, data: string): Promise<void>;
 | 
					      writeTextFile(path: string, data: string): Promise<void>;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    notification:{
 | 
					    notification: {
 | 
				
			||||||
      requestPermission(): Promise<Permission>;
 | 
					      requestPermission(): Promise<Permission>;
 | 
				
			||||||
      isPermissionGranted(): Promise<boolean>;
 | 
					      isPermissionGranted(): Promise<boolean>;
 | 
				
			||||||
      sendNotification(options: string | Options): void;
 | 
					      sendNotification(options: string | Options): void;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					    updater: {
 | 
				
			||||||
 | 
					      checkUpdate(): Promise<UpdateResult>;
 | 
				
			||||||
 | 
					      installUpdate(): Promise<void>;
 | 
				
			||||||
 | 
					      onUpdaterEvent(
 | 
				
			||||||
 | 
					        handler: (status: UpdateStatusResult) => void,
 | 
				
			||||||
 | 
					      ): Promise<UnlistenFn>;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    http: {
 | 
				
			||||||
 | 
					      fetch<T>(
 | 
				
			||||||
 | 
					        url: string,
 | 
				
			||||||
 | 
					        options?: Record<string, unknown>,
 | 
				
			||||||
 | 
					      ): Promise<Response<T>>;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								app/icons/arrow.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg class="icon--SJP_d" width="16" height="16" fill="none" viewBox="0 0 16 16" style="min-width: 16px; min-height: 16px;"><g><path data-follow-fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M5.248 14.444a.625.625 0 0 1-.005-.884l5.068-5.12a.625.625 0 0 0 0-.88L5.243 2.44a.625.625 0 1 1 .889-.88l5.067 5.121c.723.73.723 1.907 0 2.638l-5.067 5.12a.625.625 0 0 1-.884.005Z" fill="currentColor"></path></g></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 426 B  | 
							
								
								
									
										1
									
								
								app/icons/fire.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12.832 21.801c3.126-.626 7.168-2.875 7.168-8.69c0-5.291-3.873-8.815-6.658-10.434c-.619-.36-1.342.113-1.342.828v1.828c0 1.442-.606 4.074-2.29 5.169c-.86.559-1.79-.278-1.894-1.298l-.086-.838c-.1-.974-1.092-1.565-1.87-.971C4.461 8.46 3 10.33 3 13.11C3 20.221 8.289 22 10.933 22q.232 0 .484-.015C10.111 21.874 8 21.064 8 18.444c0-2.05 1.495-3.435 2.631-4.11c.306-.18.663.055.663.41v.59c0 .45.175 1.155.59 1.637c.47.546 1.159-.026 1.214-.744c.018-.226.246-.37.442-.256c.641.375 1.46 1.175 1.46 2.473c0 2.048-1.129 2.99-2.168 3.357"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 648 B  | 
							
								
								
									
										11
									
								
								app/icons/headphone.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					    <path d="M4 28C4 26.8954 4.89543 26 6 26H10V38H6C4.89543 38 4 37.1046 4 36V28Z" fill="none" />
 | 
				
			||||||
 | 
					    <path d="M38 26H42C43.1046 26 44 26.8954 44 28V36C44 37.1046 43.1046 38 42 38H38V26Z"
 | 
				
			||||||
 | 
					        fill="none" />
 | 
				
			||||||
 | 
					    <path
 | 
				
			||||||
 | 
					        d="M10 36V24C10 16.268 16.268 10 24 10C31.732 10 38 16.268 38 24V36M10 26H6C4.89543 26 4 26.8954 4 28V36C4 37.1046 4.89543 38 6 38H10V26ZM38 26H42C43.1046 26 44 26.8954 44 28V36C44 37.1046 43.1046 38 42 38H38V26Z"
 | 
				
			||||||
 | 
					        stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
 | 
				
			||||||
 | 
					    <path d="M16 32H20L22 26L26 38L28 32H32" stroke="#333" stroke-width="4" stroke-linecap="round"
 | 
				
			||||||
 | 
					        stroke-linejoin="round" />
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 808 B  | 
							
								
								
									
										14
									
								
								app/icons/llm-icons/chatglm.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 30 30" width="1em" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					    <title>ChatGLM</title>
 | 
				
			||||||
 | 
					    <rect width="30" height="30" fill="#E7F8FF" rx="6"/>
 | 
				
			||||||
 | 
					    <g transform="translate(3, 3)">
 | 
				
			||||||
 | 
					        <defs>
 | 
				
			||||||
 | 
					            <linearGradient id="lobe-icons-chatglm-fill" x1="-18.756%" x2="70.894%" y1="49.371%" y2="90.944%">
 | 
				
			||||||
 | 
					                <stop offset="0%" stop-color="#504AF4"></stop>
 | 
				
			||||||
 | 
					                <stop offset="100%" stop-color="#3485FF"></stop>
 | 
				
			||||||
 | 
					            </linearGradient>
 | 
				
			||||||
 | 
					        </defs>
 | 
				
			||||||
 | 
					        <path d="M9.917 2c4.906 0 10.178 3.947 8.93 10.58-.014.07-.037.14-.057.21l-.003-.277c-.083-3-1.534-8.934-8.87-8.934-3.393 0-8.137 3.054-7.93 8.158-.04 4.778 3.555 8.4 7.95 8.332l.073-.001c1.2-.033 2.763-.429 3.1-1.657.063-.031.26.534.268.598.048.256.112.369.192.34.981-.348 2.286-1.222 1.952-2.38-.176-.61-1.775-.147-1.921-.347.418-.979 2.234-.926 3.153-.716.443.102.657.38 1.012.442.29.052.981-.2.96.242-1.5 3.042-4.893 5.41-8.808 5.41C3.654 22 0 16.574 0 11.737 0 5.947 4.959 2 9.917 2zM9.9 5.3c.484 0 1.125.225 1.38.585 3.669.145 4.313 2.686 4.694 5.444.255 1.838.315 2.3.182 1.387l.083.59c.068.448.554.737.982.516.144-.075.254-.231.328-.47a.2.2 0 01.258-.13l.625.22a.2.2 0 01.124.238 2.172 2.172 0 01-.51.92c-.878.917-2.757.664-3.08-.62-.14-.554-.055-.626-.345-1.242-.292-.621-1.238-.709-1.69-.295-.345.315-.407.805-.406 1.282L12.6 15.9a.9.9 0 01-.9.9h-1.4a.9.9 0 01-.9-.9v-.65a1.15 1.15 0 10-2.3 0v.65a.9.9 0 01-.9.9H4.8a.9.9 0 01-.9-.9l.035-3.239c.012-1.884.356-3.658 2.47-4.134.2-.045.252.13.29.342.025.154.043.252.053.294.701 3.058 1.75 4.299 3.144 3.722l.66-.331.254-.13c.158-.082.25-.131.276-.15.012-.01-.165-.206-.407-.464l-1.012-1.067a8.925 8.925 0 01-.199-.216c-.047-.034-.116.068-.208.306-.074.157-.251.252-.272.326-.013.058.108.298.362.72.164.288.22.508-.31.343-1.04-.8-1.518-2.273-1.684-3.725-.004-.035-.162-1.913-.162-1.913a1.2 1.2 0 011.113-1.281L9.9 5.3zm12.994 8.68c.037.697-.403.704-1.213.591l-1.783-.276c-.265-.053-.385-.099-.313-.147.47-.315 3.268-.93 3.31-.168zm-.915-.083l-.926.042c-.85.077-1.452.24.338.336l.103.003c.815.012 1.264-.359.485-.381zm1.667-3.601h.01c.79.398.067 1.03-.65 1.393-.14.07-.491.176-1.052.315-.241.04-.457.092-.333.16l.01.005c1.952.958-3.123 1.534-2.495 1.285l.38-.148c.68-.266 1.614-.682 1.666-1.337.038-.48 1.253-.442 1.493-.968.048-.106 0-.236-.144-.389-.05-.047-.094-.094-.107-.148-.073-.305.7-.431 1.222-.168zm-2.568-.474c-.135 1.198-2.479 4.192-1.949 2.863l.017-.042c.298-.717.376-2.221 1.337-3.221.25-.26.636.035.595.4zm-7.976-.253c.02-.694 1.002-.968 1.346-.347.01-1.274-1.941-.768-1.346.347z"
 | 
				
			||||||
 | 
					              fill="url(#lobe-icons-chatglm-fill)" fill-rule="evenodd"></path>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 2.6 KiB  | 
							
								
								
									
										8
									
								
								app/icons/llm-icons/claude.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 30 30" width="1em" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					    <title>Claude</title>
 | 
				
			||||||
 | 
					    <rect width="30" height="30" fill="#E7F8FF" rx="6"/>
 | 
				
			||||||
 | 
					    <g transform="translate(3, 3)">
 | 
				
			||||||
 | 
					        <path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"
 | 
				
			||||||
 | 
					              fill="#D97757" fill-rule="nonzero"></path>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										8
									
								
								app/icons/llm-icons/deepseek.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 30 30" width="1em" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					    <title>DeepSeek</title>
 | 
				
			||||||
 | 
					    <rect width="30" height="30" fill="#E7F8FF" rx="6"/>
 | 
				
			||||||
 | 
					    <g transform="translate(4, 4)">
 | 
				
			||||||
 | 
					        <path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z"
 | 
				
			||||||
 | 
					              fill="#4D6BFE"></path>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 2.2 KiB  | 
							
								
								
									
										27
									
								
								app/icons/llm-icons/default.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30" height="30" fill="none"
 | 
				
			||||||
 | 
					     viewBox="0 0 30 30">
 | 
				
			||||||
 | 
					    <defs>
 | 
				
			||||||
 | 
					        <rect id="path_0" width="30" height="30" x="0" y="0"/>
 | 
				
			||||||
 | 
					        <rect id="path_1" width="20.455" height="20.455" x="0" y="0"/>
 | 
				
			||||||
 | 
					    </defs>
 | 
				
			||||||
 | 
					    <g opacity="1" transform="translate(0 0) rotate(0 14.999999999999998 14.999999999999998)">
 | 
				
			||||||
 | 
					        <rect width="30" height="30" x="0" y="0" fill="#E7F8FF" opacity="1" rx="10"
 | 
				
			||||||
 | 
					              transform="translate(0 0) rotate(0 14.999999999999998 14.999999999999998)"/>
 | 
				
			||||||
 | 
					        <mask id="bg-mask-0" fill="#fff">
 | 
				
			||||||
 | 
					            <use xlink:href="#path_0"/>
 | 
				
			||||||
 | 
					        </mask>
 | 
				
			||||||
 | 
					        <g mask="url(#bg-mask-0)">
 | 
				
			||||||
 | 
					            <g opacity="1"
 | 
				
			||||||
 | 
					               transform="translate(4.772727272727272 4.772727272727273) rotate(0 10.227272727272725 10.227272727272725)">
 | 
				
			||||||
 | 
					                <mask id="bg-mask-1" fill="#fff">
 | 
				
			||||||
 | 
					                    <use xlink:href="#path_1"/>
 | 
				
			||||||
 | 
					                </mask>
 | 
				
			||||||
 | 
					                <g mask="url(#bg-mask-1)">
 | 
				
			||||||
 | 
					                    <path id="分组 1" fill-rule="evenodd" style="fill:#1f948c"
 | 
				
			||||||
 | 
					                          d="M19.11 8.37L19.11 8.37C19.28 7.85 19.37 7.31 19.37 6.76C19.37 5.86 19.13 4.97 18.66 4.19C17.73 2.59 16 1.6 14.13 1.6C13.76 1.6 13.4 1.64 13.04 1.71C12.06 0.62 10.65 0 9.17 0L9.14 0L9.13 0C6.86 0 4.86 1.44 4.16 3.57C2.7 3.86 1.44 4.76 0.71 6.04C0.24 6.83 0 7.72 0 8.63C0 9.9 0.48 11.14 1.35 12.08C1.17 12.6 1.08 13.15 1.08 13.69C1.08 14.6 1.33 15.49 1.79 16.27C2.92 18.21 5.2 19.21 7.42 18.74C8.4 19.83 9.8 20.45 11.28 20.45L11.31 20.45L11.33 20.45C13.59 20.45 15.6 19.01 16.3 16.88C17.76 16.59 19.01 15.69 19.75 14.41C20.21 13.63 20.45 12.74 20.45 11.83C20.45 10.55 19.97 9.32 19.11 8.37Z M8.94734 18.1579C8.90734 18.1879 8.86734 18.2079 8.82734 18.2279C9.52734 18.8079 10.3973 19.1179 11.3073 19.1179L11.3173 19.1179C13.4573 19.1179 15.1973 17.3979 15.1973 15.2879L15.1973 10.5279C15.1973 10.5079 15.1773 10.4879 15.1573 10.4779L13.4173 9.48792L13.4173 15.2379C13.4173 15.4679 13.2873 15.6879 13.0773 15.8079L8.94734 18.1579Z M8.27654 17.0048L12.4465 14.6248C12.4665 14.6148 12.4765 14.5948 12.4765 14.5748L12.4765 14.5748L12.4765 12.5848L7.43654 15.4548C7.22654 15.5748 6.96654 15.5748 6.75654 15.4548L2.62654 13.1048C2.58654 13.0848 2.53654 13.0448 2.50654 13.0348C2.46654 13.2448 2.44654 13.4648 2.44654 13.6848C2.44654 14.3548 2.62654 15.0148 2.96654 15.6048L2.96654 15.5948C3.66654 16.7848 4.94654 17.5148 6.33654 17.5148C7.01654 17.5148 7.68654 17.3348 8.27654 17.0048Z M3.90324 5.16818C3.90324 5.12818 3.90324 5.06818 3.90324 5.02818C3.05324 5.33818 2.33324 5.92818 1.88324 6.70818L1.88324 6.70818C1.54324 7.28818 1.36324 7.94818 1.36324 8.61818C1.36324 9.98818 2.10324 11.2582 3.30324 11.9482L7.47324 14.3182C7.49324 14.3282 7.51324 14.3282 7.53324 14.3182L9.28324 13.3182L4.24324 10.4482C4.03324 10.3382 3.90324 10.1182 3.90324 9.87818L3.90324 9.87818L3.90324 5.16818Z M17.1561 8.50521L12.9761 6.1252C12.9561 6.1252 12.9361 6.1252 12.9161 6.1352L11.1761 7.1252L16.2161 9.9952C16.4261 10.1152 16.5561 10.3352 16.5561 10.5752C16.5561 10.5752 16.5561 10.5752 16.5561 10.5752L16.5561 15.4252C18.0761 14.8652 19.0961 13.4352 19.0961 11.8252C19.0961 10.4552 18.3561 9.1952 17.1561 8.50521Z M8.01418 5.82927C7.99418 5.83927 7.98418 5.85927 7.98418 5.87927L7.98418 5.87927L7.98418 7.86927L13.0242 4.99927C13.1242 4.93927 13.2442 4.90927 13.3642 4.90927C13.4842 4.90927 13.5942 4.93927 13.7042 4.99927L17.8342 7.34927C17.8742 7.36927 17.9142 7.39927 17.9542 7.41927L17.9542 7.41927C17.9842 7.20927 18.0042 6.98927 18.0042 6.76927C18.0042 4.65927 16.2642 2.93927 14.1242 2.93927C13.4442 2.93927 12.7742 3.11927 12.1842 3.44927L8.01418 5.82927Z M9.14676 1.33731C6.99676 1.33731 5.25676 3.05731 5.25676 5.16731L5.25676 9.92731C5.25676 9.94731 5.27676 9.95731 5.28676 9.96731L7.03676 10.9673L7.03676 5.22731L7.03676 5.21731C7.03676 4.98731 7.16676 4.76731 7.37676 4.64731L11.5068 2.29731C11.5468 2.26731 11.5968 2.23731 11.6268 2.22731C10.9268 1.64731 10.0468 1.33731 9.14676 1.33731Z M7.98345 11.5093L10.2235 12.7793L12.4735 11.5093L12.4735 8.9493L10.2235 7.6693L7.98345 8.9493L7.98345 11.5093Z"
 | 
				
			||||||
 | 
					                          opacity="1" transform="translate(0 0) rotate(0 10.227272727272725 10.227272727272725)"/>
 | 
				
			||||||
 | 
					                </g>
 | 
				
			||||||
 | 
					            </g>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										14
									
								
								app/icons/llm-icons/doubao.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 30 30" width="1em" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					    <title>Doubao</title>
 | 
				
			||||||
 | 
					    <rect width="30" height="30" fill="#E7F8FF" rx="6"/>
 | 
				
			||||||
 | 
					    <g transform="translate(3, 3)">
 | 
				
			||||||
 | 
					        <path d="M5.31 15.756c.172-3.75 1.883-5.999 2.549-6.739-3.26 2.058-5.425 5.658-6.358 8.308v1.12C1.501 21.513 4.226 24 7.59 24a6.59 6.59 0 002.2-.375c.353-.12.7-.248 1.039-.378.913-.899 1.65-1.91 2.243-2.992-4.877 2.431-7.974.072-7.763-4.5l.002.001z"
 | 
				
			||||||
 | 
					              fill="#1E37FC"></path>
 | 
				
			||||||
 | 
					        <path d="M22.57 10.283c-1.212-.901-4.109-2.404-7.397-2.8.295 3.792.093 8.766-2.1 12.773a12.782 12.782 0 01-2.244 2.992c3.764-1.448 6.746-3.457 8.596-5.219 2.82-2.683 3.353-5.178 3.361-6.66a2.737 2.737 0 00-.216-1.084v-.002z"
 | 
				
			||||||
 | 
					              fill="#37E1BE"></path>
 | 
				
			||||||
 | 
					        <path d="M14.303 1.867C12.955.7 11.248 0 9.39 0 7.532 0 5.883.677 4.545 1.807 2.791 3.29 1.627 5.557 1.5 8.125v9.201c.932-2.65 3.097-6.25 6.357-8.307.5-.318 1.025-.595 1.569-.829 1.883-.801 3.878-.932 5.746-.706-.222-2.83-.718-5.002-.87-5.617h.001z"
 | 
				
			||||||
 | 
					              fill="#A569FF"></path>
 | 
				
			||||||
 | 
					        <path d="M17.305 4.961a199.47 199.47 0 01-1.08-1.094c-.202-.213-.398-.419-.586-.622l-1.333-1.378c.151.615.648 2.786.869 5.617 3.288.395 6.185 1.898 7.396 2.8-1.306-1.275-3.475-3.487-5.266-5.323z"
 | 
				
			||||||
 | 
					              fill="#1E37FC"></path>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.3 KiB  | 
							
								
								
									
										15
									
								
								app/icons/llm-icons/gemini.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 30 30" width="1em" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					    <title>Gemini</title>
 | 
				
			||||||
 | 
					    <rect width="30" height="30" fill="#E7F8FF" rx="6"/>
 | 
				
			||||||
 | 
					    <g transform="translate(3, 3)">
 | 
				
			||||||
 | 
					        <defs>
 | 
				
			||||||
 | 
					            <linearGradient id="lobe-icons-gemini-fill" x1="0%" x2="68.73%" y1="100%" y2="30.395%">
 | 
				
			||||||
 | 
					                <stop offset="0%" stop-color="#1C7DFF"></stop>
 | 
				
			||||||
 | 
					                <stop offset="52.021%" stop-color="#1C69FF"></stop>
 | 
				
			||||||
 | 
					                <stop offset="100%" stop-color="#F0DCD6"></stop>
 | 
				
			||||||
 | 
					            </linearGradient>
 | 
				
			||||||
 | 
					        </defs>
 | 
				
			||||||
 | 
					        <path d="M12 24A14.304 14.304 0 000 12 14.304 14.304 0 0012 0a14.305 14.305 0 0012 12 14.305 14.305 0 00-12 12"
 | 
				
			||||||
 | 
					              fill="url(#lobe-icons-gemini-fill)" fill-rule="nonzero"></path>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 807 B  | 
							
								
								
									
										15
									
								
								app/icons/llm-icons/gemma.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 30 30" width="1em" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					    <title>Gemma</title>
 | 
				
			||||||
 | 
					    <rect width="30" height="30" fill="#E7F8FF" rx="6"/>
 | 
				
			||||||
 | 
					    <g transform="translate(3, 3)">
 | 
				
			||||||
 | 
					        <defs>
 | 
				
			||||||
 | 
					            <linearGradient id="lobe-icons-gemma-fill" x1="24.419%" x2="75.194%" y1="75.581%" y2="25.194%">
 | 
				
			||||||
 | 
					                <stop offset="0%" stop-color="#446EFF"></stop>
 | 
				
			||||||
 | 
					                <stop offset="36.661%" stop-color="#2E96FF"></stop>
 | 
				
			||||||
 | 
					                <stop offset="83.221%" stop-color="#B1C5FF"></stop>
 | 
				
			||||||
 | 
					            </linearGradient>
 | 
				
			||||||
 | 
					        </defs>
 | 
				
			||||||
 | 
					        <path d="M12.34 5.953a8.233 8.233 0 01-.247-1.125V3.72a8.25 8.25 0 015.562 2.232H12.34zm-.69 0c.113-.373.199-.755.257-1.145V3.72a8.25 8.25 0 00-5.562 2.232h5.304zm-5.433.187h5.373a7.98 7.98 0 01-.267.696 8.41 8.41 0 01-1.76 2.65L6.216 6.14zm-.264-.187H2.977v.187h2.915a8.436 8.436 0 00-2.357 5.767H0v.186h3.535a8.436 8.436 0 002.357 5.767H2.977v.186h2.976v2.977h.187v-2.915a8.436 8.436 0 005.767 2.357V24h.186v-3.535a8.436 8.436 0 005.767-2.357v2.915h.186v-2.977h2.977v-.186h-2.915a8.436 8.436 0 002.357-5.767H24v-.186h-3.535a8.436 8.436 0 00-2.357-5.767h2.915v-.187h-2.977V2.977h-.186v2.915a8.436 8.436 0 00-5.767-2.357V0h-.186v3.535A8.436 8.436 0 006.14 5.892V2.977h-.187v2.976zm6.14 14.326a8.25 8.25 0 005.562-2.233H12.34c-.108.367-.19.743-.247 1.126v1.107zm-.186-1.087a8.015 8.015 0 00-.258-1.146H6.345a8.25 8.25 0 005.562 2.233v-1.087zm-8.186-7.285h1.107a8.23 8.23 0 001.125-.247V6.345a8.25 8.25 0 00-2.232 5.562zm1.087.186H3.72a8.25 8.25 0 002.232 5.562v-5.304a8.012 8.012 0 00-1.145-.258zm15.47-.186a8.25 8.25 0 00-2.232-5.562v5.315c.367.108.743.19 1.126.247h1.107zm-1.086.186c-.39.058-.772.144-1.146.258v5.304a8.25 8.25 0 002.233-5.562h-1.087zm-1.332 5.69V12.41a7.97 7.97 0 00-.696.267 8.409 8.409 0 00-2.65 1.76l3.346 3.346zm0-6.18v-5.45l-.012-.013h-5.451c.076.235.162.468.26.696a8.698 8.698 0 001.819 2.688 8.698 8.698 0 002.688 1.82c.228.097.46.183.696.259zM6.14 17.848V12.41c.235.078.468.167.696.267a8.403 8.403 0 012.688 1.799 8.404 8.404 0 011.799 2.688c.1.228.19.46.267.696H6.152l-.012-.012zm0-6.245V6.326l3.29 3.29a8.716 8.716 0 01-2.594 1.728 8.14 8.14 0 01-.696.259zm6.257 6.257h5.277l-3.29-3.29a8.716 8.716 0 00-1.728 2.594 8.135 8.135 0 00-.259.696zm-2.347-7.81a9.435 9.435 0 01-2.88 1.96 9.14 9.14 0 012.88 1.94 9.14 9.14 0 011.94 2.88 9.435 9.435 0 011.96-2.88 9.14 9.14 0 012.88-1.94 9.435 9.435 0 01-2.88-1.96 9.434 9.434 0 01-1.96-2.88 9.14 9.14 0 01-1.94 2.88z"
 | 
				
			||||||
 | 
					              fill="url(#lobe-icons-gemma-fill)" fill-rule="evenodd"></path>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 2.5 KiB  |