mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	Compare commits
	
		
			897 Commits
		
	
	
		
			v2.13.1
			...
			eb6cfc61da
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					eb6cfc61da | ||
| 
						 | 
					11b37c15bd | ||
| 
						 | 
					1d0038f17d | ||
| 
						 | 
					619fa519c0 | ||
| 
						 | 
					48469bd8ca | ||
| 
						 | 
					5a5e887f2b | ||
| 
						 | 
					b6f5d75656 | ||
| 
						 | 
					0d41a17ef6 | ||
| 
						 | 
					f7cde17919 | ||
| 
						 | 
					570cbb34b6 | ||
| 
						 | 
					7aa9ae0a3e | ||
| 
						 | 
					2d4180f5be | ||
| 
						 | 
					9f0182b55e | ||
| 
						 | 
					ad6666eeaf | ||
| 
						 | 
					a2c4e468a0 | ||
| 
						 | 
					2167076652 | ||
| 
						 | 
					e123076250 | ||
| 
						 | 
					a87ec75ba6 | ||
| 
						 | 
					ebcb4db245 | ||
| 
						 | 
					0a25a1a8cb | ||
| 
						 | 
					f3154b20a5 | ||
| 
						 | 
					b709ee3983 | ||
| 
						 | 
					f5f3ce94f6 | ||
| 
						 | 
					2b5f600308 | ||
| 
						 | 
					6e082ad7ac | ||
| 
						 | 
					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 | ||
| 
						 | 
					36a0c7b8a3 | ||
| 
						 | 
					9e1e0a7252 | ||
| 
						 | 
					027e5adf67 | ||
| 
						 | 
					e986088bec | ||
| 
						 | 
					63ffd473d5 | ||
| 
						 | 
					9e5d92dc58 | ||
| 
						 | 
					8ac9141a29 | ||
| 
						 | 
					313c942350 | ||
| 
						 | 
					26c3edd023 | ||
| 
						 | 
					9a5a3d4ce4 | ||
| 
						 | 
					6e79b9a7a2 | ||
| 
						 | 
					b32d82e6c1 | ||
| 
						 | 
					a3585685df | ||
| 
						 | 
					f379865e2c | ||
| 
						 | 
					4eb4c31438 | ||
| 
						 | 
					84a7afcd94 | ||
| 
						 | 
					fa48ace39b | ||
| 
						 | 
					1b869d9305 | ||
| 
						 | 
					93bc2f5870 | ||
| 
						 | 
					37c0cfe1e9 | ||
| 
						 | 
					fc27441561 | ||
| 
						 | 
					79cfbac11f | ||
| 
						 | 
					df62736ff6 | ||
| 
						 | 
					6a464b3e5f | ||
| 
						 | 
					57fcda80df | ||
| 
						 | 
					db39fbc419 | ||
| 
						 | 
					3dabe47c78 | ||
| 
						 | 
					affc194cde | ||
| 
						 | 
					03fa580a55 | ||
| 
						 | 
					d0dce654bf | ||
| 
						 | 
					169323e238 | ||
| 
						 | 
					71df415b14 | ||
| 
						 | 
					6bb01bc564 | ||
| 
						 | 
					07c6fe5975 | ||
| 
						 | 
					5964181cb2 | ||
| 
						 | 
					4b8288a2b2 | ||
| 
						 | 
					88b1c1c6a5 | ||
| 
						 | 
					1234deabfa | ||
| 
						 | 
					ebaeb5a0d5 | ||
| 
						 | 
					45306bbb6c | ||
| 
						 | 
					18e2403b01 | ||
| 
						 | 
					e578c5f3ad | ||
| 
						 | 
					61245e3d7e | ||
| 
						 | 
					7804182d0d | ||
| 
						 | 
					f2195154f6 | ||
| 
						 | 
					35f77f45a2 | ||
| 
						 | 
					992c3a5d3a | ||
| 
						 | 
					d51d7b6797 | ||
| 
						 | 
					23ac2efd89 | ||
| 
						 | 
					daeffb2dc6 | ||
| 
						 | 
					db58ca6c1d | ||
| 
						 | 
					2ff292cbfa | ||
| 
						 | 
					5a81393863 | ||
| 
						 | 
					116a73d398 | ||
| 
						 | 
					cf0c057164 | ||
| 
						 | 
					fe5a4f4447 | ||
| 
						 | 
					c1b74201e4 | ||
| 
						 | 
					27828d9ca8 | ||
| 
						 | 
					2bd799fac6 | ||
| 
						 | 
					9275f2d753 | ||
| 
						 | 
					7455978ee5 | ||
| 
						 | 
					7c0acc7b77 | ||
| 
						 | 
					f32dd69acf | ||
| 
						 | 
					80b8f956a9 | ||
| 
						 | 
					caf50b6e6c | ||
| 
						 | 
					b590d0857c | ||
| 
						 | 
					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 | ||
| 
						 | 
					97a4a910e0 | ||
| 
						 | 
					19c7a84548 | ||
| 
						 | 
					571ce11e53 | ||
| 
						 | 
					d2cb984ced | ||
| 
						 | 
					7fc0d11931 | ||
| 
						 | 
					341a52a615 | ||
| 
						 | 
					d58b99d602 | ||
| 
						 | 
					f7a5f836db | ||
| 
						 | 
					d212df8b95 | ||
| 
						 | 
					f3f6dc57c3 | ||
| 
						 | 
					29b5cd9436 | ||
| 
						 | 
					f5209fc344 | ||
| 
						 | 
					c5168c2132 | ||
| 
						 | 
					318e0989a2 | ||
| 
						 | 
					d8b1781d7b | ||
| 
						 | 
					ed5aea0521 | ||
| 
						 | 
					e9f90a4d82 | ||
| 
						 | 
					f86b220c92 | ||
| 
						 | 
					b6bb1673d4 | ||
| 
						 | 
					93f1762e6c | ||
| 
						 | 
					2f410fc09f | ||
| 
						 | 
					7b6fe66f2a | ||
| 
						 | 
					c2fc0b4979 | ||
| 
						 | 
					0b758941a4 | ||
| 
						 | 
					492b55c893 | ||
| 
						 | 
					4060e367ad | ||
| 
						 | 
					c99cd31b6b | ||
| 
						 | 
					718782f5b1 | ||
| 
						 | 
					0c3fb5b2ce | ||
| 
						 | 
					4ec6b067e7 | ||
| 
						 | 
					1748dd6a3b | ||
| 
						 | 
					95332e50ed | ||
| 
						 | 
					56eb9d1430 | ||
| 
						 | 
					8496980cf9 | ||
| 
						 | 
					ffe32694b0 | ||
| 
						 | 
					3d5b21154b | ||
| 
						 | 
					4b9697e336 | ||
| 
						 | 
					b0e9a542ba | ||
| 
						 | 
					8b67536c23 | ||
| 
						 | 
					cd49c12181 | ||
| 
						 | 
					a6b14c7910 | ||
| 
						 | 
					e275abdb9c | ||
| 
						 | 
					09a90665d5 | ||
| 
						 | 
					6649fbdfd0 | ||
| 
						 | 
					64a0ffee7b | ||
| 
						 | 
					b529118f31 | ||
| 
						 | 
					39d7d9f13a | ||
| 
						 | 
					fcd55df969 | ||
| 
						 | 
					1e59948358 | ||
| 
						 | 
					1102ef6e6b | ||
| 
						 | 
					7ce2e8f4c4 | ||
| 
						 | 
					fd1c656bdd | ||
| 
						 | 
					82298a760a | ||
| 
						 | 
					b84bb72e07 | ||
| 
						 | 
					495b321d0a | ||
| 
						 | 
					8a38cdc1d7 | ||
| 
						 | 
					e210db7bd9 | ||
| 
						 | 
					f64763b74b | ||
| 
						 | 
					e033bef544 | ||
| 
						 | 
					d88cc575e5 | ||
| 
						 | 
					e3f499be0c | ||
| 
						 | 
					87325fad74 | ||
| 
						 | 
					86220573b6 | ||
| 
						 | 
					65ed6b02a4 | ||
| 
						 | 
					98093a1f31 | ||
| 
						 | 
					00990dc195 | ||
| 
						 | 
					122aa94c9f | ||
| 
						 | 
					3da5284a07 | ||
| 
						 | 
					cd920364f8 | ||
| 
						 | 
					e2e8a45104 | ||
| 
						 | 
					fb5fc13f72 | ||
| 
						 | 
					edb92f7bfb | ||
| 
						 | 
					1980f43b9f | ||
| 
						 | 
					c3c3dd5154 | ||
| 
						 | 
					356155fd30 | ||
| 
						 | 
					6f75ef8f0a | ||
| 
						 | 
					ca865a80dc | ||
| 
						 | 
					8f759d1c3e | ||
| 
						 | 
					44787637f2 | ||
| 
						 | 
					cf1c8e8f2a | ||
| 
						 | 
					d948be2372 | ||
| 
						 | 
					cc28aef625 | ||
| 
						 | 
					036358de7c | ||
| 
						 | 
					0958b9ee12 | ||
| 
						 | 
					aff1d7ecd6 | ||
| 
						 | 
					42fdbd9bb8 | ||
| 
						 | 
					034c82e514 | ||
| 
						 | 
					14ff46b5cd | ||
| 
						 | 
					c9099ca0a5 | ||
| 
						 | 
					58b144b345 | ||
| 
						 | 
					af21c57e77 | ||
| 
						 | 
					624e4dbaaf | ||
| 
						 | 
					9bbb4f396b | ||
| 
						 | 
					1287e39cc6 | ||
| 
						 | 
					1ef2aa35e9 | ||
| 
						 | 
					b2c1644d69 | ||
| 
						 | 
					5629f842da | ||
| 
						 | 
					f900283b09 | ||
| 
						 | 
					b667eff6bd | ||
| 
						 | 
					54fdf40f5a | ||
| 
						 | 
					690542145d | ||
| 
						 | 
					94c4cf0624 | ||
| 
						 | 
					3da717d9fc | ||
| 
						 | 
					0902efc719 | ||
| 
						 | 
					d7e2ee63d8 | ||
| 
						 | 
					7deb36ee1f | ||
| 
						 | 
					bfe4e88246 | ||
| 
						 | 
					9ab45c3969 | ||
| 
						 | 
					fec80c6c51 | ||
| 
						 | 
					a6b7432358 | ||
| 
						 | 
					3486954e07 | ||
| 
						 | 
					150fc84b9b | ||
| 
						 | 
					b023a00445 | ||
| 
						 | 
					d0e296adf8 | ||
| 
						 | 
					aa40015e9b | ||
| 
						 | 
					141ce2c99a | ||
| 
						 | 
					4a95dcb6e9 | ||
| 
						 | 
					1610675c8f | ||
| 
						 | 
					724c814bfe | ||
| 
						 | 
					764c0cb865 | ||
| 
						 | 
					8a4b8a84d6 | ||
| 
						 | 
					8ec6acc55a | ||
| 
						 | 
					b6a022b0ef | ||
| 
						 | 
					716899c030 | ||
| 
						 | 
					d9e407fd2b | ||
| 
						 | 
					deb140de73 | ||
| 
						 | 
					3c1e5e7978 | ||
| 
						 | 
					4a8e85c28a | ||
| 
						 | 
					8498cadae8 | ||
| 
						 | 
					8c83fe23a1 | ||
| 
						 | 
					a8c65e3d27 | ||
| 
						 | 
					324d30bef9 | ||
| 
						 | 
					46cb48023e | ||
| 
						 | 
					1c24ca58c7 | ||
| 
						 | 
					9193a9a0e0 | ||
| 
						 | 
					957244ba2e | ||
| 
						 | 
					ac599aa47c | ||
| 
						 | 
					67a90ffb76 | ||
| 
						 | 
					feaa6f9bf0 | ||
| 
						 | 
					753bf3b924 | ||
| 
						 | 
					b3219f57c8 | ||
| 
						 | 
					a17df037af | ||
| 
						 | 
					dfc36e5210 | ||
| 
						 | 
					c359b92ddc | ||
| 
						 | 
					e1d6131f13 | ||
| 
						 | 
					6a0bda00f5 | ||
| 
						 | 
					f85ec95877 | ||
| 
						 | 
					a024980c03 | ||
| 
						 | 
					fd9e94e078 | ||
| 
						 | 
					f6a6c51d15 | ||
| 
						 | 
					966db1e4be | ||
| 
						 | 
					b8bbc37b8e | ||
| 
						 | 
					40cbabc330 | ||
| 
						 | 
					04a4e1b39a | ||
| 
						 | 
					99f3160aa2 | ||
| 
						 | 
					8cb72d8452 | ||
| 
						 | 
					c9eb9f3eda | ||
| 
						 | 
					64c3dcd732 | ||
| 
						 | 
					d49ececcc5 | ||
| 
						 | 
					90e1fadb1e | ||
| 
						 | 
					071391ddff | ||
| 
						 | 
					d70d46b4d5 | ||
| 
						 | 
					3ef596b215 | ||
| 
						 | 
					35c5518668 | ||
| 
						 | 
					8b513537b7 | ||
| 
						 | 
					b27f394995 | ||
| 
						 | 
					3f9f556e1c | ||
| 
						 | 
					1772d5c4b6 | ||
| 
						 | 
					715d1dc02f | ||
| 
						 | 
					6737f016f5 | ||
| 
						 | 
					f2d2622172 | ||
| 
						 | 
					72d6f97024 | ||
| 
						 | 
					a0f0b4ff9e | ||
| 
						 | 
					c27ef6ffbf | ||
| 
						 | 
					f5499ff699 | ||
| 
						 | 
					c4334d4e5f | ||
| 
						 | 
					51e8f0440d | ||
| 
						 | 
					5ec0311f84 | ||
| 
						 | 
					556d563ba0 | ||
| 
						 | 
					6a083b24c4 | ||
| 
						 | 
					825929fdc8 | ||
| 
						 | 
					941a03ed6c | ||
| 
						 | 
					cf63619182 | ||
| 
						 | 
					5c04d3c5ea | ||
| 
						 | 
					46a47db2d8 | ||
| 
						 | 
					21ef9a4567 | ||
| 
						 | 
					6f0846b2af | ||
| 
						 | 
					ecd78b3bdd | ||
| 
						 | 
					d8afd1af88 | ||
| 
						 | 
					7c1bc1f1a1 | ||
| 
						 | 
					763fc89b29 | ||
| 
						 | 
					47b33f2b17 | ||
| 
						 | 
					9f0e16b045 | ||
| 
						 | 
					2efedb1736 | ||
| 
						 | 
					044116c14c | ||
| 
						 | 
					b4bf11d648 | ||
| 
						 | 
					6cc0a5a1a4 | ||
| 
						 | 
					8f14de5108 | ||
| 
						 | 
					8f6e5d73a2 | ||
| 
						 | 
					ab9f5382b2 | ||
| 
						 | 
					fd441d9303 | ||
| 
						 | 
					e31bec3aff | ||
| 
						 | 
					2a1c05a028 | ||
| 
						 | 
					421bf33c0e | ||
| 
						 | 
					9c7bacc65e | ||
| 
						 | 
					3935c725c9 | ||
| 
						 | 
					908ee0060f | ||
| 
						 | 
					82e6fd7bb5 | ||
| 
						 | 
					6b98b14179 | ||
| 
						 | 
					1ecefd88f7 | ||
| 
						 | 
					2e9e20ce7c | ||
| 
						 | 
					fb60fbb217 | ||
| 
						 | 
					4199e17da0 | ||
| 
						 | 
					86b8bfcb1f | ||
| 
						 | 
					dfd089132d | ||
| 
						 | 
					3a10f58b28 | ||
| 
						 | 
					9d55adbaf2 | ||
| 
						 | 
					00be2be24f | ||
| 
						 | 
					5b126c7e52 | ||
| 
						 | 
					1943f3b53f | ||
| 
						 | 
					4a0bef9afb | ||
| 
						 | 
					dfd2a53129 | ||
| 
						 | 
					aa4e855012 | ||
| 
						 | 
					d6089e6309 | ||
| 
						 | 
					038e6df8f0 | ||
| 
						 | 
					2fd68bcac3 | ||
| 
						 | 
					e468fecf12 | ||
| 
						 | 
					fc31d8e5d1 | ||
| 
						 | 
					115f357a07 | ||
| 
						 | 
					ac04a1cac8 | ||
| 
						 | 
					87a286ef07 | ||
| 
						 | 
					622d8a4edb | ||
| 
						 | 
					b44086f0dc | ||
| 
						 | 
					0236e13187 | ||
| 
						 | 
					a3d4a7253f | ||
| 
						 | 
					26c2598f56 | ||
| 
						 | 
					ee22fba448 | ||
| 
						 | 
					49151dabf5 | ||
| 
						 | 
					eb7c7cdcb6 | ||
| 
						 | 
					4b84fb328c | ||
| 
						 | 
					5267ad46da | ||
| 
						 | 
					94bc880b7f | ||
| 
						 | 
					bab3e0bc9b | ||
| 
						 | 
					b3a324b6f5 | ||
| 
						 | 
					a1117cd4ee | ||
| 
						 | 
					6ece818d69 | ||
| 
						 | 
					5df09d5e2a | ||
| 
						 | 
					33450ce429 | ||
| 
						 | 
					e2f0206d88 | ||
| 
						 | 
					3767b2c7f9 | ||
| 
						 | 
					dd1030139b | ||
| 
						 | 
					d61cb98ac7 | ||
| 
						 | 
					a7ceb61e27 | ||
| 
						 | 
					74b915a790 | ||
| 
						 | 
					01ea690421 | ||
| 
						 | 
					17cc9284a0 | ||
| 
						 | 
					498d0f0b8b | ||
| 
						 | 
					2b0153807c | ||
| 
						 | 
					d726c71141 | ||
| 
						 | 
					a16725ac17 | ||
| 
						 | 
					54401162bd | ||
| 
						 | 
					7fde9327a2 | ||
| 
						 | 
					bbbf59c74a | ||
| 
						 | 
					34034be0e3 | ||
| 
						 | 
					d21481173e | ||
| 
						 | 
					fa6ebadc7b | ||
| 
						 | 
					a51fb24f36 | ||
| 
						 | 
					74986803db | ||
| 
						 | 
					24bf7950d8 | ||
| 
						 | 
					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
									
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
 | 
				
			||||||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -44,3 +44,8 @@ dev
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
*.key
 | 
					*.key
 | 
				
			||||||
*.key.pub
 | 
					*.key.pub
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
									
									
									
									
									
								
							
							
						
						
									
										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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										216
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										216
									
								
								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) / [Web App Demo](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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[网页版](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,25 @@ 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 DeepSeek, China's AI star!
 | 
				
			||||||
 | 
					 > Purpose-Built UI for DeepSeek Reasoner Model
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					<img src="https://github.com/user-attachments/assets/f3952210-3af1-4dc0-9b81-40eaa4847d9a"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 🫣 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 +66,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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -88,52 +101,25 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 | 
				
			|||||||
- [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
 | 
					- [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
 | 
				
			||||||
- [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.
 | 
				
			||||||
- [ ] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 | 
					- [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 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] 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] Supports Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
 | 
				
			||||||
 | 
					- [ ] 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.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.
 | 
				
			||||||
- 🚀 v2.8 now we have a client that runs across all platforms!
 | 
					- 🚀 v2.8 now we have a client that runs across all platforms!
 | 
				
			||||||
- 🚀 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)
 | 
					 | 
				
			||||||
- [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 最新动态
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。
 | 
					 | 
				
			||||||
- 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com
 | 
					 | 
				
			||||||
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
 | 
					 | 
				
			||||||
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
 | 
					 | 
				
			||||||
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 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;
 | 
				
			||||||
@@ -141,14 +127,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:
 | 
				
			||||||
@@ -159,7 +141,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:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -175,8 +157,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:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
@@ -187,8 +167,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.
 | 
				
			||||||
@@ -271,6 +249,34 @@ Alibaba Cloud Api Key.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Alibaba Cloud Api Url.
 | 
					Alibaba Cloud Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `IFLYTEK_URL` (Optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					iflytek Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `IFLYTEK_API_KEY` (Optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					iflytek Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `IFLYTEK_API_SECRET` (Optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
@@ -304,9 +310,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.
 | 
				
			||||||
@@ -315,7 +321,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 
 | 
				
			||||||
@@ -326,13 +339,33 @@ You can use this option if you want to increase the number of webdav service add
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Customize the default template used to initialize the User Input Preprocessing configuration item in Settings.
 | 
					Customize the default template used to initialize the User Input Preprocessing configuration item in Settings.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `STABILITY_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Stability API key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `STABILITY_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -357,7 +390,6 @@ yarn dev
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Deployment
 | 
					## Deployment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> [简体中文 > 如何部署到私人服务器](./README_CN.md#部署)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Docker (Recommended)
 | 
					### Docker (Recommended)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -386,6 +418,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
 | 
				
			||||||
@@ -406,11 +448,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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -422,37 +460,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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										88
									
								
								README_CN.md
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								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` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -172,6 +173,35 @@ ByteDance Api Url.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
阿里云(千问)Api Url.
 | 
					阿里云(千问)Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `IFLYTEK_URL` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					讯飞星火Api Url.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `IFLYTEK_API_KEY` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					讯飞星火Api Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `IFLYTEK_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` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
 | 
					如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
 | 
				
			||||||
@@ -188,7 +218,7 @@ ByteDance Api Url.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
 | 
					如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### `WHITE_WEBDEV_ENDPOINTS` (可选)
 | 
					### `WHITE_WEBDAV_ENDPOINTS` (可选)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求:
 | 
					如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求:
 | 
				
			||||||
- 每一个地址必须是一个完整的 endpoint
 | 
					- 每一个地址必须是一个完整的 endpoint
 | 
				
			||||||
@@ -202,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)`的选项
 | 
				
			||||||
@@ -214,10 +244,37 @@ 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,用于初始化『设置』中的『用户输入预处理』配置项
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `STABILITY_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Stability API密钥
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `STABILITY_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					自定义的Stability API请求地址
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `ENABLE_MCP` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					启用MCP(Model Context Protocol)功能
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `SILICONFLOW_API_KEY` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SiliconFlow API Key.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### `SILICONFLOW_URL` (optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SiliconFlow API URL.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 开发
 | 
					## 开发
 | 
				
			||||||
 | 
					
 | 
				
			||||||
点击下方按钮,开始二次开发:
 | 
					点击下方按钮,开始二次开发:
 | 
				
			||||||
@@ -241,6 +298,9 @@ BASE_URL=https://b.nextweb.fun/api/proxy
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## 部署
 | 
					## 部署
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 宝塔面板部署
 | 
				
			||||||
 | 
					> [简体中文 > 如何通过宝塔一键部署](./docs/bt-cn.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 容器部署 (推荐)
 | 
					### 容器部署 (推荐)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。
 | 
					> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。
 | 
				
			||||||
@@ -267,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
									
									
									
									
									
								
							
							
						
						
									
										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` (オプション)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
『設定』の『ユーザー入力前処理』の初期設定に使用するテンプレートをカスタマイズします。
 | 
					『設定』の『ユーザー入力前処理』の初期設定に使用するテンプレートをカスタマイズします。
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										82
									
								
								app/api/[provider]/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								app/api/[provider]/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					import { ApiPath } from "@/app/constant";
 | 
				
			||||||
 | 
					import { NextRequest } from "next/server";
 | 
				
			||||||
 | 
					import { handle as openaiHandler } from "../../openai";
 | 
				
			||||||
 | 
					import { handle as azureHandler } from "../../azure";
 | 
				
			||||||
 | 
					import { handle as googleHandler } from "../../google";
 | 
				
			||||||
 | 
					import { handle as anthropicHandler } from "../../anthropic";
 | 
				
			||||||
 | 
					import { handle as baiduHandler } from "../../baidu";
 | 
				
			||||||
 | 
					import { handle as bytedanceHandler } from "../../bytedance";
 | 
				
			||||||
 | 
					import { handle as alibabaHandler } from "../../alibaba";
 | 
				
			||||||
 | 
					import { handle as moonshotHandler } from "../../moonshot";
 | 
				
			||||||
 | 
					import { handle as stabilityHandler } from "../../stability";
 | 
				
			||||||
 | 
					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(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { provider: string; path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  const apiPath = `/api/${params.provider}`;
 | 
				
			||||||
 | 
					  console.log(`[${params.provider} Route] params `, params);
 | 
				
			||||||
 | 
					  switch (apiPath) {
 | 
				
			||||||
 | 
					    case ApiPath.Azure:
 | 
				
			||||||
 | 
					      return azureHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.Google:
 | 
				
			||||||
 | 
					      return googleHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.Anthropic:
 | 
				
			||||||
 | 
					      return anthropicHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.Baidu:
 | 
				
			||||||
 | 
					      return baiduHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.ByteDance:
 | 
				
			||||||
 | 
					      return bytedanceHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.Alibaba:
 | 
				
			||||||
 | 
					      return alibabaHandler(req, { params });
 | 
				
			||||||
 | 
					    // case ApiPath.Tencent: using "/api/tencent"
 | 
				
			||||||
 | 
					    case ApiPath.Moonshot:
 | 
				
			||||||
 | 
					      return moonshotHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.Stability:
 | 
				
			||||||
 | 
					      return stabilityHandler(req, { params });
 | 
				
			||||||
 | 
					    case ApiPath.Iflytek:
 | 
				
			||||||
 | 
					      return iflytekHandler(req, { params });
 | 
				
			||||||
 | 
					    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 });
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return proxyHandler(req, { params });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const GET = handle;
 | 
				
			||||||
 | 
					export const POST = handle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const runtime = "edge";
 | 
				
			||||||
 | 
					export const preferredRegion = [
 | 
				
			||||||
 | 
					  "arn1",
 | 
				
			||||||
 | 
					  "bom1",
 | 
				
			||||||
 | 
					  "cdg1",
 | 
				
			||||||
 | 
					  "cle1",
 | 
				
			||||||
 | 
					  "cpt1",
 | 
				
			||||||
 | 
					  "dub1",
 | 
				
			||||||
 | 
					  "fra1",
 | 
				
			||||||
 | 
					  "gru1",
 | 
				
			||||||
 | 
					  "hnd1",
 | 
				
			||||||
 | 
					  "iad1",
 | 
				
			||||||
 | 
					  "icn1",
 | 
				
			||||||
 | 
					  "kix1",
 | 
				
			||||||
 | 
					  "lhr1",
 | 
				
			||||||
 | 
					  "pdx1",
 | 
				
			||||||
 | 
					  "sfo1",
 | 
				
			||||||
 | 
					  "sin1",
 | 
				
			||||||
 | 
					  "syd1",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
@@ -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,12 +8,11 @@ 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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function handle(
 | 
					export async function handle(
 | 
				
			||||||
  req: NextRequest,
 | 
					  req: NextRequest,
 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
@@ -40,30 +38,6 @@ async function handle(
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const GET = handle;
 | 
					 | 
				
			||||||
export const POST = handle;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const runtime = "edge";
 | 
					 | 
				
			||||||
export const preferredRegion = [
 | 
					 | 
				
			||||||
  "arn1",
 | 
					 | 
				
			||||||
  "bom1",
 | 
					 | 
				
			||||||
  "cdg1",
 | 
					 | 
				
			||||||
  "cle1",
 | 
					 | 
				
			||||||
  "cpt1",
 | 
					 | 
				
			||||||
  "dub1",
 | 
					 | 
				
			||||||
  "fra1",
 | 
					 | 
				
			||||||
  "gru1",
 | 
					 | 
				
			||||||
  "hnd1",
 | 
					 | 
				
			||||||
  "iad1",
 | 
					 | 
				
			||||||
  "icn1",
 | 
					 | 
				
			||||||
  "kix1",
 | 
					 | 
				
			||||||
  "lhr1",
 | 
					 | 
				
			||||||
  "pdx1",
 | 
					 | 
				
			||||||
  "sfo1",
 | 
					 | 
				
			||||||
  "sin1",
 | 
					 | 
				
			||||||
  "syd1",
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
  const controller = new AbortController();
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -115,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,19 +3,18 @@ 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]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function handle(
 | 
					export async function handle(
 | 
				
			||||||
  req: NextRequest,
 | 
					  req: NextRequest,
 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
@@ -56,30 +55,6 @@ async function handle(
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const GET = handle;
 | 
					 | 
				
			||||||
export const POST = handle;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const runtime = "edge";
 | 
					 | 
				
			||||||
export const preferredRegion = [
 | 
					 | 
				
			||||||
  "arn1",
 | 
					 | 
				
			||||||
  "bom1",
 | 
					 | 
				
			||||||
  "cdg1",
 | 
					 | 
				
			||||||
  "cle1",
 | 
					 | 
				
			||||||
  "cpt1",
 | 
					 | 
				
			||||||
  "dub1",
 | 
					 | 
				
			||||||
  "fra1",
 | 
					 | 
				
			||||||
  "gru1",
 | 
					 | 
				
			||||||
  "hnd1",
 | 
					 | 
				
			||||||
  "iad1",
 | 
					 | 
				
			||||||
  "icn1",
 | 
					 | 
				
			||||||
  "kix1",
 | 
					 | 
				
			||||||
  "lhr1",
 | 
					 | 
				
			||||||
  "pdx1",
 | 
					 | 
				
			||||||
  "sfo1",
 | 
					 | 
				
			||||||
  "sin1",
 | 
					 | 
				
			||||||
  "syd1",
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const serverConfig = getServerSideConfig();
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
@@ -122,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") ||
 | 
				
			||||||
@@ -146,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,
 | 
				
			||||||
							
								
								
									
										73
									
								
								app/api/artifacts/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								app/api/artifacts/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					import md5 from "spark-md5";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function handle(req: NextRequest, res: NextResponse) {
 | 
				
			||||||
 | 
					  const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					  const storeUrl = () =>
 | 
				
			||||||
 | 
					    `https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}`;
 | 
				
			||||||
 | 
					  const storeHeaders = () => ({
 | 
				
			||||||
 | 
					    Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  if (req.method === "POST") {
 | 
				
			||||||
 | 
					    const clonedBody = await req.text();
 | 
				
			||||||
 | 
					    const hashedCode = md5.hash(clonedBody).trim();
 | 
				
			||||||
 | 
					    const body: {
 | 
				
			||||||
 | 
					      key: string;
 | 
				
			||||||
 | 
					      value: string;
 | 
				
			||||||
 | 
					      expiration_ttl?: number;
 | 
				
			||||||
 | 
					    } = {
 | 
				
			||||||
 | 
					      key: hashedCode,
 | 
				
			||||||
 | 
					      value: clonedBody,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const ttl = parseInt(serverConfig.cloudflareKVTTL as string);
 | 
				
			||||||
 | 
					      if (ttl > 60) {
 | 
				
			||||||
 | 
					        body["expiration_ttl"] = ttl;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const res = await fetch(`${storeUrl()}/bulk`, {
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        ...storeHeaders(),
 | 
				
			||||||
 | 
					        "Content-Type": "application/json",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      method: "PUT",
 | 
				
			||||||
 | 
					      body: JSON.stringify([body]),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const result = await res.json();
 | 
				
			||||||
 | 
					    console.log("save data", result);
 | 
				
			||||||
 | 
					    if (result?.success) {
 | 
				
			||||||
 | 
					      return NextResponse.json(
 | 
				
			||||||
 | 
					        { code: 0, id: hashedCode, result },
 | 
				
			||||||
 | 
					        { status: res.status },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      { error: true, msg: "Save data error" },
 | 
				
			||||||
 | 
					      { status: 400 },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (req.method === "GET") {
 | 
				
			||||||
 | 
					    const id = req?.nextUrl?.searchParams?.get("id");
 | 
				
			||||||
 | 
					    const res = await fetch(`${storeUrl()}/values/${id}`, {
 | 
				
			||||||
 | 
					      headers: storeHeaders(),
 | 
				
			||||||
 | 
					      method: "GET",
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return new Response(res.body, {
 | 
				
			||||||
 | 
					      status: res.status,
 | 
				
			||||||
 | 
					      statusText: res.statusText,
 | 
				
			||||||
 | 
					      headers: res.headers,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return NextResponse.json(
 | 
				
			||||||
 | 
					    { error: true, msg: "Invalid request" },
 | 
				
			||||||
 | 
					    { status: 400 },
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const POST = handle;
 | 
				
			||||||
 | 
					export const GET = handle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const runtime = "edge";
 | 
				
			||||||
@@ -67,6 +67,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
 | 
				
			|||||||
    let systemApiKey: string | undefined;
 | 
					    let systemApiKey: string | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    switch (modelProvider) {
 | 
					    switch (modelProvider) {
 | 
				
			||||||
 | 
					      case ModelProvider.Stability:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.stabilityApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
      case ModelProvider.GeminiPro:
 | 
					      case ModelProvider.GeminiPro:
 | 
				
			||||||
        systemApiKey = serverConfig.googleApiKey;
 | 
					        systemApiKey = serverConfig.googleApiKey;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
@@ -82,6 +85,25 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
 | 
				
			|||||||
      case ModelProvider.Qwen:
 | 
					      case ModelProvider.Qwen:
 | 
				
			||||||
        systemApiKey = serverConfig.alibabaApiKey;
 | 
					        systemApiKey = serverConfig.alibabaApiKey;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Moonshot:
 | 
				
			||||||
 | 
					        systemApiKey = serverConfig.moonshotApiKey;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Iflytek:
 | 
				
			||||||
 | 
					        systemApiKey =
 | 
				
			||||||
 | 
					          serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
 | 
				
			||||||
 | 
					        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,11 +1,10 @@
 | 
				
			|||||||
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";
 | 
				
			||||||
import { auth } from "../../auth";
 | 
					import { auth } from "./auth";
 | 
				
			||||||
import { requestOpenai } from "../../common";
 | 
					import { requestOpenai } from "./common";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function handle(
 | 
					export async function handle(
 | 
				
			||||||
  req: NextRequest,
 | 
					  req: NextRequest,
 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
@@ -31,27 +30,3 @@ async function handle(
 | 
				
			|||||||
    return NextResponse.json(prettyObject(e));
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const GET = handle;
 | 
					 | 
				
			||||||
export const POST = handle;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const runtime = "edge";
 | 
					 | 
				
			||||||
export const preferredRegion = [
 | 
					 | 
				
			||||||
  "arn1",
 | 
					 | 
				
			||||||
  "bom1",
 | 
					 | 
				
			||||||
  "cdg1",
 | 
					 | 
				
			||||||
  "cle1",
 | 
					 | 
				
			||||||
  "cpt1",
 | 
					 | 
				
			||||||
  "dub1",
 | 
					 | 
				
			||||||
  "fra1",
 | 
					 | 
				
			||||||
  "gru1",
 | 
					 | 
				
			||||||
  "hnd1",
 | 
					 | 
				
			||||||
  "iad1",
 | 
					 | 
				
			||||||
  "icn1",
 | 
					 | 
				
			||||||
  "kix1",
 | 
					 | 
				
			||||||
  "lhr1",
 | 
					 | 
				
			||||||
  "pdx1",
 | 
					 | 
				
			||||||
  "sfo1",
 | 
					 | 
				
			||||||
  "sin1",
 | 
					 | 
				
			||||||
  "syd1",
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
@@ -3,18 +3,17 @@ 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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function handle(
 | 
					export async function handle(
 | 
				
			||||||
  req: NextRequest,
 | 
					  req: NextRequest,
 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
@@ -52,30 +51,6 @@ async function handle(
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const GET = handle;
 | 
					 | 
				
			||||||
export const POST = handle;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const runtime = "edge";
 | 
					 | 
				
			||||||
export const preferredRegion = [
 | 
					 | 
				
			||||||
  "arn1",
 | 
					 | 
				
			||||||
  "bom1",
 | 
					 | 
				
			||||||
  "cdg1",
 | 
					 | 
				
			||||||
  "cle1",
 | 
					 | 
				
			||||||
  "cpt1",
 | 
					 | 
				
			||||||
  "dub1",
 | 
					 | 
				
			||||||
  "fra1",
 | 
					 | 
				
			||||||
  "gru1",
 | 
					 | 
				
			||||||
  "hnd1",
 | 
					 | 
				
			||||||
  "iad1",
 | 
					 | 
				
			||||||
  "icn1",
 | 
					 | 
				
			||||||
  "kix1",
 | 
					 | 
				
			||||||
  "lhr1",
 | 
					 | 
				
			||||||
  "pdx1",
 | 
					 | 
				
			||||||
  "sfo1",
 | 
					 | 
				
			||||||
  "sin1",
 | 
					 | 
				
			||||||
  "syd1",
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
  const controller = new AbortController();
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -129,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,11 +8,11 @@ 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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function handle(
 | 
					export async function handle(
 | 
				
			||||||
  req: NextRequest,
 | 
					  req: NextRequest,
 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
@@ -38,30 +38,6 @@ async function handle(
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const GET = handle;
 | 
					 | 
				
			||||||
export const POST = handle;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const runtime = "edge";
 | 
					 | 
				
			||||||
export const preferredRegion = [
 | 
					 | 
				
			||||||
  "arn1",
 | 
					 | 
				
			||||||
  "bom1",
 | 
					 | 
				
			||||||
  "cdg1",
 | 
					 | 
				
			||||||
  "cle1",
 | 
					 | 
				
			||||||
  "cpt1",
 | 
					 | 
				
			||||||
  "dub1",
 | 
					 | 
				
			||||||
  "fra1",
 | 
					 | 
				
			||||||
  "gru1",
 | 
					 | 
				
			||||||
  "hnd1",
 | 
					 | 
				
			||||||
  "iad1",
 | 
					 | 
				
			||||||
  "icn1",
 | 
					 | 
				
			||||||
  "kix1",
 | 
					 | 
				
			||||||
  "lhr1",
 | 
					 | 
				
			||||||
  "pdx1",
 | 
					 | 
				
			||||||
  "sfo1",
 | 
					 | 
				
			||||||
  "sin1",
 | 
					 | 
				
			||||||
  "syd1",
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function request(req: NextRequest) {
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
  const controller = new AbortController();
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -112,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/",
 | 
				
			||||||
@@ -98,6 +90,14 @@ export async function requestOpenai(req: NextRequest) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
 | 
					  const fetchUrl = cloudflareAIGatewayUrl(`${baseUrl}/${path}`);
 | 
				
			||||||
  console.log("fetchUrl", fetchUrl);
 | 
					  console.log("fetchUrl", fetchUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let payload = await req.text();
 | 
				
			||||||
 | 
					  if (baseUrl.includes("openrouter.ai")) {
 | 
				
			||||||
 | 
					    const body = JSON.parse(payload);
 | 
				
			||||||
 | 
					    body["include_reasoning"] = true;
 | 
				
			||||||
 | 
					    payload = JSON.stringify(body);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const fetchOptions: RequestInit = {
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
    headers: {
 | 
					    headers: {
 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
@@ -108,7 +108,7 @@ export async function requestOpenai(req: NextRequest) {
 | 
				
			|||||||
      }),
 | 
					      }),
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    method: req.method,
 | 
					    method: req.method,
 | 
				
			||||||
    body: req.body,
 | 
					    body: payload,
 | 
				
			||||||
    // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
 | 
					    // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
 | 
				
			||||||
    redirect: "manual",
 | 
					    redirect: "manual",
 | 
				
			||||||
    // @ts-ignore
 | 
					    // @ts-ignore
 | 
				
			||||||
@@ -119,22 +119,18 @@ export async function requestOpenai(req: NextRequest) {
 | 
				
			|||||||
  // #1815 try to refuse gpt4 request
 | 
					  // #1815 try to refuse gpt4 request
 | 
				
			||||||
  if (serverConfig.customModels && req.body) {
 | 
					  if (serverConfig.customModels && req.body) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const clonedBody = await req.text();
 | 
					      const jsonBody = JSON.parse(payload) as { model?: string };
 | 
				
			||||||
      fetchOptions.body = clonedBody;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const jsonBody = JSON.parse(clonedBody) as { model?: string };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // 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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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,19 +1,14 @@
 | 
				
			|||||||
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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function handle(
 | 
					export async function handle(
 | 
				
			||||||
  req: NextRequest,
 | 
					  req: NextRequest,
 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					  { params }: { params: { provider: string; path: string[] } },
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  console.log("[Google Route] params ", params);
 | 
					  console.log("[Google Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -28,7 +23,8 @@ 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,
 | 
				
			||||||
							
								
								
									
										129
									
								
								app/api/iflytek.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								app/api/iflytek.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  IFLYTEK_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";
 | 
				
			||||||
 | 
					// iflytek
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[Iflytek Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.Iflytek);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[Iflytek] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // iflytek use base url or just remove the path
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Iflytek, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.iflytekUrl || IFLYTEK_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,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 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.Iflytek as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[Iflytek] 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);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										128
									
								
								app/api/moonshot.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								app/api/moonshot.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  MOONSHOT_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("[Moonshot Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.Moonshot);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[Moonshot] ", 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.Moonshot, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.moonshotUrl || MOONSHOT_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.Moonshot as string,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return NextResponse.json(
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					            message: `you are not allowed to use ${jsonBody?.model} model`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            status: 403,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(`[Moonshot] 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);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,24 +3,30 @@ import { getServerSideConfig } from "@/app/config/server";
 | 
				
			|||||||
import { ModelProvider, OpenaiPath } from "@/app/constant";
 | 
					import { ModelProvider, OpenaiPath } 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 { 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) =>
 | 
				
			||||||
 | 
					        !(
 | 
				
			||||||
 | 
					          m.id.startsWith("gpt-4") ||
 | 
				
			||||||
 | 
					          m.id.startsWith("chatgpt-4o") ||
 | 
				
			||||||
 | 
					          m.id.startsWith("o1") ||
 | 
				
			||||||
 | 
					          m.id.startsWith("o3")
 | 
				
			||||||
 | 
					        ) || m.id.startsWith("gpt-4o-mini"),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return remoteModelRes;
 | 
					  return remoteModelRes;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function handle(
 | 
					export async function handle(
 | 
				
			||||||
  req: NextRequest,
 | 
					  req: NextRequest,
 | 
				
			||||||
  { params }: { params: { path: string[] } },
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
@@ -32,7 +38,7 @@ 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(
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
@@ -70,27 +76,3 @@ async function handle(
 | 
				
			|||||||
    return NextResponse.json(prettyObject(e));
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const GET = handle;
 | 
					 | 
				
			||||||
export const POST = handle;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const runtime = "edge";
 | 
					 | 
				
			||||||
export const preferredRegion = [
 | 
					 | 
				
			||||||
  "arn1",
 | 
					 | 
				
			||||||
  "bom1",
 | 
					 | 
				
			||||||
  "cdg1",
 | 
					 | 
				
			||||||
  "cle1",
 | 
					 | 
				
			||||||
  "cpt1",
 | 
					 | 
				
			||||||
  "dub1",
 | 
					 | 
				
			||||||
  "fra1",
 | 
					 | 
				
			||||||
  "gru1",
 | 
					 | 
				
			||||||
  "hnd1",
 | 
					 | 
				
			||||||
  "iad1",
 | 
					 | 
				
			||||||
  "icn1",
 | 
					 | 
				
			||||||
  "kix1",
 | 
					 | 
				
			||||||
  "lhr1",
 | 
					 | 
				
			||||||
  "pdx1",
 | 
					 | 
				
			||||||
  "sfo1",
 | 
					 | 
				
			||||||
  "sin1",
 | 
					 | 
				
			||||||
  "syd1",
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
							
								
								
									
										89
									
								
								app/api/proxy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										99
									
								
								app/api/stability.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								app/api/stability.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
				
			|||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import { ModelProvider, STABILITY_BASE_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[Stability] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.stabilityUrl || STABILITY_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let path = `${req.nextUrl.pathname}`.replaceAll("/api/stability/", "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Stability Proxy] ", path);
 | 
				
			||||||
 | 
					  console.log("[Stability Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.Stability);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const bearToken = req.headers.get("Authorization") ?? "";
 | 
				
			||||||
 | 
					  const token = bearToken.trim().replaceAll("Bearer ", "").trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const key = token ? token : serverConfig.stabilityApiKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!key) {
 | 
				
			||||||
 | 
					    return NextResponse.json(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					        message: `missing STABILITY_API_KEY in server env vars`,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        status: 401,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = `${baseUrl}/${path}`;
 | 
				
			||||||
 | 
					  console.log("[Stability Url] ", fetchUrl);
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": req.headers.get("Content-Type") || "multipart/form-data",
 | 
				
			||||||
 | 
					      Accept: req.headers.get("Accept") || "application/json",
 | 
				
			||||||
 | 
					      Authorization: `Bearer ${key}`,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    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,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										117
									
								
								app/api/tencent/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								app/api/tencent/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
				
			|||||||
 | 
					import { getServerSideConfig } from "@/app/config/server";
 | 
				
			||||||
 | 
					import { TENCENT_BASE_URL, ModelProvider } from "@/app/constant";
 | 
				
			||||||
 | 
					import { prettyObject } from "@/app/utils/format";
 | 
				
			||||||
 | 
					import { NextRequest, NextResponse } from "next/server";
 | 
				
			||||||
 | 
					import { auth } from "@/app/api/auth";
 | 
				
			||||||
 | 
					import { getHeader } from "@/app/utils/tencent";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serverConfig = getServerSideConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function handle(
 | 
				
			||||||
 | 
					  req: NextRequest,
 | 
				
			||||||
 | 
					  { params }: { params: { path: string[] } },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  console.log("[Tencent Route] params ", params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (req.method === "OPTIONS") {
 | 
				
			||||||
 | 
					    return NextResponse.json({ body: "OK" }, { status: 200 });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const authResult = auth(req, ModelProvider.Hunyuan);
 | 
				
			||||||
 | 
					  if (authResult.error) {
 | 
				
			||||||
 | 
					    return NextResponse.json(authResult, {
 | 
				
			||||||
 | 
					      status: 401,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await request(req);
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    console.error("[Tencent] ", e);
 | 
				
			||||||
 | 
					    return NextResponse.json(prettyObject(e));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const GET = handle;
 | 
				
			||||||
 | 
					export const POST = handle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const runtime = "edge";
 | 
				
			||||||
 | 
					export const preferredRegion = [
 | 
				
			||||||
 | 
					  "arn1",
 | 
				
			||||||
 | 
					  "bom1",
 | 
				
			||||||
 | 
					  "cdg1",
 | 
				
			||||||
 | 
					  "cle1",
 | 
				
			||||||
 | 
					  "cpt1",
 | 
				
			||||||
 | 
					  "dub1",
 | 
				
			||||||
 | 
					  "fra1",
 | 
				
			||||||
 | 
					  "gru1",
 | 
				
			||||||
 | 
					  "hnd1",
 | 
				
			||||||
 | 
					  "iad1",
 | 
				
			||||||
 | 
					  "icn1",
 | 
				
			||||||
 | 
					  "kix1",
 | 
				
			||||||
 | 
					  "lhr1",
 | 
				
			||||||
 | 
					  "pdx1",
 | 
				
			||||||
 | 
					  "sfo1",
 | 
				
			||||||
 | 
					  "sin1",
 | 
				
			||||||
 | 
					  "syd1",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function request(req: NextRequest) {
 | 
				
			||||||
 | 
					  const controller = new AbortController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!baseUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					    baseUrl = `https://${baseUrl}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					    baseUrl = baseUrl.slice(0, -1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  console.log("[Base Url]", baseUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeoutId = setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      controller.abort();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    10 * 60 * 1000,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const fetchUrl = baseUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const body = await req.text();
 | 
				
			||||||
 | 
					  const headers = await getHeader(
 | 
				
			||||||
 | 
					    body,
 | 
				
			||||||
 | 
					    serverConfig.tencentSecretId as string,
 | 
				
			||||||
 | 
					    serverConfig.tencentSecretKey as string,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const fetchOptions: RequestInit = {
 | 
				
			||||||
 | 
					    headers,
 | 
				
			||||||
 | 
					    method: req.method,
 | 
				
			||||||
 | 
					    body,
 | 
				
			||||||
 | 
					    redirect: "manual",
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    duplex: "half",
 | 
				
			||||||
 | 
					    signal: controller.signal,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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) => {
 | 
				
			||||||
@@ -29,6 +29,7 @@ async function handle(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const requestUrl = new URL(req.url);
 | 
					  const requestUrl = new URL(req.url);
 | 
				
			||||||
  let endpoint = requestUrl.searchParams.get("endpoint");
 | 
					  let endpoint = requestUrl.searchParams.get("endpoint");
 | 
				
			||||||
 | 
					  let proxy_method = requestUrl.searchParams.get("proxy_method") || req.method;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Validate the endpoint to prevent potential SSRF attacks
 | 
					  // Validate the endpoint to prevent potential SSRF attacks
 | 
				
			||||||
  if (
 | 
					  if (
 | 
				
			||||||
@@ -37,9 +38,13 @@ async function handle(
 | 
				
			|||||||
      const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
 | 
					      const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
 | 
				
			||||||
      const normalizedEndpoint = normalizeUrl(endpoint as string);
 | 
					      const normalizedEndpoint = normalizeUrl(endpoint as string);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return normalizedEndpoint &&
 | 
					      return (
 | 
				
			||||||
 | 
					        normalizedEndpoint &&
 | 
				
			||||||
        normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
 | 
					        normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
 | 
				
			||||||
        normalizedEndpoint.pathname.startsWith(normalizedAllowedEndpoint.pathname);
 | 
					        normalizedEndpoint.pathname.startsWith(
 | 
				
			||||||
 | 
					          normalizedAllowedEndpoint.pathname,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    return NextResponse.json(
 | 
					    return NextResponse.json(
 | 
				
			||||||
@@ -61,7 +66,11 @@ async function handle(
 | 
				
			|||||||
  const targetPath = `${endpoint}${endpointPath}`;
 | 
					  const targetPath = `${endpoint}${endpointPath}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // only allow MKCOL, GET, PUT
 | 
					  // only allow MKCOL, GET, PUT
 | 
				
			||||||
  if (req.method !== "MKCOL" && req.method !== "GET" && req.method !== "PUT") {
 | 
					  if (
 | 
				
			||||||
 | 
					    proxy_method !== "MKCOL" &&
 | 
				
			||||||
 | 
					    proxy_method !== "GET" &&
 | 
				
			||||||
 | 
					    proxy_method !== "PUT"
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
    return NextResponse.json(
 | 
					    return NextResponse.json(
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        error: true,
 | 
					        error: true,
 | 
				
			||||||
@@ -74,7 +83,7 @@ async function handle(
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // for MKCOL request, only allow request ${folder}
 | 
					  // for MKCOL request, only allow request ${folder}
 | 
				
			||||||
  if (req.method === "MKCOL" && !targetPath.endsWith(folder)) {
 | 
					  if (proxy_method === "MKCOL" && !targetPath.endsWith(folder)) {
 | 
				
			||||||
    return NextResponse.json(
 | 
					    return NextResponse.json(
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        error: true,
 | 
					        error: true,
 | 
				
			||||||
@@ -87,7 +96,7 @@ async function handle(
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // for GET request, only allow request ending with fileName
 | 
					  // for GET request, only allow request ending with fileName
 | 
				
			||||||
  if (req.method === "GET" && !targetPath.endsWith(fileName)) {
 | 
					  if (proxy_method === "GET" && !targetPath.endsWith(fileName)) {
 | 
				
			||||||
    return NextResponse.json(
 | 
					    return NextResponse.json(
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        error: true,
 | 
					        error: true,
 | 
				
			||||||
@@ -100,7 +109,7 @@ async function handle(
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  //   for PUT request, only allow request ending with fileName
 | 
					  //   for PUT request, only allow request ending with fileName
 | 
				
			||||||
  if (req.method === "PUT" && !targetPath.endsWith(fileName)) {
 | 
					  if (proxy_method === "PUT" && !targetPath.endsWith(fileName)) {
 | 
				
			||||||
    return NextResponse.json(
 | 
					    return NextResponse.json(
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        error: true,
 | 
					        error: true,
 | 
				
			||||||
@@ -114,7 +123,7 @@ async function handle(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const targetUrl = targetPath;
 | 
					  const targetUrl = targetPath;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const method = req.method;
 | 
					  const method = proxy_method || req.method;
 | 
				
			||||||
  const shouldNotHaveBody = ["get", "head"].includes(
 | 
					  const shouldNotHaveBody = ["get", "head"].includes(
 | 
				
			||||||
    method?.toLowerCase() ?? "",
 | 
					    method?.toLowerCase() ?? "",
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
@@ -139,7 +148,7 @@ async function handle(
 | 
				
			|||||||
      "[Any Proxy]",
 | 
					      "[Any Proxy]",
 | 
				
			||||||
      targetUrl,
 | 
					      targetUrl,
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        method: req.method,
 | 
					        method: method,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        status: fetchResult?.status,
 | 
					        status: fetchResult?.status,
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										128
									
								
								app/api/xai.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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,22 +1,35 @@
 | 
				
			|||||||
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 {
 | 
				
			||||||
import { ChatGPTApi } from "./platforms/openai";
 | 
					  ChatMessageTool,
 | 
				
			||||||
 | 
					  ChatMessage,
 | 
				
			||||||
 | 
					  ModelType,
 | 
				
			||||||
 | 
					  useAccessStore,
 | 
				
			||||||
 | 
					  useChatStore,
 | 
				
			||||||
 | 
					} from "../store";
 | 
				
			||||||
 | 
					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";
 | 
				
			||||||
import { ErnieApi } from "./platforms/baidu";
 | 
					import { ErnieApi } from "./platforms/baidu";
 | 
				
			||||||
import { DoubaoApi } from "./platforms/bytedance";
 | 
					import { DoubaoApi } from "./platforms/bytedance";
 | 
				
			||||||
import { QwenApi } from "./platforms/alibaba";
 | 
					import { QwenApi } from "./platforms/alibaba";
 | 
				
			||||||
 | 
					import { HunyuanApi } from "./platforms/tencent";
 | 
				
			||||||
 | 
					import { MoonshotApi } from "./platforms/moonshot";
 | 
				
			||||||
 | 
					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 {
 | 
				
			||||||
@@ -27,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[];
 | 
				
			||||||
@@ -40,6 +58,18 @@ export interface LLMConfig {
 | 
				
			|||||||
  stream?: boolean;
 | 
					  stream?: boolean;
 | 
				
			||||||
  presence_penalty?: number;
 | 
					  presence_penalty?: number;
 | 
				
			||||||
  frequency_penalty?: number;
 | 
					  frequency_penalty?: number;
 | 
				
			||||||
 | 
					  size?: DalleRequestPayload["size"];
 | 
				
			||||||
 | 
					  quality?: DalleRequestPayload["quality"];
 | 
				
			||||||
 | 
					  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 {
 | 
				
			||||||
@@ -47,9 +77,11 @@ export interface ChatOptions {
 | 
				
			|||||||
  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 {
 | 
				
			||||||
@@ -62,16 +94,19 @@ export interface LLMModel {
 | 
				
			|||||||
  displayName?: string;
 | 
					  displayName?: string;
 | 
				
			||||||
  available: boolean;
 | 
					  available: boolean;
 | 
				
			||||||
  provider: LLMModelProvider;
 | 
					  provider: LLMModelProvider;
 | 
				
			||||||
 | 
					  sorted: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface LLMModelProvider {
 | 
					export interface LLMModelProvider {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
  providerName: string;
 | 
					  providerName: string;
 | 
				
			||||||
  providerType: string;
 | 
					  providerType: string;
 | 
				
			||||||
 | 
					  sorted: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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[]>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -117,6 +152,27 @@ export class ClientApi {
 | 
				
			|||||||
      case ModelProvider.Qwen:
 | 
					      case ModelProvider.Qwen:
 | 
				
			||||||
        this.llm = new QwenApi();
 | 
					        this.llm = new QwenApi();
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Hunyuan:
 | 
				
			||||||
 | 
					        this.llm = new HunyuanApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Moonshot:
 | 
				
			||||||
 | 
					        this.llm = new MoonshotApi();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case ModelProvider.Iflytek:
 | 
				
			||||||
 | 
					        this.llm = new SparkApi();
 | 
				
			||||||
 | 
					        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();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -168,24 +224,47 @@ export class ClientApi {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getHeaders() {
 | 
					export function getBearerToken(
 | 
				
			||||||
 | 
					  apiKey: string,
 | 
				
			||||||
 | 
					  noBearer: boolean = false,
 | 
				
			||||||
 | 
					): string {
 | 
				
			||||||
 | 
					  return validString(apiKey)
 | 
				
			||||||
 | 
					    ? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
 | 
				
			||||||
 | 
					    : "";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function validString(x: string): boolean {
 | 
				
			||||||
 | 
					  return x?.length > 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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> = {};
 | 
				
			||||||
 | 
					  if (!ignoreHeaders) {
 | 
				
			||||||
 | 
					    headers = {
 | 
				
			||||||
      "Content-Type": "application/json",
 | 
					      "Content-Type": "application/json",
 | 
				
			||||||
      Accept: "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;
 | 
				
			||||||
    const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance;
 | 
					    const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance;
 | 
				
			||||||
    const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
 | 
					    const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
 | 
				
			||||||
 | 
					    const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
@@ -197,6 +276,20 @@ export function getHeaders() {
 | 
				
			|||||||
      ? accessStore.bytedanceApiKey
 | 
					      ? accessStore.bytedanceApiKey
 | 
				
			||||||
      : isAlibaba
 | 
					      : isAlibaba
 | 
				
			||||||
      ? accessStore.alibabaApiKey
 | 
					      ? accessStore.alibabaApiKey
 | 
				
			||||||
 | 
					      : isMoonshot
 | 
				
			||||||
 | 
					      ? accessStore.moonshotApiKey
 | 
				
			||||||
 | 
					      : isXAI
 | 
				
			||||||
 | 
					      ? accessStore.xaiApiKey
 | 
				
			||||||
 | 
					      : isDeepSeek
 | 
				
			||||||
 | 
					      ? accessStore.deepseekApiKey
 | 
				
			||||||
 | 
					      : isChatGLM
 | 
				
			||||||
 | 
					      ? accessStore.chatglmApiKey
 | 
				
			||||||
 | 
					      : isSiliconFlow
 | 
				
			||||||
 | 
					      ? accessStore.siliconflowApiKey
 | 
				
			||||||
 | 
					      : isIflytek
 | 
				
			||||||
 | 
					      ? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
 | 
				
			||||||
 | 
					        ? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
 | 
				
			||||||
 | 
					        : ""
 | 
				
			||||||
      : accessStore.openaiApiKey;
 | 
					      : accessStore.openaiApiKey;
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      isGoogle,
 | 
					      isGoogle,
 | 
				
			||||||
@@ -205,40 +298,52 @@ export function getHeaders() {
 | 
				
			|||||||
      isBaidu,
 | 
					      isBaidu,
 | 
				
			||||||
      isByteDance,
 | 
					      isByteDance,
 | 
				
			||||||
      isAlibaba,
 | 
					      isAlibaba,
 | 
				
			||||||
 | 
					      isMoonshot,
 | 
				
			||||||
 | 
					      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";
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function getBearerToken(apiKey: string, noBearer: boolean = false): string {
 | 
					 | 
				
			||||||
    return validString(apiKey)
 | 
					 | 
				
			||||||
      ? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
 | 
					 | 
				
			||||||
      : "";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  function validString(x: string): boolean {
 | 
					 | 
				
			||||||
    return x?.length > 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
    isGoogle,
 | 
					    isGoogle,
 | 
				
			||||||
    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;
 | 
				
			||||||
@@ -263,6 +368,20 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
 | 
				
			|||||||
      return new ClientApi(ModelProvider.Doubao);
 | 
					      return new ClientApi(ModelProvider.Doubao);
 | 
				
			||||||
    case ServiceProvider.Alibaba:
 | 
					    case ServiceProvider.Alibaba:
 | 
				
			||||||
      return new ClientApi(ModelProvider.Qwen);
 | 
					      return new ClientApi(ModelProvider.Qwen);
 | 
				
			||||||
 | 
					    case ServiceProvider.Tencent:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.Hunyuan);
 | 
				
			||||||
 | 
					    case ServiceProvider.Moonshot:
 | 
				
			||||||
 | 
					      return new ClientApi(ModelProvider.Moonshot);
 | 
				
			||||||
 | 
					    case ServiceProvider.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 || [],
 | 
				
			||||||
        // animate response to make it looks smooth
 | 
					 | 
				
			||||||
        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 fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
					 | 
				
			||||||
            const fetchText = remainText.slice(0, fetchCount);
 | 
					 | 
				
			||||||
            responseText += fetchText;
 | 
					 | 
				
			||||||
            remainText = remainText.slice(fetchCount);
 | 
					 | 
				
			||||||
            options.onUpdate?.(responseText, fetchText);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          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(
 | 
					 | 
				
			||||||
              "[Alibaba] request response content type: ",
 | 
					 | 
				
			||||||
              contentType,
 | 
					 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 | 
					        return streamWithThink(
 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					          chatPath,
 | 
				
			||||||
              responseText = await res.clone().text();
 | 
					          requestPayload,
 | 
				
			||||||
              return finish();
 | 
					          headers,
 | 
				
			||||||
            }
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
            if (
 | 
					          controller,
 | 
				
			||||||
              !res.ok ||
 | 
					          // parseSSE
 | 
				
			||||||
              !res.headers
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
                .get("content-type")
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
                ?.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 json = JSON.parse(text);
 | 
				
			||||||
            const choices = json.output.choices as Array<{
 | 
					            const choices = json.output.choices as Array<{
 | 
				
			||||||
                message: { content: string };
 | 
					              message: {
 | 
				
			||||||
 | 
					                content: string | null | MultimodalContentForAlibaba[];
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					                reasoning_content: string | null;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
            }>;
 | 
					            }>;
 | 
				
			||||||
              const delta = choices[0]?.message?.content;
 | 
					
 | 
				
			||||||
              if (delta) {
 | 
					            if (!choices?.length) return { isThinking: false, content: "" };
 | 
				
			||||||
                remainText += delta;
 | 
					
 | 
				
			||||||
              }
 | 
					            const tool_calls = choices[0]?.message?.tool_calls;
 | 
				
			||||||
            } catch (e) {
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
              console.error("[Request] parse error", text, msg);
 | 
					              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,
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
          onclose() {
 | 
					 | 
				
			||||||
            finish();
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onerror(e) {
 | 
					 | 
				
			||||||
            options.onError?.(e);
 | 
					 | 
				
			||||||
            throw e;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          openWhenHidden: true,
 | 
					 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // @ts-ignore
 | 
				
			||||||
 | 
					                runTools[index]["function"]["arguments"] += args;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            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 (
 | 
				
			||||||
 | 
					              (!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: Array.isArray(content)
 | 
				
			||||||
 | 
					                  ? content.map((item) => item.text).join(",")
 | 
				
			||||||
 | 
					                  : content,
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              isThinking: false,
 | 
				
			||||||
 | 
					              content: "",
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            requestPayload?.input?.messages?.splice(
 | 
				
			||||||
 | 
					              requestPayload?.input?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
      } 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,6 +196,114 @@ export class ClaudeApi implements LLMApi {
 | 
				
			|||||||
    const controller = new AbortController();
 | 
					    const controller = new AbortController();
 | 
				
			||||||
    options.onController?.(controller);
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (shouldStream) {
 | 
				
			||||||
 | 
					      let index = -1;
 | 
				
			||||||
 | 
					      const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					        .getState()
 | 
				
			||||||
 | 
					        .getAsTools(
 | 
				
			||||||
 | 
					          useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      return stream(
 | 
				
			||||||
 | 
					        path,
 | 
				
			||||||
 | 
					        requestBody,
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          ...getHeaders(),
 | 
				
			||||||
 | 
					          "anthropic-version": accessStore.anthropicApiVersion,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        // @ts-ignore
 | 
				
			||||||
 | 
					        tools.map((tool) => ({
 | 
				
			||||||
 | 
					          name: tool?.function?.name,
 | 
				
			||||||
 | 
					          description: tool?.function?.description,
 | 
				
			||||||
 | 
					          input_schema: tool?.function?.parameters,
 | 
				
			||||||
 | 
					        })),
 | 
				
			||||||
 | 
					        funcs,
 | 
				
			||||||
 | 
					        controller,
 | 
				
			||||||
 | 
					        // parseSSE
 | 
				
			||||||
 | 
					        (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
 | 
					          // console.log("parseSSE", text, runTools);
 | 
				
			||||||
 | 
					          let chunkJson:
 | 
				
			||||||
 | 
					            | undefined
 | 
				
			||||||
 | 
					            | {
 | 
				
			||||||
 | 
					                type: "content_block_delta" | "content_block_stop";
 | 
				
			||||||
 | 
					                content_block?: {
 | 
				
			||||||
 | 
					                  type: "tool_use";
 | 
				
			||||||
 | 
					                  id: string;
 | 
				
			||||||
 | 
					                  name: string;
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                delta?: {
 | 
				
			||||||
 | 
					                  type: "text_delta" | "input_json_delta";
 | 
				
			||||||
 | 
					                  text?: string;
 | 
				
			||||||
 | 
					                  partial_json?: string;
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                index: number;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					          chunkJson = JSON.parse(text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (chunkJson?.content_block?.type == "tool_use") {
 | 
				
			||||||
 | 
					            index += 1;
 | 
				
			||||||
 | 
					            const id = chunkJson?.content_block.id;
 | 
				
			||||||
 | 
					            const name = chunkJson?.content_block.name;
 | 
				
			||||||
 | 
					            runTools.push({
 | 
				
			||||||
 | 
					              id,
 | 
				
			||||||
 | 
					              type: "function",
 | 
				
			||||||
 | 
					              function: {
 | 
				
			||||||
 | 
					                name,
 | 
				
			||||||
 | 
					                arguments: "",
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (
 | 
				
			||||||
 | 
					            chunkJson?.delta?.type == "input_json_delta" &&
 | 
				
			||||||
 | 
					            chunkJson?.delta?.partial_json
 | 
				
			||||||
 | 
					          ) {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            runTools[index]["function"]["arguments"] +=
 | 
				
			||||||
 | 
					              chunkJson?.delta?.partial_json;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          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 {
 | 
				
			||||||
      const payload = {
 | 
					      const payload = {
 | 
				
			||||||
        method: "POST",
 | 
					        method: "POST",
 | 
				
			||||||
        body: JSON.stringify(requestBody),
 | 
					        body: JSON.stringify(requestBody),
 | 
				
			||||||
@@ -203,108 +316,15 @@ export class ClaudeApi implements LLMApi {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (shouldStream) {
 | 
					 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const context = {
 | 
					        controller.signal.onabort = () =>
 | 
				
			||||||
          text: "",
 | 
					          options.onFinish("", new Response(null, { status: 400 }));
 | 
				
			||||||
          finished: false,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const finish = () => {
 | 
					 | 
				
			||||||
          if (!context.finished) {
 | 
					 | 
				
			||||||
            options.onFinish(context.text);
 | 
					 | 
				
			||||||
            context.finished = true;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        controller.signal.onabort = finish;
 | 
					 | 
				
			||||||
        fetchEventSource(path, {
 | 
					 | 
				
			||||||
          ...payload,
 | 
					 | 
				
			||||||
          async onopen(res) {
 | 
					 | 
				
			||||||
            const contentType = res.headers.get("content-type");
 | 
					 | 
				
			||||||
            console.log("response content type: ", contentType);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					 | 
				
			||||||
              context.text = await res.clone().text();
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              !res.ok ||
 | 
					 | 
				
			||||||
              !res.headers
 | 
					 | 
				
			||||||
                .get("content-type")
 | 
					 | 
				
			||||||
                ?.startsWith(EventStreamContentType) ||
 | 
					 | 
				
			||||||
              res.status !== 200
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              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 {
 | 
					 | 
				
			||||||
              chunkJson = JSON.parse(msg.data);
 | 
					 | 
				
			||||||
            } catch (e) {
 | 
					 | 
				
			||||||
              console.error("[Response] parse error", msg.data);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!chunkJson || chunkJson.type === "content_block_stop") {
 | 
					 | 
				
			||||||
              return finish();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const { delta } = chunkJson;
 | 
					 | 
				
			||||||
            if (delta?.text) {
 | 
					 | 
				
			||||||
              context.text += delta.text;
 | 
					 | 
				
			||||||
              options.onUpdate?.(context.text, delta.text);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onclose() {
 | 
					 | 
				
			||||||
            finish();
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onerror(e) {
 | 
					 | 
				
			||||||
            options.onError?.(e);
 | 
					 | 
				
			||||||
            throw e;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          openWhenHidden: true,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        console.error("failed to chat", e);
 | 
					 | 
				
			||||||
        options.onError?.(e as Error);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        controller.signal.onabort = () => options.onFinish("");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        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,19 +72,31 @@ 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) => ({
 | 
				
			||||||
      role: v.role,
 | 
					      // "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function",
 | 
				
			||||||
 | 
					      role: v.role === "system" ? "user" : v.role,
 | 
				
			||||||
      content: getMessageTextContent(v),
 | 
					      content: getMessageTextContent(v),
 | 
				
			||||||
    }));
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // "error_code": 336006, "error_msg": "the length of messages must be an odd number",
 | 
					    // "error_code": 336006, "error_msg": "the length of messages must be an odd number",
 | 
				
			||||||
    if (messages.length % 2 === 0) {
 | 
					    if (messages.length % 2 === 0) {
 | 
				
			||||||
 | 
					      if (messages.at(0)?.role === "user") {
 | 
				
			||||||
 | 
					        messages.splice(1, 0, {
 | 
				
			||||||
 | 
					          role: "assistant",
 | 
				
			||||||
 | 
					          content: " ",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
        messages.unshift({
 | 
					        messages.unshift({
 | 
				
			||||||
          role: "user",
 | 
					          role: "user",
 | 
				
			||||||
          content: " ",
 | 
					          content: " ",
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const modelConfig = {
 | 
					    const modelConfig = {
 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
@@ -141,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() {
 | 
				
			||||||
@@ -177,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();
 | 
				
			||||||
@@ -252,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 || [],
 | 
				
			||||||
        // animate response to make it looks smooth
 | 
					 | 
				
			||||||
        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 fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
					 | 
				
			||||||
            const fetchText = remainText.slice(0, fetchCount);
 | 
					 | 
				
			||||||
            responseText += fetchText;
 | 
					 | 
				
			||||||
            remainText = remainText.slice(fetchCount);
 | 
					 | 
				
			||||||
            options.onUpdate?.(responseText, fetchText);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          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,
 | 
					 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 | 
					        return streamWithThink(
 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					          chatPath,
 | 
				
			||||||
              responseText = await res.clone().text();
 | 
					          requestPayload,
 | 
				
			||||||
              return finish();
 | 
					          getHeaders(),
 | 
				
			||||||
            }
 | 
					          tools as any,
 | 
				
			||||||
 | 
					          funcs,
 | 
				
			||||||
            if (
 | 
					          controller,
 | 
				
			||||||
              !res.ok ||
 | 
					          // parseSSE
 | 
				
			||||||
              !res.headers
 | 
					          (text: string, runTools: ChatMessageTool[]) => {
 | 
				
			||||||
                .get("content-type")
 | 
					            // console.log("parseSSE", text, runTools);
 | 
				
			||||||
                ?.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 json = JSON.parse(text);
 | 
				
			||||||
            const choices = json.choices as Array<{
 | 
					            const choices = json.choices as Array<{
 | 
				
			||||||
                delta: { content: string };
 | 
					              delta: {
 | 
				
			||||||
 | 
					                content: string | null;
 | 
				
			||||||
 | 
					                tool_calls: ChatMessageTool[];
 | 
				
			||||||
 | 
					                reasoning_content: string | null;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
            }>;
 | 
					            }>;
 | 
				
			||||||
              const delta = choices[0]?.delta?.content;
 | 
					
 | 
				
			||||||
              if (delta) {
 | 
					            if (!choices?.length) return { isThinking: false, content: "" };
 | 
				
			||||||
                remainText += delta;
 | 
					
 | 
				
			||||||
              }
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
            } catch (e) {
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
              console.error("[Request] parse error", text, msg);
 | 
					              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,
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
          onclose() {
 | 
					 | 
				
			||||||
            finish();
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          onerror(e) {
 | 
					 | 
				
			||||||
            options.onError?.(e);
 | 
					 | 
				
			||||||
            throw e;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          openWhenHidden: true,
 | 
					 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
 | 
					              } 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: RequestPayloadForByteDance,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
      } 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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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 = "";
 | 
				
			||||||
@@ -25,11 +38,9 @@ export class GeminiProApi implements LLMApi {
 | 
				
			|||||||
      baseUrl = accessStore.googleUrl;
 | 
					      baseUrl = accessStore.googleUrl;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (baseUrl.length === 0) {
 | 
					 | 
				
			||||||
    const isApp = !!getClientConfig()?.isApp;
 | 
					    const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
      baseUrl = isApp
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
        ? DEFAULT_API_HOST + `/api/proxy/google?key=${accessStore.googleApiKey}`
 | 
					      baseUrl = isApp ? GEMINI_BASE_URL : ApiPath.Google;
 | 
				
			||||||
        : ApiPath.Google;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (baseUrl.endsWith("/")) {
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
@@ -41,19 +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";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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;
 | 
				
			||||||
@@ -106,6 +140,9 @@ export class GeminiProApi implements LLMApi {
 | 
				
			|||||||
    // if (visionModel && messages.length > 1) {
 | 
					    // if (visionModel && messages.length > 1) {
 | 
				
			||||||
    //   options.onError?.(new Error("Multiturn chat is not enabled for models/gemini-pro-vision"));
 | 
					    //   options.onError?.(new Error("Multiturn chat is not enabled for models/gemini-pro-vision"));
 | 
				
			||||||
    // }
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const modelConfig = {
 | 
					    const modelConfig = {
 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
@@ -127,19 +164,19 @@ export class GeminiProApi implements LLMApi {
 | 
				
			|||||||
      safetySettings: [
 | 
					      safetySettings: [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          category: "HARM_CATEGORY_HARASSMENT",
 | 
					          category: "HARM_CATEGORY_HARASSMENT",
 | 
				
			||||||
          threshold: "BLOCK_ONLY_HIGH",
 | 
					          threshold: accessStore.googleSafetySettings,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          category: "HARM_CATEGORY_HATE_SPEECH",
 | 
					          category: "HARM_CATEGORY_HATE_SPEECH",
 | 
				
			||||||
          threshold: "BLOCK_ONLY_HIGH",
 | 
					          threshold: accessStore.googleSafetySettings,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
 | 
					          category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
 | 
				
			||||||
          threshold: "BLOCK_ONLY_HIGH",
 | 
					          threshold: accessStore.googleSafetySettings,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          category: "HARM_CATEGORY_DANGEROUS_CONTENT",
 | 
					          category: "HARM_CATEGORY_DANGEROUS_CONTENT",
 | 
				
			||||||
          threshold: "BLOCK_ONLY_HIGH",
 | 
					          threshold: accessStore.googleSafetySettings,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
@@ -149,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",
 | 
				
			||||||
@@ -158,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 || [],
 | 
				
			||||||
        const finish = () => {
 | 
					 | 
				
			||||||
          if (!finished) {
 | 
					 | 
				
			||||||
            finished = true;
 | 
					 | 
				
			||||||
            options.onFinish(responseText + remainText);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // animate response to make it looks smooth
 | 
					 | 
				
			||||||
        function animateResponseText() {
 | 
					 | 
				
			||||||
          if (finished || controller.signal.aborted) {
 | 
					 | 
				
			||||||
            responseText += remainText;
 | 
					 | 
				
			||||||
            finish();
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (remainText.length > 0) {
 | 
					 | 
				
			||||||
            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
					 | 
				
			||||||
            const fetchText = remainText.slice(0, fetchCount);
 | 
					 | 
				
			||||||
            responseText += fetchText;
 | 
					 | 
				
			||||||
            remainText = remainText.slice(fetchCount);
 | 
					 | 
				
			||||||
            options.onUpdate?.(responseText, fetchText);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          requestAnimationFrame(animateResponseText);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // start animaion
 | 
					 | 
				
			||||||
        animateResponseText();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        controller.signal.onabort = finish;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        fetchEventSource(chatPath, {
 | 
					 | 
				
			||||||
          ...chatPayload,
 | 
					 | 
				
			||||||
          async onopen(res) {
 | 
					 | 
				
			||||||
            clearTimeout(requestTimeoutId);
 | 
					 | 
				
			||||||
            const contentType = res.headers.get("content-type");
 | 
					 | 
				
			||||||
            console.log(
 | 
					 | 
				
			||||||
              "[Gemini] request response content type: ",
 | 
					 | 
				
			||||||
              contentType,
 | 
					 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 | 
					        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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (contentType?.startsWith("text/plain")) {
 | 
					            const functionCall = chunkJson?.candidates
 | 
				
			||||||
              responseText = await res.clone().text();
 | 
					              ?.at(0)
 | 
				
			||||||
              return finish();
 | 
					              ?.content.parts.at(0)?.functionCall;
 | 
				
			||||||
            }
 | 
					            if (functionCall) {
 | 
				
			||||||
 | 
					              const { name, args } = functionCall;
 | 
				
			||||||
            if (
 | 
					              runTools.push({
 | 
				
			||||||
              !res.ok ||
 | 
					                id: nanoid(),
 | 
				
			||||||
              !res.headers
 | 
					                type: "function",
 | 
				
			||||||
                .get("content-type")
 | 
					                function: {
 | 
				
			||||||
                ?.startsWith(EventStreamContentType) ||
 | 
					                  name,
 | 
				
			||||||
              res.status !== 200
 | 
					                  arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              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 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,
 | 
					 | 
				
			||||||
              });
 | 
					              });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return chunkJson?.candidates
 | 
				
			||||||
 | 
					              ?.at(0)
 | 
				
			||||||
 | 
					              ?.content.parts?.map((part: { text: string }) => part.text)
 | 
				
			||||||
 | 
					              .join("\n\n");
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          // processToolMessage, include tool_calls message and tool call results
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            requestPayload: RequestPayload,
 | 
				
			||||||
 | 
					            toolCallMessage: any,
 | 
				
			||||||
 | 
					            toolCallResult: any[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.contents?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.contents?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                role: "model",
 | 
				
			||||||
 | 
					                parts: toolCallMessage.tool_calls.map(
 | 
				
			||||||
 | 
					                  (tool: ChatMessageTool) => ({
 | 
				
			||||||
 | 
					                    functionCall: {
 | 
				
			||||||
 | 
					                      name: tool?.function?.name,
 | 
				
			||||||
 | 
					                      args: JSON.parse(tool?.function?.arguments as string),
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  }),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              ...toolCallResult.map((result) => ({
 | 
				
			||||||
 | 
					                role: "function",
 | 
				
			||||||
 | 
					                parts: [
 | 
				
			||||||
 | 
					                  {
 | 
				
			||||||
 | 
					                    functionResponse: {
 | 
				
			||||||
 | 
					                      name: result.name,
 | 
				
			||||||
 | 
					                      response: {
 | 
				
			||||||
 | 
					                        name: result.name,
 | 
				
			||||||
 | 
					                        content: result.content, // TODO just text content...
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              })),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        const res = await fetch(chatPath, chatPayload);
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
        clearTimeout(requestTimeoutId);
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
@@ -287,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);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										253
									
								
								app/client/platforms/iflytek.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										253
									
								
								app/client/platforms/iflytek.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,253 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  IFLYTEK_BASE_URL,
 | 
				
			||||||
 | 
					  Iflytek,
 | 
				
			||||||
 | 
					  REQUEST_TIMEOUT_MS,
 | 
				
			||||||
 | 
					} from "@/app/constant";
 | 
				
			||||||
 | 
					import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} 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 { getMessageTextContent } from "@/app/utils";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SparkApi implements LLMApi {
 | 
				
			||||||
 | 
					  private disableListModels = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.iflytekUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      const apiPath = ApiPath.Iflytek;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Iflytek)) {
 | 
				
			||||||
 | 
					      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 = 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] Spark payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPath = this.path(Iflytek.ChatPath);
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        REQUEST_TIMEOUT_MS,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        let responseText = "";
 | 
				
			||||||
 | 
					        let remainText = "";
 | 
				
			||||||
 | 
					        let finished = false;
 | 
				
			||||||
 | 
					        let responseRes: Response;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Animate response text to make it look smooth
 | 
				
			||||||
 | 
					        function animateResponseText() {
 | 
				
			||||||
 | 
					          if (finished || controller.signal.aborted) {
 | 
				
			||||||
 | 
					            responseText += remainText;
 | 
				
			||||||
 | 
					            console.log("[Response Animation] finished");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (remainText.length > 0) {
 | 
				
			||||||
 | 
					            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
				
			||||||
 | 
					            const fetchText = remainText.slice(0, fetchCount);
 | 
				
			||||||
 | 
					            responseText += fetchText;
 | 
				
			||||||
 | 
					            remainText = remainText.slice(fetchCount);
 | 
				
			||||||
 | 
					            options.onUpdate?.(responseText, fetchText);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          requestAnimationFrame(animateResponseText);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Start animation
 | 
				
			||||||
 | 
					        animateResponseText();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const finish = () => {
 | 
				
			||||||
 | 
					          if (!finished) {
 | 
				
			||||||
 | 
					            finished = true;
 | 
				
			||||||
 | 
					            options.onFinish(responseText + remainText, responseRes);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        controller.signal.onabort = finish;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fetchEventSource(chatPath, {
 | 
				
			||||||
 | 
					          fetch: fetch as any,
 | 
				
			||||||
 | 
					          ...chatPayload,
 | 
				
			||||||
 | 
					          async onopen(res) {
 | 
				
			||||||
 | 
					            clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					            const contentType = res.headers.get("content-type");
 | 
				
			||||||
 | 
					            console.log("[Spark] request response content type: ", contentType);
 | 
				
			||||||
 | 
					            responseRes = res;
 | 
				
			||||||
 | 
					            if (contentType?.startsWith("text/plain")) {
 | 
				
			||||||
 | 
					              responseText = await res.clone().text();
 | 
				
			||||||
 | 
					              return finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Handle different error scenarios
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					              !res.ok ||
 | 
				
			||||||
 | 
					              !res.headers
 | 
				
			||||||
 | 
					                .get("content-type")
 | 
				
			||||||
 | 
					                ?.startsWith(EventStreamContentType) ||
 | 
				
			||||||
 | 
					              res.status !== 200
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					              let extraInfo = await res.clone().text();
 | 
				
			||||||
 | 
					              try {
 | 
				
			||||||
 | 
					                const resJson = await res.clone().json();
 | 
				
			||||||
 | 
					                extraInfo = prettyObject(resJson);
 | 
				
			||||||
 | 
					              } catch {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (res.status === 401) {
 | 
				
			||||||
 | 
					                extraInfo = Locale.Error.Unauthorized;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              options.onError?.(
 | 
				
			||||||
 | 
					                new Error(
 | 
				
			||||||
 | 
					                  `Request failed with status ${res.status}: ${extraInfo}`,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					              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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              if (delta) {
 | 
				
			||||||
 | 
					                remainText += delta;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					              console.error("[Request] parse error", text);
 | 
				
			||||||
 | 
					              options.onError?.(new Error(`Failed to parse response: ${text}`));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onclose() {
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onerror(e) {
 | 
				
			||||||
 | 
					            options.onError?.(e);
 | 
				
			||||||
 | 
					            throw e;
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          openWhenHidden: true,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        const res = await fetch(chatPath, chatPayload);
 | 
				
			||||||
 | 
					        clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!res.ok) {
 | 
				
			||||||
 | 
					          const errorText = await res.text();
 | 
				
			||||||
 | 
					          options.onError?.(
 | 
				
			||||||
 | 
					            new Error(`Request failed with status ${res.status}: ${errorText}`),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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 [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										200
									
								
								app/client/platforms/moonshot.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								app/client/platforms/moonshot.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,200 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					// azure and openai, using same models. so using same LLMApi.
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ApiPath,
 | 
				
			||||||
 | 
					  MOONSHOT_BASE_URL,
 | 
				
			||||||
 | 
					  Moonshot,
 | 
				
			||||||
 | 
					  REQUEST_TIMEOUT_MS,
 | 
				
			||||||
 | 
					} 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 } from "@/app/utils";
 | 
				
			||||||
 | 
					import { RequestPayload } from "./openai";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class MoonshotApi implements LLMApi {
 | 
				
			||||||
 | 
					  private disableListModels = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  path(path: string): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.moonshotUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      const apiPath = ApiPath.Moonshot;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Moonshot)) {
 | 
				
			||||||
 | 
					      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 = 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(Moonshot.ChatPath);
 | 
				
			||||||
 | 
					      const chatPayload = {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify(requestPayload),
 | 
				
			||||||
 | 
					        signal: controller.signal,
 | 
				
			||||||
 | 
					        headers: getHeaders(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // make a fetch request
 | 
				
			||||||
 | 
					      const requestTimeoutId = setTimeout(
 | 
				
			||||||
 | 
					        () => controller.abort(),
 | 
				
			||||||
 | 
					        REQUEST_TIMEOUT_MS,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      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 [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -2,17 +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,
 | 
					 | 
				
			||||||
  DEFAULT_MODELS,
 | 
					 | 
				
			||||||
  OpenaiPath,
 | 
					 | 
				
			||||||
  Azure,
 | 
					  Azure,
 | 
				
			||||||
 | 
					  DEFAULT_MODELS,
 | 
				
			||||||
 | 
					  OPENAI_BASE_URL,
 | 
				
			||||||
 | 
					  OpenaiPath,
 | 
				
			||||||
  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 { preProcessImageContent } from "@/app/utils/chat";
 | 
					import {
 | 
				
			||||||
 | 
					  base64Image2Blob,
 | 
				
			||||||
 | 
					  preProcessImageContent,
 | 
				
			||||||
 | 
					  streamWithThink,
 | 
				
			||||||
 | 
					  uploadImage,
 | 
				
			||||||
 | 
					} from "@/app/utils/chat";
 | 
				
			||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
					import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
				
			||||||
 | 
					import { DalleQuality, DalleStyle, ModelSize } from "@/app/typing";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ChatOptions,
 | 
					  ChatOptions,
 | 
				
			||||||
@@ -21,19 +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,
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					  isDalle3 as _isDalle3,
 | 
				
			||||||
  isVisionModel,
 | 
					  isVisionModel,
 | 
				
			||||||
} from "@/app/utils";
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface OpenAIListModelResponse {
 | 
					export interface OpenAIListModelResponse {
 | 
				
			||||||
  object: string;
 | 
					  object: string;
 | 
				
			||||||
@@ -56,6 +66,17 @@ 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 {
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					  prompt: string;
 | 
				
			||||||
 | 
					  response_format: "url" | "b64_json";
 | 
				
			||||||
 | 
					  n: number;
 | 
				
			||||||
 | 
					  size: ModelSize;
 | 
				
			||||||
 | 
					  quality: DalleQuality;
 | 
				
			||||||
 | 
					  style: DalleStyle;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ChatGPTApi implements LLMApi {
 | 
					export class ChatGPTApi implements LLMApi {
 | 
				
			||||||
@@ -80,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("/")) {
 | 
				
			||||||
@@ -100,20 +121,69 @@ export class ChatGPTApi implements LLMApi {
 | 
				
			|||||||
    return cloudflareAIGatewayUrl([baseUrl, path].join("/"));
 | 
					    return cloudflareAIGatewayUrl([baseUrl, path].join("/"));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  extractMessage(res: any) {
 | 
					  async extractMessage(res: any) {
 | 
				
			||||||
    return res.choices?.at(0)?.message?.content ?? "";
 | 
					    if (res.error) {
 | 
				
			||||||
 | 
					      return "```\n" + JSON.stringify(res, null, 4) + "\n```";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // dalle3 model return url, using url create image message
 | 
				
			||||||
 | 
					    if (res.data) {
 | 
				
			||||||
 | 
					      let url = res.data?.at(0)?.url ?? "";
 | 
				
			||||||
 | 
					      const b64_json = res.data?.at(0)?.b64_json ?? "";
 | 
				
			||||||
 | 
					      if (!url && b64_json) {
 | 
				
			||||||
 | 
					        // uploadImage
 | 
				
			||||||
 | 
					        url = await uploadImage(base64Image2Blob(b64_json, "image/png"));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: "image_url",
 | 
				
			||||||
 | 
					          image_url: {
 | 
				
			||||||
 | 
					            url,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    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 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 = {
 | 
					    const modelConfig = {
 | 
				
			||||||
      ...useAppConfig.getState().modelConfig,
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
@@ -123,26 +193,64 @@ export class ChatGPTApi implements LLMApi {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const requestPayload: RequestPayload = {
 | 
					    let requestPayload: RequestPayload | DalleRequestPayload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isDalle3 = _isDalle3(options.config.model);
 | 
				
			||||||
 | 
					    const isO1OrO3 =
 | 
				
			||||||
 | 
					      options.config.model.startsWith("o1") ||
 | 
				
			||||||
 | 
					      options.config.model.startsWith("o3");
 | 
				
			||||||
 | 
					    if (isDalle3) {
 | 
				
			||||||
 | 
					      const prompt = getMessageTextContent(
 | 
				
			||||||
 | 
					        options.messages.slice(-1)?.pop() as any,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      requestPayload = {
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					        prompt,
 | 
				
			||||||
 | 
					        // URLs are only valid for 60 minutes after the image has been generated.
 | 
				
			||||||
 | 
					        response_format: "b64_json", // using b64_json, and save image in CacheStorage
 | 
				
			||||||
 | 
					        n: 1,
 | 
				
			||||||
 | 
					        size: options.config?.size ?? "1024x1024",
 | 
				
			||||||
 | 
					        quality: options.config?.quality ?? "standard",
 | 
				
			||||||
 | 
					        style: options.config?.style ?? "vivid",
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      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);
 | 
				
			||||||
 | 
					        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 = {
 | 
				
			||||||
        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) {
 | 
				
			||||||
        requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
 | 
					        requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    console.log("[Request] openai payload: ", requestPayload);
 | 
					    console.log("[Request] openai payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const shouldStream = !!options.config.stream;
 | 
					    const shouldStream = !isDalle3 && !!options.config.stream;
 | 
				
			||||||
    const controller = new AbortController();
 | 
					    const controller = new AbortController();
 | 
				
			||||||
    options.onController?.(controller);
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -168,14 +276,126 @@ export class ChatGPTApi implements LLMApi {
 | 
				
			|||||||
            model?.provider?.providerName === ServiceProvider.Azure,
 | 
					            model?.provider?.providerName === ServiceProvider.Azure,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        chatPath = this.path(
 | 
					        chatPath = this.path(
 | 
				
			||||||
          Azure.ChatPath(
 | 
					          (isDalle3 ? Azure.ImagePath : Azure.ChatPath)(
 | 
				
			||||||
            (model?.displayName ?? model?.name) as string,
 | 
					            (model?.displayName ?? model?.name) as string,
 | 
				
			||||||
            useCustomConfig ? useAccessStore.getState().azureApiVersion : "",
 | 
					            useCustomConfig ? useAccessStore.getState().azureApiVersion : "",
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        chatPath = this.path(OpenaiPath.ChatPath);
 | 
					        chatPath = this.path(
 | 
				
			||||||
 | 
					          isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      if (shouldStream) {
 | 
				
			||||||
 | 
					        let index = -1;
 | 
				
			||||||
 | 
					        const [tools, funcs] = usePluginStore
 | 
				
			||||||
 | 
					          .getState()
 | 
				
			||||||
 | 
					          .getAsTools(
 | 
				
			||||||
 | 
					            useChatStore.getState().currentSession().mask?.plugin || [],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        // console.log("getAsTools", tools, funcs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Add "include_reasoning" for OpenRouter: https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models
 | 
				
			||||||
 | 
					        if (chatPath.includes("openrouter.ai")) {
 | 
				
			||||||
 | 
					          // @ts-ignore
 | 
				
			||||||
 | 
					          requestPayload["include_reasoning"] = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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;
 | 
				
			||||||
 | 
					                reasoning: string | null;
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!choices?.length) return { isThinking: false, content: "" };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const tool_calls = choices[0]?.delta?.tool_calls;
 | 
				
			||||||
 | 
					            if (tool_calls?.length > 0) {
 | 
				
			||||||
 | 
					              const id = tool_calls[0]?.id;
 | 
				
			||||||
 | 
					              const args = tool_calls[0]?.function?.arguments;
 | 
				
			||||||
 | 
					              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;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const reasoning =
 | 
				
			||||||
 | 
					              choices[0]?.delta?.reasoning_content ||
 | 
				
			||||||
 | 
					              choices[0]?.delta?.reasoning;
 | 
				
			||||||
 | 
					            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[],
 | 
				
			||||||
 | 
					          ) => {
 | 
				
			||||||
 | 
					            // reset index value
 | 
				
			||||||
 | 
					            index = -1;
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            requestPayload?.messages?.splice(
 | 
				
			||||||
 | 
					              // @ts-ignore
 | 
				
			||||||
 | 
					              requestPayload?.messages?.length,
 | 
				
			||||||
 | 
					              0,
 | 
				
			||||||
 | 
					              toolCallMessage,
 | 
				
			||||||
 | 
					              ...toolCallResult,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
        const chatPayload = {
 | 
					        const chatPayload = {
 | 
				
			||||||
          method: "POST",
 | 
					          method: "POST",
 | 
				
			||||||
          body: JSON.stringify(requestPayload),
 | 
					          body: JSON.stringify(requestPayload),
 | 
				
			||||||
@@ -186,145 +406,22 @@ export class ChatGPTApi 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) {
 | 
					 | 
				
			||||||
        let responseText = "";
 | 
					 | 
				
			||||||
        let remainText = "";
 | 
					 | 
				
			||||||
        let finished = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // animate response to make it looks smooth
 | 
					 | 
				
			||||||
        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 fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
					 | 
				
			||||||
            const fetchText = remainText.slice(0, fetchCount);
 | 
					 | 
				
			||||||
            responseText += fetchText;
 | 
					 | 
				
			||||||
            remainText = remainText.slice(fetchCount);
 | 
					 | 
				
			||||||
            options.onUpdate?.(responseText, fetchText);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          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(
 | 
					 | 
				
			||||||
              "[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 = 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);
 | 
				
			||||||
      options.onError?.(e as Error);
 | 
					      options.onError?.(e as Error);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async usage() {
 | 
					  async usage() {
 | 
				
			||||||
    const formatDate = (d: Date) =>
 | 
					    const formatDate = (d: Date) =>
 | 
				
			||||||
      `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d
 | 
					      `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d
 | 
				
			||||||
@@ -404,22 +501,29 @@ 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) {
 | 
				
			||||||
      return [];
 | 
					      return [];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //由于目前 OpenAI 的 disableListModels 默认为 true,所以当前实际不会运行到这场
 | 
				
			||||||
 | 
					    let seq = 1000; //同 Constant.ts 中的排序保持一致
 | 
				
			||||||
    return chatModels.map((m) => ({
 | 
					    return chatModels.map((m) => ({
 | 
				
			||||||
      name: m.id,
 | 
					      name: m.id,
 | 
				
			||||||
      available: true,
 | 
					      available: true,
 | 
				
			||||||
 | 
					      sorted: seq++,
 | 
				
			||||||
      provider: {
 | 
					      provider: {
 | 
				
			||||||
        id: "openai",
 | 
					        id: "openai",
 | 
				
			||||||
        providerName: "OpenAI",
 | 
					        providerName: "OpenAI",
 | 
				
			||||||
        providerType: "openai",
 | 
					        providerType: "openai",
 | 
				
			||||||
 | 
					        sorted: 1,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    }));
 | 
					    }));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { OpenaiPath };
 | 
					export { OpenaiPath };
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										287
									
								
								app/client/platforms/siliconflow.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										278
									
								
								app/client/platforms/tencent.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								app/client/platforms/tencent.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,278 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					import { ApiPath, TENCENT_BASE_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ChatOptions,
 | 
				
			||||||
 | 
					  getHeaders,
 | 
				
			||||||
 | 
					  LLMApi,
 | 
				
			||||||
 | 
					  LLMModel,
 | 
				
			||||||
 | 
					  MultimodalContent,
 | 
				
			||||||
 | 
					  SpeechOptions,
 | 
				
			||||||
 | 
					} 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 {
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  isVisionModel,
 | 
				
			||||||
 | 
					  getTimeoutMSByModel,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import mapKeys from "lodash-es/mapKeys";
 | 
				
			||||||
 | 
					import mapValues from "lodash-es/mapValues";
 | 
				
			||||||
 | 
					import isArray from "lodash-es/isArray";
 | 
				
			||||||
 | 
					import isObject from "lodash-es/isObject";
 | 
				
			||||||
 | 
					import { fetch } from "@/app/utils/stream";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface OpenAIListModelResponse {
 | 
				
			||||||
 | 
					  object: string;
 | 
				
			||||||
 | 
					  data: Array<{
 | 
				
			||||||
 | 
					    id: string;
 | 
				
			||||||
 | 
					    object: string;
 | 
				
			||||||
 | 
					    root: string;
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface RequestPayload {
 | 
				
			||||||
 | 
					  Messages: {
 | 
				
			||||||
 | 
					    Role: "system" | "user" | "assistant";
 | 
				
			||||||
 | 
					    Content: string | MultimodalContent[];
 | 
				
			||||||
 | 
					  }[];
 | 
				
			||||||
 | 
					  Stream?: boolean;
 | 
				
			||||||
 | 
					  Model: string;
 | 
				
			||||||
 | 
					  Temperature: number;
 | 
				
			||||||
 | 
					  TopP: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function capitalizeKeys(obj: any): any {
 | 
				
			||||||
 | 
					  if (isArray(obj)) {
 | 
				
			||||||
 | 
					    return obj.map(capitalizeKeys);
 | 
				
			||||||
 | 
					  } else if (isObject(obj)) {
 | 
				
			||||||
 | 
					    return mapValues(
 | 
				
			||||||
 | 
					      mapKeys(obj, (value: any, key: string) =>
 | 
				
			||||||
 | 
					        key.replace(/(^|_)(\w)/g, (m, $1, $2) => $2.toUpperCase()),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      capitalizeKeys,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return obj;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class HunyuanApi implements LLMApi {
 | 
				
			||||||
 | 
					  path(): string {
 | 
				
			||||||
 | 
					    const accessStore = useAccessStore.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let baseUrl = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (accessStore.useCustomConfig) {
 | 
				
			||||||
 | 
					      baseUrl = accessStore.tencentUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.length === 0) {
 | 
				
			||||||
 | 
					      const isApp = !!getClientConfig()?.isApp;
 | 
				
			||||||
 | 
					      baseUrl = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (baseUrl.endsWith("/")) {
 | 
				
			||||||
 | 
					      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Tencent)) {
 | 
				
			||||||
 | 
					      baseUrl = "https://" + baseUrl;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Proxy Endpoint] ", baseUrl);
 | 
				
			||||||
 | 
					    return baseUrl;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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 = options.messages.map((v, index) => ({
 | 
				
			||||||
 | 
					      // "Messages 中 system 角色必须位于列表的最开始"
 | 
				
			||||||
 | 
					      role: index !== 0 && v.role === "system" ? "user" : v.role,
 | 
				
			||||||
 | 
					      content: visionModel ? v.content : getMessageTextContent(v),
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modelConfig = {
 | 
				
			||||||
 | 
					      ...useAppConfig.getState().modelConfig,
 | 
				
			||||||
 | 
					      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
				
			||||||
 | 
					      ...{
 | 
				
			||||||
 | 
					        model: options.config.model,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const requestPayload: RequestPayload = capitalizeKeys({
 | 
				
			||||||
 | 
					      model: modelConfig.model,
 | 
				
			||||||
 | 
					      messages,
 | 
				
			||||||
 | 
					      temperature: modelConfig.temperature,
 | 
				
			||||||
 | 
					      top_p: modelConfig.top_p,
 | 
				
			||||||
 | 
					      stream: options.config.stream,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("[Request] Tencent payload: ", requestPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldStream = !!options.config.stream;
 | 
				
			||||||
 | 
					    const controller = new AbortController();
 | 
				
			||||||
 | 
					    options.onController?.(controller);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const chatPath = this.path();
 | 
				
			||||||
 | 
					      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) {
 | 
				
			||||||
 | 
					        let responseText = "";
 | 
				
			||||||
 | 
					        let remainText = "";
 | 
				
			||||||
 | 
					        let finished = false;
 | 
				
			||||||
 | 
					        let responseRes: Response;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // animate response to make it looks smooth
 | 
				
			||||||
 | 
					        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 fetchCount = Math.max(1, Math.round(remainText.length / 60));
 | 
				
			||||||
 | 
					            const fetchText = remainText.slice(0, fetchCount);
 | 
				
			||||||
 | 
					            responseText += fetchText;
 | 
				
			||||||
 | 
					            remainText = remainText.slice(fetchCount);
 | 
				
			||||||
 | 
					            options.onUpdate?.(responseText, fetchText);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          requestAnimationFrame(animateResponseText);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // start animaion
 | 
				
			||||||
 | 
					        animateResponseText();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const finish = () => {
 | 
				
			||||||
 | 
					          if (!finished) {
 | 
				
			||||||
 | 
					            finished = true;
 | 
				
			||||||
 | 
					            options.onFinish(responseText + remainText, responseRes);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        controller.signal.onabort = finish;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fetchEventSource(chatPath, {
 | 
				
			||||||
 | 
					          fetch: fetch as any,
 | 
				
			||||||
 | 
					          ...chatPayload,
 | 
				
			||||||
 | 
					          async onopen(res) {
 | 
				
			||||||
 | 
					            clearTimeout(requestTimeoutId);
 | 
				
			||||||
 | 
					            const contentType = res.headers.get("content-type");
 | 
				
			||||||
 | 
					            console.log(
 | 
				
			||||||
 | 
					              "[Tencent] request response content type: ",
 | 
				
			||||||
 | 
					              contentType,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            responseRes = res;
 | 
				
			||||||
 | 
					            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;
 | 
				
			||||||
 | 
					              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 {
 | 
				
			||||||
 | 
					        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 [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										194
									
								
								app/client/platforms/xai.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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,16 +38,20 @@ interface ChatCommands {
 | 
				
			|||||||
  next?: Command;
 | 
					  next?: Command;
 | 
				
			||||||
  prev?: Command;
 | 
					  prev?: Command;
 | 
				
			||||||
  clear?: Command;
 | 
					  clear?: Command;
 | 
				
			||||||
 | 
					  fork?: Command;
 | 
				
			||||||
  del?: Command;
 | 
					  del?: Command;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ChatCommandPrefix = ":";
 | 
					// Compatible with Chinese colon character ":"
 | 
				
			||||||
 | 
					export const ChatCommandPrefix = /^[::]/;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useChatCommand(commands: ChatCommands = {}) {
 | 
					export function useChatCommand(commands: ChatCommands = {}) {
 | 
				
			||||||
  function extract(userInput: string) {
 | 
					  function extract(userInput: string) {
 | 
				
			||||||
    return (
 | 
					    const match = userInput.match(ChatCommandPrefix);
 | 
				
			||||||
      userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput
 | 
					    if (match) {
 | 
				
			||||||
    ) as keyof ChatCommands;
 | 
					      return userInput.slice(1) as keyof ChatCommands;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return userInput as keyof ChatCommands;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function search(userInput: string) {
 | 
					  function search(userInput: string) {
 | 
				
			||||||
@@ -57,7 +61,7 @@ export function useChatCommand(commands: ChatCommands = {}) {
 | 
				
			|||||||
      .filter((c) => c.startsWith(input))
 | 
					      .filter((c) => c.startsWith(input))
 | 
				
			||||||
      .map((c) => ({
 | 
					      .map((c) => ({
 | 
				
			||||||
        title: desc[c as keyof ChatCommands],
 | 
					        title: desc[c as keyof ChatCommands],
 | 
				
			||||||
        content: ChatCommandPrefix + c,
 | 
					        content: ":" + c,
 | 
				
			||||||
      }));
 | 
					      }));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										31
									
								
								app/components/artifacts.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								app/components/artifacts.module.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					.artifacts {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  &-header {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    height: 36px;
 | 
				
			||||||
 | 
					    padding: 20px;
 | 
				
			||||||
 | 
					    background: var(--second);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  &-title {
 | 
				
			||||||
 | 
					    flex: 1;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    font-size: 24px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  &-content {
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    padding: 0 20px 20px 20px;
 | 
				
			||||||
 | 
					    background-color: var(--second);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.artifacts-iframe {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  border: var(--border-in-light);
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  background-color: var(--gray);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										266
									
								
								app/components/artifacts.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								app/components/artifacts.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,266 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					  useEffect,
 | 
				
			||||||
 | 
					  useState,
 | 
				
			||||||
 | 
					  useRef,
 | 
				
			||||||
 | 
					  useMemo,
 | 
				
			||||||
 | 
					  forwardRef,
 | 
				
			||||||
 | 
					  useImperativeHandle,
 | 
				
			||||||
 | 
					} from "react";
 | 
				
			||||||
 | 
					import { useParams } from "react-router";
 | 
				
			||||||
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
 | 
					import { nanoid } from "nanoid";
 | 
				
			||||||
 | 
					import ExportIcon from "../icons/share.svg";
 | 
				
			||||||
 | 
					import CopyIcon from "../icons/copy.svg";
 | 
				
			||||||
 | 
					import DownloadIcon from "../icons/download.svg";
 | 
				
			||||||
 | 
					import GithubIcon from "../icons/github.svg";
 | 
				
			||||||
 | 
					import LoadingButtonIcon from "../icons/loading.svg";
 | 
				
			||||||
 | 
					import ReloadButtonIcon from "../icons/reload.svg";
 | 
				
			||||||
 | 
					import Locale from "../locales";
 | 
				
			||||||
 | 
					import { Modal, showToast } from "./ui-lib";
 | 
				
			||||||
 | 
					import { copyToClipboard, downloadAs } from "../utils";
 | 
				
			||||||
 | 
					import { Path, ApiPath, REPO_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					import { Loading } from "./home";
 | 
				
			||||||
 | 
					import styles from "./artifacts.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type HTMLPreviewProps = {
 | 
				
			||||||
 | 
					  code: string;
 | 
				
			||||||
 | 
					  autoHeight?: boolean;
 | 
				
			||||||
 | 
					  height?: number | string;
 | 
				
			||||||
 | 
					  onLoad?: (title?: string) => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type HTMLPreviewHander = {
 | 
				
			||||||
 | 
					  reload: () => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
 | 
				
			||||||
 | 
					  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);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <iframe
 | 
				
			||||||
 | 
					        className={styles["artifacts-iframe"]}
 | 
				
			||||||
 | 
					        key={frameId}
 | 
				
			||||||
 | 
					        ref={iframeRef}
 | 
				
			||||||
 | 
					        sandbox="allow-forms allow-modals allow-scripts"
 | 
				
			||||||
 | 
					        style={{ height }}
 | 
				
			||||||
 | 
					        srcDoc={srcDoc}
 | 
				
			||||||
 | 
					        onLoad={handleOnLoad}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ArtifactsShareButton({
 | 
				
			||||||
 | 
					  getCode,
 | 
				
			||||||
 | 
					  id,
 | 
				
			||||||
 | 
					  style,
 | 
				
			||||||
 | 
					  fileName,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  getCode: () => string;
 | 
				
			||||||
 | 
					  id?: string;
 | 
				
			||||||
 | 
					  style?: any;
 | 
				
			||||||
 | 
					  fileName?: string;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const [loading, setLoading] = useState(false);
 | 
				
			||||||
 | 
					  const [name, setName] = useState(id);
 | 
				
			||||||
 | 
					  const [show, setShow] = useState(false);
 | 
				
			||||||
 | 
					  const shareUrl = useMemo(
 | 
				
			||||||
 | 
					    () => [location.origin, "#", Path.Artifacts, "/", name].join(""),
 | 
				
			||||||
 | 
					    [name],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const upload = (code: string) =>
 | 
				
			||||||
 | 
					    id
 | 
				
			||||||
 | 
					      ? Promise.resolve({ id })
 | 
				
			||||||
 | 
					      : fetch(ApiPath.Artifacts, {
 | 
				
			||||||
 | 
					          method: "POST",
 | 
				
			||||||
 | 
					          body: code,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					          .then((res) => res.json())
 | 
				
			||||||
 | 
					          .then(({ id }) => {
 | 
				
			||||||
 | 
					            if (id) {
 | 
				
			||||||
 | 
					              return { id };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            throw Error();
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          .catch((e) => {
 | 
				
			||||||
 | 
					            showToast(Locale.Export.Artifacts.Error);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <div className="window-action-button" style={style}>
 | 
				
			||||||
 | 
					        <IconButton
 | 
				
			||||||
 | 
					          icon={loading ? <LoadingButtonIcon /> : <ExportIcon />}
 | 
				
			||||||
 | 
					          bordered
 | 
				
			||||||
 | 
					          title={Locale.Export.Artifacts.Title}
 | 
				
			||||||
 | 
					          onClick={() => {
 | 
				
			||||||
 | 
					            if (loading) return;
 | 
				
			||||||
 | 
					            setLoading(true);
 | 
				
			||||||
 | 
					            upload(getCode())
 | 
				
			||||||
 | 
					              .then((res) => {
 | 
				
			||||||
 | 
					                if (res?.id) {
 | 
				
			||||||
 | 
					                  setShow(true);
 | 
				
			||||||
 | 
					                  setName(res?.id);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					              .finally(() => setLoading(false));
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      {show && (
 | 
				
			||||||
 | 
					        <div className="modal-mask">
 | 
				
			||||||
 | 
					          <Modal
 | 
				
			||||||
 | 
					            title={Locale.Export.Artifacts.Title}
 | 
				
			||||||
 | 
					            onClose={() => setShow(false)}
 | 
				
			||||||
 | 
					            actions={[
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                key="download"
 | 
				
			||||||
 | 
					                icon={<DownloadIcon />}
 | 
				
			||||||
 | 
					                bordered
 | 
				
			||||||
 | 
					                text={Locale.Export.Download}
 | 
				
			||||||
 | 
					                onClick={() => {
 | 
				
			||||||
 | 
					                  downloadAs(getCode(), `${fileName || name}.html`).then(() =>
 | 
				
			||||||
 | 
					                    setShow(false),
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />,
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                key="copy"
 | 
				
			||||||
 | 
					                icon={<CopyIcon />}
 | 
				
			||||||
 | 
					                bordered
 | 
				
			||||||
 | 
					                text={Locale.Chat.Actions.Copy}
 | 
				
			||||||
 | 
					                onClick={() => {
 | 
				
			||||||
 | 
					                  copyToClipboard(shareUrl).then(() => setShow(false));
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />,
 | 
				
			||||||
 | 
					            ]}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <div>
 | 
				
			||||||
 | 
					              <a target="_blank" href={shareUrl}>
 | 
				
			||||||
 | 
					                {shareUrl}
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </Modal>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Artifacts() {
 | 
				
			||||||
 | 
					  const { id } = useParams();
 | 
				
			||||||
 | 
					  const [code, setCode] = useState("");
 | 
				
			||||||
 | 
					  const [loading, setLoading] = useState(true);
 | 
				
			||||||
 | 
					  const [fileName, setFileName] = useState("");
 | 
				
			||||||
 | 
					  const previewRef = useRef<HTMLPreviewHander>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (id) {
 | 
				
			||||||
 | 
					      fetch(`${ApiPath.Artifacts}?id=${id}`)
 | 
				
			||||||
 | 
					        .then((res) => {
 | 
				
			||||||
 | 
					          if (res.status > 300) {
 | 
				
			||||||
 | 
					            throw Error("can not get content");
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return res;
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .then((res) => res.text())
 | 
				
			||||||
 | 
					        .then(setCode)
 | 
				
			||||||
 | 
					        .catch((e) => {
 | 
				
			||||||
 | 
					          showToast(Locale.Export.Artifacts.Error);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [id]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={styles["artifacts"]}>
 | 
				
			||||||
 | 
					      <div className={styles["artifacts-header"]}>
 | 
				
			||||||
 | 
					        <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
 | 
				
			||||||
 | 
					          <IconButton bordered icon={<GithubIcon />} shadow />
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					        <IconButton
 | 
				
			||||||
 | 
					          bordered
 | 
				
			||||||
 | 
					          style={{ marginLeft: 20 }}
 | 
				
			||||||
 | 
					          icon={<ReloadButtonIcon />}
 | 
				
			||||||
 | 
					          shadow
 | 
				
			||||||
 | 
					          onClick={() => previewRef.current?.reload()}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <div className={styles["artifacts-title"]}>NextChat Artifacts</div>
 | 
				
			||||||
 | 
					        <ArtifactsShareButton
 | 
				
			||||||
 | 
					          id={id}
 | 
				
			||||||
 | 
					          getCode={() => code}
 | 
				
			||||||
 | 
					          fileName={fileName}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div className={styles["artifacts-content"]}>
 | 
				
			||||||
 | 
					        {loading && <Loading />}
 | 
				
			||||||
 | 
					        {code && (
 | 
				
			||||||
 | 
					          <HTMLPreview
 | 
				
			||||||
 | 
					            code={code}
 | 
				
			||||||
 | 
					            ref={previewRef}
 | 
				
			||||||
 | 
					            autoHeight={false}
 | 
				
			||||||
 | 
					            height={"100%"}
 | 
				
			||||||
 | 
					            onLoad={(title) => {
 | 
				
			||||||
 | 
					              setFileName(title as string);
 | 
				
			||||||
 | 
					              setLoading(false);
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
import * as React from "react";
 | 
					import * as React from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import styles from "./button.module.scss";
 | 
					import styles from "./button.module.scss";
 | 
				
			||||||
 | 
					import { CSSProperties } from "react";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ButtonType = "primary" | "danger" | null;
 | 
					export type ButtonType = "primary" | "danger" | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -16,35 +18,48 @@ export function IconButton(props: {
 | 
				
			|||||||
  disabled?: boolean;
 | 
					  disabled?: boolean;
 | 
				
			||||||
  tabIndex?: number;
 | 
					  tabIndex?: number;
 | 
				
			||||||
  autoFocus?: boolean;
 | 
					  autoFocus?: boolean;
 | 
				
			||||||
 | 
					  style?: CSSProperties;
 | 
				
			||||||
 | 
					  aria?: string;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  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}
 | 
				
			||||||
      role="button"
 | 
					      role="button"
 | 
				
			||||||
      tabIndex={props.tabIndex}
 | 
					      tabIndex={props.tabIndex}
 | 
				
			||||||
      autoFocus={props.autoFocus}
 | 
					      autoFocus={props.autoFocus}
 | 
				
			||||||
 | 
					      style={props.style}
 | 
				
			||||||
 | 
					      aria-label={props.aria}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      {props.icon && (
 | 
					      {props.icon && (
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
          className={
 | 
					          aria-label={props.text || props.title}
 | 
				
			||||||
            styles["icon-button-icon"] +
 | 
					          className={clsx(styles["icon-button-icon"], {
 | 
				
			||||||
            ` ${props.type === "primary" && "no-dark"}`
 | 
					            "no-dark": props.type === "primary",
 | 
				
			||||||
          }
 | 
					          })}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          {props.icon}
 | 
					          {props.icon}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {props.text && (
 | 
					      {props.text && (
 | 
				
			||||||
        <div className={styles["icon-button-text"]}>{props.text}</div>
 | 
					        <div
 | 
				
			||||||
 | 
					          aria-label={props.text || props.title}
 | 
				
			||||||
 | 
					          className={styles["icon-button-text"]}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {props.text}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"], {
 | 
				
			||||||
 | 
					            [styles["chat-item-selected"]]:
 | 
				
			||||||
              props.selected &&
 | 
					              props.selected &&
 | 
				
			||||||
            (currentPath === Path.Chat || currentPath === Path.Home) &&
 | 
					              (currentPath === Path.Chat || currentPath === Path.Home),
 | 
				
			||||||
            styles["chat-item-selected"]
 | 
					          })}
 | 
				
			||||||
          }`}
 | 
					 | 
				
			||||||
          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(
 | 
				
			||||||
 | 
					    to right,
 | 
				
			||||||
    rgba(0, 0, 0, 0),
 | 
					    rgba(0, 0, 0, 0),
 | 
				
			||||||
    rgba(0, 0, 0, 1),
 | 
					    rgba(0, 0, 0, 1),
 | 
				
			||||||
      rgba(0, 0, 0, 0));
 | 
					    rgba(0, 0, 0, 0)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
  mask-image: $linear;
 | 
					  mask-image: $linear;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @mixin show {
 | 
					  @mixin show {
 | 
				
			||||||
@@ -346,6 +352,12 @@
 | 
				
			|||||||
      flex-wrap: nowrap;
 | 
					      flex-wrap: nowrap;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chat-model-name {
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					    color: var(--black);
 | 
				
			||||||
 | 
					    margin-left: 6px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-message-container {
 | 
					.chat-message-container {
 | 
				
			||||||
@@ -367,7 +379,7 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chat-message-user>.chat-message-container {
 | 
					.chat-message-user > .chat-message-container {
 | 
				
			||||||
  align-items: flex-end;
 | 
					  align-items: flex-end;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -407,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%;
 | 
				
			||||||
@@ -422,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;
 | 
				
			||||||
@@ -450,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;
 | 
				
			||||||
@@ -460,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;
 | 
				
			||||||
@@ -476,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);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -494,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 {
 | 
				
			||||||
@@ -605,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);
 | 
				
			||||||
@@ -625,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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -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>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import React from "react";
 | 
					import React from "react";
 | 
				
			||||||
import { IconButton } from "./button";
 | 
					import { IconButton } from "./button";
 | 
				
			||||||
import GithubIcon from "../icons/github.svg";
 | 
					import GithubIcon from "../icons/github.svg";
 | 
				
			||||||
@@ -6,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;
 | 
				
			||||||
@@ -28,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"
 | 
				
			||||||
@@ -541,12 +526,15 @@ export function ImagePreviewer(props: {
 | 
				
			|||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
            <div className={styles["main-title"]}>NextChat</div>
 | 
					            <div className={styles["main-title"]}>NextChat</div>
 | 
				
			||||||
            <div className={styles["sub-title"]}>
 | 
					            <div className={styles["sub-title"]}>
 | 
				
			||||||
              github.com/Yidadaa/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,19 +558,25 @@ 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"]}>
 | 
				
			||||||
                <Markdown
 | 
					                <Markdown
 | 
				
			||||||
                  content={getMessageTextContent(m)}
 | 
					                  content={getMessageTextContent(m)}
 | 
				
			||||||
                  fontSize={config.fontSize}
 | 
					                  fontSize={config.fontSize}
 | 
				
			||||||
 | 
					                  fontFamily={config.fontFamily}
 | 
				
			||||||
                  defaultShow
 | 
					                  defaultShow
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
                {getMessageImages(m).length == 1 && (
 | 
					                {getMessageImages(m).length == 1 && (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -137,12 +137,21 @@
 | 
				
			|||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  padding-top: 20px;
 | 
					  padding-top: 20px;
 | 
				
			||||||
  padding-bottom: 20px;
 | 
					  padding-bottom: 20px;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  &-narrow {
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.sidebar-logo {
 | 
					.sidebar-logo {
 | 
				
			||||||
  position: absolute;
 | 
					  display: inline-flex;
 | 
				
			||||||
  right: 0;
 | 
					}
 | 
				
			||||||
  bottom: 18px;
 | 
					
 | 
				
			||||||
 | 
					.sidebar-title-container {
 | 
				
			||||||
 | 
					  display: inline-flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.sidebar-title {
 | 
					.sidebar-title {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,16 +28,22 @@ 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>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Artifacts = dynamic(async () => (await import("./artifacts")).Artifacts, {
 | 
				
			||||||
 | 
					  loading: () => <Loading noLogo />,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Settings = dynamic(async () => (await import("./settings")).Settings, {
 | 
					const Settings = dynamic(async () => (await import("./settings")).Settings, {
 | 
				
			||||||
  loading: () => <Loading noLogo />,
 | 
					  loading: () => <Loading noLogo />,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -55,6 +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, {
 | 
				
			||||||
 | 
					  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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -122,11 +149,23 @@ const loadAsyncGoogleFont = () => {
 | 
				
			|||||||
  document.head.appendChild(linkEl);
 | 
					  document.head.appendChild(linkEl);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function WindowContent(props: { children: React.ReactNode }) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={styles["window-content"]} id={SlotID.AppBody}>
 | 
				
			||||||
 | 
					      {props?.children}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function Screen() {
 | 
					function Screen() {
 | 
				
			||||||
  const config = useAppConfig();
 | 
					  const config = useAppConfig();
 | 
				
			||||||
  const location = useLocation();
 | 
					  const location = useLocation();
 | 
				
			||||||
 | 
					  const isArtifact = location.pathname.includes(Path.Artifacts);
 | 
				
			||||||
  const isHome = location.pathname === Path.Home;
 | 
					  const isHome = location.pathname === Path.Home;
 | 
				
			||||||
  const isAuth = location.pathname === Path.Auth;
 | 
					  const isAuth = location.pathname === Path.Auth;
 | 
				
			||||||
 | 
					  const isSd = location.pathname === Path.Sd;
 | 
				
			||||||
 | 
					  const isSdNew = location.pathname === Path.SdNew;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const isMobileScreen = useMobileScreen();
 | 
					  const isMobileScreen = useMobileScreen();
 | 
				
			||||||
  const shouldTightBorder =
 | 
					  const shouldTightBorder =
 | 
				
			||||||
    getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
 | 
					    getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
 | 
				
			||||||
@@ -135,34 +174,48 @@ function Screen() {
 | 
				
			|||||||
    loadAsyncGoogleFont();
 | 
					    loadAsyncGoogleFont();
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (isArtifact) {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
    <div
 | 
					      <Routes>
 | 
				
			||||||
      className={
 | 
					        <Route path="/artifacts/:id" element={<Artifacts />} />
 | 
				
			||||||
        styles.container +
 | 
					      </Routes>
 | 
				
			||||||
        ` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
 | 
					    );
 | 
				
			||||||
          getLang() === "ar" ? styles["rtl-screen"] : ""
 | 
					 | 
				
			||||||
        }`
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
    >
 | 
					  const renderContent = () => {
 | 
				
			||||||
      {isAuth ? (
 | 
					    if (isAuth) return <AuthPage />;
 | 
				
			||||||
 | 
					    if (isSd) return <Sd />;
 | 
				
			||||||
 | 
					    if (isSdNew) return <Sd />;
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
      <>
 | 
					      <>
 | 
				
			||||||
          <AuthPage />
 | 
					        <SideBar
 | 
				
			||||||
        </>
 | 
					          className={clsx({
 | 
				
			||||||
      ) : (
 | 
					            [styles["sidebar-show"]]: isHome,
 | 
				
			||||||
        <>
 | 
					          })}
 | 
				
			||||||
          <SideBar className={isHome ? styles["sidebar-show"] : ""} />
 | 
					        />
 | 
				
			||||||
 | 
					        <WindowContent>
 | 
				
			||||||
          <div className={styles["window-content"]} id={SlotID.AppBody}>
 | 
					 | 
				
			||||||
          <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>
 | 
				
			||||||
          </div>
 | 
					        </WindowContent>
 | 
				
			||||||
      </>
 | 
					      </>
 | 
				
			||||||
      )}
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={clsx(styles.container, {
 | 
				
			||||||
 | 
					        [styles["tight-container"]]: shouldTightBorder,
 | 
				
			||||||
 | 
					        [styles["rtl-screen"]]: getLang() === "ar",
 | 
				
			||||||
 | 
					      })}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {renderContent()}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -189,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>;
 | 
				
			||||||
@@ -9,6 +10,7 @@ interface InputRangeProps {
 | 
				
			|||||||
  min: string;
 | 
					  min: string;
 | 
				
			||||||
  max: string;
 | 
					  max: string;
 | 
				
			||||||
  step: string;
 | 
					  step: string;
 | 
				
			||||||
 | 
					  aria: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function InputRange({
 | 
					export function InputRange({
 | 
				
			||||||
@@ -19,11 +21,13 @@ export function InputRange({
 | 
				
			|||||||
  min,
 | 
					  min,
 | 
				
			||||||
  max,
 | 
					  max,
 | 
				
			||||||
  step,
 | 
					  step,
 | 
				
			||||||
 | 
					  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}
 | 
				
			||||||
        type="range"
 | 
					        type="range"
 | 
				
			||||||
        title={title}
 | 
					        title={title}
 | 
				
			||||||
        value={value}
 | 
					        value={value}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,13 +6,24 @@ import RehypeKatex from "rehype-katex";
 | 
				
			|||||||
import RemarkGfm from "remark-gfm";
 | 
					import RemarkGfm from "remark-gfm";
 | 
				
			||||||
import RehypeHighlight from "rehype-highlight";
 | 
					import RehypeHighlight from "rehype-highlight";
 | 
				
			||||||
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
 | 
					import { useRef, useState, RefObject, useEffect, useMemo } from "react";
 | 
				
			||||||
import { copyToClipboard } 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 } from "./ui-lib";
 | 
					import { showImageModal, FullScreen } from "./ui-lib";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ArtifactsShareButton,
 | 
				
			||||||
 | 
					  HTMLPreview,
 | 
				
			||||||
 | 
					  HTMLPreviewHander,
 | 
				
			||||||
 | 
					} from "./artifacts";
 | 
				
			||||||
 | 
					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);
 | 
				
			||||||
@@ -47,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",
 | 
				
			||||||
@@ -62,58 +73,159 @@ 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 { height } = useWindowSize();
 | 
				
			||||||
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const renderMermaid = useDebouncedCallback(() => {
 | 
					  const renderArtifacts = useDebouncedCallback(() => {
 | 
				
			||||||
    if (!ref.current) return;
 | 
					    if (!ref.current) return;
 | 
				
			||||||
    const mermaidDom = ref.current.querySelector("code.language-mermaid");
 | 
					    const mermaidDom = ref.current.querySelector("code.language-mermaid");
 | 
				
			||||||
    if (mermaidDom) {
 | 
					    if (mermaidDom) {
 | 
				
			||||||
      setMermaidCode((mermaidDom as HTMLElement).innerText);
 | 
					      setMermaidCode((mermaidDom as HTMLElement).innerText);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    const htmlDom = ref.current.querySelector("code.language-html");
 | 
				
			||||||
 | 
					    const refText = ref.current.querySelector("code")?.innerText;
 | 
				
			||||||
 | 
					    if (htmlDom) {
 | 
				
			||||||
 | 
					      setHtmlCode((htmlDom as HTMLElement).innerText);
 | 
				
			||||||
 | 
					    } else if (
 | 
				
			||||||
 | 
					      refText?.startsWith("<!DOCTYPE") ||
 | 
				
			||||||
 | 
					      refText?.startsWith("<svg") ||
 | 
				
			||||||
 | 
					      refText?.startsWith("<?xml")
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      setHtmlCode(refText);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }, 600);
 | 
					  }, 600);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const enableArtifacts =
 | 
				
			||||||
 | 
					    session.mask?.enableArtifacts !== false && config.enableArtifacts;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //Wrap the paragraph for plain-text
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    setTimeout(renderMermaid, 1);
 | 
					    if (ref.current) {
 | 
				
			||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					      const codeElements = ref.current.querySelectorAll(
 | 
				
			||||||
  }, [refText]);
 | 
					        "code",
 | 
				
			||||||
 | 
					      ) as NodeListOf<HTMLElement>;
 | 
				
			||||||
 | 
					      const wrapLanguages = [
 | 
				
			||||||
 | 
					        "",
 | 
				
			||||||
 | 
					        "md",
 | 
				
			||||||
 | 
					        "markdown",
 | 
				
			||||||
 | 
					        "text",
 | 
				
			||||||
 | 
					        "txt",
 | 
				
			||||||
 | 
					        "plaintext",
 | 
				
			||||||
 | 
					        "tex",
 | 
				
			||||||
 | 
					        "latex",
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					      codeElements.forEach((codeElement) => {
 | 
				
			||||||
 | 
					        let languageClass = codeElement.className.match(/language-(\w+)/);
 | 
				
			||||||
 | 
					        let name = languageClass ? languageClass[1] : "";
 | 
				
			||||||
 | 
					        if (wrapLanguages.includes(name)) {
 | 
				
			||||||
 | 
					          codeElement.style.whiteSpace = "pre-wrap";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      setTimeout(renderArtifacts, 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      {mermaidCode.length > 0 && (
 | 
					 | 
				
			||||||
        <Mermaid code={mermaidCode} key={mermaidCode} />
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
      <pre ref={ref}>
 | 
					      <pre ref={ref}>
 | 
				
			||||||
        <span
 | 
					        <span
 | 
				
			||||||
          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>
 | 
				
			||||||
        {props.children}
 | 
					        {props.children}
 | 
				
			||||||
      </pre>
 | 
					      </pre>
 | 
				
			||||||
 | 
					      {mermaidCode.length > 0 && (
 | 
				
			||||||
 | 
					        <Mermaid code={mermaidCode} key={mermaidCode} />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {htmlCode.length > 0 && enableArtifacts && (
 | 
				
			||||||
 | 
					        <FullScreen className="no-dark html" right={70}>
 | 
				
			||||||
 | 
					          <ArtifactsShareButton
 | 
				
			||||||
 | 
					            style={{ position: "absolute", right: 20, top: 10 }}
 | 
				
			||||||
 | 
					            getCode={() => htmlCode}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            style={{ position: "absolute", right: 120, top: 10 }}
 | 
				
			||||||
 | 
					            bordered
 | 
				
			||||||
 | 
					            icon={<ReloadButtonIcon />}
 | 
				
			||||||
 | 
					            shadow
 | 
				
			||||||
 | 
					            onClick={() => previewRef.current?.reload()}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <HTMLPreview
 | 
				
			||||||
 | 
					            ref={previewRef}
 | 
				
			||||||
 | 
					            code={htmlCode}
 | 
				
			||||||
 | 
					            autoHeight={!document.fullscreenElement}
 | 
				
			||||||
 | 
					            height={!document.fullscreenElement ? 600 : height}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </FullScreen>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function escapeDollarNumber(text: string) {
 | 
					function CustomCode(props: { children: any; className?: string }) {
 | 
				
			||||||
  let escapedText = "";
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					  const session = chatStore.currentSession();
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const enableCodeFold =
 | 
				
			||||||
 | 
					    session.mask?.enableCodeFold !== false && config.enableCodeFold;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for (let i = 0; i < text.length; i += 1) {
 | 
					  const ref = useRef<HTMLPreElement>(null);
 | 
				
			||||||
    let char = text[i];
 | 
					  const [collapsed, setCollapsed] = useState(true);
 | 
				
			||||||
    const nextChar = text[i + 1] || " ";
 | 
					  const [showToggle, setShowToggle] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (char === "$" && nextChar >= "0" && nextChar <= "9") {
 | 
					  useEffect(() => {
 | 
				
			||||||
      char = "\\$";
 | 
					    if (ref.current) {
 | 
				
			||||||
 | 
					      const codeHeight = ref.current.scrollHeight;
 | 
				
			||||||
 | 
					      setShowToggle(codeHeight > 400);
 | 
				
			||||||
 | 
					      ref.current.scrollTop = ref.current.scrollHeight;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }, [props.children]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    escapedText += char;
 | 
					  const toggleCollapsed = () => {
 | 
				
			||||||
 | 
					    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 (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <code
 | 
				
			||||||
 | 
					        className={clsx(props?.className)}
 | 
				
			||||||
 | 
					        ref={ref}
 | 
				
			||||||
 | 
					        style={{
 | 
				
			||||||
 | 
					          maxHeight: enableCodeFold && collapsed ? "400px" : "none",
 | 
				
			||||||
 | 
					          overflowY: "hidden",
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {props.children}
 | 
				
			||||||
 | 
					      </code>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return escapedText;
 | 
					      {renderShowMoreButton()}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function escapeBrackets(text: string) {
 | 
					function escapeBrackets(text: string) {
 | 
				
			||||||
@@ -134,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 (
 | 
				
			||||||
@@ -154,9 +287,24 @@ function _MarkDownContent(props: { content: string }) {
 | 
				
			|||||||
      ]}
 | 
					      ]}
 | 
				
			||||||
      components={{
 | 
					      components={{
 | 
				
			||||||
        pre: PreCode,
 | 
					        pre: PreCode,
 | 
				
			||||||
 | 
					        code: CustomCode,
 | 
				
			||||||
        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} />;
 | 
				
			||||||
@@ -175,6 +323,7 @@ export function Markdown(
 | 
				
			|||||||
    content: string;
 | 
					    content: string;
 | 
				
			||||||
    loading?: boolean;
 | 
					    loading?: boolean;
 | 
				
			||||||
    fontSize?: number;
 | 
					    fontSize?: number;
 | 
				
			||||||
 | 
					    fontFamily?: string;
 | 
				
			||||||
    parentRef?: RefObject<HTMLDivElement>;
 | 
					    parentRef?: RefObject<HTMLDivElement>;
 | 
				
			||||||
    defaultShow?: boolean;
 | 
					    defaultShow?: boolean;
 | 
				
			||||||
  } & React.DOMAttributes<HTMLDivElement>,
 | 
					  } & React.DOMAttributes<HTMLDivElement>,
 | 
				
			||||||
@@ -186,6 +335,7 @@ export function Markdown(
 | 
				
			|||||||
      className="markdown-body"
 | 
					      className="markdown-body"
 | 
				
			||||||
      style={{
 | 
					      style={{
 | 
				
			||||||
        fontSize: `${props.fontSize ?? 14}px`,
 | 
					        fontSize: `${props.fontSize ?? 14}px`,
 | 
				
			||||||
 | 
					        fontFamily: props.fontFamily || "inherit",
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
      ref={mdRef}
 | 
					      ref={mdRef}
 | 
				
			||||||
      onContextMenu={props.onContextMenu}
 | 
					      onContextMenu={props.onContextMenu}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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[] {
 | 
				
			||||||
@@ -127,6 +127,8 @@ export function MaskConfig(props: {
 | 
				
			|||||||
            onClose={() => setShowPicker(false)}
 | 
					            onClose={() => setShowPicker(false)}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
 | 
					              tabIndex={0}
 | 
				
			||||||
 | 
					              aria-label={Locale.Mask.Config.Avatar}
 | 
				
			||||||
              onClick={() => setShowPicker(true)}
 | 
					              onClick={() => setShowPicker(true)}
 | 
				
			||||||
              style={{ cursor: "pointer" }}
 | 
					              style={{ cursor: "pointer" }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
@@ -139,6 +141,7 @@ export function MaskConfig(props: {
 | 
				
			|||||||
        </ListItem>
 | 
					        </ListItem>
 | 
				
			||||||
        <ListItem title={Locale.Mask.Config.Name}>
 | 
					        <ListItem title={Locale.Mask.Config.Name}>
 | 
				
			||||||
          <input
 | 
					          <input
 | 
				
			||||||
 | 
					            aria-label={Locale.Mask.Config.Name}
 | 
				
			||||||
            type="text"
 | 
					            type="text"
 | 
				
			||||||
            value={props.mask.name}
 | 
					            value={props.mask.name}
 | 
				
			||||||
            onInput={(e) =>
 | 
					            onInput={(e) =>
 | 
				
			||||||
@@ -153,6 +156,7 @@ export function MaskConfig(props: {
 | 
				
			|||||||
          subTitle={Locale.Mask.Config.HideContext.SubTitle}
 | 
					          subTitle={Locale.Mask.Config.HideContext.SubTitle}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <input
 | 
					          <input
 | 
				
			||||||
 | 
					            aria-label={Locale.Mask.Config.HideContext.Title}
 | 
				
			||||||
            type="checkbox"
 | 
					            type="checkbox"
 | 
				
			||||||
            checked={props.mask.hideContext}
 | 
					            checked={props.mask.hideContext}
 | 
				
			||||||
            onChange={(e) => {
 | 
					            onChange={(e) => {
 | 
				
			||||||
@@ -163,12 +167,48 @@ 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}
 | 
				
			||||||
            subTitle={Locale.Mask.Config.Share.SubTitle}
 | 
					            subTitle={Locale.Mask.Config.Share.SubTitle}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <IconButton
 | 
					            <IconButton
 | 
				
			||||||
 | 
					              aria={Locale.Mask.Config.Share.Title}
 | 
				
			||||||
              icon={<CopyIcon />}
 | 
					              icon={<CopyIcon />}
 | 
				
			||||||
              text={Locale.Mask.Config.Share.Action}
 | 
					              text={Locale.Mask.Config.Share.Action}
 | 
				
			||||||
              onClick={copyMaskLink}
 | 
					              onClick={copyMaskLink}
 | 
				
			||||||
@@ -182,6 +222,7 @@ export function MaskConfig(props: {
 | 
				
			|||||||
            subTitle={Locale.Mask.Config.Sync.SubTitle}
 | 
					            subTitle={Locale.Mask.Config.Sync.SubTitle}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <input
 | 
					            <input
 | 
				
			||||||
 | 
					              aria-label={Locale.Mask.Config.Sync.Title}
 | 
				
			||||||
              type="checkbox"
 | 
					              type="checkbox"
 | 
				
			||||||
              checked={props.mask.syncGlobalConfig}
 | 
					              checked={props.mask.syncGlobalConfig}
 | 
				
			||||||
              onChange={async (e) => {
 | 
					              onChange={async (e) => {
 | 
				
			||||||
@@ -404,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()
 | 
				
			||||||
@@ -520,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);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }}
 | 
					              }}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
@@ -557,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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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,34 +5,48 @@ 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 (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <ListItem title={Locale.Settings.Model}>
 | 
					      <ListItem title={Locale.Settings.Model}>
 | 
				
			||||||
        <Select
 | 
					        <Select
 | 
				
			||||||
 | 
					          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
 | 
				
			||||||
@@ -40,6 +54,7 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
        subTitle={Locale.Settings.Temperature.SubTitle}
 | 
					        subTitle={Locale.Settings.Temperature.SubTitle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <InputRange
 | 
					        <InputRange
 | 
				
			||||||
 | 
					          aria={Locale.Settings.Temperature.Title}
 | 
				
			||||||
          value={props.modelConfig.temperature?.toFixed(1)}
 | 
					          value={props.modelConfig.temperature?.toFixed(1)}
 | 
				
			||||||
          min="0"
 | 
					          min="0"
 | 
				
			||||||
          max="1" // lets limit it to 0-1
 | 
					          max="1" // lets limit it to 0-1
 | 
				
			||||||
@@ -59,6 +74,7 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
        subTitle={Locale.Settings.TopP.SubTitle}
 | 
					        subTitle={Locale.Settings.TopP.SubTitle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <InputRange
 | 
					        <InputRange
 | 
				
			||||||
 | 
					          aria={Locale.Settings.TopP.Title}
 | 
				
			||||||
          value={(props.modelConfig.top_p ?? 1).toFixed(1)}
 | 
					          value={(props.modelConfig.top_p ?? 1).toFixed(1)}
 | 
				
			||||||
          min="0"
 | 
					          min="0"
 | 
				
			||||||
          max="1"
 | 
					          max="1"
 | 
				
			||||||
@@ -78,6 +94,7 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
        subTitle={Locale.Settings.MaxTokens.SubTitle}
 | 
					        subTitle={Locale.Settings.MaxTokens.SubTitle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <input
 | 
					        <input
 | 
				
			||||||
 | 
					          aria-label={Locale.Settings.MaxTokens.Title}
 | 
				
			||||||
          type="number"
 | 
					          type="number"
 | 
				
			||||||
          min={1024}
 | 
					          min={1024}
 | 
				
			||||||
          max={512000}
 | 
					          max={512000}
 | 
				
			||||||
@@ -100,6 +117,7 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
            subTitle={Locale.Settings.PresencePenalty.SubTitle}
 | 
					            subTitle={Locale.Settings.PresencePenalty.SubTitle}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <InputRange
 | 
					            <InputRange
 | 
				
			||||||
 | 
					              aria={Locale.Settings.PresencePenalty.Title}
 | 
				
			||||||
              value={props.modelConfig.presence_penalty?.toFixed(1)}
 | 
					              value={props.modelConfig.presence_penalty?.toFixed(1)}
 | 
				
			||||||
              min="-2"
 | 
					              min="-2"
 | 
				
			||||||
              max="2"
 | 
					              max="2"
 | 
				
			||||||
@@ -121,6 +139,7 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
            subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
 | 
					            subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <InputRange
 | 
					            <InputRange
 | 
				
			||||||
 | 
					              aria={Locale.Settings.FrequencyPenalty.Title}
 | 
				
			||||||
              value={props.modelConfig.frequency_penalty?.toFixed(1)}
 | 
					              value={props.modelConfig.frequency_penalty?.toFixed(1)}
 | 
				
			||||||
              min="-2"
 | 
					              min="-2"
 | 
				
			||||||
              max="2"
 | 
					              max="2"
 | 
				
			||||||
@@ -142,6 +161,7 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
            subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
 | 
					            subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <input
 | 
					            <input
 | 
				
			||||||
 | 
					              aria-label={Locale.Settings.InjectSystemPrompts.Title}
 | 
				
			||||||
              type="checkbox"
 | 
					              type="checkbox"
 | 
				
			||||||
              checked={props.modelConfig.enableInjectSystemPrompts}
 | 
					              checked={props.modelConfig.enableInjectSystemPrompts}
 | 
				
			||||||
              onChange={(e) =>
 | 
					              onChange={(e) =>
 | 
				
			||||||
@@ -159,6 +179,7 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
            subTitle={Locale.Settings.InputTemplate.SubTitle}
 | 
					            subTitle={Locale.Settings.InputTemplate.SubTitle}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <input
 | 
					            <input
 | 
				
			||||||
 | 
					              aria-label={Locale.Settings.InputTemplate.Title}
 | 
				
			||||||
              type="text"
 | 
					              type="text"
 | 
				
			||||||
              value={props.modelConfig.template}
 | 
					              value={props.modelConfig.template}
 | 
				
			||||||
              onChange={(e) =>
 | 
					              onChange={(e) =>
 | 
				
			||||||
@@ -175,6 +196,7 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
        subTitle={Locale.Settings.HistoryCount.SubTitle}
 | 
					        subTitle={Locale.Settings.HistoryCount.SubTitle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <InputRange
 | 
					        <InputRange
 | 
				
			||||||
 | 
					          aria={Locale.Settings.HistoryCount.Title}
 | 
				
			||||||
          title={props.modelConfig.historyMessageCount.toString()}
 | 
					          title={props.modelConfig.historyMessageCount.toString()}
 | 
				
			||||||
          value={props.modelConfig.historyMessageCount}
 | 
					          value={props.modelConfig.historyMessageCount}
 | 
				
			||||||
          min="0"
 | 
					          min="0"
 | 
				
			||||||
@@ -193,6 +215,7 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
        subTitle={Locale.Settings.CompressThreshold.SubTitle}
 | 
					        subTitle={Locale.Settings.CompressThreshold.SubTitle}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <input
 | 
					        <input
 | 
				
			||||||
 | 
					          aria-label={Locale.Settings.CompressThreshold.Title}
 | 
				
			||||||
          type="number"
 | 
					          type="number"
 | 
				
			||||||
          min={500}
 | 
					          min={500}
 | 
				
			||||||
          max={4000}
 | 
					          max={4000}
 | 
				
			||||||
@@ -208,6 +231,7 @@ export function ModelConfigList(props: {
 | 
				
			|||||||
      </ListItem>
 | 
					      </ListItem>
 | 
				
			||||||
      <ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
 | 
					      <ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
 | 
				
			||||||
        <input
 | 
					        <input
 | 
				
			||||||
 | 
					          aria-label={Locale.Memory.Title}
 | 
				
			||||||
          type="checkbox"
 | 
					          type="checkbox"
 | 
				
			||||||
          checked={props.modelConfig.sendMemory}
 | 
					          checked={props.modelConfig.sendMemory}
 | 
				
			||||||
          onChange={(e) =>
 | 
					          onChange={(e) =>
 | 
				
			||||||
@@ -217,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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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>
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										2
									
								
								app/components/sd/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								app/components/sd/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					export * from "./sd";
 | 
				
			||||||
 | 
					export * from "./sd-panel";
 | 
				
			||||||
							
								
								
									
										45
									
								
								app/components/sd/sd-panel.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/components/sd/sd-panel.module.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					.ctrl-param-item {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  min-height: 40px;
 | 
				
			||||||
 | 
					  padding: 10px 0;
 | 
				
			||||||
 | 
					  animation: slide-in ease 0.6s;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .ctrl-param-item-header {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .ctrl-param-item-title {
 | 
				
			||||||
 | 
					      font-size: 14px;
 | 
				
			||||||
 | 
					      font-weight: bolder;
 | 
				
			||||||
 | 
					      margin-bottom: 5px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .ctrl-param-item-sub-title {
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					    font-weight: normal;
 | 
				
			||||||
 | 
					    margin-top: 3px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  textarea {
 | 
				
			||||||
 | 
					    appearance: none;
 | 
				
			||||||
 | 
					    border-radius: 10px;
 | 
				
			||||||
 | 
					    border: var(--border-in-light);
 | 
				
			||||||
 | 
					    min-height: 36px;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    background: var(--white);
 | 
				
			||||||
 | 
					    color: var(--black);
 | 
				
			||||||
 | 
					    padding: 0 10px;
 | 
				
			||||||
 | 
					    max-width: 50%;
 | 
				
			||||||
 | 
					    font-family: inherit;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.ai-models {
 | 
				
			||||||
 | 
					  button {
 | 
				
			||||||
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
 | 
					    padding: 10px;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										321
									
								
								app/components/sd/sd-panel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										321
									
								
								app/components/sd/sd-panel.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,321 @@
 | 
				
			|||||||
 | 
					import styles from "./sd-panel.module.scss";
 | 
				
			||||||
 | 
					import React from "react";
 | 
				
			||||||
 | 
					import { Select } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					import { IconButton } from "@/app/components/button";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { useSdStore } from "@/app/store/sd";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const params = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: Locale.SdPanel.Prompt,
 | 
				
			||||||
 | 
					    value: "prompt",
 | 
				
			||||||
 | 
					    type: "textarea",
 | 
				
			||||||
 | 
					    placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.Prompt),
 | 
				
			||||||
 | 
					    required: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: Locale.SdPanel.ModelVersion,
 | 
				
			||||||
 | 
					    value: "model",
 | 
				
			||||||
 | 
					    type: "select",
 | 
				
			||||||
 | 
					    default: "sd3-medium",
 | 
				
			||||||
 | 
					    support: ["sd3"],
 | 
				
			||||||
 | 
					    options: [
 | 
				
			||||||
 | 
					      { name: "SD3 Medium", value: "sd3-medium" },
 | 
				
			||||||
 | 
					      { name: "SD3 Large", value: "sd3-large" },
 | 
				
			||||||
 | 
					      { name: "SD3 Large Turbo", value: "sd3-large-turbo" },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: Locale.SdPanel.NegativePrompt,
 | 
				
			||||||
 | 
					    value: "negative_prompt",
 | 
				
			||||||
 | 
					    type: "textarea",
 | 
				
			||||||
 | 
					    placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.NegativePrompt),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: Locale.SdPanel.AspectRatio,
 | 
				
			||||||
 | 
					    value: "aspect_ratio",
 | 
				
			||||||
 | 
					    type: "select",
 | 
				
			||||||
 | 
					    default: "1:1",
 | 
				
			||||||
 | 
					    options: [
 | 
				
			||||||
 | 
					      { name: "1:1", value: "1:1" },
 | 
				
			||||||
 | 
					      { name: "16:9", value: "16:9" },
 | 
				
			||||||
 | 
					      { name: "21:9", value: "21:9" },
 | 
				
			||||||
 | 
					      { name: "2:3", value: "2:3" },
 | 
				
			||||||
 | 
					      { name: "3:2", value: "3:2" },
 | 
				
			||||||
 | 
					      { name: "4:5", value: "4:5" },
 | 
				
			||||||
 | 
					      { name: "5:4", value: "5:4" },
 | 
				
			||||||
 | 
					      { name: "9:16", value: "9:16" },
 | 
				
			||||||
 | 
					      { name: "9:21", value: "9:21" },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: Locale.SdPanel.ImageStyle,
 | 
				
			||||||
 | 
					    value: "style",
 | 
				
			||||||
 | 
					    type: "select",
 | 
				
			||||||
 | 
					    default: "3d-model",
 | 
				
			||||||
 | 
					    support: ["core"],
 | 
				
			||||||
 | 
					    options: [
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.D3Model, value: "3d-model" },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.AnalogFilm, value: "analog-film" },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.Anime, value: "anime" },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.Cinematic, value: "cinematic" },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.ComicBook, value: "comic-book" },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.DigitalArt, value: "digital-art" },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.Enhance, value: "enhance" },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.FantasyArt, value: "fantasy-art" },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.Isometric, value: "isometric" },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.LineArt, value: "line-art" },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.LowPoly, value: "low-poly" },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        name: Locale.SdPanel.Styles.ModelingCompound,
 | 
				
			||||||
 | 
					        value: "modeling-compound",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.NeonPunk, value: "neon-punk" },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.Origami, value: "origami" },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.Photographic, value: "photographic" },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.PixelArt, value: "pixel-art" },
 | 
				
			||||||
 | 
					      { name: Locale.SdPanel.Styles.TileTexture, value: "tile-texture" },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "Seed",
 | 
				
			||||||
 | 
					    value: "seed",
 | 
				
			||||||
 | 
					    type: "number",
 | 
				
			||||||
 | 
					    default: 0,
 | 
				
			||||||
 | 
					    min: 0,
 | 
				
			||||||
 | 
					    max: 4294967294,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: Locale.SdPanel.OutFormat,
 | 
				
			||||||
 | 
					    value: "output_format",
 | 
				
			||||||
 | 
					    type: "select",
 | 
				
			||||||
 | 
					    default: "png",
 | 
				
			||||||
 | 
					    options: [
 | 
				
			||||||
 | 
					      { name: "PNG", value: "png" },
 | 
				
			||||||
 | 
					      { name: "JPEG", value: "jpeg" },
 | 
				
			||||||
 | 
					      { name: "WebP", value: "webp" },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const sdCommonParams = (model: string, data: any) => {
 | 
				
			||||||
 | 
					  return params.filter((item) => {
 | 
				
			||||||
 | 
					    return !(item.support && !item.support.includes(model));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const models = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "Stable Image Ultra",
 | 
				
			||||||
 | 
					    value: "ultra",
 | 
				
			||||||
 | 
					    params: (data: any) => sdCommonParams("ultra", data),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "Stable Image Core",
 | 
				
			||||||
 | 
					    value: "core",
 | 
				
			||||||
 | 
					    params: (data: any) => sdCommonParams("core", data),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "Stable Diffusion 3",
 | 
				
			||||||
 | 
					    value: "sd3",
 | 
				
			||||||
 | 
					    params: (data: any) => {
 | 
				
			||||||
 | 
					      return sdCommonParams("sd3", data).filter((item) => {
 | 
				
			||||||
 | 
					        return !(
 | 
				
			||||||
 | 
					          data.model === "sd3-large-turbo" && item.value == "negative_prompt"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ControlParamItem(props: {
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  subTitle?: string;
 | 
				
			||||||
 | 
					  required?: boolean;
 | 
				
			||||||
 | 
					  children?: JSX.Element | JSX.Element[];
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={clsx(styles["ctrl-param-item"], props.className)}>
 | 
				
			||||||
 | 
					      <div className={styles["ctrl-param-item-header"]}>
 | 
				
			||||||
 | 
					        <div className={styles["ctrl-param-item-title"]}>
 | 
				
			||||||
 | 
					          <div>
 | 
				
			||||||
 | 
					            {props.title}
 | 
				
			||||||
 | 
					            {props.required && <span style={{ color: "red" }}>*</span>}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      {props.children}
 | 
				
			||||||
 | 
					      {props.subTitle && (
 | 
				
			||||||
 | 
					        <div className={styles["ctrl-param-item-sub-title"]}>
 | 
				
			||||||
 | 
					          {props.subTitle}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ControlParam(props: {
 | 
				
			||||||
 | 
					  columns: any[];
 | 
				
			||||||
 | 
					  data: any;
 | 
				
			||||||
 | 
					  onChange: (field: string, val: any) => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      {props.columns?.map((item) => {
 | 
				
			||||||
 | 
					        let element: null | JSX.Element;
 | 
				
			||||||
 | 
					        switch (item.type) {
 | 
				
			||||||
 | 
					          case "textarea":
 | 
				
			||||||
 | 
					            element = (
 | 
				
			||||||
 | 
					              <ControlParamItem
 | 
				
			||||||
 | 
					                title={item.name}
 | 
				
			||||||
 | 
					                subTitle={item.sub}
 | 
				
			||||||
 | 
					                required={item.required}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <textarea
 | 
				
			||||||
 | 
					                  rows={item.rows || 3}
 | 
				
			||||||
 | 
					                  style={{ maxWidth: "100%", width: "100%", padding: "10px" }}
 | 
				
			||||||
 | 
					                  placeholder={item.placeholder}
 | 
				
			||||||
 | 
					                  onChange={(e) => {
 | 
				
			||||||
 | 
					                    props.onChange(item.value, e.currentTarget.value);
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                  value={props.data[item.value]}
 | 
				
			||||||
 | 
					                ></textarea>
 | 
				
			||||||
 | 
					              </ControlParamItem>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          case "select":
 | 
				
			||||||
 | 
					            element = (
 | 
				
			||||||
 | 
					              <ControlParamItem
 | 
				
			||||||
 | 
					                title={item.name}
 | 
				
			||||||
 | 
					                subTitle={item.sub}
 | 
				
			||||||
 | 
					                required={item.required}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Select
 | 
				
			||||||
 | 
					                  aria-label={item.name}
 | 
				
			||||||
 | 
					                  value={props.data[item.value]}
 | 
				
			||||||
 | 
					                  onChange={(e) => {
 | 
				
			||||||
 | 
					                    props.onChange(item.value, e.currentTarget.value);
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  {item.options.map((opt: any) => {
 | 
				
			||||||
 | 
					                    return (
 | 
				
			||||||
 | 
					                      <option value={opt.value} key={opt.value}>
 | 
				
			||||||
 | 
					                        {opt.name}
 | 
				
			||||||
 | 
					                      </option>
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                  })}
 | 
				
			||||||
 | 
					                </Select>
 | 
				
			||||||
 | 
					              </ControlParamItem>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          case "number":
 | 
				
			||||||
 | 
					            element = (
 | 
				
			||||||
 | 
					              <ControlParamItem
 | 
				
			||||||
 | 
					                title={item.name}
 | 
				
			||||||
 | 
					                subTitle={item.sub}
 | 
				
			||||||
 | 
					                required={item.required}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <input
 | 
				
			||||||
 | 
					                  aria-label={item.name}
 | 
				
			||||||
 | 
					                  type="number"
 | 
				
			||||||
 | 
					                  min={item.min}
 | 
				
			||||||
 | 
					                  max={item.max}
 | 
				
			||||||
 | 
					                  value={props.data[item.value] || 0}
 | 
				
			||||||
 | 
					                  onChange={(e) => {
 | 
				
			||||||
 | 
					                    props.onChange(item.value, parseInt(e.currentTarget.value));
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </ControlParamItem>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          default:
 | 
				
			||||||
 | 
					            element = (
 | 
				
			||||||
 | 
					              <ControlParamItem
 | 
				
			||||||
 | 
					                title={item.name}
 | 
				
			||||||
 | 
					                subTitle={item.sub}
 | 
				
			||||||
 | 
					                required={item.required}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <input
 | 
				
			||||||
 | 
					                  aria-label={item.name}
 | 
				
			||||||
 | 
					                  type="text"
 | 
				
			||||||
 | 
					                  value={props.data[item.value]}
 | 
				
			||||||
 | 
					                  style={{ maxWidth: "100%", width: "100%" }}
 | 
				
			||||||
 | 
					                  onChange={(e) => {
 | 
				
			||||||
 | 
					                    props.onChange(item.value, e.currentTarget.value);
 | 
				
			||||||
 | 
					                  }}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </ControlParamItem>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return <div key={item.value}>{element}</div>;
 | 
				
			||||||
 | 
					      })}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getModelParamBasicData = (
 | 
				
			||||||
 | 
					  columns: any[],
 | 
				
			||||||
 | 
					  data: any,
 | 
				
			||||||
 | 
					  clearText?: boolean,
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
					  const newParams: any = {};
 | 
				
			||||||
 | 
					  columns.forEach((item: any) => {
 | 
				
			||||||
 | 
					    if (clearText && ["text", "textarea", "number"].includes(item.type)) {
 | 
				
			||||||
 | 
					      newParams[item.value] = item.default || "";
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // @ts-ignore
 | 
				
			||||||
 | 
					      newParams[item.value] = data[item.value] || item.default || "";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return newParams;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getParams = (model: any, params: any) => {
 | 
				
			||||||
 | 
					  return models.find((m) => m.value === model.value)?.params(params) || [];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function SdPanel() {
 | 
				
			||||||
 | 
					  const sdStore = useSdStore();
 | 
				
			||||||
 | 
					  const currentModel = sdStore.currentModel;
 | 
				
			||||||
 | 
					  const setCurrentModel = sdStore.setCurrentModel;
 | 
				
			||||||
 | 
					  const params = sdStore.currentParams;
 | 
				
			||||||
 | 
					  const setParams = sdStore.setCurrentParams;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleValueChange = (field: string, val: any) => {
 | 
				
			||||||
 | 
					    setParams({
 | 
				
			||||||
 | 
					      ...params,
 | 
				
			||||||
 | 
					      [field]: val,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const handleModelChange = (model: any) => {
 | 
				
			||||||
 | 
					    setCurrentModel(model);
 | 
				
			||||||
 | 
					    setParams(getModelParamBasicData(model.params({}), params));
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <ControlParamItem title={Locale.SdPanel.AIModel}>
 | 
				
			||||||
 | 
					        <div className={styles["ai-models"]}>
 | 
				
			||||||
 | 
					          {models.map((item) => {
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                text={item.name}
 | 
				
			||||||
 | 
					                key={item.value}
 | 
				
			||||||
 | 
					                type={currentModel.value == item.value ? "primary" : null}
 | 
				
			||||||
 | 
					                shadow
 | 
				
			||||||
 | 
					                onClick={() => handleModelChange(item)}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          })}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </ControlParamItem>
 | 
				
			||||||
 | 
					      <ControlParam
 | 
				
			||||||
 | 
					        columns={getParams?.(currentModel, params) as any[]}
 | 
				
			||||||
 | 
					        data={params}
 | 
				
			||||||
 | 
					        onChange={handleValueChange}
 | 
				
			||||||
 | 
					      ></ControlParam>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										140
									
								
								app/components/sd/sd-sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								app/components/sd/sd-sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,140 @@
 | 
				
			|||||||
 | 
					import { IconButton } from "@/app/components/button";
 | 
				
			||||||
 | 
					import GithubIcon from "@/app/icons/github.svg";
 | 
				
			||||||
 | 
					import SDIcon from "@/app/icons/sd.svg";
 | 
				
			||||||
 | 
					import ReturnIcon from "@/app/icons/return.svg";
 | 
				
			||||||
 | 
					import HistoryIcon from "@/app/icons/history.svg";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Path, REPO_URL } from "@/app/constant";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
 | 
					import dynamic from "next/dynamic";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  SideBarContainer,
 | 
				
			||||||
 | 
					  SideBarBody,
 | 
				
			||||||
 | 
					  SideBarHeader,
 | 
				
			||||||
 | 
					  SideBarTail,
 | 
				
			||||||
 | 
					  useDragSideBar,
 | 
				
			||||||
 | 
					  useHotKey,
 | 
				
			||||||
 | 
					} from "@/app/components/sidebar";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { getParams, getModelParamBasicData } from "./sd-panel";
 | 
				
			||||||
 | 
					import { useSdStore } from "@/app/store/sd";
 | 
				
			||||||
 | 
					import { showToast } from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					import { useMobileScreen } from "@/app/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SdPanel = dynamic(
 | 
				
			||||||
 | 
					  async () => (await import("@/app/components/sd")).SdPanel,
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    loading: () => null,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function SideBar(props: { className?: string }) {
 | 
				
			||||||
 | 
					  useHotKey();
 | 
				
			||||||
 | 
					  const isMobileScreen = useMobileScreen();
 | 
				
			||||||
 | 
					  const { onDragStart, shouldNarrow } = useDragSideBar();
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const sdStore = useSdStore();
 | 
				
			||||||
 | 
					  const currentModel = sdStore.currentModel;
 | 
				
			||||||
 | 
					  const params = sdStore.currentParams;
 | 
				
			||||||
 | 
					  const setParams = sdStore.setCurrentParams;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit = () => {
 | 
				
			||||||
 | 
					    const columns = getParams?.(currentModel, params);
 | 
				
			||||||
 | 
					    const reqParams: any = {};
 | 
				
			||||||
 | 
					    for (let i = 0; i < columns.length; i++) {
 | 
				
			||||||
 | 
					      const item = columns[i];
 | 
				
			||||||
 | 
					      reqParams[item.value] = params[item.value] ?? null;
 | 
				
			||||||
 | 
					      if (item.required) {
 | 
				
			||||||
 | 
					        if (!reqParams[item.value]) {
 | 
				
			||||||
 | 
					          showToast(Locale.SdPanel.ParamIsRequired(item.name));
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let data: any = {
 | 
				
			||||||
 | 
					      model: currentModel.value,
 | 
				
			||||||
 | 
					      model_name: currentModel.name,
 | 
				
			||||||
 | 
					      status: "wait",
 | 
				
			||||||
 | 
					      params: reqParams,
 | 
				
			||||||
 | 
					      created_at: new Date().toLocaleString(),
 | 
				
			||||||
 | 
					      img_data: "",
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    sdStore.sendTask(data, () => {
 | 
				
			||||||
 | 
					      setParams(getModelParamBasicData(columns, params, true));
 | 
				
			||||||
 | 
					      navigate(Path.SdNew);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <SideBarContainer
 | 
				
			||||||
 | 
					      onDragStart={onDragStart}
 | 
				
			||||||
 | 
					      shouldNarrow={shouldNarrow}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {isMobileScreen ? (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className="window-header"
 | 
				
			||||||
 | 
					          data-tauri-drag-region
 | 
				
			||||||
 | 
					          style={{
 | 
				
			||||||
 | 
					            paddingLeft: 0,
 | 
				
			||||||
 | 
					            paddingRight: 0,
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className="window-actions">
 | 
				
			||||||
 | 
					            <div className="window-action-button">
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                icon={<ReturnIcon />}
 | 
				
			||||||
 | 
					                bordered
 | 
				
			||||||
 | 
					                title={Locale.Sd.Actions.ReturnHome}
 | 
				
			||||||
 | 
					                onClick={() => navigate(Path.Home)}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <SDIcon width={50} height={50} />
 | 
				
			||||||
 | 
					          <div className="window-actions">
 | 
				
			||||||
 | 
					            <div className="window-action-button">
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                icon={<HistoryIcon />}
 | 
				
			||||||
 | 
					                bordered
 | 
				
			||||||
 | 
					                title={Locale.Sd.Actions.History}
 | 
				
			||||||
 | 
					                onClick={() => navigate(Path.SdNew)}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      ) : (
 | 
				
			||||||
 | 
					        <SideBarHeader
 | 
				
			||||||
 | 
					          title={
 | 
				
			||||||
 | 
					            <IconButton
 | 
				
			||||||
 | 
					              icon={<ReturnIcon />}
 | 
				
			||||||
 | 
					              bordered
 | 
				
			||||||
 | 
					              title={Locale.Sd.Actions.ReturnHome}
 | 
				
			||||||
 | 
					              onClick={() => navigate(Path.Home)}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          logo={<SDIcon width={38} height={"100%"} />}
 | 
				
			||||||
 | 
					        ></SideBarHeader>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      <SideBarBody>
 | 
				
			||||||
 | 
					        <SdPanel />
 | 
				
			||||||
 | 
					      </SideBarBody>
 | 
				
			||||||
 | 
					      <SideBarTail
 | 
				
			||||||
 | 
					        primaryAction={
 | 
				
			||||||
 | 
					          <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
 | 
				
			||||||
 | 
					            <IconButton icon={<GithubIcon />} shadow />
 | 
				
			||||||
 | 
					          </a>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        secondaryAction={
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            text={Locale.SdPanel.Submit}
 | 
				
			||||||
 | 
					            type="primary"
 | 
				
			||||||
 | 
					            shadow
 | 
				
			||||||
 | 
					            onClick={handleSubmit}
 | 
				
			||||||
 | 
					          ></IconButton>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </SideBarContainer>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										53
									
								
								app/components/sd/sd.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								app/components/sd/sd.module.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					.sd-img-list{
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  .sd-img-item{
 | 
				
			||||||
 | 
					    width: 48%;
 | 
				
			||||||
 | 
					    .sd-img-item-info{
 | 
				
			||||||
 | 
					      flex:1;
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      overflow: hidden;
 | 
				
			||||||
 | 
					      user-select: text;
 | 
				
			||||||
 | 
					      p{
 | 
				
			||||||
 | 
					        margin: 6px;
 | 
				
			||||||
 | 
					        font-size: 12px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .line-1{
 | 
				
			||||||
 | 
					        overflow: hidden;
 | 
				
			||||||
 | 
					        white-space: nowrap;
 | 
				
			||||||
 | 
					        text-overflow: ellipsis;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .pre-img{
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      width: 130px;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      background-color: var(--second);
 | 
				
			||||||
 | 
					      border-radius: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .img{
 | 
				
			||||||
 | 
					      width: 130px;
 | 
				
			||||||
 | 
					      height: 130px;
 | 
				
			||||||
 | 
					      border-radius: 10px;
 | 
				
			||||||
 | 
					      overflow: hidden;
 | 
				
			||||||
 | 
					      cursor: pointer;
 | 
				
			||||||
 | 
					      transition: all .3s;
 | 
				
			||||||
 | 
					      &:hover{
 | 
				
			||||||
 | 
					        opacity: .7;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    &:not(:last-child){
 | 
				
			||||||
 | 
					      margin-bottom: 20px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (max-width: 600px) {
 | 
				
			||||||
 | 
					  .sd-img-list{
 | 
				
			||||||
 | 
					    .sd-img-item{
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										340
									
								
								app/components/sd/sd.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										340
									
								
								app/components/sd/sd.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,340 @@
 | 
				
			|||||||
 | 
					import chatStyles from "@/app/components/chat.module.scss";
 | 
				
			||||||
 | 
					import styles from "@/app/components/sd/sd.module.scss";
 | 
				
			||||||
 | 
					import homeStyles from "@/app/components/home.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { IconButton } from "@/app/components/button";
 | 
				
			||||||
 | 
					import ReturnIcon from "@/app/icons/return.svg";
 | 
				
			||||||
 | 
					import Locale from "@/app/locales";
 | 
				
			||||||
 | 
					import { Path } from "@/app/constant";
 | 
				
			||||||
 | 
					import React, { useEffect, useMemo, useRef, useState } from "react";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  copyToClipboard,
 | 
				
			||||||
 | 
					  getMessageTextContent,
 | 
				
			||||||
 | 
					  useMobileScreen,
 | 
				
			||||||
 | 
					} from "@/app/utils";
 | 
				
			||||||
 | 
					import { useNavigate, useLocation } from "react-router-dom";
 | 
				
			||||||
 | 
					import { useAppConfig } from "@/app/store";
 | 
				
			||||||
 | 
					import MinIcon from "@/app/icons/min.svg";
 | 
				
			||||||
 | 
					import MaxIcon from "@/app/icons/max.svg";
 | 
				
			||||||
 | 
					import { getClientConfig } from "@/app/config/client";
 | 
				
			||||||
 | 
					import { ChatAction } from "@/app/components/chat";
 | 
				
			||||||
 | 
					import DeleteIcon from "@/app/icons/clear.svg";
 | 
				
			||||||
 | 
					import CopyIcon from "@/app/icons/copy.svg";
 | 
				
			||||||
 | 
					import PromptIcon from "@/app/icons/prompt.svg";
 | 
				
			||||||
 | 
					import ResetIcon from "@/app/icons/reload.svg";
 | 
				
			||||||
 | 
					import { useSdStore } from "@/app/store/sd";
 | 
				
			||||||
 | 
					import LoadingIcon from "@/app/icons/three-dots.svg";
 | 
				
			||||||
 | 
					import ErrorIcon from "@/app/icons/delete.svg";
 | 
				
			||||||
 | 
					import SDIcon from "@/app/icons/sd.svg";
 | 
				
			||||||
 | 
					import { Property } from "csstype";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  showConfirm,
 | 
				
			||||||
 | 
					  showImageModal,
 | 
				
			||||||
 | 
					  showModal,
 | 
				
			||||||
 | 
					} from "@/app/components/ui-lib";
 | 
				
			||||||
 | 
					import { removeImage } from "@/app/utils/chat";
 | 
				
			||||||
 | 
					import { SideBar } from "./sd-sidebar";
 | 
				
			||||||
 | 
					import { WindowContent } from "@/app/components/home";
 | 
				
			||||||
 | 
					import { params } from "./sd-panel";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getSdTaskStatus(item: any) {
 | 
				
			||||||
 | 
					  let s: string;
 | 
				
			||||||
 | 
					  let color: Property.Color | undefined = undefined;
 | 
				
			||||||
 | 
					  switch (item.status) {
 | 
				
			||||||
 | 
					    case "success":
 | 
				
			||||||
 | 
					      s = Locale.Sd.Status.Success;
 | 
				
			||||||
 | 
					      color = "green";
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case "error":
 | 
				
			||||||
 | 
					      s = Locale.Sd.Status.Error;
 | 
				
			||||||
 | 
					      color = "red";
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case "wait":
 | 
				
			||||||
 | 
					      s = Locale.Sd.Status.Wait;
 | 
				
			||||||
 | 
					      color = "yellow";
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case "running":
 | 
				
			||||||
 | 
					      s = Locale.Sd.Status.Running;
 | 
				
			||||||
 | 
					      color = "blue";
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      s = item.status.toUpperCase();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <p className={styles["line-1"]} title={item.error} style={{ color: color }}>
 | 
				
			||||||
 | 
					      <span>
 | 
				
			||||||
 | 
					        {Locale.Sd.Status.Name}: {s}
 | 
				
			||||||
 | 
					      </span>
 | 
				
			||||||
 | 
					      {item.status === "error" && (
 | 
				
			||||||
 | 
					        <span
 | 
				
			||||||
 | 
					          className="clickable"
 | 
				
			||||||
 | 
					          onClick={() => {
 | 
				
			||||||
 | 
					            showModal({
 | 
				
			||||||
 | 
					              title: Locale.Sd.Detail,
 | 
				
			||||||
 | 
					              children: (
 | 
				
			||||||
 | 
					                <div style={{ color: color, userSelect: "text" }}>
 | 
				
			||||||
 | 
					                  {item.error}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          - {item.error}
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Sd() {
 | 
				
			||||||
 | 
					  const isMobileScreen = useMobileScreen();
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const location = useLocation();
 | 
				
			||||||
 | 
					  const clientConfig = useMemo(() => getClientConfig(), []);
 | 
				
			||||||
 | 
					  const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  const scrollRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					  const sdStore = useSdStore();
 | 
				
			||||||
 | 
					  const [sdImages, setSdImages] = useState(sdStore.draw);
 | 
				
			||||||
 | 
					  const isSd = location.pathname === Path.Sd;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setSdImages(sdStore.draw);
 | 
				
			||||||
 | 
					  }, [sdStore.currentId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <SideBar className={clsx({ [homeStyles["sidebar-show"]]: isSd })} />
 | 
				
			||||||
 | 
					      <WindowContent>
 | 
				
			||||||
 | 
					        <div className={chatStyles.chat} key={"1"}>
 | 
				
			||||||
 | 
					          <div className="window-header" data-tauri-drag-region>
 | 
				
			||||||
 | 
					            {isMobileScreen && (
 | 
				
			||||||
 | 
					              <div className="window-actions">
 | 
				
			||||||
 | 
					                <div className={"window-action-button"}>
 | 
				
			||||||
 | 
					                  <IconButton
 | 
				
			||||||
 | 
					                    icon={<ReturnIcon />}
 | 
				
			||||||
 | 
					                    bordered
 | 
				
			||||||
 | 
					                    title={Locale.Chat.Actions.ChatList}
 | 
				
			||||||
 | 
					                    onClick={() => navigate(Path.Sd)}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					              className={clsx(
 | 
				
			||||||
 | 
					                "window-header-title",
 | 
				
			||||||
 | 
					                chatStyles["chat-body-title"],
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div className={`window-header-main-title`}>Stability AI</div>
 | 
				
			||||||
 | 
					              <div className="window-header-sub-title">
 | 
				
			||||||
 | 
					                {Locale.Sd.SubTitle(sdImages.length || 0)}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div className="window-actions">
 | 
				
			||||||
 | 
					              {showMaxIcon && (
 | 
				
			||||||
 | 
					                <div className="window-action-button">
 | 
				
			||||||
 | 
					                  <IconButton
 | 
				
			||||||
 | 
					                    aria={Locale.Chat.Actions.FullScreen}
 | 
				
			||||||
 | 
					                    icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
 | 
				
			||||||
 | 
					                    bordered
 | 
				
			||||||
 | 
					                    onClick={() => {
 | 
				
			||||||
 | 
					                      config.update(
 | 
				
			||||||
 | 
					                        (config) => (config.tightBorder = !config.tightBorder),
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					              {isMobileScreen && <SDIcon width={50} height={50} />}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className={chatStyles["chat-body"]} ref={scrollRef}>
 | 
				
			||||||
 | 
					            <div className={styles["sd-img-list"]}>
 | 
				
			||||||
 | 
					              {sdImages.length > 0 ? (
 | 
				
			||||||
 | 
					                sdImages.map((item: any) => {
 | 
				
			||||||
 | 
					                  return (
 | 
				
			||||||
 | 
					                    <div
 | 
				
			||||||
 | 
					                      key={item.id}
 | 
				
			||||||
 | 
					                      style={{ display: "flex" }}
 | 
				
			||||||
 | 
					                      className={styles["sd-img-item"]}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      {item.status === "success" ? (
 | 
				
			||||||
 | 
					                        <img
 | 
				
			||||||
 | 
					                          className={styles["img"]}
 | 
				
			||||||
 | 
					                          src={item.img_data}
 | 
				
			||||||
 | 
					                          alt={item.id}
 | 
				
			||||||
 | 
					                          onClick={(e) =>
 | 
				
			||||||
 | 
					                            showImageModal(
 | 
				
			||||||
 | 
					                              item.img_data,
 | 
				
			||||||
 | 
					                              true,
 | 
				
			||||||
 | 
					                              isMobileScreen
 | 
				
			||||||
 | 
					                                ? { width: "100%", height: "fit-content" }
 | 
				
			||||||
 | 
					                                : { maxWidth: "100%", maxHeight: "100%" },
 | 
				
			||||||
 | 
					                              isMobileScreen
 | 
				
			||||||
 | 
					                                ? { width: "100%", height: "fit-content" }
 | 
				
			||||||
 | 
					                                : { width: "100%", height: "100%" },
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
 | 
					                          }
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                      ) : item.status === "error" ? (
 | 
				
			||||||
 | 
					                        <div className={styles["pre-img"]}>
 | 
				
			||||||
 | 
					                          <ErrorIcon />
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      ) : (
 | 
				
			||||||
 | 
					                        <div className={styles["pre-img"]}>
 | 
				
			||||||
 | 
					                          <LoadingIcon />
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      )}
 | 
				
			||||||
 | 
					                      <div
 | 
				
			||||||
 | 
					                        style={{ marginLeft: "10px" }}
 | 
				
			||||||
 | 
					                        className={styles["sd-img-item-info"]}
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <p className={styles["line-1"]}>
 | 
				
			||||||
 | 
					                          {Locale.SdPanel.Prompt}:{" "}
 | 
				
			||||||
 | 
					                          <span
 | 
				
			||||||
 | 
					                            className="clickable"
 | 
				
			||||||
 | 
					                            title={item.params.prompt}
 | 
				
			||||||
 | 
					                            onClick={() => {
 | 
				
			||||||
 | 
					                              showModal({
 | 
				
			||||||
 | 
					                                title: Locale.Sd.Detail,
 | 
				
			||||||
 | 
					                                children: (
 | 
				
			||||||
 | 
					                                  <div style={{ userSelect: "text" }}>
 | 
				
			||||||
 | 
					                                    {item.params.prompt}
 | 
				
			||||||
 | 
					                                  </div>
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              });
 | 
				
			||||||
 | 
					                            }}
 | 
				
			||||||
 | 
					                          >
 | 
				
			||||||
 | 
					                            {item.params.prompt}
 | 
				
			||||||
 | 
					                          </span>
 | 
				
			||||||
 | 
					                        </p>
 | 
				
			||||||
 | 
					                        <p>
 | 
				
			||||||
 | 
					                          {Locale.SdPanel.AIModel}: {item.model_name}
 | 
				
			||||||
 | 
					                        </p>
 | 
				
			||||||
 | 
					                        {getSdTaskStatus(item)}
 | 
				
			||||||
 | 
					                        <p>{item.created_at}</p>
 | 
				
			||||||
 | 
					                        <div className={chatStyles["chat-message-actions"]}>
 | 
				
			||||||
 | 
					                          <div className={chatStyles["chat-input-actions"]}>
 | 
				
			||||||
 | 
					                            <ChatAction
 | 
				
			||||||
 | 
					                              text={Locale.Sd.Actions.Params}
 | 
				
			||||||
 | 
					                              icon={<PromptIcon />}
 | 
				
			||||||
 | 
					                              onClick={() => {
 | 
				
			||||||
 | 
					                                showModal({
 | 
				
			||||||
 | 
					                                  title: Locale.Sd.GenerateParams,
 | 
				
			||||||
 | 
					                                  children: (
 | 
				
			||||||
 | 
					                                    <div style={{ userSelect: "text" }}>
 | 
				
			||||||
 | 
					                                      {Object.keys(item.params).map((key) => {
 | 
				
			||||||
 | 
					                                        let label = key;
 | 
				
			||||||
 | 
					                                        let value = item.params[key];
 | 
				
			||||||
 | 
					                                        switch (label) {
 | 
				
			||||||
 | 
					                                          case "prompt":
 | 
				
			||||||
 | 
					                                            label = Locale.SdPanel.Prompt;
 | 
				
			||||||
 | 
					                                            break;
 | 
				
			||||||
 | 
					                                          case "negative_prompt":
 | 
				
			||||||
 | 
					                                            label =
 | 
				
			||||||
 | 
					                                              Locale.SdPanel.NegativePrompt;
 | 
				
			||||||
 | 
					                                            break;
 | 
				
			||||||
 | 
					                                          case "aspect_ratio":
 | 
				
			||||||
 | 
					                                            label = Locale.SdPanel.AspectRatio;
 | 
				
			||||||
 | 
					                                            break;
 | 
				
			||||||
 | 
					                                          case "seed":
 | 
				
			||||||
 | 
					                                            label = "Seed";
 | 
				
			||||||
 | 
					                                            value = value || 0;
 | 
				
			||||||
 | 
					                                            break;
 | 
				
			||||||
 | 
					                                          case "output_format":
 | 
				
			||||||
 | 
					                                            label = Locale.SdPanel.OutFormat;
 | 
				
			||||||
 | 
					                                            value = value?.toUpperCase();
 | 
				
			||||||
 | 
					                                            break;
 | 
				
			||||||
 | 
					                                          case "style":
 | 
				
			||||||
 | 
					                                            label = Locale.SdPanel.ImageStyle;
 | 
				
			||||||
 | 
					                                            value = params
 | 
				
			||||||
 | 
					                                              .find(
 | 
				
			||||||
 | 
					                                                (item) =>
 | 
				
			||||||
 | 
					                                                  item.value === "style",
 | 
				
			||||||
 | 
					                                              )
 | 
				
			||||||
 | 
					                                              ?.options?.find(
 | 
				
			||||||
 | 
					                                                (item) => item.value === value,
 | 
				
			||||||
 | 
					                                              )?.name;
 | 
				
			||||||
 | 
					                                            break;
 | 
				
			||||||
 | 
					                                          default:
 | 
				
			||||||
 | 
					                                            break;
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        return (
 | 
				
			||||||
 | 
					                                          <div
 | 
				
			||||||
 | 
					                                            key={key}
 | 
				
			||||||
 | 
					                                            style={{ margin: "10px" }}
 | 
				
			||||||
 | 
					                                          >
 | 
				
			||||||
 | 
					                                            <strong>{label}: </strong>
 | 
				
			||||||
 | 
					                                            {value}
 | 
				
			||||||
 | 
					                                          </div>
 | 
				
			||||||
 | 
					                                        );
 | 
				
			||||||
 | 
					                                      })}
 | 
				
			||||||
 | 
					                                    </div>
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                });
 | 
				
			||||||
 | 
					                              }}
 | 
				
			||||||
 | 
					                            />
 | 
				
			||||||
 | 
					                            <ChatAction
 | 
				
			||||||
 | 
					                              text={Locale.Sd.Actions.Copy}
 | 
				
			||||||
 | 
					                              icon={<CopyIcon />}
 | 
				
			||||||
 | 
					                              onClick={() =>
 | 
				
			||||||
 | 
					                                copyToClipboard(
 | 
				
			||||||
 | 
					                                  getMessageTextContent({
 | 
				
			||||||
 | 
					                                    role: "user",
 | 
				
			||||||
 | 
					                                    content: item.params.prompt,
 | 
				
			||||||
 | 
					                                  }),
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                              }
 | 
				
			||||||
 | 
					                            />
 | 
				
			||||||
 | 
					                            <ChatAction
 | 
				
			||||||
 | 
					                              text={Locale.Sd.Actions.Retry}
 | 
				
			||||||
 | 
					                              icon={<ResetIcon />}
 | 
				
			||||||
 | 
					                              onClick={() => {
 | 
				
			||||||
 | 
					                                const reqData = {
 | 
				
			||||||
 | 
					                                  model: item.model,
 | 
				
			||||||
 | 
					                                  model_name: item.model_name,
 | 
				
			||||||
 | 
					                                  status: "wait",
 | 
				
			||||||
 | 
					                                  params: { ...item.params },
 | 
				
			||||||
 | 
					                                  created_at: new Date().toLocaleString(),
 | 
				
			||||||
 | 
					                                  img_data: "",
 | 
				
			||||||
 | 
					                                };
 | 
				
			||||||
 | 
					                                sdStore.sendTask(reqData);
 | 
				
			||||||
 | 
					                              }}
 | 
				
			||||||
 | 
					                            />
 | 
				
			||||||
 | 
					                            <ChatAction
 | 
				
			||||||
 | 
					                              text={Locale.Sd.Actions.Delete}
 | 
				
			||||||
 | 
					                              icon={<DeleteIcon />}
 | 
				
			||||||
 | 
					                              onClick={async () => {
 | 
				
			||||||
 | 
					                                if (
 | 
				
			||||||
 | 
					                                  await showConfirm(Locale.Sd.Danger.Delete)
 | 
				
			||||||
 | 
					                                ) {
 | 
				
			||||||
 | 
					                                  // remove img_data + remove item in list
 | 
				
			||||||
 | 
					                                  removeImage(item.img_data).finally(() => {
 | 
				
			||||||
 | 
					                                    sdStore.draw = sdImages.filter(
 | 
				
			||||||
 | 
					                                      (i: any) => i.id !== item.id,
 | 
				
			||||||
 | 
					                                    );
 | 
				
			||||||
 | 
					                                    sdStore.getNextId();
 | 
				
			||||||
 | 
					                                  });
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                              }}
 | 
				
			||||||
 | 
					                            />
 | 
				
			||||||
 | 
					                          </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					              ) : (
 | 
				
			||||||
 | 
					                <div>{Locale.Sd.EmptyRecord}</div>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </WindowContent>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										167
									
								
								app/components/search-chat.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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 ;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
import { useEffect, useRef, useMemo } from "react";
 | 
					import React, { Fragment, useEffect, useMemo, useRef, useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import styles from "./home.module.scss";
 | 
					import styles from "./home.module.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -7,11 +7,11 @@ 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 PluginIcon from "../icons/plugin.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 Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -29,13 +29,21 @@ import {
 | 
				
			|||||||
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, showToast } 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,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function useHotKey() {
 | 
					export function useHotKey() {
 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					  const chatStore = useChatStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
@@ -54,7 +62,7 @@ function useHotKey() {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function useDragSideBar() {
 | 
					export function useDragSideBar() {
 | 
				
			||||||
  const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
 | 
					  const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const config = useAppConfig();
 | 
					  const config = useAppConfig();
 | 
				
			||||||
@@ -128,43 +136,125 @@ function useDragSideBar() {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function SideBar(props: { className?: string }) {
 | 
					export function SideBarContainer(props: {
 | 
				
			||||||
  const chatStore = useChatStore();
 | 
					  children: React.ReactNode;
 | 
				
			||||||
 | 
					  onDragStart: (e: MouseEvent) => void;
 | 
				
			||||||
  // drag side bar
 | 
					  shouldNarrow: boolean;
 | 
				
			||||||
  const { onDragStart, shouldNarrow } = useDragSideBar();
 | 
					  className?: string;
 | 
				
			||||||
  const navigate = useNavigate();
 | 
					}) {
 | 
				
			||||||
  const config = useAppConfig();
 | 
					 | 
				
			||||||
  const isMobileScreen = useMobileScreen();
 | 
					  const isMobileScreen = useMobileScreen();
 | 
				
			||||||
  const isIOSMobile = useMemo(
 | 
					  const isIOSMobile = useMemo(
 | 
				
			||||||
    () => isIOS() && isMobileScreen,
 | 
					    () => isIOS() && isMobileScreen,
 | 
				
			||||||
    [isMobileScreen],
 | 
					    [isMobileScreen],
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					  const { children, className, onDragStart, shouldNarrow } = props;
 | 
				
			||||||
  useHotKey();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      className={`${styles.sidebar} ${props.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,
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <div className={styles["sidebar-header"]} data-tauri-drag-region>
 | 
					      {children}
 | 
				
			||||||
        <div className={styles["sidebar-title"]} data-tauri-drag-region>
 | 
					      <div
 | 
				
			||||||
          NextChat
 | 
					        className={styles["sidebar-drag"]}
 | 
				
			||||||
        </div>
 | 
					        onPointerDown={(e) => onDragStart(e as any)}
 | 
				
			||||||
        <div className={styles["sidebar-sub-title"]}>
 | 
					      >
 | 
				
			||||||
          Build your own AI assistant.
 | 
					        <DragIcon />
 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div className={styles["sidebar-logo"] + " no-dark"}>
 | 
					 | 
				
			||||||
          <ChatGptIcon />
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function SideBarHeader(props: {
 | 
				
			||||||
 | 
					  title?: string | React.ReactNode;
 | 
				
			||||||
 | 
					  subTitle?: string | React.ReactNode;
 | 
				
			||||||
 | 
					  logo?: React.ReactNode;
 | 
				
			||||||
 | 
					  children?: React.ReactNode;
 | 
				
			||||||
 | 
					  shouldNarrow?: boolean;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const { title, subTitle, logo, children, shouldNarrow } = props;
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Fragment>
 | 
				
			||||||
 | 
					      <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"]} data-tauri-drag-region>
 | 
				
			||||||
 | 
					            {title}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className={styles["sidebar-sub-title"]}>{subTitle}</div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div className={clsx(styles["sidebar-logo"], "no-dark")}>{logo}</div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					    </Fragment>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function SideBarBody(props: {
 | 
				
			||||||
 | 
					  children: React.ReactNode;
 | 
				
			||||||
 | 
					  onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const { onClick, children } = props;
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={styles["sidebar-body"]} onClick={onClick}>
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function SideBarTail(props: {
 | 
				
			||||||
 | 
					  primaryAction?: React.ReactNode;
 | 
				
			||||||
 | 
					  secondaryAction?: React.ReactNode;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const { primaryAction, secondaryAction } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={styles["sidebar-tail"]}>
 | 
				
			||||||
 | 
					      <div className={styles["sidebar-actions"]}>{primaryAction}</div>
 | 
				
			||||||
 | 
					      <div className={styles["sidebar-actions"]}>{secondaryAction}</div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function SideBar(props: { className?: string }) {
 | 
				
			||||||
 | 
					  useHotKey();
 | 
				
			||||||
 | 
					  const { onDragStart, shouldNarrow } = useDragSideBar();
 | 
				
			||||||
 | 
					  const [showDiscoverySelector, setshowDiscoverySelector] = useState(false);
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const config = useAppConfig();
 | 
				
			||||||
 | 
					  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 (
 | 
				
			||||||
 | 
					    <SideBarContainer
 | 
				
			||||||
 | 
					      onDragStart={onDragStart}
 | 
				
			||||||
 | 
					      shouldNarrow={shouldNarrow}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <SideBarHeader
 | 
				
			||||||
 | 
					        title="NextChat"
 | 
				
			||||||
 | 
					        subTitle="Build your own AI assistant."
 | 
				
			||||||
 | 
					        logo={<ChatGptIcon />}
 | 
				
			||||||
 | 
					        shouldNarrow={shouldNarrow}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        <div className={styles["sidebar-header-bar"]}>
 | 
					        <div className={styles["sidebar-header-bar"]}>
 | 
				
			||||||
          <IconButton
 | 
					          <IconButton
 | 
				
			||||||
            icon={<MaskIcon />}
 | 
					            icon={<MaskIcon />}
 | 
				
			||||||
@@ -179,17 +269,43 @@ export function SideBar(props: { className?: string }) {
 | 
				
			|||||||
            }}
 | 
					            }}
 | 
				
			||||||
            shadow
 | 
					            shadow
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
 | 
					          {mcpEnabled && (
 | 
				
			||||||
            <IconButton
 | 
					            <IconButton
 | 
				
			||||||
          icon={<PluginIcon />}
 | 
					              icon={<McpIcon />}
 | 
				
			||||||
          text={shouldNarrow ? undefined : Locale.Plugin.Name}
 | 
					              text={shouldNarrow ? undefined : Locale.Mcp.Name}
 | 
				
			||||||
              className={styles["sidebar-bar-button"]}
 | 
					              className={styles["sidebar-bar-button"]}
 | 
				
			||||||
          onClick={() => showToast(Locale.WIP)}
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                navigate(Path.McpMarket, { state: { fromHome: true } });
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					              shadow
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					          <IconButton
 | 
				
			||||||
 | 
					            icon={<DiscoveryIcon />}
 | 
				
			||||||
 | 
					            text={shouldNarrow ? undefined : Locale.Discovery.Name}
 | 
				
			||||||
 | 
					            className={styles["sidebar-bar-button"]}
 | 
				
			||||||
 | 
					            onClick={() => setshowDiscoverySelector(true)}
 | 
				
			||||||
            shadow
 | 
					            shadow
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					        {showDiscoverySelector && (
 | 
				
			||||||
      <div
 | 
					          <Selector
 | 
				
			||||||
        className={styles["sidebar-body"]}
 | 
					            items={[
 | 
				
			||||||
 | 
					              ...DISCOVERY.map((item) => {
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                  title: item.name,
 | 
				
			||||||
 | 
					                  value: item.path,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					              }),
 | 
				
			||||||
 | 
					            ]}
 | 
				
			||||||
 | 
					            onClose={() => setshowDiscoverySelector(false)}
 | 
				
			||||||
 | 
					            onSelection={(s) => {
 | 
				
			||||||
 | 
					              navigate(s[0], { state: { fromHome: true } });
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </SideBarHeader>
 | 
				
			||||||
 | 
					      <SideBarBody
 | 
				
			||||||
        onClick={(e) => {
 | 
					        onClick={(e) => {
 | 
				
			||||||
          if (e.target === e.currentTarget) {
 | 
					          if (e.target === e.currentTarget) {
 | 
				
			||||||
            navigate(Path.Home);
 | 
					            navigate(Path.Home);
 | 
				
			||||||
@@ -197,11 +313,11 @@ export function SideBar(props: { className?: string }) {
 | 
				
			|||||||
        }}
 | 
					        }}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <ChatList narrow={shouldNarrow} />
 | 
					        <ChatList narrow={shouldNarrow} />
 | 
				
			||||||
      </div>
 | 
					      </SideBarBody>
 | 
				
			||||||
 | 
					      <SideBarTail
 | 
				
			||||||
      <div className={styles["sidebar-tail"]}>
 | 
					        primaryAction={
 | 
				
			||||||
        <div className={styles["sidebar-actions"]}>
 | 
					          <>
 | 
				
			||||||
          <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 () => {
 | 
				
			||||||
@@ -213,16 +329,25 @@ export function SideBar(props: { className?: string }) {
 | 
				
			|||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div className={styles["sidebar-action"]}>
 | 
					            <div className={styles["sidebar-action"]}>
 | 
				
			||||||
              <Link to={Path.Settings}>
 | 
					              <Link to={Path.Settings}>
 | 
				
			||||||
              <IconButton icon={<SettingsIcon />} shadow />
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  aria={Locale.Settings.Title}
 | 
				
			||||||
 | 
					                  icon={<SettingsIcon />}
 | 
				
			||||||
 | 
					                  shadow
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
              </Link>
 | 
					              </Link>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div className={styles["sidebar-action"]}>
 | 
					            <div className={styles["sidebar-action"]}>
 | 
				
			||||||
              <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
 | 
					              <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
 | 
				
			||||||
              <IconButton icon={<GithubIcon />} shadow />
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  aria={Locale.Export.MessageFromChatGPT}
 | 
				
			||||||
 | 
					                  icon={<GithubIcon />}
 | 
				
			||||||
 | 
					                  shadow
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
              </a>
 | 
					              </a>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					          </>
 | 
				
			||||||
        <div>
 | 
					        }
 | 
				
			||||||
 | 
					        secondaryAction={
 | 
				
			||||||
          <IconButton
 | 
					          <IconButton
 | 
				
			||||||
            icon={<AddIcon />}
 | 
					            icon={<AddIcon />}
 | 
				
			||||||
            text={shouldNarrow ? undefined : Locale.Home.NewChat}
 | 
					            text={shouldNarrow ? undefined : Locale.Home.NewChat}
 | 
				
			||||||
@@ -236,15 +361,8 @@ export function SideBar(props: { className?: string }) {
 | 
				
			|||||||
            }}
 | 
					            }}
 | 
				
			||||||
            shadow
 | 
					            shadow
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        }
 | 
				
			||||||
      </div>
 | 
					      />
 | 
				
			||||||
 | 
					    </SideBarContainer>
 | 
				
			||||||
      <div
 | 
					 | 
				
			||||||
        className={styles["sidebar-drag"]}
 | 
					 | 
				
			||||||
        onPointerDown={(e) => onDragStart(e as any)}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <DragIcon />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										133
									
								
								app/components/tts-config.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -61,6 +61,19 @@
 | 
				
			|||||||
      font-weight: normal;
 | 
					      font-weight: normal;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &.vertical {
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    align-items: start;
 | 
				
			||||||
 | 
					    .list-header {
 | 
				
			||||||
 | 
					      .list-item-title {
 | 
				
			||||||
 | 
					        margin-bottom: 5px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .list-item-sub-title {
 | 
				
			||||||
 | 
					        margin-bottom: 2px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.list {
 | 
					.list {
 | 
				
			||||||
@@ -239,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);
 | 
				
			||||||
@@ -291,7 +310,12 @@
 | 
				
			|||||||
  justify-content: center;
 | 
					  justify-content: center;
 | 
				
			||||||
  z-index: 999;
 | 
					  z-index: 999;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .selector-item-disabled {
 | 
				
			||||||
 | 
					    opacity: 0.6;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &-content {
 | 
					  &-content {
 | 
				
			||||||
 | 
					    min-width: 300px;
 | 
				
			||||||
    .list {
 | 
					    .list {
 | 
				
			||||||
      max-height: 90vh;
 | 
					      max-height: 90vh;
 | 
				
			||||||
      overflow-x: hidden;
 | 
					      overflow-x: hidden;
 | 
				
			||||||
@@ -312,3 +336,4 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,8 +13,18 @@ import MinIcon from "../icons/min.svg";
 | 
				
			|||||||
import Locale from "../locales";
 | 
					import Locale from "../locales";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { createRoot } from "react-dom/client";
 | 
					import { createRoot } from "react-dom/client";
 | 
				
			||||||
import React, { HTMLProps, useEffect, useState } from "react";
 | 
					import React, {
 | 
				
			||||||
 | 
					  CSSProperties,
 | 
				
			||||||
 | 
					  HTMLProps,
 | 
				
			||||||
 | 
					  MouseEvent,
 | 
				
			||||||
 | 
					  useEffect,
 | 
				
			||||||
 | 
					  useState,
 | 
				
			||||||
 | 
					  useCallback,
 | 
				
			||||||
 | 
					  useRef,
 | 
				
			||||||
 | 
					} 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;
 | 
				
			||||||
@@ -37,21 +47,28 @@ 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;
 | 
				
			||||||
  onClick?: () => void;
 | 
					  onClick?: (e: MouseEvent) => void;
 | 
				
			||||||
 | 
					  vertical?: boolean;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      className={styles["list-item"] + ` ${props.className || ""}`}
 | 
					      className={clsx(
 | 
				
			||||||
 | 
					        styles["list-item"],
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          [styles["vertical"]]: props.vertical,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        props.className,
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
      onClick={props.onClick}
 | 
					      onClick={props.onClick}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <div className={styles["list-header"]}>
 | 
					      <div className={styles["list-header"]}>
 | 
				
			||||||
@@ -122,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>
 | 
				
			||||||
@@ -247,14 +264,15 @@ export function Input(props: InputProps) {
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <textarea
 | 
					    <textarea
 | 
				
			||||||
      {...props}
 | 
					      {...props}
 | 
				
			||||||
      className={`${styles["input"]} ${props.className}`}
 | 
					      className={clsx(styles["input"], props.className)}
 | 
				
			||||||
    ></textarea>
 | 
					    ></textarea>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
 | 
					export function PasswordInput(
 | 
				
			||||||
 | 
					  props: HTMLProps<HTMLInputElement> & { aria?: string },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
  const [visible, setVisible] = useState(false);
 | 
					  const [visible, setVisible] = useState(false);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  function changeVisibility() {
 | 
					  function changeVisibility() {
 | 
				
			||||||
    setVisible(!visible);
 | 
					    setVisible(!visible);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -262,6 +280,7 @@ export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={"password-input-container"}>
 | 
					    <div className={"password-input-container"}>
 | 
				
			||||||
      <IconButton
 | 
					      <IconButton
 | 
				
			||||||
 | 
					        aria={props.aria}
 | 
				
			||||||
        icon={visible ? <EyeIcon /> : <EyeOffIcon />}
 | 
					        icon={visible ? <EyeIcon /> : <EyeOffIcon />}
 | 
				
			||||||
        onClick={changeVisibility}
 | 
					        onClick={changeVisibility}
 | 
				
			||||||
        className={"password-eye"}
 | 
					        className={"password-eye"}
 | 
				
			||||||
@@ -277,13 +296,23 @@ export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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>
 | 
				
			||||||
@@ -420,17 +449,25 @@ export function showPrompt(content: any, value = "", rows = 3) {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function showImageModal(img: string) {
 | 
					export function showImageModal(
 | 
				
			||||||
 | 
					  img: string,
 | 
				
			||||||
 | 
					  defaultMax?: boolean,
 | 
				
			||||||
 | 
					  style?: CSSProperties,
 | 
				
			||||||
 | 
					  boxStyle?: CSSProperties,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
  showModal({
 | 
					  showModal({
 | 
				
			||||||
    title: Locale.Export.Image.Modal,
 | 
					    title: Locale.Export.Image.Modal,
 | 
				
			||||||
 | 
					    defaultMax: defaultMax,
 | 
				
			||||||
    children: (
 | 
					    children: (
 | 
				
			||||||
      <div>
 | 
					      <div style={{ display: "flex", justifyContent: "center", ...boxStyle }}>
 | 
				
			||||||
        <img
 | 
					        <img
 | 
				
			||||||
          src={img}
 | 
					          src={img}
 | 
				
			||||||
          alt="preview"
 | 
					          alt="preview"
 | 
				
			||||||
          style={{
 | 
					          style={
 | 
				
			||||||
 | 
					            style ?? {
 | 
				
			||||||
              maxWidth: "100%",
 | 
					              maxWidth: "100%",
 | 
				
			||||||
          }}
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        ></img>
 | 
					        ></img>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
@@ -442,27 +479,57 @@ export function Selector<T>(props: {
 | 
				
			|||||||
    title: string;
 | 
					    title: string;
 | 
				
			||||||
    subTitle?: string;
 | 
					    subTitle?: string;
 | 
				
			||||||
    value: T;
 | 
					    value: T;
 | 
				
			||||||
 | 
					    disable?: boolean;
 | 
				
			||||||
  }>;
 | 
					  }>;
 | 
				
			||||||
  defaultSelectedValue?: T;
 | 
					  defaultSelectedValue?: T[] | T;
 | 
				
			||||||
  onSelection?: (selection: T[]) => void;
 | 
					  onSelection?: (selection: T[]) => void;
 | 
				
			||||||
  onClose?: () => void;
 | 
					  onClose?: () => void;
 | 
				
			||||||
  multiple?: boolean;
 | 
					  multiple?: boolean;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
 | 
					  const [selectedValues, setSelectedValues] = useState<T[]>(
 | 
				
			||||||
 | 
					    Array.isArray(props.defaultSelectedValue)
 | 
				
			||||||
 | 
					      ? props.defaultSelectedValue
 | 
				
			||||||
 | 
					      : props.defaultSelectedValue !== undefined
 | 
				
			||||||
 | 
					      ? [props.defaultSelectedValue]
 | 
				
			||||||
 | 
					      : [],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSelection = (e: MouseEvent, value: T) => {
 | 
				
			||||||
 | 
					    if (props.multiple) {
 | 
				
			||||||
 | 
					      e.stopPropagation();
 | 
				
			||||||
 | 
					      const newSelectedValues = selectedValues.includes(value)
 | 
				
			||||||
 | 
					        ? selectedValues.filter((v) => v !== value)
 | 
				
			||||||
 | 
					        : [...selectedValues, value];
 | 
				
			||||||
 | 
					      setSelectedValues(newSelectedValues);
 | 
				
			||||||
 | 
					      props.onSelection?.(newSelectedValues);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      setSelectedValues([value]);
 | 
				
			||||||
 | 
					      props.onSelection?.([value]);
 | 
				
			||||||
 | 
					      props.onClose?.();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className={styles["selector"]} onClick={() => props.onClose?.()}>
 | 
					    <div className={styles["selector"]} onClick={() => props.onClose?.()}>
 | 
				
			||||||
      <div className={styles["selector-content"]}>
 | 
					      <div className={styles["selector-content"]}>
 | 
				
			||||||
        <List>
 | 
					        <List>
 | 
				
			||||||
          {props.items.map((item, i) => {
 | 
					          {props.items.map((item, i) => {
 | 
				
			||||||
            const selected = props.defaultSelectedValue === item.value;
 | 
					            const selected = selectedValues.includes(item.value);
 | 
				
			||||||
            return (
 | 
					            return (
 | 
				
			||||||
              <ListItem
 | 
					              <ListItem
 | 
				
			||||||
                className={styles["selector-item"]}
 | 
					                className={clsx(styles["selector-item"], {
 | 
				
			||||||
 | 
					                  [styles["selector-item-disabled"]]: item.disable,
 | 
				
			||||||
 | 
					                })}
 | 
				
			||||||
                key={i}
 | 
					                key={i}
 | 
				
			||||||
                title={item.title}
 | 
					                title={item.title}
 | 
				
			||||||
                subTitle={item.subTitle}
 | 
					                subTitle={item.subTitle}
 | 
				
			||||||
                onClick={() => {
 | 
					                icon={<Avatar model={item.value as string} />}
 | 
				
			||||||
                  props.onSelection?.([item.value]);
 | 
					                onClick={(e) => {
 | 
				
			||||||
                  props.onClose?.();
 | 
					                  if (item.disable) {
 | 
				
			||||||
 | 
					                    e.stopPropagation();
 | 
				
			||||||
 | 
					                  } else {
 | 
				
			||||||
 | 
					                    handleSelection(e, item.value);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
                }}
 | 
					                }}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                {selected ? (
 | 
					                {selected ? (
 | 
				
			||||||
@@ -485,3 +552,38 @@ export function Selector<T>(props: {
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					export function FullScreen(props: any) {
 | 
				
			||||||
 | 
					  const { children, right = 10, top = 10, ...rest } = props;
 | 
				
			||||||
 | 
					  const ref = useRef<HTMLDivElement>();
 | 
				
			||||||
 | 
					  const [fullScreen, setFullScreen] = useState(false);
 | 
				
			||||||
 | 
					  const toggleFullscreen = useCallback(() => {
 | 
				
			||||||
 | 
					    if (!document.fullscreenElement) {
 | 
				
			||||||
 | 
					      ref.current?.requestFullscreen();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      document.exitFullscreen();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const handleScreenChange = (e: any) => {
 | 
				
			||||||
 | 
					      if (e.target === ref.current) {
 | 
				
			||||||
 | 
					        setFullScreen(!!document.fullscreenElement);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    document.addEventListener("fullscreenchange", handleScreenChange);
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      document.removeEventListener("fullscreenchange", handleScreenChange);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div ref={ref} style={{ position: "relative" }} {...rest}>
 | 
				
			||||||
 | 
					      <div style={{ position: "absolute", right, top }}>
 | 
				
			||||||
 | 
					        <IconButton
 | 
				
			||||||
 | 
					          icon={fullScreen ? <MinIcon /> : <MaxIcon />}
 | 
				
			||||||
 | 
					          onClick={toggleFullscreen}
 | 
				
			||||||
 | 
					          bordered
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								app/components/voice-print/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,7 +3,7 @@ import { BuildConfig, getBuildConfig } from "./build";
 | 
				
			|||||||
export function getClientConfig() {
 | 
					export function getClientConfig() {
 | 
				
			||||||
  if (typeof document !== "undefined") {
 | 
					  if (typeof document !== "undefined") {
 | 
				
			||||||
    // client side
 | 
					    // client side
 | 
				
			||||||
    return JSON.parse(queryMeta("config")) as BuildConfig;
 | 
					    return JSON.parse(queryMeta("config") || "{}") as BuildConfig;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (typeof process !== "undefined") {
 | 
					  if (typeof process !== "undefined") {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import md5 from "spark-md5";
 | 
					import md5 from "spark-md5";
 | 
				
			||||||
import { DEFAULT_MODELS } 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,11 @@ 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_URL?: string;
 | 
				
			||||||
 | 
					      STABILITY_API_KEY?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // azure only
 | 
					      // azure only
 | 
				
			||||||
      AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
 | 
					      AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
 | 
				
			||||||
@@ -53,8 +59,39 @@ declare global {
 | 
				
			|||||||
      ALIBABA_URL?: string;
 | 
					      ALIBABA_URL?: string;
 | 
				
			||||||
      ALIBABA_API_KEY?: string;
 | 
					      ALIBABA_API_KEY?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // tencent only
 | 
				
			||||||
 | 
					      TENCENT_URL?: string;
 | 
				
			||||||
 | 
					      TENCENT_SECRET_KEY?: string;
 | 
				
			||||||
 | 
					      TENCENT_SECRET_ID?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // moonshot only
 | 
				
			||||||
 | 
					      MOONSHOT_URL?: string;
 | 
				
			||||||
 | 
					      MOONSHOT_API_KEY?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // iflytek only
 | 
				
			||||||
 | 
					      IFLYTEK_URL?: string;
 | 
				
			||||||
 | 
					      IFLYTEK_API_KEY?: 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
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -98,22 +135,34 @@ 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((m) => m.name.startsWith("gpt-4"))
 | 
					    customModels += DEFAULT_MODELS.filter((m) => isGPT4Model(m.name))
 | 
				
			||||||
      .map((m) => "-" + m.name)
 | 
					      .map((m) => "-" + m.name)
 | 
				
			||||||
      .join(",");
 | 
					      .join(",");
 | 
				
			||||||
    if (defaultModel.startsWith("gpt-4")) defaultModel = "";
 | 
					    if (defaultModel && isGPT4Model(defaultModel)) {
 | 
				
			||||||
 | 
					      defaultModel = "";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const isStability = !!process.env.STABILITY_API_KEY;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const isAzure = !!process.env.AZURE_URL;
 | 
					  const isAzure = !!process.env.AZURE_URL;
 | 
				
			||||||
  const isGoogle = !!process.env.GOOGLE_API_KEY;
 | 
					  const isGoogle = !!process.env.GOOGLE_API_KEY;
 | 
				
			||||||
  const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
 | 
					  const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
 | 
				
			||||||
 | 
					  const isTencent = !!process.env.TENCENT_API_KEY;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const isBaidu = !!process.env.BAIDU_API_KEY;
 | 
					  const isBaidu = !!process.env.BAIDU_API_KEY;
 | 
				
			||||||
  const isBytedance = !!process.env.BYTEDANCE_API_KEY;
 | 
					  const isBytedance = !!process.env.BYTEDANCE_API_KEY;
 | 
				
			||||||
  const isAlibaba = !!process.env.ALIBABA_API_KEY;
 | 
					  const isAlibaba = !!process.env.ALIBABA_API_KEY;
 | 
				
			||||||
 | 
					  const isMoonshot = !!process.env.MOONSHOT_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);
 | 
				
			||||||
@@ -122,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 {
 | 
				
			||||||
@@ -131,6 +180,10 @@ export const getServerSideConfig = () => {
 | 
				
			|||||||
    apiKey: getApiKey(process.env.OPENAI_API_KEY),
 | 
					    apiKey: getApiKey(process.env.OPENAI_API_KEY),
 | 
				
			||||||
    openaiOrgId: process.env.OPENAI_ORG_ID,
 | 
					    openaiOrgId: process.env.OPENAI_ORG_ID,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isStability,
 | 
				
			||||||
 | 
					    stabilityUrl: process.env.STABILITY_URL,
 | 
				
			||||||
 | 
					    stabilityApiKey: getApiKey(process.env.STABILITY_API_KEY),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    isAzure,
 | 
					    isAzure,
 | 
				
			||||||
    azureUrl: process.env.AZURE_URL,
 | 
					    azureUrl: process.env.AZURE_URL,
 | 
				
			||||||
    azureApiKey: getApiKey(process.env.AZURE_API_KEY),
 | 
					    azureApiKey: getApiKey(process.env.AZURE_API_KEY),
 | 
				
			||||||
@@ -158,7 +211,43 @@ export const getServerSideConfig = () => {
 | 
				
			|||||||
    alibabaUrl: process.env.ALIBABA_URL,
 | 
					    alibabaUrl: process.env.ALIBABA_URL,
 | 
				
			||||||
    alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY),
 | 
					    alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isTencent,
 | 
				
			||||||
 | 
					    tencentUrl: process.env.TENCENT_URL,
 | 
				
			||||||
 | 
					    tencentSecretKey: getApiKey(process.env.TENCENT_SECRET_KEY),
 | 
				
			||||||
 | 
					    tencentSecretId: process.env.TENCENT_SECRET_ID,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isMoonshot,
 | 
				
			||||||
 | 
					    moonshotUrl: process.env.MOONSHOT_URL,
 | 
				
			||||||
 | 
					    moonshotApiKey: getApiKey(process.env.MOONSHOT_API_KEY),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isIflytek,
 | 
				
			||||||
 | 
					    iflytekUrl: process.env.IFLYTEK_URL,
 | 
				
			||||||
 | 
					    iflytekApiKey: process.env.IFLYTEK_API_KEY,
 | 
				
			||||||
 | 
					    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,
 | 
				
			||||||
 | 
					    cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
 | 
				
			||||||
 | 
					    cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
 | 
				
			||||||
 | 
					    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,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    needCode: ACCESS_CODES.size > 0,
 | 
					    needCode: ACCESS_CODES.size > 0,
 | 
				
			||||||
    code: process.env.CODE,
 | 
					    code: process.env.CODE,
 | 
				
			||||||
@@ -173,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",
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										520
									
								
								app/constant.ts
									
									
									
									
									
								
							
							
						
						
									
										520
									
								
								app/constant.ts
									
									
									
									
									
								
							@@ -1,6 +1,7 @@
 | 
				
			|||||||
export const OWNER = "Yidadaa";
 | 
					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`;
 | 
				
			||||||
@@ -8,7 +9,8 @@ export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/c
 | 
				
			|||||||
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
 | 
					export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
 | 
				
			||||||
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
 | 
					export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DEFAULT_API_HOST = "https://api.nextchat.dev";
 | 
					export const STABILITY_BASE_URL = "https://api.stability.ai";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -21,6 +23,19 @@ export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
 | 
					export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
 | 
				
			||||||
 | 
					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`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,7 +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",
 | 
				
			||||||
 | 
					  SdNew = "/sd-new",
 | 
				
			||||||
 | 
					  Artifacts = "/artifacts",
 | 
				
			||||||
 | 
					  SearchChat = "/search-chat",
 | 
				
			||||||
 | 
					  McpMarket = "/mcp-market",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum ApiPath {
 | 
					export enum ApiPath {
 | 
				
			||||||
@@ -42,6 +63,15 @@ export enum ApiPath {
 | 
				
			|||||||
  Baidu = "/api/baidu",
 | 
					  Baidu = "/api/baidu",
 | 
				
			||||||
  ByteDance = "/api/bytedance",
 | 
					  ByteDance = "/api/bytedance",
 | 
				
			||||||
  Alibaba = "/api/alibaba",
 | 
					  Alibaba = "/api/alibaba",
 | 
				
			||||||
 | 
					  Tencent = "/api/tencent",
 | 
				
			||||||
 | 
					  Moonshot = "/api/moonshot",
 | 
				
			||||||
 | 
					  Iflytek = "/api/iflytek",
 | 
				
			||||||
 | 
					  Stability = "/api/stability",
 | 
				
			||||||
 | 
					  Artifacts = "/api/artifacts",
 | 
				
			||||||
 | 
					  XAI = "/api/xai",
 | 
				
			||||||
 | 
					  ChatGLM = "/api/chatglm",
 | 
				
			||||||
 | 
					  DeepSeek = "/api/deepseek",
 | 
				
			||||||
 | 
					  SiliconFlow = "/api/siliconflow",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum SlotID {
 | 
					export enum SlotID {
 | 
				
			||||||
@@ -56,12 +86,15 @@ export enum FileName {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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",
 | 
				
			||||||
  Prompt = "prompt-store",
 | 
					  Prompt = "prompt-store",
 | 
				
			||||||
  Update = "chat-update",
 | 
					  Update = "chat-update",
 | 
				
			||||||
  Sync = "sync",
 | 
					  Sync = "sync",
 | 
				
			||||||
 | 
					  SdList = "sd-list",
 | 
				
			||||||
 | 
					  Mcp = "mcp-store",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DEFAULT_SIDEBAR_WIDTH = 300;
 | 
					export const DEFAULT_SIDEBAR_WIDTH = 300;
 | 
				
			||||||
@@ -77,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";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -88,17 +122,47 @@ export enum ServiceProvider {
 | 
				
			|||||||
  Baidu = "Baidu",
 | 
					  Baidu = "Baidu",
 | 
				
			||||||
  ByteDance = "ByteDance",
 | 
					  ByteDance = "ByteDance",
 | 
				
			||||||
  Alibaba = "Alibaba",
 | 
					  Alibaba = "Alibaba",
 | 
				
			||||||
 | 
					  Tencent = "Tencent",
 | 
				
			||||||
 | 
					  Moonshot = "Moonshot",
 | 
				
			||||||
 | 
					  Stability = "Stability",
 | 
				
			||||||
 | 
					  Iflytek = "Iflytek",
 | 
				
			||||||
 | 
					  XAI = "XAI",
 | 
				
			||||||
 | 
					  ChatGLM = "ChatGLM",
 | 
				
			||||||
 | 
					  DeepSeek = "DeepSeek",
 | 
				
			||||||
 | 
					  SiliconFlow = "SiliconFlow",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
 | 
				
			||||||
 | 
					// BLOCK_NONE will not block any content, and BLOCK_ONLY_HIGH will block only high-risk content.
 | 
				
			||||||
 | 
					export enum GoogleSafetySettingsThreshold {
 | 
				
			||||||
 | 
					  BLOCK_NONE = "BLOCK_NONE",
 | 
				
			||||||
 | 
					  BLOCK_ONLY_HIGH = "BLOCK_ONLY_HIGH",
 | 
				
			||||||
 | 
					  BLOCK_MEDIUM_AND_ABOVE = "BLOCK_MEDIUM_AND_ABOVE",
 | 
				
			||||||
 | 
					  BLOCK_LOW_AND_ABOVE = "BLOCK_LOW_AND_ABOVE",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum ModelProvider {
 | 
					export enum ModelProvider {
 | 
				
			||||||
 | 
					  Stability = "Stability",
 | 
				
			||||||
  GPT = "GPT",
 | 
					  GPT = "GPT",
 | 
				
			||||||
  GeminiPro = "GeminiPro",
 | 
					  GeminiPro = "GeminiPro",
 | 
				
			||||||
  Claude = "Claude",
 | 
					  Claude = "Claude",
 | 
				
			||||||
  Ernie = "Ernie",
 | 
					  Ernie = "Ernie",
 | 
				
			||||||
  Doubao = "Doubao",
 | 
					  Doubao = "Doubao",
 | 
				
			||||||
  Qwen = "Qwen",
 | 
					  Qwen = "Qwen",
 | 
				
			||||||
 | 
					  Hunyuan = "Hunyuan",
 | 
				
			||||||
 | 
					  Moonshot = "Moonshot",
 | 
				
			||||||
 | 
					  Iflytek = "Iflytek",
 | 
				
			||||||
 | 
					  XAI = "XAI",
 | 
				
			||||||
 | 
					  ChatGLM = "ChatGLM",
 | 
				
			||||||
 | 
					  DeepSeek = "DeepSeek",
 | 
				
			||||||
 | 
					  SiliconFlow = "SiliconFlow",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const Stability = {
 | 
				
			||||||
 | 
					  GeneratePath: "v2beta/stable-image/generate",
 | 
				
			||||||
 | 
					  ExampleEndpoint: "https://api.stability.ai",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Anthropic = {
 | 
					export const Anthropic = {
 | 
				
			||||||
  ChatPath: "v1/messages",
 | 
					  ChatPath: "v1/messages",
 | 
				
			||||||
  ChatPath1: "v1/complete",
 | 
					  ChatPath1: "v1/complete",
 | 
				
			||||||
@@ -108,6 +172,8 @@ 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",
 | 
				
			||||||
  UsagePath: "dashboard/billing/usage",
 | 
					  UsagePath: "dashboard/billing/usage",
 | 
				
			||||||
  SubsPath: "dashboard/billing/subscription",
 | 
					  SubsPath: "dashboard/billing/subscription",
 | 
				
			||||||
  ListModelPath: "v1/models",
 | 
					  ListModelPath: "v1/models",
 | 
				
			||||||
@@ -116,7 +182,10 @@ export const OpenaiPath = {
 | 
				
			|||||||
export const Azure = {
 | 
					export const Azure = {
 | 
				
			||||||
  ChatPath: (deployName: string, apiVersion: string) =>
 | 
					  ChatPath: (deployName: string, apiVersion: string) =>
 | 
				
			||||||
    `deployments/${deployName}/chat/completions?api-version=${apiVersion}`,
 | 
					    `deployments/${deployName}/chat/completions?api-version=${apiVersion}`,
 | 
				
			||||||
  ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}",
 | 
					  // https://<your_resource_name>.openai.azure.com/openai/deployments/<your_deployment_name>/images/generations?api-version=<api_version>
 | 
				
			||||||
 | 
					  ImagePath: (deployName: string, apiVersion: string) =>
 | 
				
			||||||
 | 
					    `deployments/${deployName}/images/generations?api-version=${apiVersion}`,
 | 
				
			||||||
 | 
					  ExampleEndpoint: "https://{resource-url}/openai",
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Google = {
 | 
					export const Google = {
 | 
				
			||||||
@@ -138,9 +207,6 @@ export const Baidu = {
 | 
				
			|||||||
    if (modelName === "ernie-3.5-8k") {
 | 
					    if (modelName === "ernie-3.5-8k") {
 | 
				
			||||||
      endpoint = "completions";
 | 
					      endpoint = "completions";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (modelName === "ernie-speed-128k") {
 | 
					 | 
				
			||||||
      endpoint = "ernie-speed-128k";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (modelName === "ernie-speed-8k") {
 | 
					    if (modelName === "ernie-speed-8k") {
 | 
				
			||||||
      endpoint = "ernie_speed";
 | 
					      endpoint = "ernie_speed";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -155,7 +221,49 @@ 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 = {
 | 
				
			||||||
 | 
					  ExampleEndpoint: TENCENT_BASE_URL,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const Moonshot = {
 | 
				
			||||||
 | 
					  ExampleEndpoint: MOONSHOT_BASE_URL,
 | 
				
			||||||
 | 
					  ChatPath: "v1/chat/completions",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const Iflytek = {
 | 
				
			||||||
 | 
					  ExampleEndpoint: IFLYTEK_BASE_URL,
 | 
				
			||||||
 | 
					  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
 | 
				
			||||||
@@ -176,26 +284,207 @@ Latex inline: \\(x^2\\)
 | 
				
			|||||||
Latex block: $$e=mc^2$$
 | 
					Latex block: $$e=mc^2$$
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
 | 
					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 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-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,
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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",
 | 
				
			||||||
@@ -205,20 +494,54 @@ 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-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",
 | 
				
			||||||
 | 
					  "o1-mini",
 | 
				
			||||||
 | 
					  "o1-preview",
 | 
				
			||||||
 | 
					  "o3-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 = [
 | 
				
			||||||
@@ -227,8 +550,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 = [
 | 
				
			||||||
@@ -262,70 +592,235 @@ 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 = [
 | 
				
			||||||
 | 
					  "hunyuan-pro",
 | 
				
			||||||
 | 
					  "hunyuan-standard",
 | 
				
			||||||
 | 
					  "hunyuan-lite",
 | 
				
			||||||
 | 
					  "hunyuan-role",
 | 
				
			||||||
 | 
					  "hunyuan-functioncall",
 | 
				
			||||||
 | 
					  "hunyuan-code",
 | 
				
			||||||
 | 
					  "hunyuan-vision",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const moonshotModes = ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const iflytekModels = [
 | 
				
			||||||
 | 
					  "general",
 | 
				
			||||||
 | 
					  "generalv3",
 | 
				
			||||||
 | 
					  "pro-128k",
 | 
				
			||||||
 | 
					  "generalv3.5",
 | 
				
			||||||
 | 
					  "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开始
 | 
				
			||||||
export const DEFAULT_MODELS = [
 | 
					export const DEFAULT_MODELS = [
 | 
				
			||||||
  ...openaiModels.map((name) => ({
 | 
					  ...openaiModels.map((name) => ({
 | 
				
			||||||
    name,
 | 
					    name,
 | 
				
			||||||
    available: true,
 | 
					    available: true,
 | 
				
			||||||
 | 
					    sorted: seq++, // Global sequence sort(index)
 | 
				
			||||||
    provider: {
 | 
					    provider: {
 | 
				
			||||||
      id: "openai",
 | 
					      id: "openai",
 | 
				
			||||||
      providerName: "OpenAI",
 | 
					      providerName: "OpenAI",
 | 
				
			||||||
      providerType: "openai",
 | 
					      providerType: "openai",
 | 
				
			||||||
 | 
					      sorted: 1, // 这里是固定的,确保顺序与之前内置的版本一致
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  })),
 | 
					  })),
 | 
				
			||||||
  ...openaiModels.map((name) => ({
 | 
					  ...openaiModels.map((name) => ({
 | 
				
			||||||
    name,
 | 
					    name,
 | 
				
			||||||
    available: true,
 | 
					    available: true,
 | 
				
			||||||
 | 
					    sorted: seq++,
 | 
				
			||||||
    provider: {
 | 
					    provider: {
 | 
				
			||||||
      id: "azure",
 | 
					      id: "azure",
 | 
				
			||||||
      providerName: "Azure",
 | 
					      providerName: "Azure",
 | 
				
			||||||
      providerType: "azure",
 | 
					      providerType: "azure",
 | 
				
			||||||
 | 
					      sorted: 2,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  })),
 | 
					  })),
 | 
				
			||||||
  ...googleModels.map((name) => ({
 | 
					  ...googleModels.map((name) => ({
 | 
				
			||||||
    name,
 | 
					    name,
 | 
				
			||||||
    available: true,
 | 
					    available: true,
 | 
				
			||||||
 | 
					    sorted: seq++,
 | 
				
			||||||
    provider: {
 | 
					    provider: {
 | 
				
			||||||
      id: "google",
 | 
					      id: "google",
 | 
				
			||||||
      providerName: "Google",
 | 
					      providerName: "Google",
 | 
				
			||||||
      providerType: "google",
 | 
					      providerType: "google",
 | 
				
			||||||
 | 
					      sorted: 3,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  })),
 | 
					  })),
 | 
				
			||||||
  ...anthropicModels.map((name) => ({
 | 
					  ...anthropicModels.map((name) => ({
 | 
				
			||||||
    name,
 | 
					    name,
 | 
				
			||||||
    available: true,
 | 
					    available: true,
 | 
				
			||||||
 | 
					    sorted: seq++,
 | 
				
			||||||
    provider: {
 | 
					    provider: {
 | 
				
			||||||
      id: "anthropic",
 | 
					      id: "anthropic",
 | 
				
			||||||
      providerName: "Anthropic",
 | 
					      providerName: "Anthropic",
 | 
				
			||||||
      providerType: "anthropic",
 | 
					      providerType: "anthropic",
 | 
				
			||||||
 | 
					      sorted: 4,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  })),
 | 
					  })),
 | 
				
			||||||
  ...baiduModels.map((name) => ({
 | 
					  ...baiduModels.map((name) => ({
 | 
				
			||||||
    name,
 | 
					    name,
 | 
				
			||||||
    available: true,
 | 
					    available: true,
 | 
				
			||||||
 | 
					    sorted: seq++,
 | 
				
			||||||
    provider: {
 | 
					    provider: {
 | 
				
			||||||
      id: "baidu",
 | 
					      id: "baidu",
 | 
				
			||||||
      providerName: "Baidu",
 | 
					      providerName: "Baidu",
 | 
				
			||||||
      providerType: "baidu",
 | 
					      providerType: "baidu",
 | 
				
			||||||
 | 
					      sorted: 5,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  })),
 | 
					  })),
 | 
				
			||||||
  ...bytedanceModels.map((name) => ({
 | 
					  ...bytedanceModels.map((name) => ({
 | 
				
			||||||
    name,
 | 
					    name,
 | 
				
			||||||
    available: true,
 | 
					    available: true,
 | 
				
			||||||
 | 
					    sorted: seq++,
 | 
				
			||||||
    provider: {
 | 
					    provider: {
 | 
				
			||||||
      id: "bytedance",
 | 
					      id: "bytedance",
 | 
				
			||||||
      providerName: "ByteDance",
 | 
					      providerName: "ByteDance",
 | 
				
			||||||
      providerType: "bytedance",
 | 
					      providerType: "bytedance",
 | 
				
			||||||
 | 
					      sorted: 6,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  })),
 | 
					  })),
 | 
				
			||||||
  ...alibabaModes.map((name) => ({
 | 
					  ...alibabaModes.map((name) => ({
 | 
				
			||||||
    name,
 | 
					    name,
 | 
				
			||||||
    available: true,
 | 
					    available: true,
 | 
				
			||||||
 | 
					    sorted: seq++,
 | 
				
			||||||
    provider: {
 | 
					    provider: {
 | 
				
			||||||
      id: "alibaba",
 | 
					      id: "alibaba",
 | 
				
			||||||
      providerName: "Alibaba",
 | 
					      providerName: "Alibaba",
 | 
				
			||||||
      providerType: "alibaba",
 | 
					      providerType: "alibaba",
 | 
				
			||||||
 | 
					      sorted: 7,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })),
 | 
				
			||||||
 | 
					  ...tencentModels.map((name) => ({
 | 
				
			||||||
 | 
					    name,
 | 
				
			||||||
 | 
					    available: true,
 | 
				
			||||||
 | 
					    sorted: seq++,
 | 
				
			||||||
 | 
					    provider: {
 | 
				
			||||||
 | 
					      id: "tencent",
 | 
				
			||||||
 | 
					      providerName: "Tencent",
 | 
				
			||||||
 | 
					      providerType: "tencent",
 | 
				
			||||||
 | 
					      sorted: 8,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })),
 | 
				
			||||||
 | 
					  ...moonshotModes.map((name) => ({
 | 
				
			||||||
 | 
					    name,
 | 
				
			||||||
 | 
					    available: true,
 | 
				
			||||||
 | 
					    sorted: seq++,
 | 
				
			||||||
 | 
					    provider: {
 | 
				
			||||||
 | 
					      id: "moonshot",
 | 
				
			||||||
 | 
					      providerName: "Moonshot",
 | 
				
			||||||
 | 
					      providerType: "moonshot",
 | 
				
			||||||
 | 
					      sorted: 9,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })),
 | 
				
			||||||
 | 
					  ...iflytekModels.map((name) => ({
 | 
				
			||||||
 | 
					    name,
 | 
				
			||||||
 | 
					    available: true,
 | 
				
			||||||
 | 
					    sorted: seq++,
 | 
				
			||||||
 | 
					    provider: {
 | 
				
			||||||
 | 
					      id: "iflytek",
 | 
				
			||||||
 | 
					      providerName: "Iflytek",
 | 
				
			||||||
 | 
					      providerType: "iflytek",
 | 
				
			||||||
 | 
					      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;
 | 
				
			||||||
@@ -345,3 +840,8 @@ export const internalAllowedWebDavEndpoints = [
 | 
				
			|||||||
  "https://webdav.yandex.com",
 | 
					  "https://webdav.yandex.com",
 | 
				
			||||||
  "https://app.koofr.net/dav/Koofr",
 | 
					  "https://app.koofr.net/dav/Koofr",
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DEFAULT_GA_ID = "G-89WN60ZK2E";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
									
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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  | 
							
								
								
									
										7
									
								
								app/icons/discovery.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/icons/discovery.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" height="1.2rem" viewBox="0 0 24 24">
 | 
				
			||||||
 | 
					    <g fill="none" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
 | 
				
			||||||
 | 
					        <circle cx="12" cy="12" r="9" />
 | 
				
			||||||
 | 
					        <path
 | 
				
			||||||
 | 
					            d="M11.307 9.739L15 9l-.739 3.693a2 2 0 0 1-1.568 1.569L9 15l.739-3.693a2 2 0 0 1 1.568-1.568" />
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 371 B  | 
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user