mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	Compare commits
	
		
			192 Commits
		
	
	
		
			v2.13.1
			...
			87325fad74
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					87325fad74 | ||
| 
						 | 
					122aa94c9f | ||
| 
						 | 
					e2e8a45104 | ||
| 
						 | 
					fb5fc13f72 | ||
| 
						 | 
					edb92f7bfb | ||
| 
						 | 
					ca865a80dc | ||
| 
						 | 
					cf1c8e8f2a | ||
| 
						 | 
					d948be2372 | ||
| 
						 | 
					cc28aef625 | ||
| 
						 | 
					036358de7c | ||
| 
						 | 
					0958b9ee12 | ||
| 
						 | 
					aff1d7ecd6 | ||
| 
						 | 
					42fdbd9bb8 | ||
| 
						 | 
					034c82e514 | ||
| 
						 | 
					14ff46b5cd | ||
| 
						 | 
					c9099ca0a5 | ||
| 
						 | 
					58b144b345 | ||
| 
						 | 
					af21c57e77 | ||
| 
						 | 
					624e4dbaaf | ||
| 
						 | 
					9bbb4f396b | ||
| 
						 | 
					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 | ||
| 
						 | 
					3935c725c9 | ||
| 
						 | 
					908ee0060f | ||
| 
						 | 
					82e6fd7bb5 | ||
| 
						 | 
					6b98b14179 | ||
| 
						 | 
					1ecefd88f7 | ||
| 
						 | 
					2e9e20ce7c | ||
| 
						 | 
					fb60fbb217 | ||
| 
						 | 
					4199e17da0 | ||
| 
						 | 
					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 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -44,3 +44,5 @@ dev
 | 
			
		||||
 | 
			
		||||
*.key
 | 
			
		||||
*.key.pub
 | 
			
		||||
 | 
			
		||||
masks.json
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										41
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								README.md
									
									
									
									
									
								
							@@ -88,10 +88,15 @@ 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] 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.
 | 
			
		||||
- [ ] 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 artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 | 
			
		||||
  - [x] artifacts
 | 
			
		||||
  - [ ] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 | 
			
		||||
- [ ] local knowledge base
 | 
			
		||||
 | 
			
		||||
## What's New
 | 
			
		||||
 | 
			
		||||
- 🚀 v2.14.0 Now supports  Artifacts & SD 
 | 
			
		||||
- 🚀 v2.10.1 support Google Gemini Pro model.
 | 
			
		||||
- 🚀 v2.9.11 you can use azure endpoint now.
 | 
			
		||||
- 🚀 v2.8 now we have a client that runs across all platforms!
 | 
			
		||||
@@ -120,15 +125,21 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
 | 
			
		||||
- [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)
 | 
			
		||||
- [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
 | 
			
		||||
- [x] 插件机制,支持 artifacts,联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 | 
			
		||||
   - [x] artifacts
 | 
			
		||||
   - [ ] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 | 
			
		||||
 - [ ] 本地知识库
 | 
			
		||||
 | 
			
		||||
## 最新动态
 | 
			
		||||
 | 
			
		||||
- 🚀 v2.14.0 现在支持 Artifacts & SD 了。
 | 
			
		||||
- 🚀 v2.10.1 现在支持 Gemini Pro 模型。
 | 
			
		||||
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
 | 
			
		||||
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
 | 
			
		||||
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
 | 
			
		||||
- 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。
 | 
			
		||||
- 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com
 | 
			
		||||
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
 | 
			
		||||
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
 | 
			
		||||
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
 | 
			
		||||
 | 
			
		||||
## Get Started
 | 
			
		||||
 | 
			
		||||
@@ -271,6 +282,18 @@ Alibaba Cloud Api Key.
 | 
			
		||||
 | 
			
		||||
Alibaba Cloud Api Url.
 | 
			
		||||
 | 
			
		||||
### `IFLYTEK_URL` (Optional)
 | 
			
		||||
 | 
			
		||||
iflytek Api Url.
 | 
			
		||||
 | 
			
		||||
### `IFLYTEK_API_KEY` (Optional)
 | 
			
		||||
 | 
			
		||||
iflytek Api Key.
 | 
			
		||||
 | 
			
		||||
### `IFLYTEK_API_SECRET` (Optional)
 | 
			
		||||
 | 
			
		||||
iflytek Api Secret.
 | 
			
		||||
 | 
			
		||||
### `HIDE_USER_API_KEY` (optional)
 | 
			
		||||
 | 
			
		||||
> Default: Empty
 | 
			
		||||
@@ -326,6 +349,14 @@ 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.
 | 
			
		||||
 | 
			
		||||
### `STABILITY_API_KEY` (optional)
 | 
			
		||||
 | 
			
		||||
Stability API key.
 | 
			
		||||
 | 
			
		||||
### `STABILITY_URL` (optional)
 | 
			
		||||
 | 
			
		||||
Customize Stability API url.
 | 
			
		||||
 | 
			
		||||
## Requirements
 | 
			
		||||
 | 
			
		||||
NodeJS >= 18, Docker >= 20
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								README_CN.md
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								README_CN.md
									
									
									
									
									
								
							@@ -172,6 +172,20 @@ ByteDance Api Url.
 | 
			
		||||
 | 
			
		||||
阿里云(千问)Api Url.
 | 
			
		||||
 | 
			
		||||
### `IFLYTEK_URL` (可选)
 | 
			
		||||
 | 
			
		||||
讯飞星火Api Url.
 | 
			
		||||
 | 
			
		||||
### `IFLYTEK_API_KEY` (可选)
 | 
			
		||||
 | 
			
		||||
讯飞星火Api Key.
 | 
			
		||||
 | 
			
		||||
### `IFLYTEK_API_SECRET` (可选)
 | 
			
		||||
 | 
			
		||||
讯飞星火Api Secret.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### `HIDE_USER_API_KEY` (可选)
 | 
			
		||||
 | 
			
		||||
如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
 | 
			
		||||
@@ -218,6 +232,15 @@ ByteDance Api Url.
 | 
			
		||||
 | 
			
		||||
自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项
 | 
			
		||||
 | 
			
		||||
### `STABILITY_API_KEY` (optional)
 | 
			
		||||
 | 
			
		||||
Stability API密钥
 | 
			
		||||
 | 
			
		||||
### `STABILITY_URL` (optional)
 | 
			
		||||
 | 
			
		||||
自定义的Stability API请求地址
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## 开发
 | 
			
		||||
 | 
			
		||||
点击下方按钮,开始二次开发:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										66
									
								
								app/api/[provider]/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								app/api/[provider]/[...path]/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
import { ApiPath } from "@/app/constant";
 | 
			
		||||
import { NextRequest, NextResponse } 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";
 | 
			
		||||
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 });
 | 
			
		||||
    default:
 | 
			
		||||
      return openaiHandler(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",
 | 
			
		||||
];
 | 
			
		||||
@@ -14,7 +14,7 @@ import type { RequestPayload } from "@/app/client/platforms/openai";
 | 
			
		||||
 | 
			
		||||
const serverConfig = getServerSideConfig();
 | 
			
		||||
 | 
			
		||||
async function handle(
 | 
			
		||||
export async function handle(
 | 
			
		||||
  req: NextRequest,
 | 
			
		||||
  { params }: { params: { path: string[] } },
 | 
			
		||||
) {
 | 
			
		||||
@@ -40,30 +40,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) {
 | 
			
		||||
  const controller = new AbortController();
 | 
			
		||||
 | 
			
		||||
@@ -9,13 +9,13 @@ import {
 | 
			
		||||
} from "@/app/constant";
 | 
			
		||||
import { prettyObject } from "@/app/utils/format";
 | 
			
		||||
import { NextRequest, NextResponse } from "next/server";
 | 
			
		||||
import { auth } from "../../auth";
 | 
			
		||||
import { auth } from "./auth";
 | 
			
		||||
import { isModelAvailableInServer } from "@/app/utils/model";
 | 
			
		||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
			
		||||
 | 
			
		||||
const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
 | 
			
		||||
 | 
			
		||||
async function handle(
 | 
			
		||||
export async function handle(
 | 
			
		||||
  req: NextRequest,
 | 
			
		||||
  { params }: { params: { path: string[] } },
 | 
			
		||||
) {
 | 
			
		||||
@@ -56,30 +56,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();
 | 
			
		||||
 | 
			
		||||
async function request(req: NextRequest) {
 | 
			
		||||
							
								
								
									
										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;
 | 
			
		||||
 | 
			
		||||
    switch (modelProvider) {
 | 
			
		||||
      case ModelProvider.Stability:
 | 
			
		||||
        systemApiKey = serverConfig.stabilityApiKey;
 | 
			
		||||
        break;
 | 
			
		||||
      case ModelProvider.GeminiPro:
 | 
			
		||||
        systemApiKey = serverConfig.googleApiKey;
 | 
			
		||||
        break;
 | 
			
		||||
@@ -82,6 +85,13 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
 | 
			
		||||
      case ModelProvider.Qwen:
 | 
			
		||||
        systemApiKey = serverConfig.alibabaApiKey;
 | 
			
		||||
        break;
 | 
			
		||||
      case ModelProvider.Moonshot:
 | 
			
		||||
        systemApiKey = serverConfig.moonshotApiKey;
 | 
			
		||||
        break;
 | 
			
		||||
      case ModelProvider.Iflytek:
 | 
			
		||||
        systemApiKey =
 | 
			
		||||
          serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
 | 
			
		||||
        break;
 | 
			
		||||
      case ModelProvider.GPT:
 | 
			
		||||
      default:
 | 
			
		||||
        if (req.nextUrl.pathname.includes("azure/deployments")) {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,10 @@ import { getServerSideConfig } from "@/app/config/server";
 | 
			
		||||
import { ModelProvider } from "@/app/constant";
 | 
			
		||||
import { prettyObject } from "@/app/utils/format";
 | 
			
		||||
import { NextRequest, NextResponse } from "next/server";
 | 
			
		||||
import { auth } from "../../auth";
 | 
			
		||||
import { requestOpenai } from "../../common";
 | 
			
		||||
import { auth } from "./auth";
 | 
			
		||||
import { requestOpenai } from "./common";
 | 
			
		||||
 | 
			
		||||
async function handle(
 | 
			
		||||
export async function handle(
 | 
			
		||||
  req: NextRequest,
 | 
			
		||||
  { params }: { params: { path: string[] } },
 | 
			
		||||
) {
 | 
			
		||||
@@ -31,27 +31,3 @@ async function handle(
 | 
			
		||||
    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",
 | 
			
		||||
];
 | 
			
		||||
@@ -14,7 +14,7 @@ import { getAccessToken } from "@/app/utils/baidu";
 | 
			
		||||
 | 
			
		||||
const serverConfig = getServerSideConfig();
 | 
			
		||||
 | 
			
		||||
async function handle(
 | 
			
		||||
export async function handle(
 | 
			
		||||
  req: NextRequest,
 | 
			
		||||
  { params }: { params: { path: string[] } },
 | 
			
		||||
) {
 | 
			
		||||
@@ -52,30 +52,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) {
 | 
			
		||||
  const controller = new AbortController();
 | 
			
		||||
 | 
			
		||||
@@ -12,7 +12,7 @@ import { isModelAvailableInServer } from "@/app/utils/model";
 | 
			
		||||
 | 
			
		||||
const serverConfig = getServerSideConfig();
 | 
			
		||||
 | 
			
		||||
async function handle(
 | 
			
		||||
export async function handle(
 | 
			
		||||
  req: NextRequest,
 | 
			
		||||
  { 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) {
 | 
			
		||||
  const controller = new AbortController();
 | 
			
		||||
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { NextRequest, NextResponse } from "next/server";
 | 
			
		||||
import { auth } from "../../auth";
 | 
			
		||||
import { auth } from "./auth";
 | 
			
		||||
import { getServerSideConfig } from "@/app/config/server";
 | 
			
		||||
import {
 | 
			
		||||
  ApiPath,
 | 
			
		||||
@@ -11,9 +11,9 @@ import { prettyObject } from "@/app/utils/format";
 | 
			
		||||
 | 
			
		||||
const serverConfig = getServerSideConfig();
 | 
			
		||||
 | 
			
		||||
async function handle(
 | 
			
		||||
export async function handle(
 | 
			
		||||
  req: NextRequest,
 | 
			
		||||
  { params }: { params: { path: string[] } },
 | 
			
		||||
  { params }: { params: { provider: string; path: string[] } },
 | 
			
		||||
) {
 | 
			
		||||
  console.log("[Google Route] params ", params);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										131
									
								
								app/api/iflytek.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								app/api/iflytek.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,131 @@
 | 
			
		||||
import { getServerSideConfig } from "@/app/config/server";
 | 
			
		||||
import {
 | 
			
		||||
  Iflytek,
 | 
			
		||||
  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 { isModelAvailableInServer } from "@/app/utils/model";
 | 
			
		||||
import type { RequestPayload } from "@/app/client/platforms/openai";
 | 
			
		||||
// 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 (
 | 
			
		||||
        isModelAvailableInServer(
 | 
			
		||||
          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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										130
									
								
								app/api/moonshot.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								app/api/moonshot.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,130 @@
 | 
			
		||||
import { getServerSideConfig } from "@/app/config/server";
 | 
			
		||||
import {
 | 
			
		||||
  Moonshot,
 | 
			
		||||
  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 { isModelAvailableInServer } from "@/app/utils/model";
 | 
			
		||||
import type { RequestPayload } from "@/app/client/platforms/openai";
 | 
			
		||||
 | 
			
		||||
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 (
 | 
			
		||||
        isModelAvailableInServer(
 | 
			
		||||
          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,8 +3,8 @@ import { getServerSideConfig } from "@/app/config/server";
 | 
			
		||||
import { ModelProvider, OpenaiPath } from "@/app/constant";
 | 
			
		||||
import { prettyObject } from "@/app/utils/format";
 | 
			
		||||
import { NextRequest, NextResponse } from "next/server";
 | 
			
		||||
import { auth } from "../../auth";
 | 
			
		||||
import { requestOpenai } from "../../common";
 | 
			
		||||
import { auth } from "./auth";
 | 
			
		||||
import { requestOpenai } from "./common";
 | 
			
		||||
 | 
			
		||||
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
 | 
			
		||||
 | 
			
		||||
@@ -13,14 +13,14 @@ function getModels(remoteModelRes: OpenAIListModelResponse) {
 | 
			
		||||
 | 
			
		||||
  if (config.disableGPT4) {
 | 
			
		||||
    remoteModelRes.data = remoteModelRes.data.filter(
 | 
			
		||||
      (m) => !m.id.startsWith("gpt-4"),
 | 
			
		||||
      (m) => !m.id.startsWith("gpt-4") || m.id.startsWith("gpt-4o-mini"),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return remoteModelRes;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function handle(
 | 
			
		||||
export async function handle(
 | 
			
		||||
  req: NextRequest,
 | 
			
		||||
  { params }: { params: { path: string[] } },
 | 
			
		||||
) {
 | 
			
		||||
@@ -70,27 +70,3 @@ async function handle(
 | 
			
		||||
    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",
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								app/api/tencent/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								app/api/tencent/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
import { getServerSideConfig } from "@/app/config/server";
 | 
			
		||||
import {
 | 
			
		||||
  TENCENT_BASE_URL,
 | 
			
		||||
  ApiPath,
 | 
			
		||||
  ModelProvider,
 | 
			
		||||
  ServiceProvider,
 | 
			
		||||
  Tencent,
 | 
			
		||||
} from "@/app/constant";
 | 
			
		||||
import { prettyObject } from "@/app/utils/format";
 | 
			
		||||
import { NextRequest, NextResponse } from "next/server";
 | 
			
		||||
import { auth } from "@/app/api/auth";
 | 
			
		||||
import { isModelAvailableInServer } from "@/app/utils/model";
 | 
			
		||||
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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -29,6 +29,7 @@ async function handle(
 | 
			
		||||
 | 
			
		||||
  const requestUrl = new URL(req.url);
 | 
			
		||||
  let endpoint = requestUrl.searchParams.get("endpoint");
 | 
			
		||||
  let proxy_method = requestUrl.searchParams.get("proxy_method") || req.method;
 | 
			
		||||
 | 
			
		||||
  // Validate the endpoint to prevent potential SSRF attacks
 | 
			
		||||
  if (
 | 
			
		||||
@@ -37,9 +38,13 @@ async function handle(
 | 
			
		||||
      const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
 | 
			
		||||
      const normalizedEndpoint = normalizeUrl(endpoint as string);
 | 
			
		||||
 | 
			
		||||
      return normalizedEndpoint &&
 | 
			
		||||
      return (
 | 
			
		||||
        normalizedEndpoint &&
 | 
			
		||||
        normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
 | 
			
		||||
        normalizedEndpoint.pathname.startsWith(normalizedAllowedEndpoint.pathname);
 | 
			
		||||
        normalizedEndpoint.pathname.startsWith(
 | 
			
		||||
          normalizedAllowedEndpoint.pathname,
 | 
			
		||||
        )
 | 
			
		||||
      );
 | 
			
		||||
    })
 | 
			
		||||
  ) {
 | 
			
		||||
    return NextResponse.json(
 | 
			
		||||
@@ -61,7 +66,11 @@ async function handle(
 | 
			
		||||
  const targetPath = `${endpoint}${endpointPath}`;
 | 
			
		||||
 | 
			
		||||
  // 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(
 | 
			
		||||
      {
 | 
			
		||||
        error: true,
 | 
			
		||||
@@ -74,7 +83,7 @@ async function handle(
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // for MKCOL request, only allow request ${folder}
 | 
			
		||||
  if (req.method === "MKCOL" && !targetPath.endsWith(folder)) {
 | 
			
		||||
  if (proxy_method === "MKCOL" && !targetPath.endsWith(folder)) {
 | 
			
		||||
    return NextResponse.json(
 | 
			
		||||
      {
 | 
			
		||||
        error: true,
 | 
			
		||||
@@ -87,7 +96,7 @@ async function handle(
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 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(
 | 
			
		||||
      {
 | 
			
		||||
        error: true,
 | 
			
		||||
@@ -100,7 +109,7 @@ async function handle(
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  //   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(
 | 
			
		||||
      {
 | 
			
		||||
        error: true,
 | 
			
		||||
@@ -114,7 +123,7 @@ async function handle(
 | 
			
		||||
 | 
			
		||||
  const targetUrl = targetPath;
 | 
			
		||||
 | 
			
		||||
  const method = req.method;
 | 
			
		||||
  const method = proxy_method || req.method;
 | 
			
		||||
  const shouldNotHaveBody = ["get", "head"].includes(
 | 
			
		||||
    method?.toLowerCase() ?? "",
 | 
			
		||||
  );
 | 
			
		||||
@@ -139,7 +148,7 @@ async function handle(
 | 
			
		||||
      "[Any Proxy]",
 | 
			
		||||
      targetUrl,
 | 
			
		||||
      {
 | 
			
		||||
        method: req.method,
 | 
			
		||||
        method: method,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        status: fetchResult?.status,
 | 
			
		||||
 
 | 
			
		||||
@@ -6,12 +6,15 @@ import {
 | 
			
		||||
  ServiceProvider,
 | 
			
		||||
} from "../constant";
 | 
			
		||||
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
 | 
			
		||||
import { ChatGPTApi } from "./platforms/openai";
 | 
			
		||||
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
 | 
			
		||||
import { GeminiProApi } from "./platforms/google";
 | 
			
		||||
import { ClaudeApi } from "./platforms/anthropic";
 | 
			
		||||
import { ErnieApi } from "./platforms/baidu";
 | 
			
		||||
import { DoubaoApi } from "./platforms/bytedance";
 | 
			
		||||
import { QwenApi } from "./platforms/alibaba";
 | 
			
		||||
import { HunyuanApi } from "./platforms/tencent";
 | 
			
		||||
import { MoonshotApi } from "./platforms/moonshot";
 | 
			
		||||
import { SparkApi } from "./platforms/iflytek";
 | 
			
		||||
 | 
			
		||||
export const ROLES = ["system", "user", "assistant"] as const;
 | 
			
		||||
export type MessageRole = (typeof ROLES)[number];
 | 
			
		||||
@@ -40,6 +43,9 @@ export interface LLMConfig {
 | 
			
		||||
  stream?: boolean;
 | 
			
		||||
  presence_penalty?: number;
 | 
			
		||||
  frequency_penalty?: number;
 | 
			
		||||
  size?: DalleRequestPayload["size"];
 | 
			
		||||
  quality?: DalleRequestPayload["quality"];
 | 
			
		||||
  style?: DalleRequestPayload["style"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ChatOptions {
 | 
			
		||||
@@ -62,12 +68,14 @@ export interface LLMModel {
 | 
			
		||||
  displayName?: string;
 | 
			
		||||
  available: boolean;
 | 
			
		||||
  provider: LLMModelProvider;
 | 
			
		||||
  sorted: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface LLMModelProvider {
 | 
			
		||||
  id: string;
 | 
			
		||||
  providerName: string;
 | 
			
		||||
  providerType: string;
 | 
			
		||||
  sorted: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export abstract class LLMApi {
 | 
			
		||||
@@ -117,6 +125,15 @@ export class ClientApi {
 | 
			
		||||
      case ModelProvider.Qwen:
 | 
			
		||||
        this.llm = new QwenApi();
 | 
			
		||||
        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;
 | 
			
		||||
      default:
 | 
			
		||||
        this.llm = new ChatGPTApi();
 | 
			
		||||
    }
 | 
			
		||||
@@ -168,6 +185,19 @@ export class ClientApi {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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() {
 | 
			
		||||
  const accessStore = useAccessStore.getState();
 | 
			
		||||
  const chatStore = useChatStore.getState();
 | 
			
		||||
@@ -186,6 +216,8 @@ export function getHeaders() {
 | 
			
		||||
    const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
 | 
			
		||||
    const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance;
 | 
			
		||||
    const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
 | 
			
		||||
    const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
 | 
			
		||||
    const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
 | 
			
		||||
    const isEnabledAccessControl = accessStore.enabledAccessControl();
 | 
			
		||||
    const apiKey = isGoogle
 | 
			
		||||
      ? accessStore.googleApiKey
 | 
			
		||||
@@ -197,6 +229,12 @@ export function getHeaders() {
 | 
			
		||||
      ? accessStore.bytedanceApiKey
 | 
			
		||||
      : isAlibaba
 | 
			
		||||
      ? accessStore.alibabaApiKey
 | 
			
		||||
      : isMoonshot
 | 
			
		||||
      ? accessStore.moonshotApiKey
 | 
			
		||||
      : isIflytek
 | 
			
		||||
      ? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
 | 
			
		||||
        ? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
 | 
			
		||||
        : ""
 | 
			
		||||
      : accessStore.openaiApiKey;
 | 
			
		||||
    return {
 | 
			
		||||
      isGoogle,
 | 
			
		||||
@@ -205,6 +243,8 @@ export function getHeaders() {
 | 
			
		||||
      isBaidu,
 | 
			
		||||
      isByteDance,
 | 
			
		||||
      isAlibaba,
 | 
			
		||||
      isMoonshot,
 | 
			
		||||
      isIflytek,
 | 
			
		||||
      apiKey,
 | 
			
		||||
      isEnabledAccessControl,
 | 
			
		||||
    };
 | 
			
		||||
@@ -214,15 +254,6 @@ export function getHeaders() {
 | 
			
		||||
    return isAzure ? "api-key" : isAnthropic ? "x-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 {
 | 
			
		||||
    isGoogle,
 | 
			
		||||
    isAzure,
 | 
			
		||||
@@ -263,6 +294,12 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
 | 
			
		||||
      return new ClientApi(ModelProvider.Doubao);
 | 
			
		||||
    case ServiceProvider.Alibaba:
 | 
			
		||||
      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);
 | 
			
		||||
    default:
 | 
			
		||||
      return new ClientApi(ModelProvider.GPT);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -77,16 +77,24 @@ export class ErnieApi implements LLMApi {
 | 
			
		||||
 | 
			
		||||
  async chat(options: ChatOptions) {
 | 
			
		||||
    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),
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    // "error_code": 336006, "error_msg": "the length of messages must be an odd number",
 | 
			
		||||
    if (messages.length % 2 === 0) {
 | 
			
		||||
      messages.unshift({
 | 
			
		||||
        role: "user",
 | 
			
		||||
        content: " ",
 | 
			
		||||
      });
 | 
			
		||||
      if (messages.at(0)?.role === "user") {
 | 
			
		||||
        messages.splice(1, 0, {
 | 
			
		||||
          role: "assistant",
 | 
			
		||||
          content: " ",
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        messages.unshift({
 | 
			
		||||
          role: "user",
 | 
			
		||||
          content: " ",
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const modelConfig = {
 | 
			
		||||
 
 | 
			
		||||
@@ -25,11 +25,9 @@ export class GeminiProApi implements LLMApi {
 | 
			
		||||
      baseUrl = accessStore.googleUrl;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const isApp = !!getClientConfig()?.isApp;
 | 
			
		||||
    if (baseUrl.length === 0) {
 | 
			
		||||
      const isApp = !!getClientConfig()?.isApp;
 | 
			
		||||
      baseUrl = isApp
 | 
			
		||||
        ? DEFAULT_API_HOST + `/api/proxy/google?key=${accessStore.googleApiKey}`
 | 
			
		||||
        : ApiPath.Google;
 | 
			
		||||
      baseUrl = isApp ? DEFAULT_API_HOST + `/api/proxy/google` : ApiPath.Google;
 | 
			
		||||
    }
 | 
			
		||||
    if (baseUrl.endsWith("/")) {
 | 
			
		||||
      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
 | 
			
		||||
@@ -43,6 +41,10 @@ export class GeminiProApi implements LLMApi {
 | 
			
		||||
    let chatPath = [baseUrl, path].join("/");
 | 
			
		||||
 | 
			
		||||
    chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
 | 
			
		||||
    // if chatPath.startsWith('http') then add key in query string
 | 
			
		||||
    if (chatPath.startsWith("http") && accessStore.googleApiKey) {
 | 
			
		||||
      chatPath += `&key=${accessStore.googleApiKey}`;
 | 
			
		||||
    }
 | 
			
		||||
    return chatPath;
 | 
			
		||||
  }
 | 
			
		||||
  extractMessage(res: any) {
 | 
			
		||||
@@ -106,6 +108,9 @@ export class GeminiProApi implements LLMApi {
 | 
			
		||||
    // if (visionModel && messages.length > 1) {
 | 
			
		||||
    //   options.onError?.(new Error("Multiturn chat is not enabled for models/gemini-pro-vision"));
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    const accessStore = useAccessStore.getState();
 | 
			
		||||
 | 
			
		||||
    const modelConfig = {
 | 
			
		||||
      ...useAppConfig.getState().modelConfig,
 | 
			
		||||
      ...useChatStore.getState().currentSession().mask.modelConfig,
 | 
			
		||||
@@ -127,19 +132,19 @@ export class GeminiProApi implements LLMApi {
 | 
			
		||||
      safetySettings: [
 | 
			
		||||
        {
 | 
			
		||||
          category: "HARM_CATEGORY_HARASSMENT",
 | 
			
		||||
          threshold: "BLOCK_ONLY_HIGH",
 | 
			
		||||
          threshold: accessStore.googleSafetySettings,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          category: "HARM_CATEGORY_HATE_SPEECH",
 | 
			
		||||
          threshold: "BLOCK_ONLY_HIGH",
 | 
			
		||||
          threshold: accessStore.googleSafetySettings,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
 | 
			
		||||
          threshold: "BLOCK_ONLY_HIGH",
 | 
			
		||||
          threshold: accessStore.googleSafetySettings,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          category: "HARM_CATEGORY_DANGEROUS_CONTENT",
 | 
			
		||||
          threshold: "BLOCK_ONLY_HIGH",
 | 
			
		||||
          threshold: accessStore.googleSafetySettings,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										240
									
								
								app/client/platforms/iflytek.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								app/client/platforms/iflytek.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,240 @@
 | 
			
		||||
"use client";
 | 
			
		||||
import {
 | 
			
		||||
  ApiPath,
 | 
			
		||||
  DEFAULT_API_HOST,
 | 
			
		||||
  Iflytek,
 | 
			
		||||
  REQUEST_TIMEOUT_MS,
 | 
			
		||||
} from "@/app/constant";
 | 
			
		||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
			
		||||
 | 
			
		||||
import { ChatOptions, getHeaders, LLMApi, LLMModel } 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 { OpenAIListModelResponse, 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 ? DEFAULT_API_HOST + "/proxy" + apiPath : 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 ?? "";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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;
 | 
			
		||||
 | 
			
		||||
        // 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);
 | 
			
		||||
          }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        controller.signal.onabort = finish;
 | 
			
		||||
 | 
			
		||||
        fetchEventSource(chatPath, {
 | 
			
		||||
          ...chatPayload,
 | 
			
		||||
          async onopen(res) {
 | 
			
		||||
            clearTimeout(requestTimeoutId);
 | 
			
		||||
            const contentType = res.headers.get("content-type");
 | 
			
		||||
            console.log("[Spark] request response content type: ", contentType);
 | 
			
		||||
 | 
			
		||||
            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);
 | 
			
		||||
      }
 | 
			
		||||
    } 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 [];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										251
									
								
								app/client/platforms/moonshot.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								app/client/platforms/moonshot.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,251 @@
 | 
			
		||||
"use client";
 | 
			
		||||
// azure and openai, using same models. so using same LLMApi.
 | 
			
		||||
import {
 | 
			
		||||
  ApiPath,
 | 
			
		||||
  DEFAULT_API_HOST,
 | 
			
		||||
  DEFAULT_MODELS,
 | 
			
		||||
  Moonshot,
 | 
			
		||||
  REQUEST_TIMEOUT_MS,
 | 
			
		||||
  ServiceProvider,
 | 
			
		||||
} from "@/app/constant";
 | 
			
		||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
			
		||||
import { collectModelsWithDefaultModel } from "@/app/utils/model";
 | 
			
		||||
import { preProcessImageContent } from "@/app/utils/chat";
 | 
			
		||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  ChatOptions,
 | 
			
		||||
  getHeaders,
 | 
			
		||||
  LLMApi,
 | 
			
		||||
  LLMModel,
 | 
			
		||||
  LLMUsage,
 | 
			
		||||
  MultimodalContent,
 | 
			
		||||
} 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 { OpenAIListModelResponse, RequestPayload } from "./openai";
 | 
			
		||||
 | 
			
		||||
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 ? DEFAULT_API_HOST + "/proxy" + apiPath : 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 ?? "";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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) {
 | 
			
		||||
        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;
 | 
			
		||||
              }
 | 
			
		||||
            } 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);
 | 
			
		||||
      }
 | 
			
		||||
    } 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 [];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,8 +11,13 @@ import {
 | 
			
		||||
} from "@/app/constant";
 | 
			
		||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
			
		||||
import { collectModelsWithDefaultModel } from "@/app/utils/model";
 | 
			
		||||
import { preProcessImageContent } from "@/app/utils/chat";
 | 
			
		||||
import {
 | 
			
		||||
  preProcessImageContent,
 | 
			
		||||
  uploadImage,
 | 
			
		||||
  base64Image2Blob,
 | 
			
		||||
} from "@/app/utils/chat";
 | 
			
		||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
 | 
			
		||||
import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  ChatOptions,
 | 
			
		||||
@@ -33,6 +38,7 @@ import {
 | 
			
		||||
  getMessageTextContent,
 | 
			
		||||
  getMessageImages,
 | 
			
		||||
  isVisionModel,
 | 
			
		||||
  isDalle3 as _isDalle3,
 | 
			
		||||
} from "@/app/utils";
 | 
			
		||||
 | 
			
		||||
export interface OpenAIListModelResponse {
 | 
			
		||||
@@ -58,6 +64,16 @@ export interface RequestPayload {
 | 
			
		||||
  max_tokens?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DalleRequestPayload {
 | 
			
		||||
  model: string;
 | 
			
		||||
  prompt: string;
 | 
			
		||||
  response_format: "url" | "b64_json";
 | 
			
		||||
  n: number;
 | 
			
		||||
  size: DalleSize;
 | 
			
		||||
  quality: DalleQuality;
 | 
			
		||||
  style: DalleStyle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ChatGPTApi implements LLMApi {
 | 
			
		||||
  private disableListModels = true;
 | 
			
		||||
 | 
			
		||||
@@ -100,20 +116,31 @@ export class ChatGPTApi implements LLMApi {
 | 
			
		||||
    return cloudflareAIGatewayUrl([baseUrl, path].join("/"));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  extractMessage(res: any) {
 | 
			
		||||
    return res.choices?.at(0)?.message?.content ?? "";
 | 
			
		||||
  async extractMessage(res: any) {
 | 
			
		||||
    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 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,
 | 
			
		||||
@@ -123,26 +150,54 @@ export class ChatGPTApi implements LLMApi {
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    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.
 | 
			
		||||
    };
 | 
			
		||||
    let requestPayload: RequestPayload | DalleRequestPayload;
 | 
			
		||||
 | 
			
		||||
    // add max_tokens to vision model
 | 
			
		||||
    if (visionModel && modelConfig.model.includes("preview")) {
 | 
			
		||||
      requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
 | 
			
		||||
    const isDalle3 = _isDalle3(options.config.model);
 | 
			
		||||
    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);
 | 
			
		||||
        messages.push({ role: v.role, content });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      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.
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      // add max_tokens to vision model
 | 
			
		||||
      if (visionModel && modelConfig.model.includes("preview")) {
 | 
			
		||||
        requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log("[Request] openai payload: ", requestPayload);
 | 
			
		||||
 | 
			
		||||
    const shouldStream = !!options.config.stream;
 | 
			
		||||
    const shouldStream = !isDalle3 && !!options.config.stream;
 | 
			
		||||
    const controller = new AbortController();
 | 
			
		||||
    options.onController?.(controller);
 | 
			
		||||
 | 
			
		||||
@@ -168,13 +223,15 @@ export class ChatGPTApi implements LLMApi {
 | 
			
		||||
            model?.provider?.providerName === ServiceProvider.Azure,
 | 
			
		||||
        );
 | 
			
		||||
        chatPath = this.path(
 | 
			
		||||
          Azure.ChatPath(
 | 
			
		||||
          (isDalle3 ? Azure.ImagePath : Azure.ChatPath)(
 | 
			
		||||
            (model?.displayName ?? model?.name) as string,
 | 
			
		||||
            useCustomConfig ? useAccessStore.getState().azureApiVersion : "",
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        chatPath = this.path(OpenaiPath.ChatPath);
 | 
			
		||||
        chatPath = this.path(
 | 
			
		||||
          isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      const chatPayload = {
 | 
			
		||||
        method: "POST",
 | 
			
		||||
@@ -186,7 +243,7 @@ export class ChatGPTApi implements LLMApi {
 | 
			
		||||
      // make a fetch request
 | 
			
		||||
      const requestTimeoutId = setTimeout(
 | 
			
		||||
        () => controller.abort(),
 | 
			
		||||
        REQUEST_TIMEOUT_MS,
 | 
			
		||||
        isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (shouldStream) {
 | 
			
		||||
@@ -317,7 +374,7 @@ export class ChatGPTApi implements LLMApi {
 | 
			
		||||
        clearTimeout(requestTimeoutId);
 | 
			
		||||
 | 
			
		||||
        const resJson = await res.json();
 | 
			
		||||
        const message = this.extractMessage(resJson);
 | 
			
		||||
        const message = await this.extractMessage(resJson);
 | 
			
		||||
        options.onFinish(message);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
@@ -411,13 +468,17 @@ export class ChatGPTApi implements LLMApi {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //由于目前 OpenAI 的 disableListModels 默认为 true,所以当前实际不会运行到这场
 | 
			
		||||
    let seq = 1000; //同 Constant.ts 中的排序保持一致
 | 
			
		||||
    return chatModels.map((m) => ({
 | 
			
		||||
      name: m.id,
 | 
			
		||||
      available: true,
 | 
			
		||||
      sorted: seq++,
 | 
			
		||||
      provider: {
 | 
			
		||||
        id: "openai",
 | 
			
		||||
        providerName: "OpenAI",
 | 
			
		||||
        providerType: "openai",
 | 
			
		||||
        sorted: 1,
 | 
			
		||||
      },
 | 
			
		||||
    }));
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										268
									
								
								app/client/platforms/tencent.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								app/client/platforms/tencent.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,268 @@
 | 
			
		||||
"use client";
 | 
			
		||||
import { ApiPath, DEFAULT_API_HOST, REQUEST_TIMEOUT_MS } from "@/app/constant";
 | 
			
		||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  ChatOptions,
 | 
			
		||||
  getHeaders,
 | 
			
		||||
  LLMApi,
 | 
			
		||||
  LLMModel,
 | 
			
		||||
  MultimodalContent,
 | 
			
		||||
} 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 } 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";
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
        ? DEFAULT_API_HOST + "/api/proxy/tencent"
 | 
			
		||||
        : 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 ?? "";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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(),
 | 
			
		||||
        REQUEST_TIMEOUT_MS,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      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(
 | 
			
		||||
              "[Tencent] 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;
 | 
			
		||||
              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);
 | 
			
		||||
      }
 | 
			
		||||
    } 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 [];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -41,13 +41,16 @@ interface ChatCommands {
 | 
			
		||||
  del?: Command;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ChatCommandPrefix = ":";
 | 
			
		||||
// Compatible with Chinese colon character ":"
 | 
			
		||||
export const ChatCommandPrefix = /^[::]/;
 | 
			
		||||
 | 
			
		||||
export function useChatCommand(commands: ChatCommands = {}) {
 | 
			
		||||
  function extract(userInput: string) {
 | 
			
		||||
    return (
 | 
			
		||||
      userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput
 | 
			
		||||
    ) as keyof ChatCommands;
 | 
			
		||||
    const match = userInput.match(ChatCommandPrefix);
 | 
			
		||||
    if (match) {
 | 
			
		||||
      return userInput.slice(1) as keyof ChatCommands;
 | 
			
		||||
    }
 | 
			
		||||
    return userInput as keyof ChatCommands;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function search(userInput: string) {
 | 
			
		||||
@@ -57,7 +60,7 @@ export function useChatCommand(commands: ChatCommands = {}) {
 | 
			
		||||
      .filter((c) => c.startsWith(input))
 | 
			
		||||
      .map((c) => ({
 | 
			
		||||
        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);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										234
									
								
								app/components/artifacts.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								app/components/artifacts.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,234 @@
 | 
			
		||||
import { useEffect, useState, useRef, useMemo } from "react";
 | 
			
		||||
import { useParams } from "react-router";
 | 
			
		||||
import { useWindowSize } from "@/app/utils";
 | 
			
		||||
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 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";
 | 
			
		||||
 | 
			
		||||
export function HTMLPreview(props: {
 | 
			
		||||
  code: string;
 | 
			
		||||
  autoHeight?: boolean;
 | 
			
		||||
  height?: number | string;
 | 
			
		||||
  onLoad?: (title?: string) => void;
 | 
			
		||||
}) {
 | 
			
		||||
  const ref = useRef<HTMLIFrameElement>(null);
 | 
			
		||||
  const frameId = useRef<string>(nanoid());
 | 
			
		||||
  const [iframeHeight, setIframeHeight] = useState(600);
 | 
			
		||||
  const [title, setTitle] = useState("");
 | 
			
		||||
  /*
 | 
			
		||||
   * https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
 | 
			
		||||
   * 1. using srcdoc
 | 
			
		||||
   * 2. using src with dataurl:
 | 
			
		||||
   *    easy to share
 | 
			
		||||
   *    length limit (Data URIs cannot be larger than 32,768 characters.)
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleMessage = (e: any) => {
 | 
			
		||||
      const { id, height, title } = e.data;
 | 
			
		||||
      setTitle(title);
 | 
			
		||||
      if (id == frameId.current) {
 | 
			
		||||
        setIframeHeight(height);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    window.addEventListener("message", handleMessage);
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.removeEventListener("message", handleMessage);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  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>new ResizeObserver((entries) => parent.postMessage({id: '${frameId.current}', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>`;
 | 
			
		||||
    if (props.code.includes("</head>")) {
 | 
			
		||||
      props.code.replace("</head>", "</head>" + script);
 | 
			
		||||
    }
 | 
			
		||||
    return props.code + script;
 | 
			
		||||
  }, [props.code]);
 | 
			
		||||
 | 
			
		||||
  const handleOnLoad = () => {
 | 
			
		||||
    if (props?.onLoad) {
 | 
			
		||||
      props.onLoad(title);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <iframe
 | 
			
		||||
      className={styles["artifacts-iframe"]}
 | 
			
		||||
      id={frameId.current}
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      sandbox="allow-forms allow-modals allow-scripts"
 | 
			
		||||
      style={{ height }}
 | 
			
		||||
      srcDoc={srcDoc}
 | 
			
		||||
      onLoad={handleOnLoad}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ArtifactsShareButton({
 | 
			
		||||
  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("");
 | 
			
		||||
 | 
			
		||||
  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>
 | 
			
		||||
        <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}
 | 
			
		||||
            autoHeight={false}
 | 
			
		||||
            height={"100%"}
 | 
			
		||||
            onLoad={(title) => {
 | 
			
		||||
              setFileName(title as string);
 | 
			
		||||
              setLoading(false);
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
import styles from "./button.module.scss";
 | 
			
		||||
import { CSSProperties } from "react";
 | 
			
		||||
 | 
			
		||||
export type ButtonType = "primary" | "danger" | null;
 | 
			
		||||
 | 
			
		||||
@@ -16,6 +17,8 @@ export function IconButton(props: {
 | 
			
		||||
  disabled?: boolean;
 | 
			
		||||
  tabIndex?: number;
 | 
			
		||||
  autoFocus?: boolean;
 | 
			
		||||
  style?: CSSProperties;
 | 
			
		||||
  aria?: string;
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <button
 | 
			
		||||
@@ -31,9 +34,12 @@ export function IconButton(props: {
 | 
			
		||||
      role="button"
 | 
			
		||||
      tabIndex={props.tabIndex}
 | 
			
		||||
      autoFocus={props.autoFocus}
 | 
			
		||||
      style={props.style}
 | 
			
		||||
      aria-label={props.aria}
 | 
			
		||||
    >
 | 
			
		||||
      {props.icon && (
 | 
			
		||||
        <div
 | 
			
		||||
          aria-label={props.text || props.title}
 | 
			
		||||
          className={
 | 
			
		||||
            styles["icon-button-icon"] +
 | 
			
		||||
            ` ${props.type === "primary" && "no-dark"}`
 | 
			
		||||
@@ -44,7 +50,12 @@ export function IconButton(props: {
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {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>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -346,6 +346,12 @@
 | 
			
		||||
      flex-wrap: nowrap;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .chat-model-name {
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
    color: var(--black);
 | 
			
		||||
    margin-left: 6px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chat-message-container {
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,10 @@ import AutoIcon from "../icons/auto.svg";
 | 
			
		||||
import BottomIcon from "../icons/bottom.svg";
 | 
			
		||||
import StopIcon from "../icons/pause.svg";
 | 
			
		||||
import RobotIcon from "../icons/robot.svg";
 | 
			
		||||
import SizeIcon from "../icons/size.svg";
 | 
			
		||||
import QualityIcon from "../icons/hd.svg";
 | 
			
		||||
import StyleIcon from "../icons/palette.svg";
 | 
			
		||||
import PluginIcon from "../icons/plugin.svg";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  ChatMessage,
 | 
			
		||||
@@ -59,6 +63,7 @@ import {
 | 
			
		||||
  getMessageTextContent,
 | 
			
		||||
  getMessageImages,
 | 
			
		||||
  isVisionModel,
 | 
			
		||||
  isDalle3,
 | 
			
		||||
} from "../utils";
 | 
			
		||||
 | 
			
		||||
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
 | 
			
		||||
@@ -66,6 +71,7 @@ import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
 | 
			
		||||
import dynamic from "next/dynamic";
 | 
			
		||||
 | 
			
		||||
import { ChatControllerPool } from "../client/controller";
 | 
			
		||||
import { DalleSize, DalleQuality, DalleStyle } from "../typing";
 | 
			
		||||
import { Prompt, usePromptStore } from "../store/prompt";
 | 
			
		||||
import Locale from "../locales";
 | 
			
		||||
 | 
			
		||||
@@ -89,6 +95,7 @@ import {
 | 
			
		||||
  REQUEST_TIMEOUT_MS,
 | 
			
		||||
  UNFINISHED_INPUT,
 | 
			
		||||
  ServiceProvider,
 | 
			
		||||
  Plugin,
 | 
			
		||||
} from "../constant";
 | 
			
		||||
import { Avatar } from "./emoji";
 | 
			
		||||
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
 | 
			
		||||
@@ -338,7 +345,7 @@ function ClearContextDivider() {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ChatAction(props: {
 | 
			
		||||
export function ChatAction(props: {
 | 
			
		||||
  text: string;
 | 
			
		||||
  icon: JSX.Element;
 | 
			
		||||
  onClick: () => void;
 | 
			
		||||
@@ -476,8 +483,22 @@ export function ChatActions(props: {
 | 
			
		||||
    return model?.displayName ?? "";
 | 
			
		||||
  }, [models, currentModel, currentProviderName]);
 | 
			
		||||
  const [showModelSelector, setShowModelSelector] = useState(false);
 | 
			
		||||
  const [showPluginSelector, setShowPluginSelector] = useState(false);
 | 
			
		||||
  const [showUploadImage, setShowUploadImage] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const [showSizeSelector, setShowSizeSelector] = useState(false);
 | 
			
		||||
  const [showQualitySelector, setShowQualitySelector] = useState(false);
 | 
			
		||||
  const [showStyleSelector, setShowStyleSelector] = useState(false);
 | 
			
		||||
  const dalle3Sizes: DalleSize[] = ["1024x1024", "1792x1024", "1024x1792"];
 | 
			
		||||
  const dalle3Qualitys: DalleQuality[] = ["standard", "hd"];
 | 
			
		||||
  const dalle3Styles: DalleStyle[] = ["vivid", "natural"];
 | 
			
		||||
  const currentSize =
 | 
			
		||||
    chatStore.currentSession().mask.modelConfig?.size ?? "1024x1024";
 | 
			
		||||
  const currentQuality =
 | 
			
		||||
    chatStore.currentSession().mask.modelConfig?.quality ?? "standard";
 | 
			
		||||
  const currentStyle =
 | 
			
		||||
    chatStore.currentSession().mask.modelConfig?.style ?? "vivid";
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const show = isVisionModel(currentModel);
 | 
			
		||||
    setShowUploadImage(show);
 | 
			
		||||
@@ -620,6 +641,115 @@ export function ChatActions(props: {
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {isDalle3(currentModel) && (
 | 
			
		||||
        <ChatAction
 | 
			
		||||
          onClick={() => setShowSizeSelector(true)}
 | 
			
		||||
          text={currentSize}
 | 
			
		||||
          icon={<SizeIcon />}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {showSizeSelector && (
 | 
			
		||||
        <Selector
 | 
			
		||||
          defaultSelectedValue={currentSize}
 | 
			
		||||
          items={dalle3Sizes.map((m) => ({
 | 
			
		||||
            title: m,
 | 
			
		||||
            value: m,
 | 
			
		||||
          }))}
 | 
			
		||||
          onClose={() => setShowSizeSelector(false)}
 | 
			
		||||
          onSelection={(s) => {
 | 
			
		||||
            if (s.length === 0) return;
 | 
			
		||||
            const size = s[0];
 | 
			
		||||
            chatStore.updateCurrentSession((session) => {
 | 
			
		||||
              session.mask.modelConfig.size = size;
 | 
			
		||||
            });
 | 
			
		||||
            showToast(size);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {isDalle3(currentModel) && (
 | 
			
		||||
        <ChatAction
 | 
			
		||||
          onClick={() => setShowQualitySelector(true)}
 | 
			
		||||
          text={currentQuality}
 | 
			
		||||
          icon={<QualityIcon />}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {showQualitySelector && (
 | 
			
		||||
        <Selector
 | 
			
		||||
          defaultSelectedValue={currentQuality}
 | 
			
		||||
          items={dalle3Qualitys.map((m) => ({
 | 
			
		||||
            title: m,
 | 
			
		||||
            value: m,
 | 
			
		||||
          }))}
 | 
			
		||||
          onClose={() => setShowQualitySelector(false)}
 | 
			
		||||
          onSelection={(q) => {
 | 
			
		||||
            if (q.length === 0) return;
 | 
			
		||||
            const quality = q[0];
 | 
			
		||||
            chatStore.updateCurrentSession((session) => {
 | 
			
		||||
              session.mask.modelConfig.quality = quality;
 | 
			
		||||
            });
 | 
			
		||||
            showToast(quality);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {isDalle3(currentModel) && (
 | 
			
		||||
        <ChatAction
 | 
			
		||||
          onClick={() => setShowStyleSelector(true)}
 | 
			
		||||
          text={currentStyle}
 | 
			
		||||
          icon={<StyleIcon />}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {showStyleSelector && (
 | 
			
		||||
        <Selector
 | 
			
		||||
          defaultSelectedValue={currentStyle}
 | 
			
		||||
          items={dalle3Styles.map((m) => ({
 | 
			
		||||
            title: m,
 | 
			
		||||
            value: m,
 | 
			
		||||
          }))}
 | 
			
		||||
          onClose={() => setShowStyleSelector(false)}
 | 
			
		||||
          onSelection={(s) => {
 | 
			
		||||
            if (s.length === 0) return;
 | 
			
		||||
            const style = s[0];
 | 
			
		||||
            chatStore.updateCurrentSession((session) => {
 | 
			
		||||
              session.mask.modelConfig.style = style;
 | 
			
		||||
            });
 | 
			
		||||
            showToast(style);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <ChatAction
 | 
			
		||||
        onClick={() => setShowPluginSelector(true)}
 | 
			
		||||
        text={Locale.Plugin.Name}
 | 
			
		||||
        icon={<PluginIcon />}
 | 
			
		||||
      />
 | 
			
		||||
      {showPluginSelector && (
 | 
			
		||||
        <Selector
 | 
			
		||||
          multiple
 | 
			
		||||
          defaultSelectedValue={chatStore.currentSession().mask?.plugin}
 | 
			
		||||
          items={[
 | 
			
		||||
            {
 | 
			
		||||
              title: Locale.Plugin.Artifacts,
 | 
			
		||||
              value: Plugin.Artifacts,
 | 
			
		||||
            },
 | 
			
		||||
          ]}
 | 
			
		||||
          onClose={() => setShowPluginSelector(false)}
 | 
			
		||||
          onSelection={(s) => {
 | 
			
		||||
            const plugin = s[0];
 | 
			
		||||
            chatStore.updateCurrentSession((session) => {
 | 
			
		||||
              session.mask.plugin = s;
 | 
			
		||||
            });
 | 
			
		||||
            if (plugin) {
 | 
			
		||||
              showToast(plugin);
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -701,6 +831,7 @@ function _Chat() {
 | 
			
		||||
  const session = chatStore.currentSession();
 | 
			
		||||
  const config = useAppConfig();
 | 
			
		||||
  const fontSize = config.fontSize;
 | 
			
		||||
  const fontFamily = config.fontFamily;
 | 
			
		||||
 | 
			
		||||
  const [showExport, setShowExport] = useState(false);
 | 
			
		||||
 | 
			
		||||
@@ -780,7 +911,7 @@ function _Chat() {
 | 
			
		||||
    // clear search results
 | 
			
		||||
    if (n === 0) {
 | 
			
		||||
      setPromptHints([]);
 | 
			
		||||
    } else if (text.startsWith(ChatCommandPrefix)) {
 | 
			
		||||
    } else if (text.match(ChatCommandPrefix)) {
 | 
			
		||||
      setPromptHints(chatCommands.search(text));
 | 
			
		||||
    } else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
 | 
			
		||||
      // check if need to trigger auto completion
 | 
			
		||||
@@ -1270,6 +1401,8 @@ function _Chat() {
 | 
			
		||||
              <IconButton
 | 
			
		||||
                icon={<RenameIcon />}
 | 
			
		||||
                bordered
 | 
			
		||||
                title={Locale.Chat.EditMessage.Title}
 | 
			
		||||
                aria={Locale.Chat.EditMessage.Title}
 | 
			
		||||
                onClick={() => setIsEditingMessage(true)}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
@@ -1289,6 +1422,8 @@ function _Chat() {
 | 
			
		||||
              <IconButton
 | 
			
		||||
                icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
 | 
			
		||||
                bordered
 | 
			
		||||
                title={Locale.Chat.Actions.FullScreen}
 | 
			
		||||
                aria={Locale.Chat.Actions.FullScreen}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  config.update(
 | 
			
		||||
                    (config) => (config.tightBorder = !config.tightBorder),
 | 
			
		||||
@@ -1340,6 +1475,7 @@ function _Chat() {
 | 
			
		||||
                      <div className={styles["chat-message-edit"]}>
 | 
			
		||||
                        <IconButton
 | 
			
		||||
                          icon={<EditIcon />}
 | 
			
		||||
                          aria={Locale.Chat.Actions.Edit}
 | 
			
		||||
                          onClick={async () => {
 | 
			
		||||
                            const newMessage = await showPrompt(
 | 
			
		||||
                              Locale.Chat.Actions.Edit,
 | 
			
		||||
@@ -1388,6 +1524,11 @@ function _Chat() {
 | 
			
		||||
                        </>
 | 
			
		||||
                      )}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {!isUser && (
 | 
			
		||||
                      <div className={styles["chat-model-name"]}>
 | 
			
		||||
                        {message.model}
 | 
			
		||||
                      </div>
 | 
			
		||||
                    )}
 | 
			
		||||
 | 
			
		||||
                    {showActions && (
 | 
			
		||||
                      <div className={styles["chat-message-actions"]}>
 | 
			
		||||
@@ -1439,6 +1580,7 @@ function _Chat() {
 | 
			
		||||
                  )}
 | 
			
		||||
                  <div className={styles["chat-message-item"]}>
 | 
			
		||||
                    <Markdown
 | 
			
		||||
                      key={message.streaming ? "loading" : "done"}
 | 
			
		||||
                      content={getMessageTextContent(message)}
 | 
			
		||||
                      loading={
 | 
			
		||||
                        (message.preview || message.streaming) &&
 | 
			
		||||
@@ -1451,6 +1593,7 @@ function _Chat() {
 | 
			
		||||
                        setUserInput(getMessageTextContent(message));
 | 
			
		||||
                      }}
 | 
			
		||||
                      fontSize={fontSize}
 | 
			
		||||
                      fontFamily={fontFamily}
 | 
			
		||||
                      parentRef={scrollRef}
 | 
			
		||||
                      defaultShow={i >= messages.length - 6}
 | 
			
		||||
                    />
 | 
			
		||||
@@ -1545,6 +1688,7 @@ function _Chat() {
 | 
			
		||||
            autoFocus={autoFocus}
 | 
			
		||||
            style={{
 | 
			
		||||
              fontSize: config.fontSize,
 | 
			
		||||
              fontFamily: config.fontFamily,
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          {attachImages.length != 0 && (
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { IconButton } from "./button";
 | 
			
		||||
import GithubIcon from "../icons/github.svg";
 | 
			
		||||
 
 | 
			
		||||
@@ -541,7 +541,7 @@ export function ImagePreviewer(props: {
 | 
			
		||||
          <div>
 | 
			
		||||
            <div className={styles["main-title"]}>NextChat</div>
 | 
			
		||||
            <div className={styles["sub-title"]}>
 | 
			
		||||
              github.com/Yidadaa/ChatGPT-Next-Web
 | 
			
		||||
              github.com/ChatGPTNextWeb/ChatGPT-Next-Web
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className={styles["icons"]}>
 | 
			
		||||
              <ExportAvatar avatar={config.avatar} />
 | 
			
		||||
@@ -583,6 +583,7 @@ export function ImagePreviewer(props: {
 | 
			
		||||
                <Markdown
 | 
			
		||||
                  content={getMessageTextContent(m)}
 | 
			
		||||
                  fontSize={config.fontSize}
 | 
			
		||||
                  fontFamily={config.fontFamily}
 | 
			
		||||
                  defaultShow
 | 
			
		||||
                />
 | 
			
		||||
                {getMessageImages(m).length == 1 && (
 | 
			
		||||
 
 | 
			
		||||
@@ -137,12 +137,18 @@
 | 
			
		||||
  position: relative;
 | 
			
		||||
  padding-top: 20px;
 | 
			
		||||
  padding-bottom: 20px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sidebar-logo {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  bottom: 18px;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sidebar-title-container {
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sidebar-title {
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,10 @@ export function Loading(props: { noLogo?: boolean }) {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Artifacts = dynamic(async () => (await import("./artifacts")).Artifacts, {
 | 
			
		||||
  loading: () => <Loading noLogo />,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const Settings = dynamic(async () => (await import("./settings")).Settings, {
 | 
			
		||||
  loading: () => <Loading noLogo />,
 | 
			
		||||
});
 | 
			
		||||
@@ -55,6 +59,10 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
 | 
			
		||||
  loading: () => <Loading noLogo />,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const Sd = dynamic(async () => (await import("./sd")).Sd, {
 | 
			
		||||
  loading: () => <Loading noLogo />,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export function useSwitchTheme() {
 | 
			
		||||
  const config = useAppConfig();
 | 
			
		||||
 | 
			
		||||
@@ -122,11 +130,23 @@ const loadAsyncGoogleFont = () => {
 | 
			
		||||
  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() {
 | 
			
		||||
  const config = useAppConfig();
 | 
			
		||||
  const location = useLocation();
 | 
			
		||||
  const isArtifact = location.pathname.includes(Path.Artifacts);
 | 
			
		||||
  const isHome = location.pathname === Path.Home;
 | 
			
		||||
  const isAuth = location.pathname === Path.Auth;
 | 
			
		||||
  const isSd = location.pathname === Path.Sd;
 | 
			
		||||
  const isSdNew = location.pathname === Path.SdNew;
 | 
			
		||||
 | 
			
		||||
  const isMobileScreen = useMobileScreen();
 | 
			
		||||
  const shouldTightBorder =
 | 
			
		||||
    getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
 | 
			
		||||
@@ -135,34 +155,40 @@ function Screen() {
 | 
			
		||||
    loadAsyncGoogleFont();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  if (isArtifact) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Routes>
 | 
			
		||||
        <Route path="/artifacts/:id" element={<Artifacts />} />
 | 
			
		||||
      </Routes>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  const renderContent = () => {
 | 
			
		||||
    if (isAuth) return <AuthPage />;
 | 
			
		||||
    if (isSd) return <Sd />;
 | 
			
		||||
    if (isSdNew) return <Sd />;
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <SideBar className={isHome ? styles["sidebar-show"] : ""} />
 | 
			
		||||
        <WindowContent>
 | 
			
		||||
          <Routes>
 | 
			
		||||
            <Route path={Path.Home} element={<Chat />} />
 | 
			
		||||
            <Route path={Path.NewChat} element={<NewChat />} />
 | 
			
		||||
            <Route path={Path.Masks} element={<MaskPage />} />
 | 
			
		||||
            <Route path={Path.Chat} element={<Chat />} />
 | 
			
		||||
            <Route path={Path.Settings} element={<Settings />} />
 | 
			
		||||
          </Routes>
 | 
			
		||||
        </WindowContent>
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={
 | 
			
		||||
        styles.container +
 | 
			
		||||
        ` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
 | 
			
		||||
          getLang() === "ar" ? styles["rtl-screen"] : ""
 | 
			
		||||
        }`
 | 
			
		||||
      }
 | 
			
		||||
      className={`${styles.container} ${
 | 
			
		||||
        shouldTightBorder ? styles["tight-container"] : styles.container
 | 
			
		||||
      } ${getLang() === "ar" ? styles["rtl-screen"] : ""}`}
 | 
			
		||||
    >
 | 
			
		||||
      {isAuth ? (
 | 
			
		||||
        <>
 | 
			
		||||
          <AuthPage />
 | 
			
		||||
        </>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          <SideBar className={isHome ? styles["sidebar-show"] : ""} />
 | 
			
		||||
 | 
			
		||||
          <div className={styles["window-content"]} id={SlotID.AppBody}>
 | 
			
		||||
            <Routes>
 | 
			
		||||
              <Route path={Path.Home} element={<Chat />} />
 | 
			
		||||
              <Route path={Path.NewChat} element={<NewChat />} />
 | 
			
		||||
              <Route path={Path.Masks} element={<MaskPage />} />
 | 
			
		||||
              <Route path={Path.Chat} element={<Chat />} />
 | 
			
		||||
              <Route path={Path.Settings} element={<Settings />} />
 | 
			
		||||
            </Routes>
 | 
			
		||||
          </div>
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
      {renderContent()}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ interface InputRangeProps {
 | 
			
		||||
  min: string;
 | 
			
		||||
  max: string;
 | 
			
		||||
  step: string;
 | 
			
		||||
  aria: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function InputRange({
 | 
			
		||||
@@ -19,11 +20,13 @@ export function InputRange({
 | 
			
		||||
  min,
 | 
			
		||||
  max,
 | 
			
		||||
  step,
 | 
			
		||||
  aria,
 | 
			
		||||
}: InputRangeProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={styles["input-range"] + ` ${className ?? ""}`}>
 | 
			
		||||
      {title || value}
 | 
			
		||||
      <input
 | 
			
		||||
        aria-label={aria}
 | 
			
		||||
        type="range"
 | 
			
		||||
        title={title}
 | 
			
		||||
        value={value}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,14 +6,16 @@ import RehypeKatex from "rehype-katex";
 | 
			
		||||
import RemarkGfm from "remark-gfm";
 | 
			
		||||
import RehypeHighlight from "rehype-highlight";
 | 
			
		||||
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
 | 
			
		||||
import { copyToClipboard } from "../utils";
 | 
			
		||||
import { copyToClipboard, useWindowSize } from "../utils";
 | 
			
		||||
import mermaid from "mermaid";
 | 
			
		||||
 | 
			
		||||
import LoadingIcon from "../icons/three-dots.svg";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { useDebouncedCallback } from "use-debounce";
 | 
			
		||||
import { showImageModal } from "./ui-lib";
 | 
			
		||||
 | 
			
		||||
import { showImageModal, FullScreen } from "./ui-lib";
 | 
			
		||||
import { ArtifactsShareButton, HTMLPreview } from "./artifacts";
 | 
			
		||||
import { Plugin } from "../constant";
 | 
			
		||||
import { useChatStore } from "../store";
 | 
			
		||||
export function Mermaid(props: { code: string }) {
 | 
			
		||||
  const ref = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const [hasError, setHasError] = useState(false);
 | 
			
		||||
@@ -64,25 +66,64 @@ export function PreCode(props: { children: any }) {
 | 
			
		||||
  const ref = useRef<HTMLPreElement>(null);
 | 
			
		||||
  const refText = ref.current?.innerText;
 | 
			
		||||
  const [mermaidCode, setMermaidCode] = useState("");
 | 
			
		||||
  const [htmlCode, setHtmlCode] = useState("");
 | 
			
		||||
  const { height } = useWindowSize();
 | 
			
		||||
  const chatStore = useChatStore();
 | 
			
		||||
  const session = chatStore.currentSession();
 | 
			
		||||
  const plugins = session.mask?.plugin;
 | 
			
		||||
 | 
			
		||||
  const renderMermaid = useDebouncedCallback(() => {
 | 
			
		||||
  const renderArtifacts = useDebouncedCallback(() => {
 | 
			
		||||
    if (!ref.current) return;
 | 
			
		||||
    const mermaidDom = ref.current.querySelector("code.language-mermaid");
 | 
			
		||||
    if (mermaidDom) {
 | 
			
		||||
      setMermaidCode((mermaidDom as HTMLElement).innerText);
 | 
			
		||||
    }
 | 
			
		||||
    const htmlDom = ref.current.querySelector("code.language-html");
 | 
			
		||||
    if (htmlDom) {
 | 
			
		||||
      setHtmlCode((htmlDom as HTMLElement).innerText);
 | 
			
		||||
    } else if (refText?.startsWith("<!DOCTYPE")) {
 | 
			
		||||
      setHtmlCode(refText);
 | 
			
		||||
    }
 | 
			
		||||
  }, 600);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setTimeout(renderMermaid, 1);
 | 
			
		||||
    setTimeout(renderArtifacts, 1);
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [refText]);
 | 
			
		||||
 | 
			
		||||
  const enableArtifacts = useMemo(
 | 
			
		||||
    () => plugins?.includes(Plugin.Artifacts),
 | 
			
		||||
    [plugins],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  //Wrap the paragraph for plain-text
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (ref.current) {
 | 
			
		||||
      const codeElements = ref.current.querySelectorAll(
 | 
			
		||||
        "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";
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {mermaidCode.length > 0 && (
 | 
			
		||||
        <Mermaid code={mermaidCode} key={mermaidCode} />
 | 
			
		||||
      )}
 | 
			
		||||
      <pre ref={ref}>
 | 
			
		||||
        <span
 | 
			
		||||
          className="copy-code-button"
 | 
			
		||||
@@ -95,6 +136,22 @@ export function PreCode(props: { children: any }) {
 | 
			
		||||
        ></span>
 | 
			
		||||
        {props.children}
 | 
			
		||||
      </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}
 | 
			
		||||
          />
 | 
			
		||||
          <HTMLPreview
 | 
			
		||||
            code={htmlCode}
 | 
			
		||||
            autoHeight={!document.fullscreenElement}
 | 
			
		||||
            height={!document.fullscreenElement ? 600 : height}
 | 
			
		||||
          />
 | 
			
		||||
        </FullScreen>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -175,6 +232,7 @@ export function Markdown(
 | 
			
		||||
    content: string;
 | 
			
		||||
    loading?: boolean;
 | 
			
		||||
    fontSize?: number;
 | 
			
		||||
    fontFamily?: string;
 | 
			
		||||
    parentRef?: RefObject<HTMLDivElement>;
 | 
			
		||||
    defaultShow?: boolean;
 | 
			
		||||
  } & React.DOMAttributes<HTMLDivElement>,
 | 
			
		||||
@@ -186,6 +244,7 @@ export function Markdown(
 | 
			
		||||
      className="markdown-body"
 | 
			
		||||
      style={{
 | 
			
		||||
        fontSize: `${props.fontSize ?? 14}px`,
 | 
			
		||||
        fontFamily: props.fontFamily || "inherit",
 | 
			
		||||
      }}
 | 
			
		||||
      ref={mdRef}
 | 
			
		||||
      onContextMenu={props.onContextMenu}
 | 
			
		||||
 
 | 
			
		||||
@@ -127,6 +127,8 @@ export function MaskConfig(props: {
 | 
			
		||||
            onClose={() => setShowPicker(false)}
 | 
			
		||||
          >
 | 
			
		||||
            <div
 | 
			
		||||
              tabIndex={0}
 | 
			
		||||
              aria-label={Locale.Mask.Config.Avatar}
 | 
			
		||||
              onClick={() => setShowPicker(true)}
 | 
			
		||||
              style={{ cursor: "pointer" }}
 | 
			
		||||
            >
 | 
			
		||||
@@ -139,6 +141,7 @@ export function MaskConfig(props: {
 | 
			
		||||
        </ListItem>
 | 
			
		||||
        <ListItem title={Locale.Mask.Config.Name}>
 | 
			
		||||
          <input
 | 
			
		||||
            aria-label={Locale.Mask.Config.Name}
 | 
			
		||||
            type="text"
 | 
			
		||||
            value={props.mask.name}
 | 
			
		||||
            onInput={(e) =>
 | 
			
		||||
@@ -153,6 +156,7 @@ export function MaskConfig(props: {
 | 
			
		||||
          subTitle={Locale.Mask.Config.HideContext.SubTitle}
 | 
			
		||||
        >
 | 
			
		||||
          <input
 | 
			
		||||
            aria-label={Locale.Mask.Config.HideContext.Title}
 | 
			
		||||
            type="checkbox"
 | 
			
		||||
            checked={props.mask.hideContext}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
@@ -169,6 +173,7 @@ export function MaskConfig(props: {
 | 
			
		||||
            subTitle={Locale.Mask.Config.Share.SubTitle}
 | 
			
		||||
          >
 | 
			
		||||
            <IconButton
 | 
			
		||||
              aria={Locale.Mask.Config.Share.Title}
 | 
			
		||||
              icon={<CopyIcon />}
 | 
			
		||||
              text={Locale.Mask.Config.Share.Action}
 | 
			
		||||
              onClick={copyMaskLink}
 | 
			
		||||
@@ -182,6 +187,7 @@ export function MaskConfig(props: {
 | 
			
		||||
            subTitle={Locale.Mask.Config.Sync.SubTitle}
 | 
			
		||||
          >
 | 
			
		||||
            <input
 | 
			
		||||
              aria-label={Locale.Mask.Config.Sync.Title}
 | 
			
		||||
              type="checkbox"
 | 
			
		||||
              checked={props.mask.syncGlobalConfig}
 | 
			
		||||
              onChange={async (e) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ export function ModelConfigList(props: {
 | 
			
		||||
    <>
 | 
			
		||||
      <ListItem title={Locale.Settings.Model}>
 | 
			
		||||
        <Select
 | 
			
		||||
          aria-label={Locale.Settings.Model}
 | 
			
		||||
          value={value}
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            const [model, providerName] = e.currentTarget.value.split("@");
 | 
			
		||||
@@ -40,6 +41,7 @@ export function ModelConfigList(props: {
 | 
			
		||||
        subTitle={Locale.Settings.Temperature.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <InputRange
 | 
			
		||||
          aria={Locale.Settings.Temperature.Title}
 | 
			
		||||
          value={props.modelConfig.temperature?.toFixed(1)}
 | 
			
		||||
          min="0"
 | 
			
		||||
          max="1" // lets limit it to 0-1
 | 
			
		||||
@@ -59,6 +61,7 @@ export function ModelConfigList(props: {
 | 
			
		||||
        subTitle={Locale.Settings.TopP.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <InputRange
 | 
			
		||||
          aria={Locale.Settings.TopP.Title}
 | 
			
		||||
          value={(props.modelConfig.top_p ?? 1).toFixed(1)}
 | 
			
		||||
          min="0"
 | 
			
		||||
          max="1"
 | 
			
		||||
@@ -78,6 +81,7 @@ export function ModelConfigList(props: {
 | 
			
		||||
        subTitle={Locale.Settings.MaxTokens.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <input
 | 
			
		||||
          aria-label={Locale.Settings.MaxTokens.Title}
 | 
			
		||||
          type="number"
 | 
			
		||||
          min={1024}
 | 
			
		||||
          max={512000}
 | 
			
		||||
@@ -100,6 +104,7 @@ export function ModelConfigList(props: {
 | 
			
		||||
            subTitle={Locale.Settings.PresencePenalty.SubTitle}
 | 
			
		||||
          >
 | 
			
		||||
            <InputRange
 | 
			
		||||
              aria={Locale.Settings.PresencePenalty.Title}
 | 
			
		||||
              value={props.modelConfig.presence_penalty?.toFixed(1)}
 | 
			
		||||
              min="-2"
 | 
			
		||||
              max="2"
 | 
			
		||||
@@ -121,6 +126,7 @@ export function ModelConfigList(props: {
 | 
			
		||||
            subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
 | 
			
		||||
          >
 | 
			
		||||
            <InputRange
 | 
			
		||||
              aria={Locale.Settings.FrequencyPenalty.Title}
 | 
			
		||||
              value={props.modelConfig.frequency_penalty?.toFixed(1)}
 | 
			
		||||
              min="-2"
 | 
			
		||||
              max="2"
 | 
			
		||||
@@ -142,6 +148,7 @@ export function ModelConfigList(props: {
 | 
			
		||||
            subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
 | 
			
		||||
          >
 | 
			
		||||
            <input
 | 
			
		||||
              aria-label={Locale.Settings.InjectSystemPrompts.Title}
 | 
			
		||||
              type="checkbox"
 | 
			
		||||
              checked={props.modelConfig.enableInjectSystemPrompts}
 | 
			
		||||
              onChange={(e) =>
 | 
			
		||||
@@ -159,6 +166,7 @@ export function ModelConfigList(props: {
 | 
			
		||||
            subTitle={Locale.Settings.InputTemplate.SubTitle}
 | 
			
		||||
          >
 | 
			
		||||
            <input
 | 
			
		||||
              aria-label={Locale.Settings.InputTemplate.Title}
 | 
			
		||||
              type="text"
 | 
			
		||||
              value={props.modelConfig.template}
 | 
			
		||||
              onChange={(e) =>
 | 
			
		||||
@@ -175,6 +183,7 @@ export function ModelConfigList(props: {
 | 
			
		||||
        subTitle={Locale.Settings.HistoryCount.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <InputRange
 | 
			
		||||
          aria={Locale.Settings.HistoryCount.Title}
 | 
			
		||||
          title={props.modelConfig.historyMessageCount.toString()}
 | 
			
		||||
          value={props.modelConfig.historyMessageCount}
 | 
			
		||||
          min="0"
 | 
			
		||||
@@ -193,6 +202,7 @@ export function ModelConfigList(props: {
 | 
			
		||||
        subTitle={Locale.Settings.CompressThreshold.SubTitle}
 | 
			
		||||
      >
 | 
			
		||||
        <input
 | 
			
		||||
          aria-label={Locale.Settings.CompressThreshold.Title}
 | 
			
		||||
          type="number"
 | 
			
		||||
          min={500}
 | 
			
		||||
          max={4000}
 | 
			
		||||
@@ -208,6 +218,7 @@ export function ModelConfigList(props: {
 | 
			
		||||
      </ListItem>
 | 
			
		||||
      <ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
 | 
			
		||||
        <input
 | 
			
		||||
          aria-label={Locale.Memory.Title}
 | 
			
		||||
          type="checkbox"
 | 
			
		||||
          checked={props.modelConfig.sendMemory}
 | 
			
		||||
          onChange={(e) =>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										320
									
								
								app/components/sd/sd-panel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										320
									
								
								app/components/sd/sd-panel.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,320 @@
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
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={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%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										336
									
								
								app/components/sd/sd.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								app/components/sd/sd.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,336 @@
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
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={isSd ? homeStyles["sidebar-show"] : ""} />
 | 
			
		||||
      <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={`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>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
import { useEffect, useRef, useMemo } from "react";
 | 
			
		||||
import React, { useEffect, useRef, useMemo, useState, Fragment } from "react";
 | 
			
		||||
 | 
			
		||||
import styles from "./home.module.scss";
 | 
			
		||||
 | 
			
		||||
@@ -10,8 +10,8 @@ import AddIcon from "../icons/add.svg";
 | 
			
		||||
import CloseIcon from "../icons/close.svg";
 | 
			
		||||
import DeleteIcon from "../icons/delete.svg";
 | 
			
		||||
import MaskIcon from "../icons/mask.svg";
 | 
			
		||||
import PluginIcon from "../icons/plugin.svg";
 | 
			
		||||
import DragIcon from "../icons/drag.svg";
 | 
			
		||||
import DiscoveryIcon from "../icons/discovery.svg";
 | 
			
		||||
 | 
			
		||||
import Locale from "../locales";
 | 
			
		||||
 | 
			
		||||
@@ -23,19 +23,20 @@ import {
 | 
			
		||||
  MIN_SIDEBAR_WIDTH,
 | 
			
		||||
  NARROW_SIDEBAR_WIDTH,
 | 
			
		||||
  Path,
 | 
			
		||||
  PLUGINS,
 | 
			
		||||
  REPO_URL,
 | 
			
		||||
} from "../constant";
 | 
			
		||||
 | 
			
		||||
import { Link, useNavigate } from "react-router-dom";
 | 
			
		||||
import { isIOS, useMobileScreen } from "../utils";
 | 
			
		||||
import dynamic from "next/dynamic";
 | 
			
		||||
import { showConfirm, showToast } from "./ui-lib";
 | 
			
		||||
import { showConfirm, Selector } from "./ui-lib";
 | 
			
		||||
 | 
			
		||||
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
 | 
			
		||||
  loading: () => null,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function useHotKey() {
 | 
			
		||||
export function useHotKey() {
 | 
			
		||||
  const chatStore = useChatStore();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
@@ -54,7 +55,7 @@ function useHotKey() {
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useDragSideBar() {
 | 
			
		||||
export function useDragSideBar() {
 | 
			
		||||
  const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
 | 
			
		||||
 | 
			
		||||
  const config = useAppConfig();
 | 
			
		||||
@@ -127,25 +128,21 @@ function useDragSideBar() {
 | 
			
		||||
    shouldNarrow,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function SideBar(props: { className?: string }) {
 | 
			
		||||
  const chatStore = useChatStore();
 | 
			
		||||
 | 
			
		||||
  // drag side bar
 | 
			
		||||
  const { onDragStart, shouldNarrow } = useDragSideBar();
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const config = useAppConfig();
 | 
			
		||||
export function SideBarContainer(props: {
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
  onDragStart: (e: MouseEvent) => void;
 | 
			
		||||
  shouldNarrow: boolean;
 | 
			
		||||
  className?: string;
 | 
			
		||||
}) {
 | 
			
		||||
  const isMobileScreen = useMobileScreen();
 | 
			
		||||
  const isIOSMobile = useMemo(
 | 
			
		||||
    () => isIOS() && isMobileScreen,
 | 
			
		||||
    [isMobileScreen],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useHotKey();
 | 
			
		||||
 | 
			
		||||
  const { children, className, onDragStart, shouldNarrow } = props;
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={`${styles.sidebar} ${props.className} ${
 | 
			
		||||
      className={`${styles.sidebar} ${className} ${
 | 
			
		||||
        shouldNarrow && styles["narrow-sidebar"]
 | 
			
		||||
      }`}
 | 
			
		||||
      style={{
 | 
			
		||||
@@ -153,43 +150,130 @@ export function SideBar(props: { className?: string }) {
 | 
			
		||||
        transition: isMobileScreen && isIOSMobile ? "none" : undefined,
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <div className={styles["sidebar-header"]} data-tauri-drag-region>
 | 
			
		||||
        <div className={styles["sidebar-title"]} data-tauri-drag-region>
 | 
			
		||||
          NextChat
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className={styles["sidebar-sub-title"]}>
 | 
			
		||||
          Build your own AI assistant.
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className={styles["sidebar-logo"] + " no-dark"}>
 | 
			
		||||
          <ChatGptIcon />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className={styles["sidebar-header-bar"]}>
 | 
			
		||||
        <IconButton
 | 
			
		||||
          icon={<MaskIcon />}
 | 
			
		||||
          text={shouldNarrow ? undefined : Locale.Mask.Name}
 | 
			
		||||
          className={styles["sidebar-bar-button"]}
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            if (config.dontShowMaskSplashScreen !== true) {
 | 
			
		||||
              navigate(Path.NewChat, { state: { fromHome: true } });
 | 
			
		||||
            } else {
 | 
			
		||||
              navigate(Path.Masks, { state: { fromHome: true } });
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
          shadow
 | 
			
		||||
        />
 | 
			
		||||
        <IconButton
 | 
			
		||||
          icon={<PluginIcon />}
 | 
			
		||||
          text={shouldNarrow ? undefined : Locale.Plugin.Name}
 | 
			
		||||
          className={styles["sidebar-bar-button"]}
 | 
			
		||||
          onClick={() => showToast(Locale.WIP)}
 | 
			
		||||
          shadow
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {children}
 | 
			
		||||
      <div
 | 
			
		||||
        className={styles["sidebar-body"]}
 | 
			
		||||
        className={styles["sidebar-drag"]}
 | 
			
		||||
        onPointerDown={(e) => onDragStart(e as any)}
 | 
			
		||||
      >
 | 
			
		||||
        <DragIcon />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function SideBarHeader(props: {
 | 
			
		||||
  title?: string | React.ReactNode;
 | 
			
		||||
  subTitle?: string | React.ReactNode;
 | 
			
		||||
  logo?: React.ReactNode;
 | 
			
		||||
  children?: React.ReactNode;
 | 
			
		||||
}) {
 | 
			
		||||
  const { title, subTitle, logo, children } = props;
 | 
			
		||||
  return (
 | 
			
		||||
    <Fragment>
 | 
			
		||||
      <div className={styles["sidebar-header"]} 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={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 [showPluginSelector, setShowPluginSelector] = useState(false);
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const config = useAppConfig();
 | 
			
		||||
  const chatStore = useChatStore();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SideBarContainer
 | 
			
		||||
      onDragStart={onDragStart}
 | 
			
		||||
      shouldNarrow={shouldNarrow}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <SideBarHeader
 | 
			
		||||
        title="NextChat"
 | 
			
		||||
        subTitle="Build your own AI assistant."
 | 
			
		||||
        logo={<ChatGptIcon />}
 | 
			
		||||
      >
 | 
			
		||||
        <div className={styles["sidebar-header-bar"]}>
 | 
			
		||||
          <IconButton
 | 
			
		||||
            icon={<MaskIcon />}
 | 
			
		||||
            text={shouldNarrow ? undefined : Locale.Mask.Name}
 | 
			
		||||
            className={styles["sidebar-bar-button"]}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              if (config.dontShowMaskSplashScreen !== true) {
 | 
			
		||||
                navigate(Path.NewChat, { state: { fromHome: true } });
 | 
			
		||||
              } else {
 | 
			
		||||
                navigate(Path.Masks, { state: { fromHome: true } });
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
            shadow
 | 
			
		||||
          />
 | 
			
		||||
          <IconButton
 | 
			
		||||
            icon={<DiscoveryIcon />}
 | 
			
		||||
            text={shouldNarrow ? undefined : Locale.Discovery.Name}
 | 
			
		||||
            className={styles["sidebar-bar-button"]}
 | 
			
		||||
            onClick={() => setShowPluginSelector(true)}
 | 
			
		||||
            shadow
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        {showPluginSelector && (
 | 
			
		||||
          <Selector
 | 
			
		||||
            items={[
 | 
			
		||||
              {
 | 
			
		||||
                title: "👇 Please select the plugin you need to use",
 | 
			
		||||
                value: "-",
 | 
			
		||||
                disable: true,
 | 
			
		||||
              },
 | 
			
		||||
              ...PLUGINS.map((item) => {
 | 
			
		||||
                return {
 | 
			
		||||
                  title: item.name,
 | 
			
		||||
                  value: item.path,
 | 
			
		||||
                };
 | 
			
		||||
              }),
 | 
			
		||||
            ]}
 | 
			
		||||
            onClose={() => setShowPluginSelector(false)}
 | 
			
		||||
            onSelection={(s) => {
 | 
			
		||||
              navigate(s[0], { state: { fromHome: true } });
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </SideBarHeader>
 | 
			
		||||
      <SideBarBody
 | 
			
		||||
        onClick={(e) => {
 | 
			
		||||
          if (e.target === e.currentTarget) {
 | 
			
		||||
            navigate(Path.Home);
 | 
			
		||||
@@ -197,32 +281,41 @@ export function SideBar(props: { className?: string }) {
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <ChatList narrow={shouldNarrow} />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className={styles["sidebar-tail"]}>
 | 
			
		||||
        <div className={styles["sidebar-actions"]}>
 | 
			
		||||
          <div className={styles["sidebar-action"] + " " + styles.mobile}>
 | 
			
		||||
            <IconButton
 | 
			
		||||
              icon={<DeleteIcon />}
 | 
			
		||||
              onClick={async () => {
 | 
			
		||||
                if (await showConfirm(Locale.Home.DeleteChat)) {
 | 
			
		||||
                  chatStore.deleteSession(chatStore.currentSessionIndex);
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className={styles["sidebar-action"]}>
 | 
			
		||||
            <Link to={Path.Settings}>
 | 
			
		||||
              <IconButton icon={<SettingsIcon />} shadow />
 | 
			
		||||
            </Link>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className={styles["sidebar-action"]}>
 | 
			
		||||
            <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
 | 
			
		||||
              <IconButton icon={<GithubIcon />} shadow />
 | 
			
		||||
            </a>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div>
 | 
			
		||||
      </SideBarBody>
 | 
			
		||||
      <SideBarTail
 | 
			
		||||
        primaryAction={
 | 
			
		||||
          <>
 | 
			
		||||
            <div className={styles["sidebar-action"] + " " + styles.mobile}>
 | 
			
		||||
              <IconButton
 | 
			
		||||
                icon={<DeleteIcon />}
 | 
			
		||||
                onClick={async () => {
 | 
			
		||||
                  if (await showConfirm(Locale.Home.DeleteChat)) {
 | 
			
		||||
                    chatStore.deleteSession(chatStore.currentSessionIndex);
 | 
			
		||||
                  }
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className={styles["sidebar-action"]}>
 | 
			
		||||
              <Link to={Path.Settings}>
 | 
			
		||||
                <IconButton
 | 
			
		||||
                  aria={Locale.Settings.Title}
 | 
			
		||||
                  icon={<SettingsIcon />}
 | 
			
		||||
                  shadow
 | 
			
		||||
                />
 | 
			
		||||
              </Link>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className={styles["sidebar-action"]}>
 | 
			
		||||
              <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
 | 
			
		||||
                <IconButton
 | 
			
		||||
                  aria={Locale.Export.MessageFromChatGPT}
 | 
			
		||||
                  icon={<GithubIcon />}
 | 
			
		||||
                  shadow
 | 
			
		||||
                />
 | 
			
		||||
              </a>
 | 
			
		||||
            </div>
 | 
			
		||||
          </>
 | 
			
		||||
        }
 | 
			
		||||
        secondaryAction={
 | 
			
		||||
          <IconButton
 | 
			
		||||
            icon={<AddIcon />}
 | 
			
		||||
            text={shouldNarrow ? undefined : Locale.Home.NewChat}
 | 
			
		||||
@@ -236,15 +329,8 @@ export function SideBar(props: { className?: string }) {
 | 
			
		||||
            }}
 | 
			
		||||
            shadow
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
        className={styles["sidebar-drag"]}
 | 
			
		||||
        onPointerDown={(e) => onDragStart(e as any)}
 | 
			
		||||
      >
 | 
			
		||||
        <DragIcon />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
    </SideBarContainer>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -61,6 +61,19 @@
 | 
			
		||||
      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 {
 | 
			
		||||
@@ -291,7 +304,12 @@
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  z-index: 999;
 | 
			
		||||
 | 
			
		||||
  .selector-item-disabled{
 | 
			
		||||
    opacity: 0.6;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &-content {
 | 
			
		||||
    min-width: 300px;
 | 
			
		||||
    .list {
 | 
			
		||||
      max-height: 90vh;
 | 
			
		||||
      overflow-x: hidden;
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,15 @@ import MinIcon from "../icons/min.svg";
 | 
			
		||||
import Locale from "../locales";
 | 
			
		||||
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
export function Popover(props: {
 | 
			
		||||
@@ -47,11 +55,16 @@ export function ListItem(props: {
 | 
			
		||||
  children?: JSX.Element | JSX.Element[];
 | 
			
		||||
  icon?: JSX.Element;
 | 
			
		||||
  className?: string;
 | 
			
		||||
  onClick?: () => void;
 | 
			
		||||
  onClick?: (e: MouseEvent) => void;
 | 
			
		||||
  vertical?: boolean;
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={styles["list-item"] + ` ${props.className || ""}`}
 | 
			
		||||
      className={
 | 
			
		||||
        styles["list-item"] +
 | 
			
		||||
        ` ${props.vertical ? styles["vertical"] : ""} ` +
 | 
			
		||||
        ` ${props.className || ""}`
 | 
			
		||||
      }
 | 
			
		||||
      onClick={props.onClick}
 | 
			
		||||
    >
 | 
			
		||||
      <div className={styles["list-header"]}>
 | 
			
		||||
@@ -252,9 +265,10 @@ export function Input(props: InputProps) {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
 | 
			
		||||
export function PasswordInput(
 | 
			
		||||
  props: HTMLProps<HTMLInputElement> & { aria?: string },
 | 
			
		||||
) {
 | 
			
		||||
  const [visible, setVisible] = useState(false);
 | 
			
		||||
 | 
			
		||||
  function changeVisibility() {
 | 
			
		||||
    setVisible(!visible);
 | 
			
		||||
  }
 | 
			
		||||
@@ -262,6 +276,7 @@ export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={"password-input-container"}>
 | 
			
		||||
      <IconButton
 | 
			
		||||
        aria={props.aria}
 | 
			
		||||
        icon={visible ? <EyeIcon /> : <EyeOffIcon />}
 | 
			
		||||
        onClick={changeVisibility}
 | 
			
		||||
        className={"password-eye"}
 | 
			
		||||
@@ -420,17 +435,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({
 | 
			
		||||
    title: Locale.Export.Image.Modal,
 | 
			
		||||
    defaultMax: defaultMax,
 | 
			
		||||
    children: (
 | 
			
		||||
      <div>
 | 
			
		||||
      <div style={{ display: "flex", justifyContent: "center", ...boxStyle }}>
 | 
			
		||||
        <img
 | 
			
		||||
          src={img}
 | 
			
		||||
          alt="preview"
 | 
			
		||||
          style={{
 | 
			
		||||
            maxWidth: "100%",
 | 
			
		||||
          }}
 | 
			
		||||
          style={
 | 
			
		||||
            style ?? {
 | 
			
		||||
              maxWidth: "100%",
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ></img>
 | 
			
		||||
      </div>
 | 
			
		||||
    ),
 | 
			
		||||
@@ -442,27 +465,56 @@ export function Selector<T>(props: {
 | 
			
		||||
    title: string;
 | 
			
		||||
    subTitle?: string;
 | 
			
		||||
    value: T;
 | 
			
		||||
    disable?: boolean;
 | 
			
		||||
  }>;
 | 
			
		||||
  defaultSelectedValue?: T;
 | 
			
		||||
  defaultSelectedValue?: T[] | T;
 | 
			
		||||
  onSelection?: (selection: T[]) => void;
 | 
			
		||||
  onClose?: () => void;
 | 
			
		||||
  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 (
 | 
			
		||||
    <div className={styles["selector"]} onClick={() => props.onClose?.()}>
 | 
			
		||||
      <div className={styles["selector-content"]}>
 | 
			
		||||
        <List>
 | 
			
		||||
          {props.items.map((item, i) => {
 | 
			
		||||
            const selected = props.defaultSelectedValue === item.value;
 | 
			
		||||
            const selected = selectedValues.includes(item.value);
 | 
			
		||||
            return (
 | 
			
		||||
              <ListItem
 | 
			
		||||
                className={styles["selector-item"]}
 | 
			
		||||
                className={`${styles["selector-item"]} ${
 | 
			
		||||
                  item.disable && styles["selector-item-disabled"]
 | 
			
		||||
                }`}
 | 
			
		||||
                key={i}
 | 
			
		||||
                title={item.title}
 | 
			
		||||
                subTitle={item.subTitle}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  props.onSelection?.([item.value]);
 | 
			
		||||
                  props.onClose?.();
 | 
			
		||||
                onClick={(e) => {
 | 
			
		||||
                  if (item.disable) {
 | 
			
		||||
                    e.stopPropagation();
 | 
			
		||||
                  } else {
 | 
			
		||||
                    handleSelection(e, item.value);
 | 
			
		||||
                  }
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {selected ? (
 | 
			
		||||
@@ -485,3 +537,38 @@ export function Selector<T>(props: {
 | 
			
		||||
    </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>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import { BuildConfig, getBuildConfig } from "./build";
 | 
			
		||||
export function getClientConfig() {
 | 
			
		||||
  if (typeof document !== "undefined") {
 | 
			
		||||
    // client side
 | 
			
		||||
    return JSON.parse(queryMeta("config")) as BuildConfig;
 | 
			
		||||
    return JSON.parse(queryMeta("config") || "{}") as BuildConfig;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (typeof process !== "undefined") {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,10 @@ declare global {
 | 
			
		||||
      CUSTOM_MODELS?: string; // to control custom models
 | 
			
		||||
      DEFAULT_MODEL?: string; // to control default model in every new chat window
 | 
			
		||||
 | 
			
		||||
      // stability only
 | 
			
		||||
      STABILITY_URL?: string;
 | 
			
		||||
      STABILITY_API_KEY?: string;
 | 
			
		||||
 | 
			
		||||
      // azure only
 | 
			
		||||
      AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
 | 
			
		||||
      AZURE_API_KEY?: string;
 | 
			
		||||
@@ -53,6 +57,20 @@ declare global {
 | 
			
		||||
      ALIBABA_URL?: 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;
 | 
			
		||||
 | 
			
		||||
      // custom template for preprocessing user input
 | 
			
		||||
      DEFAULT_INPUT_TEMPLATE?: string;
 | 
			
		||||
    }
 | 
			
		||||
@@ -101,19 +119,30 @@ export const getServerSideConfig = () => {
 | 
			
		||||
 | 
			
		||||
  if (disableGPT4) {
 | 
			
		||||
    if (customModels) customModels += ",";
 | 
			
		||||
    customModels += DEFAULT_MODELS.filter((m) => m.name.startsWith("gpt-4"))
 | 
			
		||||
    customModels += DEFAULT_MODELS.filter(
 | 
			
		||||
      (m) => m.name.startsWith("gpt-4") && !m.name.startsWith("gpt-4o-mini"),
 | 
			
		||||
    )
 | 
			
		||||
      .map((m) => "-" + m.name)
 | 
			
		||||
      .join(",");
 | 
			
		||||
    if (defaultModel.startsWith("gpt-4")) defaultModel = "";
 | 
			
		||||
    if (
 | 
			
		||||
      defaultModel.startsWith("gpt-4") &&
 | 
			
		||||
      !defaultModel.startsWith("gpt-4o-mini")
 | 
			
		||||
    )
 | 
			
		||||
      defaultModel = "";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const isStability = !!process.env.STABILITY_API_KEY;
 | 
			
		||||
 | 
			
		||||
  const isAzure = !!process.env.AZURE_URL;
 | 
			
		||||
  const isGoogle = !!process.env.GOOGLE_API_KEY;
 | 
			
		||||
  const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
 | 
			
		||||
  const isTencent = !!process.env.TENCENT_API_KEY;
 | 
			
		||||
 | 
			
		||||
  const isBaidu = !!process.env.BAIDU_API_KEY;
 | 
			
		||||
  const isBytedance = !!process.env.BYTEDANCE_API_KEY;
 | 
			
		||||
  const isAlibaba = !!process.env.ALIBABA_API_KEY;
 | 
			
		||||
  const isMoonshot = !!process.env.MOONSHOT_API_KEY;
 | 
			
		||||
  const isIflytek = !!process.env.IFLYTEK_API_KEY;
 | 
			
		||||
  // const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
 | 
			
		||||
  // const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
 | 
			
		||||
  // const randomIndex = Math.floor(Math.random() * apiKeys.length);
 | 
			
		||||
@@ -131,6 +160,10 @@ export const getServerSideConfig = () => {
 | 
			
		||||
    apiKey: getApiKey(process.env.OPENAI_API_KEY),
 | 
			
		||||
    openaiOrgId: process.env.OPENAI_ORG_ID,
 | 
			
		||||
 | 
			
		||||
    isStability,
 | 
			
		||||
    stabilityUrl: process.env.STABILITY_URL,
 | 
			
		||||
    stabilityApiKey: getApiKey(process.env.STABILITY_API_KEY),
 | 
			
		||||
 | 
			
		||||
    isAzure,
 | 
			
		||||
    azureUrl: process.env.AZURE_URL,
 | 
			
		||||
    azureApiKey: getApiKey(process.env.AZURE_API_KEY),
 | 
			
		||||
@@ -158,6 +191,25 @@ export const getServerSideConfig = () => {
 | 
			
		||||
    alibabaUrl: process.env.ALIBABA_URL,
 | 
			
		||||
    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,
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
 | 
			
		||||
    gtmId: process.env.GTM_ID,
 | 
			
		||||
 | 
			
		||||
    needCode: ACCESS_CODES.size > 0,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										142
									
								
								app/constant.ts
									
									
									
									
									
								
							
							
						
						
									
										142
									
								
								app/constant.ts
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
export const OWNER = "Yidadaa";
 | 
			
		||||
export const OWNER = "ChatGPTNextWeb";
 | 
			
		||||
export const REPO = "ChatGPT-Next-Web";
 | 
			
		||||
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
 | 
			
		||||
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
 | 
			
		||||
@@ -8,6 +8,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 RUNTIME_CONFIG_DOM = "danger-runtime-config";
 | 
			
		||||
 | 
			
		||||
export const STABILITY_BASE_URL = "https://api.stability.ai";
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_API_HOST = "https://api.nextchat.dev";
 | 
			
		||||
export const OPENAI_BASE_URL = "https://api.openai.com";
 | 
			
		||||
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
 | 
			
		||||
@@ -21,6 +23,11 @@ export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com";
 | 
			
		||||
 | 
			
		||||
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 CACHE_URL_PREFIX = "/api/cache";
 | 
			
		||||
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
 | 
			
		||||
 | 
			
		||||
@@ -31,6 +38,9 @@ export enum Path {
 | 
			
		||||
  NewChat = "/new-chat",
 | 
			
		||||
  Masks = "/masks",
 | 
			
		||||
  Auth = "/auth",
 | 
			
		||||
  Sd = "/sd",
 | 
			
		||||
  SdNew = "/sd-new",
 | 
			
		||||
  Artifacts = "/artifacts",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum ApiPath {
 | 
			
		||||
@@ -42,6 +52,11 @@ export enum ApiPath {
 | 
			
		||||
  Baidu = "/api/baidu",
 | 
			
		||||
  ByteDance = "/api/bytedance",
 | 
			
		||||
  Alibaba = "/api/alibaba",
 | 
			
		||||
  Tencent = "/api/tencent",
 | 
			
		||||
  Moonshot = "/api/moonshot",
 | 
			
		||||
  Iflytek = "/api/iflytek",
 | 
			
		||||
  Stability = "/api/stability",
 | 
			
		||||
  Artifacts = "/api/artifacts",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum SlotID {
 | 
			
		||||
@@ -54,6 +69,10 @@ export enum FileName {
 | 
			
		||||
  Prompts = "prompts.json",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum Plugin {
 | 
			
		||||
  Artifacts = "artifacts",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum StoreKey {
 | 
			
		||||
  Chat = "chat-next-web-store",
 | 
			
		||||
  Access = "access-control",
 | 
			
		||||
@@ -62,6 +81,7 @@ export enum StoreKey {
 | 
			
		||||
  Prompt = "prompt-store",
 | 
			
		||||
  Update = "chat-update",
 | 
			
		||||
  Sync = "sync",
 | 
			
		||||
  SdList = "sd-list",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_SIDEBAR_WIDTH = 300;
 | 
			
		||||
@@ -88,17 +108,39 @@ export enum ServiceProvider {
 | 
			
		||||
  Baidu = "Baidu",
 | 
			
		||||
  ByteDance = "ByteDance",
 | 
			
		||||
  Alibaba = "Alibaba",
 | 
			
		||||
  Tencent = "Tencent",
 | 
			
		||||
  Moonshot = "Moonshot",
 | 
			
		||||
  Stability = "Stability",
 | 
			
		||||
  Iflytek = "Iflytek",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 {
 | 
			
		||||
  Stability = "Stability",
 | 
			
		||||
  GPT = "GPT",
 | 
			
		||||
  GeminiPro = "GeminiPro",
 | 
			
		||||
  Claude = "Claude",
 | 
			
		||||
  Ernie = "Ernie",
 | 
			
		||||
  Doubao = "Doubao",
 | 
			
		||||
  Qwen = "Qwen",
 | 
			
		||||
  Hunyuan = "Hunyuan",
 | 
			
		||||
  Moonshot = "Moonshot",
 | 
			
		||||
  Iflytek = "Iflytek",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Stability = {
 | 
			
		||||
  GeneratePath: "v2beta/stable-image/generate",
 | 
			
		||||
  ExampleEndpoint: "https://api.stability.ai",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Anthropic = {
 | 
			
		||||
  ChatPath: "v1/messages",
 | 
			
		||||
  ChatPath1: "v1/complete",
 | 
			
		||||
@@ -108,6 +150,7 @@ export const Anthropic = {
 | 
			
		||||
 | 
			
		||||
export const OpenaiPath = {
 | 
			
		||||
  ChatPath: "v1/chat/completions",
 | 
			
		||||
  ImagePath: "v1/images/generations",
 | 
			
		||||
  UsagePath: "dashboard/billing/usage",
 | 
			
		||||
  SubsPath: "dashboard/billing/subscription",
 | 
			
		||||
  ListModelPath: "v1/models",
 | 
			
		||||
@@ -116,7 +159,10 @@ export const OpenaiPath = {
 | 
			
		||||
export const Azure = {
 | 
			
		||||
  ChatPath: (deployName: string, apiVersion: string) =>
 | 
			
		||||
    `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 = {
 | 
			
		||||
@@ -138,9 +184,6 @@ export const Baidu = {
 | 
			
		||||
    if (modelName === "ernie-3.5-8k") {
 | 
			
		||||
      endpoint = "completions";
 | 
			
		||||
    }
 | 
			
		||||
    if (modelName === "ernie-speed-128k") {
 | 
			
		||||
      endpoint = "ernie-speed-128k";
 | 
			
		||||
    }
 | 
			
		||||
    if (modelName === "ernie-speed-8k") {
 | 
			
		||||
      endpoint = "ernie_speed";
 | 
			
		||||
    }
 | 
			
		||||
@@ -158,6 +201,20 @@ export const Alibaba = {
 | 
			
		||||
  ChatPath: "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 DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
 | 
			
		||||
// export const DEFAULT_SYSTEM_TEMPLATE = `
 | 
			
		||||
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
 | 
			
		||||
@@ -176,7 +233,7 @@ Latex inline: \\(x^2\\)
 | 
			
		||||
Latex block: $$e=mc^2$$
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
 | 
			
		||||
export const SUMMARIZE_MODEL = "gpt-4o-mini";
 | 
			
		||||
export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
 | 
			
		||||
 | 
			
		||||
export const KnowledgeCutOffDate: Record<string, string> = {
 | 
			
		||||
@@ -186,6 +243,7 @@ export const KnowledgeCutOffDate: Record<string, string> = {
 | 
			
		||||
  "gpt-4-turbo-preview": "2023-12",
 | 
			
		||||
  "gpt-4o": "2023-10",
 | 
			
		||||
  "gpt-4o-2024-05-13": "2023-10",
 | 
			
		||||
  "gpt-4o-2024-08-06": "2023-10",
 | 
			
		||||
  "gpt-4o-mini": "2023-10",
 | 
			
		||||
  "gpt-4o-mini-2024-07-18": "2023-10",
 | 
			
		||||
  "gpt-4-vision-preview": "2023-04",
 | 
			
		||||
@@ -207,11 +265,13 @@ const openaiModels = [
 | 
			
		||||
  "gpt-4-turbo-preview",
 | 
			
		||||
  "gpt-4o",
 | 
			
		||||
  "gpt-4o-2024-05-13",
 | 
			
		||||
  "gpt-4o-2024-08-06",
 | 
			
		||||
  "gpt-4o-mini",
 | 
			
		||||
  "gpt-4o-mini-2024-07-18",
 | 
			
		||||
  "gpt-4-vision-preview",
 | 
			
		||||
  "gpt-4-turbo-2024-04-09",
 | 
			
		||||
  "gpt-4-1106-preview",
 | 
			
		||||
  "dall-e-3",
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const googleModels = [
 | 
			
		||||
@@ -264,68 +324,136 @@ const alibabaModes = [
 | 
			
		||||
  "qwen-max-longcontext",
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
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",
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
let seq = 1000; // 内置的模型序号生成器从1000开始
 | 
			
		||||
export const DEFAULT_MODELS = [
 | 
			
		||||
  ...openaiModels.map((name) => ({
 | 
			
		||||
    name,
 | 
			
		||||
    available: true,
 | 
			
		||||
    sorted: seq++, // Global sequence sort(index)
 | 
			
		||||
    provider: {
 | 
			
		||||
      id: "openai",
 | 
			
		||||
      providerName: "OpenAI",
 | 
			
		||||
      providerType: "openai",
 | 
			
		||||
      sorted: 1, // 这里是固定的,确保顺序与之前内置的版本一致
 | 
			
		||||
    },
 | 
			
		||||
  })),
 | 
			
		||||
  ...openaiModels.map((name) => ({
 | 
			
		||||
    name,
 | 
			
		||||
    available: true,
 | 
			
		||||
    sorted: seq++,
 | 
			
		||||
    provider: {
 | 
			
		||||
      id: "azure",
 | 
			
		||||
      providerName: "Azure",
 | 
			
		||||
      providerType: "azure",
 | 
			
		||||
      sorted: 2,
 | 
			
		||||
    },
 | 
			
		||||
  })),
 | 
			
		||||
  ...googleModels.map((name) => ({
 | 
			
		||||
    name,
 | 
			
		||||
    available: true,
 | 
			
		||||
    sorted: seq++,
 | 
			
		||||
    provider: {
 | 
			
		||||
      id: "google",
 | 
			
		||||
      providerName: "Google",
 | 
			
		||||
      providerType: "google",
 | 
			
		||||
      sorted: 3,
 | 
			
		||||
    },
 | 
			
		||||
  })),
 | 
			
		||||
  ...anthropicModels.map((name) => ({
 | 
			
		||||
    name,
 | 
			
		||||
    available: true,
 | 
			
		||||
    sorted: seq++,
 | 
			
		||||
    provider: {
 | 
			
		||||
      id: "anthropic",
 | 
			
		||||
      providerName: "Anthropic",
 | 
			
		||||
      providerType: "anthropic",
 | 
			
		||||
      sorted: 4,
 | 
			
		||||
    },
 | 
			
		||||
  })),
 | 
			
		||||
  ...baiduModels.map((name) => ({
 | 
			
		||||
    name,
 | 
			
		||||
    available: true,
 | 
			
		||||
    sorted: seq++,
 | 
			
		||||
    provider: {
 | 
			
		||||
      id: "baidu",
 | 
			
		||||
      providerName: "Baidu",
 | 
			
		||||
      providerType: "baidu",
 | 
			
		||||
      sorted: 5,
 | 
			
		||||
    },
 | 
			
		||||
  })),
 | 
			
		||||
  ...bytedanceModels.map((name) => ({
 | 
			
		||||
    name,
 | 
			
		||||
    available: true,
 | 
			
		||||
    sorted: seq++,
 | 
			
		||||
    provider: {
 | 
			
		||||
      id: "bytedance",
 | 
			
		||||
      providerName: "ByteDance",
 | 
			
		||||
      providerType: "bytedance",
 | 
			
		||||
      sorted: 6,
 | 
			
		||||
    },
 | 
			
		||||
  })),
 | 
			
		||||
  ...alibabaModes.map((name) => ({
 | 
			
		||||
    name,
 | 
			
		||||
    available: true,
 | 
			
		||||
    sorted: seq++,
 | 
			
		||||
    provider: {
 | 
			
		||||
      id: "alibaba",
 | 
			
		||||
      providerName: "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,
 | 
			
		||||
    },
 | 
			
		||||
  })),
 | 
			
		||||
] as const;
 | 
			
		||||
@@ -345,3 +473,5 @@ export const internalAllowedWebDavEndpoints = [
 | 
			
		||||
  "https://webdav.yandex.com",
 | 
			
		||||
  "https://app.koofr.net/dav/Koofr",
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const PLUGINS = [{ name: "Stable Diffusion", path: Path.Sd }];
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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  | 
							
								
								
									
										4
									
								
								app/icons/hd.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/icons/hd.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#333" class="bi bi-badge-hd" viewBox="0 0 16 16">
 | 
			
		||||
  <path d="M7.396 11V5.001H6.209v2.44H3.687V5H2.5v6h1.187V8.43h2.522V11zM8.5 5.001V11h2.188c1.811 0 2.685-1.107 2.685-3.015 0-1.894-.86-2.984-2.684-2.984zm1.187.967h.843c1.112 0 1.622.686 1.622 2.04 0 1.353-.505 2.02-1.622 2.02h-.843z"/>
 | 
			
		||||
  <path d="M14 3a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 514 B  | 
							
								
								
									
										10
									
								
								app/icons/history.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/icons/history.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
    <path d="M5.81836 6.72729V14H13.0911" stroke="#333" stroke-width="4" stroke-linecap="round"
 | 
			
		||||
        stroke-linejoin="round" />
 | 
			
		||||
    <path
 | 
			
		||||
        d="M4 24C4 35.0457 12.9543 44 24 44V44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C16.598 4 10.1351 8.02111 6.67677 13.9981"
 | 
			
		||||
        stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
 | 
			
		||||
    <path d="M24.005 12L24.0038 24.0088L32.4832 32.4882" stroke="#333" stroke-width="4"
 | 
			
		||||
        stroke-linecap="round" stroke-linejoin="round" />
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 660 B  | 
							
								
								
									
										4
									
								
								app/icons/palette.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/icons/palette.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#333" class="bi bi-palette" viewBox="0 0 16 16">
 | 
			
		||||
  <path d="M8 5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3m4 3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3M5.5 7a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m.5 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"/>
 | 
			
		||||
  <path d="M16 8c0 3.15-1.866 2.585-3.567 2.07C11.42 9.763 10.465 9.473 10 10c-.603.683-.475 1.819-.351 2.92C9.826 14.495 9.996 16 8 16a8 8 0 1 1 8-8m-8 7c.611 0 .654-.171.655-.176.078-.146.124-.464.07-1.119-.014-.168-.037-.37-.061-.591-.052-.464-.112-1.005-.118-1.462-.01-.707.083-1.61.704-2.314.369-.417.845-.578 1.272-.618.404-.038.812.026 1.16.104.343.077.702.186 1.025.284l.028.008c.346.105.658.199.953.266.653.148.904.083.991.024C14.717 9.38 15 9.161 15 8a7 7 0 1 0-7 7"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 781 B  | 
							
								
								
									
										12
									
								
								app/icons/sd.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/icons/sd.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.21em" height="1em" viewBox="0 0 256 213">
 | 
			
		||||
    <defs>
 | 
			
		||||
        <linearGradient id="logosStabilityAiIcon0" x1="50%" x2="50%" y1="0%" y2="100%">
 | 
			
		||||
            <stop offset="0%" stop-color="#9d39ff" />
 | 
			
		||||
            <stop offset="100%" stop-color="#a380ff" />
 | 
			
		||||
        </linearGradient>
 | 
			
		||||
    </defs>
 | 
			
		||||
    <path fill="url(#logosStabilityAiIcon0)"
 | 
			
		||||
        d="M72.418 212.45c49.478 0 81.658-26.205 81.658-65.626c0-30.572-19.572-49.998-54.569-58.043l-22.469-6.74c-19.71-4.424-31.215-9.738-28.505-23.312c2.255-11.292 9.002-17.667 24.69-17.667c49.872 0 68.35 17.667 68.35 17.667V16.237S123.583 0 73.223 0C25.757 0 0 24.424 0 62.236c0 30.571 17.85 48.35 54.052 56.798q3.802.95 3.885.976q8.26 2.556 22.293 6.755c18.504 4.425 23.262 9.121 23.262 23.2c0 12.872-13.374 20.19-31.074 20.19C21.432 170.154 0 144.36 0 144.36v47.078s13.402 21.01 72.418 21.01" />
 | 
			
		||||
    <path fill="#e80000"
 | 
			
		||||
        d="M225.442 209.266c17.515 0 30.558-12.67 30.558-29.812c0-17.515-12.67-29.813-30.558-29.813c-17.515 0-30.185 12.298-30.185 29.813s12.67 29.812 30.185 29.812" />
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
							
								
								
									
										1
									
								
								app/icons/size.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/icons/size.svg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?><svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M42 7H6C4.89543 7 4 7.89543 4 9V39C4 40.1046 4.89543 41 6 41H42C43.1046 41 44 40.1046 44 39V9C44 7.89543 43.1046 7 42 7Z" fill="none" stroke="#333" stroke-width="4"/><path d="M30 30V18L38 30V18" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 30V18L18 30V18" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M24 20V21" stroke="#333" stroke-width="4" stroke-linecap="round"/><path d="M24 27V28" stroke="#333" stroke-width="4" stroke-linecap="round"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 681 B  | 
@@ -37,7 +37,10 @@ export default function RootLayout({
 | 
			
		||||
    <html lang="en">
 | 
			
		||||
      <head>
 | 
			
		||||
        <meta name="config" content={JSON.stringify(getClientConfig())} />
 | 
			
		||||
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
 | 
			
		||||
        <meta
 | 
			
		||||
          name="viewport"
 | 
			
		||||
          content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
 | 
			
		||||
        />
 | 
			
		||||
        <link rel="manifest" href="/site.webmanifest"></link>
 | 
			
		||||
        <script src="/serviceWorkerRegister.js" defer></script>
 | 
			
		||||
      </head>
 | 
			
		||||
 
 | 
			
		||||
@@ -111,6 +111,11 @@ const ar: PartialLocaleType = {
 | 
			
		||||
      Title: "حجم الخط",
 | 
			
		||||
      SubTitle: "ضبط حجم الخط لمحتوى الدردشة",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "خط الدردشة",
 | 
			
		||||
      SubTitle: "خط محتوى الدردشة، اتركه فارغًا لتطبيق الخط الافتراضي العالمي",
 | 
			
		||||
      Placeholder: "اسم الخط",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "حقن تلميحات النظام",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
 
 | 
			
		||||
@@ -136,6 +136,12 @@ const bn: PartialLocaleType = {
 | 
			
		||||
      Title: "ফন্ট সাইজ",
 | 
			
		||||
      SubTitle: "চ্যাট সামগ্রীর ফন্ট সাইজ সংশোধন করুন",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "চ্যাট ফন্ট",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "চ্যাট সামগ্রীর ফন্ট, বিশ্বব্যাপী ডিফল্ট ফন্ট প্রয়োগ করতে খালি রাখুন",
 | 
			
		||||
      Placeholder: "ফন্টের নাম",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "حقن تلميحات النظام",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,7 @@ const cn = {
 | 
			
		||||
      PinToastAction: "查看",
 | 
			
		||||
      Delete: "删除",
 | 
			
		||||
      Edit: "编辑",
 | 
			
		||||
      FullScreen: "全屏",
 | 
			
		||||
    },
 | 
			
		||||
    Commands: {
 | 
			
		||||
      new: "新建聊天",
 | 
			
		||||
@@ -104,6 +105,10 @@ const cn = {
 | 
			
		||||
      Toast: "正在生成截图",
 | 
			
		||||
      Modal: "长按或右键保存图片",
 | 
			
		||||
    },
 | 
			
		||||
    Artifacts: {
 | 
			
		||||
      Title: "分享页面",
 | 
			
		||||
      Error: "分享失败",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  Select: {
 | 
			
		||||
    Search: "搜索消息",
 | 
			
		||||
@@ -128,6 +133,7 @@ const cn = {
 | 
			
		||||
  Settings: {
 | 
			
		||||
    Title: "设置",
 | 
			
		||||
    SubTitle: "所有设置选项",
 | 
			
		||||
    ShowPassword: "显示密码",
 | 
			
		||||
 | 
			
		||||
    Danger: {
 | 
			
		||||
      Reset: {
 | 
			
		||||
@@ -152,6 +158,11 @@ const cn = {
 | 
			
		||||
      Title: "字体大小",
 | 
			
		||||
      SubTitle: "聊天内容的字体大小",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "聊天字体",
 | 
			
		||||
      SubTitle: "聊天内容的字体,若置空则应用全局默认字体",
 | 
			
		||||
      Placeholder: "字体名称",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "注入系统级提示信息",
 | 
			
		||||
      SubTitle: "强制给每次请求的消息列表开头添加一个模拟 ChatGPT 的系统提示",
 | 
			
		||||
@@ -346,6 +357,10 @@ const cn = {
 | 
			
		||||
          Title: "API 版本(仅适用于 gemini-pro)",
 | 
			
		||||
          SubTitle: "选择一个特定的 API 版本",
 | 
			
		||||
        },
 | 
			
		||||
        GoogleSafetySettings: {
 | 
			
		||||
          Title: "Google 安全过滤级别",
 | 
			
		||||
          SubTitle: "设置内容过滤级别",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      Baidu: {
 | 
			
		||||
        ApiKey: {
 | 
			
		||||
@@ -363,6 +378,22 @@ const cn = {
 | 
			
		||||
          SubTitle: "不支持自定义前往.env配置",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      Tencent: {
 | 
			
		||||
        ApiKey: {
 | 
			
		||||
          Title: "API Key",
 | 
			
		||||
          SubTitle: "使用自定义腾讯云API Key",
 | 
			
		||||
          Placeholder: "Tencent API Key",
 | 
			
		||||
        },
 | 
			
		||||
        SecretKey: {
 | 
			
		||||
          Title: "Secret Key",
 | 
			
		||||
          SubTitle: "使用自定义腾讯云Secret Key",
 | 
			
		||||
          Placeholder: "Tencent Secret Key",
 | 
			
		||||
        },
 | 
			
		||||
        Endpoint: {
 | 
			
		||||
          Title: "接口地址",
 | 
			
		||||
          SubTitle: "不支持自定义前往.env配置",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      ByteDance: {
 | 
			
		||||
        ApiKey: {
 | 
			
		||||
          Title: "接口密钥",
 | 
			
		||||
@@ -385,6 +416,44 @@ const cn = {
 | 
			
		||||
          SubTitle: "样例:",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      Moonshot: {
 | 
			
		||||
        ApiKey: {
 | 
			
		||||
          Title: "接口密钥",
 | 
			
		||||
          SubTitle: "使用自定义月之暗面API Key",
 | 
			
		||||
          Placeholder: "Moonshot API Key",
 | 
			
		||||
        },
 | 
			
		||||
        Endpoint: {
 | 
			
		||||
          Title: "接口地址",
 | 
			
		||||
          SubTitle: "样例:",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      Stability: {
 | 
			
		||||
        ApiKey: {
 | 
			
		||||
          Title: "接口密钥",
 | 
			
		||||
          SubTitle: "使用自定义 Stability API Key",
 | 
			
		||||
          Placeholder: "Stability API Key",
 | 
			
		||||
        },
 | 
			
		||||
        Endpoint: {
 | 
			
		||||
          Title: "接口地址",
 | 
			
		||||
          SubTitle: "样例:",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      Iflytek: {
 | 
			
		||||
        ApiKey: {
 | 
			
		||||
          Title: "ApiKey",
 | 
			
		||||
          SubTitle: "从讯飞星火控制台获取的 APIKey",
 | 
			
		||||
          Placeholder: "APIKey",
 | 
			
		||||
        },
 | 
			
		||||
        ApiSecret: {
 | 
			
		||||
          Title: "ApiSecret",
 | 
			
		||||
          SubTitle: "从讯飞星火控制台获取的 APISecret",
 | 
			
		||||
          Placeholder: "APISecret",
 | 
			
		||||
        },
 | 
			
		||||
        Endpoint: {
 | 
			
		||||
          Title: "接口地址",
 | 
			
		||||
          SubTitle: "样例:",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      CustomModel: {
 | 
			
		||||
        Title: "自定义模型名",
 | 
			
		||||
        SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
 | 
			
		||||
@@ -442,6 +511,10 @@ const cn = {
 | 
			
		||||
  },
 | 
			
		||||
  Plugin: {
 | 
			
		||||
    Name: "插件",
 | 
			
		||||
    Artifacts: "Artifacts",
 | 
			
		||||
  },
 | 
			
		||||
  Discovery: {
 | 
			
		||||
    Name: "发现",
 | 
			
		||||
  },
 | 
			
		||||
  FineTuned: {
 | 
			
		||||
    Sysmessage: "你是一个助手",
 | 
			
		||||
@@ -522,6 +595,61 @@ const cn = {
 | 
			
		||||
    Topic: "主题",
 | 
			
		||||
    Time: "时间",
 | 
			
		||||
  },
 | 
			
		||||
  SdPanel: {
 | 
			
		||||
    Prompt: "画面提示",
 | 
			
		||||
    NegativePrompt: "否定提示",
 | 
			
		||||
    PleaseInput: (name: string) => `请输入${name}`,
 | 
			
		||||
    AspectRatio: "横纵比",
 | 
			
		||||
    ImageStyle: "图像风格",
 | 
			
		||||
    OutFormat: "输出格式",
 | 
			
		||||
    AIModel: "AI模型",
 | 
			
		||||
    ModelVersion: "模型版本",
 | 
			
		||||
    Submit: "提交生成",
 | 
			
		||||
    ParamIsRequired: (name: string) => `${name}不能为空`,
 | 
			
		||||
    Styles: {
 | 
			
		||||
      D3Model: "3D模型",
 | 
			
		||||
      AnalogFilm: "模拟电影",
 | 
			
		||||
      Anime: "动漫",
 | 
			
		||||
      Cinematic: "电影风格",
 | 
			
		||||
      ComicBook: "漫画书",
 | 
			
		||||
      DigitalArt: "数字艺术",
 | 
			
		||||
      Enhance: "增强",
 | 
			
		||||
      FantasyArt: "幻想艺术",
 | 
			
		||||
      Isometric: "等角",
 | 
			
		||||
      LineArt: "线描",
 | 
			
		||||
      LowPoly: "低多边形",
 | 
			
		||||
      ModelingCompound: "建模材料",
 | 
			
		||||
      NeonPunk: "霓虹朋克",
 | 
			
		||||
      Origami: "折纸",
 | 
			
		||||
      Photographic: "摄影",
 | 
			
		||||
      PixelArt: "像素艺术",
 | 
			
		||||
      TileTexture: "贴图",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  Sd: {
 | 
			
		||||
    SubTitle: (count: number) => `共 ${count} 条绘画`,
 | 
			
		||||
    Actions: {
 | 
			
		||||
      Params: "查看参数",
 | 
			
		||||
      Copy: "复制提示词",
 | 
			
		||||
      Delete: "删除",
 | 
			
		||||
      Retry: "重试",
 | 
			
		||||
      ReturnHome: "返回首页",
 | 
			
		||||
      History: "查看历史",
 | 
			
		||||
    },
 | 
			
		||||
    EmptyRecord: "暂无绘画记录",
 | 
			
		||||
    Status: {
 | 
			
		||||
      Name: "状态",
 | 
			
		||||
      Success: "成功",
 | 
			
		||||
      Error: "失败",
 | 
			
		||||
      Wait: "等待中",
 | 
			
		||||
      Running: "运行中",
 | 
			
		||||
    },
 | 
			
		||||
    Danger: {
 | 
			
		||||
      Delete: "确认删除?",
 | 
			
		||||
    },
 | 
			
		||||
    GenerateParams: "生成参数",
 | 
			
		||||
    Detail: "详情",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type DeepPartial<T> = T extends object
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,12 @@ const cs: PartialLocaleType = {
 | 
			
		||||
      Title: "Velikost písma",
 | 
			
		||||
      SubTitle: "Nastavení velikosti písma obsahu chatu",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "Chatové Písmo",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "Písmo obsahu chatu, ponechejte prázdné pro použití globálního výchozího písma",
 | 
			
		||||
      Placeholder: "Název Písma",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "Vložit systémové prompty",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,12 @@ const de: PartialLocaleType = {
 | 
			
		||||
      Title: "Schriftgröße",
 | 
			
		||||
      SubTitle: "Schriftgröße des Chat-Inhalts anpassen",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "Chat-Schriftart",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "Schriftart des Chat-Inhalts, leer lassen, um die globale Standardschriftart anzuwenden",
 | 
			
		||||
      Placeholder: "Schriftartname",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "System-Prompts einfügen",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
 
 | 
			
		||||
@@ -44,6 +44,7 @@ const en: LocaleType = {
 | 
			
		||||
      PinToastAction: "View",
 | 
			
		||||
      Delete: "Delete",
 | 
			
		||||
      Edit: "Edit",
 | 
			
		||||
      FullScreen: "FullScreen",
 | 
			
		||||
    },
 | 
			
		||||
    Commands: {
 | 
			
		||||
      new: "Start a new chat",
 | 
			
		||||
@@ -106,6 +107,10 @@ const en: LocaleType = {
 | 
			
		||||
      Toast: "Capturing Image...",
 | 
			
		||||
      Modal: "Long press or right click to save image",
 | 
			
		||||
    },
 | 
			
		||||
    Artifacts: {
 | 
			
		||||
      Title: "Share Artifacts",
 | 
			
		||||
      Error: "Share Error",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  Select: {
 | 
			
		||||
    Search: "Search",
 | 
			
		||||
@@ -131,6 +136,7 @@ const en: LocaleType = {
 | 
			
		||||
  Settings: {
 | 
			
		||||
    Title: "Settings",
 | 
			
		||||
    SubTitle: "All Settings",
 | 
			
		||||
    ShowPassword: "ShowPassword",
 | 
			
		||||
    Danger: {
 | 
			
		||||
      Reset: {
 | 
			
		||||
        Title: "Reset All Settings",
 | 
			
		||||
@@ -154,6 +160,12 @@ const en: LocaleType = {
 | 
			
		||||
      Title: "Font Size",
 | 
			
		||||
      SubTitle: "Adjust font size of chat content",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "Chat Font Family",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "Font Family of the chat content, leave empty to apply global default font",
 | 
			
		||||
      Placeholder: "Font Family Name",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "Inject System Prompts",
 | 
			
		||||
      SubTitle: "Inject a global system prompt for every request",
 | 
			
		||||
@@ -350,6 +362,22 @@ const en: LocaleType = {
 | 
			
		||||
          SubTitle: "not supported, configure in .env",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      Tencent: {
 | 
			
		||||
        ApiKey: {
 | 
			
		||||
          Title: "Tencent API Key",
 | 
			
		||||
          SubTitle: "Use a custom Tencent API Key",
 | 
			
		||||
          Placeholder: "Tencent API Key",
 | 
			
		||||
        },
 | 
			
		||||
        SecretKey: {
 | 
			
		||||
          Title: "Tencent Secret Key",
 | 
			
		||||
          SubTitle: "Use a custom Tencent Secret Key",
 | 
			
		||||
          Placeholder: "Tencent Secret Key",
 | 
			
		||||
        },
 | 
			
		||||
        Endpoint: {
 | 
			
		||||
          Title: "Endpoint Address",
 | 
			
		||||
          SubTitle: "not supported, configure in .env",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      ByteDance: {
 | 
			
		||||
        ApiKey: {
 | 
			
		||||
          Title: "ByteDance API Key",
 | 
			
		||||
@@ -372,6 +400,44 @@ const en: LocaleType = {
 | 
			
		||||
          SubTitle: "Example: ",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      Moonshot: {
 | 
			
		||||
        ApiKey: {
 | 
			
		||||
          Title: "Moonshot API Key",
 | 
			
		||||
          SubTitle: "Use a custom Moonshot API Key",
 | 
			
		||||
          Placeholder: "Moonshot API Key",
 | 
			
		||||
        },
 | 
			
		||||
        Endpoint: {
 | 
			
		||||
          Title: "Endpoint Address",
 | 
			
		||||
          SubTitle: "Example: ",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      Stability: {
 | 
			
		||||
        ApiKey: {
 | 
			
		||||
          Title: "Stability API Key",
 | 
			
		||||
          SubTitle: "Use a custom Stability API Key",
 | 
			
		||||
          Placeholder: "Stability API Key",
 | 
			
		||||
        },
 | 
			
		||||
        Endpoint: {
 | 
			
		||||
          Title: "Endpoint Address",
 | 
			
		||||
          SubTitle: "Example: ",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      Iflytek: {
 | 
			
		||||
        ApiKey: {
 | 
			
		||||
          Title: "Iflytek API Key",
 | 
			
		||||
          SubTitle: "Use a Iflytek API Key",
 | 
			
		||||
          Placeholder: "Iflytek API Key",
 | 
			
		||||
        },
 | 
			
		||||
        ApiSecret: {
 | 
			
		||||
          Title: "Iflytek API Secret",
 | 
			
		||||
          SubTitle: "Use a Iflytek API Secret",
 | 
			
		||||
          Placeholder: "Iflytek API Secret",
 | 
			
		||||
        },
 | 
			
		||||
        Endpoint: {
 | 
			
		||||
          Title: "Endpoint Address",
 | 
			
		||||
          SubTitle: "Example: ",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      CustomModel: {
 | 
			
		||||
        Title: "Custom Models",
 | 
			
		||||
        SubTitle: "Custom model options, seperated by comma",
 | 
			
		||||
@@ -392,6 +458,10 @@ const en: LocaleType = {
 | 
			
		||||
          Title: "API Version (specific to gemini-pro)",
 | 
			
		||||
          SubTitle: "Select a specific API version",
 | 
			
		||||
        },
 | 
			
		||||
        GoogleSafetySettings: {
 | 
			
		||||
          Title: "Google Safety Settings",
 | 
			
		||||
          SubTitle: "Select a safety filtering level",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@@ -449,6 +519,10 @@ const en: LocaleType = {
 | 
			
		||||
  },
 | 
			
		||||
  Plugin: {
 | 
			
		||||
    Name: "Plugin",
 | 
			
		||||
    Artifacts: "Artifacts",
 | 
			
		||||
  },
 | 
			
		||||
  Discovery: {
 | 
			
		||||
    Name: "Discovery",
 | 
			
		||||
  },
 | 
			
		||||
  FineTuned: {
 | 
			
		||||
    Sysmessage: "You are an assistant that",
 | 
			
		||||
@@ -524,11 +598,65 @@ const en: LocaleType = {
 | 
			
		||||
    Topic: "Topic",
 | 
			
		||||
    Time: "Time",
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  URLCommand: {
 | 
			
		||||
    Code: "Detected access code from url, confirm to apply? ",
 | 
			
		||||
    Settings: "Detected settings from url, confirm to apply?",
 | 
			
		||||
  },
 | 
			
		||||
  SdPanel: {
 | 
			
		||||
    Prompt: "Prompt",
 | 
			
		||||
    NegativePrompt: "Negative Prompt",
 | 
			
		||||
    PleaseInput: (name: string) => `Please input ${name}`,
 | 
			
		||||
    AspectRatio: "Aspect Ratio",
 | 
			
		||||
    ImageStyle: "Image Style",
 | 
			
		||||
    OutFormat: "Output Format",
 | 
			
		||||
    AIModel: "AI Model",
 | 
			
		||||
    ModelVersion: "Model Version",
 | 
			
		||||
    Submit: "Submit",
 | 
			
		||||
    ParamIsRequired: (name: string) => `${name} is required`,
 | 
			
		||||
    Styles: {
 | 
			
		||||
      D3Model: "3d-model",
 | 
			
		||||
      AnalogFilm: "analog-film",
 | 
			
		||||
      Anime: "anime",
 | 
			
		||||
      Cinematic: "cinematic",
 | 
			
		||||
      ComicBook: "comic-book",
 | 
			
		||||
      DigitalArt: "digital-art",
 | 
			
		||||
      Enhance: "enhance",
 | 
			
		||||
      FantasyArt: "fantasy-art",
 | 
			
		||||
      Isometric: "isometric",
 | 
			
		||||
      LineArt: "line-art",
 | 
			
		||||
      LowPoly: "low-poly",
 | 
			
		||||
      ModelingCompound: "modeling-compound",
 | 
			
		||||
      NeonPunk: "neon-punk",
 | 
			
		||||
      Origami: "origami",
 | 
			
		||||
      Photographic: "photographic",
 | 
			
		||||
      PixelArt: "pixel-art",
 | 
			
		||||
      TileTexture: "tile-texture",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  Sd: {
 | 
			
		||||
    SubTitle: (count: number) => `${count} images`,
 | 
			
		||||
    Actions: {
 | 
			
		||||
      Params: "See Params",
 | 
			
		||||
      Copy: "Copy Prompt",
 | 
			
		||||
      Delete: "Delete",
 | 
			
		||||
      Retry: "Retry",
 | 
			
		||||
      ReturnHome: "Return Home",
 | 
			
		||||
      History: "History",
 | 
			
		||||
    },
 | 
			
		||||
    EmptyRecord: "No images yet",
 | 
			
		||||
    Status: {
 | 
			
		||||
      Name: "Status",
 | 
			
		||||
      Success: "Success",
 | 
			
		||||
      Error: "Error",
 | 
			
		||||
      Wait: "Waiting",
 | 
			
		||||
      Running: "Running",
 | 
			
		||||
    },
 | 
			
		||||
    Danger: {
 | 
			
		||||
      Delete: "Confirm to delete?",
 | 
			
		||||
    },
 | 
			
		||||
    GenerateParams: "Generate Params",
 | 
			
		||||
    Detail: "Detail",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default en;
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,12 @@ const es: PartialLocaleType = {
 | 
			
		||||
      Title: "Tamaño de fuente",
 | 
			
		||||
      SubTitle: "Ajustar el tamaño de fuente del contenido del chat",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "Fuente del Chat",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "Fuente del contenido del chat, dejar vacío para aplicar la fuente predeterminada global",
 | 
			
		||||
      Placeholder: "Nombre de la Fuente",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "Inyectar Prompts del Sistema",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
 
 | 
			
		||||
@@ -111,6 +111,12 @@ const fr: PartialLocaleType = {
 | 
			
		||||
      Title: "Taille des polices",
 | 
			
		||||
      SubTitle: "Ajuste la taille de police du contenu de la conversation",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "Police de Chat",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "Police du contenu du chat, laissez vide pour appliquer la police par défaut globale",
 | 
			
		||||
      Placeholder: "Nom de la Police",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "Injecter des invites système",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
 
 | 
			
		||||
@@ -140,6 +140,12 @@ const id: PartialLocaleType = {
 | 
			
		||||
      Title: "Ukuran Font",
 | 
			
		||||
      SubTitle: "Ubah ukuran font konten chat",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "Font Obrolan",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "Font dari konten obrolan, biarkan kosong untuk menerapkan font default global",
 | 
			
		||||
      Placeholder: "Nama Font",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "Suntikkan Petunjuk Sistem",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
@@ -369,7 +375,7 @@ const id: PartialLocaleType = {
 | 
			
		||||
  },
 | 
			
		||||
  Exporter: {
 | 
			
		||||
    Description: {
 | 
			
		||||
      Title: "Hanya pesan setelah menghapus konteks yang akan ditampilkan"
 | 
			
		||||
      Title: "Hanya pesan setelah menghapus konteks yang akan ditampilkan",
 | 
			
		||||
    },
 | 
			
		||||
    Model: "Model",
 | 
			
		||||
    Messages: "Pesan",
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,12 @@ const it: PartialLocaleType = {
 | 
			
		||||
      Title: "Dimensione carattere",
 | 
			
		||||
      SubTitle: "Regolare la dimensione dei caratteri del contenuto della chat",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "Font della Chat",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "Carattere del contenuto della chat, lascia vuoto per applicare il carattere predefinito globale",
 | 
			
		||||
      Placeholder: "Nome del Font",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "Inserisci Prompts di Sistema",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
 
 | 
			
		||||
@@ -118,6 +118,12 @@ const jp: PartialLocaleType = {
 | 
			
		||||
      Title: "フォントサイズ",
 | 
			
		||||
      SubTitle: "チャット内容のフォントサイズ",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "チャットフォント",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "チャットコンテンツのフォント、空白の場合はグローバルデフォルトフォントを適用します",
 | 
			
		||||
      Placeholder: "フォント名",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "システムプロンプトの挿入",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
 
 | 
			
		||||
@@ -72,6 +72,11 @@ const ko: PartialLocaleType = {
 | 
			
		||||
      Title: "글꼴 크기",
 | 
			
		||||
      SubTitle: "채팅 내용의 글꼴 크기 조정",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "채팅 폰트",
 | 
			
		||||
      SubTitle: "채팅 내용의 폰트, 비워 두면 글로벌 기본 폰트를 적용",
 | 
			
		||||
      Placeholder: "폰트 이름",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "시스템 프롬프트 주입",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
 
 | 
			
		||||
@@ -66,6 +66,12 @@ const no: PartialLocaleType = {
 | 
			
		||||
      Title: "Fontstørrelsen",
 | 
			
		||||
      SubTitle: "Juster fontstørrelsen for samtaleinnholdet.",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "Chat-skrifttype",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "Skrifttypen for chatinnhold, la stå tom for å bruke global standardskrifttype",
 | 
			
		||||
      Placeholder: "Skriftnavn",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "Sett inn systemprompter",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
 
 | 
			
		||||
@@ -153,6 +153,12 @@ const pt: PartialLocaleType = {
 | 
			
		||||
      Title: "Tamanho da Fonte",
 | 
			
		||||
      SubTitle: "Ajustar o tamanho da fonte do conteúdo do chat",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "Fonte do Chat",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "Fonte do conteúdo do chat, deixe vazio para aplicar a fonte padrão global",
 | 
			
		||||
      Placeholder: "Nome da Fonte",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "Inserir Prompts de Sistema",
 | 
			
		||||
      SubTitle: "Inserir um prompt de sistema global para cada requisição",
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,12 @@ const ru: PartialLocaleType = {
 | 
			
		||||
      Title: "Размер шрифта",
 | 
			
		||||
      SubTitle: "Настроить размер шрифта контента чата",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "Шрифт чата",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "Шрифт содержимого чата, оставьте пустым для применения глобального шрифта по умолчанию",
 | 
			
		||||
      Placeholder: "Название шрифта",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "Вставить системные подсказки",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
 
 | 
			
		||||
@@ -155,6 +155,12 @@ const sk: PartialLocaleType = {
 | 
			
		||||
      Title: "Veľkosť písma",
 | 
			
		||||
      SubTitle: "Nastaviť veľkosť písma obsahu chatu",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "Chatové Písmo",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "Písmo obsahu chatu, ponechajte prázdne pre použitie globálneho predvoleného písma",
 | 
			
		||||
      Placeholder: "Názov Písma",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "Vložiť systémové výzvy",
 | 
			
		||||
      SubTitle: "Vložiť globálnu systémovú výzvu pre každú požiadavku",
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,12 @@ const tr: PartialLocaleType = {
 | 
			
		||||
      Title: "Yazı Boyutu",
 | 
			
		||||
      SubTitle: "Sohbet içeriğinin yazı boyutunu ayarlayın",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "Sohbet Yazı Tipi",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "Sohbet içeriğinin yazı tipi, boş bırakıldığında küresel varsayılan yazı tipi uygulanır",
 | 
			
		||||
      Placeholder: "Yazı Tipi Adı",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "Sistem İpucu Ekleyin",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
 
 | 
			
		||||
@@ -153,6 +153,11 @@ const tw = {
 | 
			
		||||
      Title: "字型大小",
 | 
			
		||||
      SubTitle: "聊天內容的字型大小",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "聊天字體",
 | 
			
		||||
      SubTitle: "聊天內容的字體,若置空則應用全局默認字體",
 | 
			
		||||
      Placeholder: "字體名稱",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "匯入系統提示",
 | 
			
		||||
      SubTitle: "強制在每個請求的訊息列表開頭新增一個模擬 ChatGPT 的系統提示",
 | 
			
		||||
@@ -241,7 +246,7 @@ const tw = {
 | 
			
		||||
      },
 | 
			
		||||
      List: "自訂提示詞列表",
 | 
			
		||||
      ListCount: (builtin: number, custom: number) =>
 | 
			
		||||
      `內建 ${builtin} 條,使用者自訂 ${custom} 條`,
 | 
			
		||||
        `內建 ${builtin} 條,使用者自訂 ${custom} 條`,
 | 
			
		||||
      Edit: "編輯",
 | 
			
		||||
      Modal: {
 | 
			
		||||
        Title: "提示詞列表",
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,12 @@ const vi: PartialLocaleType = {
 | 
			
		||||
      Title: "Font chữ",
 | 
			
		||||
      SubTitle: "Thay đổi font chữ của nội dung trò chuyện",
 | 
			
		||||
    },
 | 
			
		||||
    FontFamily: {
 | 
			
		||||
      Title: "Phông Chữ Trò Chuyện",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
        "Phông chữ của nội dung trò chuyện, để trống để áp dụng phông chữ mặc định toàn cầu",
 | 
			
		||||
      Placeholder: "Tên Phông Chữ",
 | 
			
		||||
    },
 | 
			
		||||
    InjectSystemPrompts: {
 | 
			
		||||
      Title: "Tiêm Prompt Hệ thống",
 | 
			
		||||
      SubTitle:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import {
 | 
			
		||||
  ApiPath,
 | 
			
		||||
  DEFAULT_API_HOST,
 | 
			
		||||
  GoogleSafetySettingsThreshold,
 | 
			
		||||
  ServiceProvider,
 | 
			
		||||
  StoreKey,
 | 
			
		||||
} from "../constant";
 | 
			
		||||
@@ -38,7 +39,21 @@ const DEFAULT_ALIBABA_URL = isApp
 | 
			
		||||
  ? DEFAULT_API_HOST + "/api/proxy/alibaba"
 | 
			
		||||
  : ApiPath.Alibaba;
 | 
			
		||||
 | 
			
		||||
console.log("DEFAULT_ANTHROPIC_URL", DEFAULT_ANTHROPIC_URL);
 | 
			
		||||
const DEFAULT_TENCENT_URL = isApp
 | 
			
		||||
  ? DEFAULT_API_HOST + "/api/proxy/tencent"
 | 
			
		||||
  : ApiPath.Tencent;
 | 
			
		||||
 | 
			
		||||
const DEFAULT_MOONSHOT_URL = isApp
 | 
			
		||||
  ? DEFAULT_API_HOST + "/api/proxy/moonshot"
 | 
			
		||||
  : ApiPath.Moonshot;
 | 
			
		||||
 | 
			
		||||
const DEFAULT_STABILITY_URL = isApp
 | 
			
		||||
  ? DEFAULT_API_HOST + "/api/proxy/stability"
 | 
			
		||||
  : ApiPath.Stability;
 | 
			
		||||
 | 
			
		||||
const DEFAULT_IFLYTEK_URL = isApp
 | 
			
		||||
  ? DEFAULT_API_HOST + "/api/proxy/iflytek"
 | 
			
		||||
  : ApiPath.Iflytek;
 | 
			
		||||
 | 
			
		||||
const DEFAULT_ACCESS_STATE = {
 | 
			
		||||
  accessCode: "",
 | 
			
		||||
@@ -59,6 +74,7 @@ const DEFAULT_ACCESS_STATE = {
 | 
			
		||||
  googleUrl: DEFAULT_GOOGLE_URL,
 | 
			
		||||
  googleApiKey: "",
 | 
			
		||||
  googleApiVersion: "v1",
 | 
			
		||||
  googleSafetySettings: GoogleSafetySettingsThreshold.BLOCK_ONLY_HIGH,
 | 
			
		||||
 | 
			
		||||
  // anthropic
 | 
			
		||||
  anthropicUrl: DEFAULT_ANTHROPIC_URL,
 | 
			
		||||
@@ -78,6 +94,24 @@ const DEFAULT_ACCESS_STATE = {
 | 
			
		||||
  alibabaUrl: DEFAULT_ALIBABA_URL,
 | 
			
		||||
  alibabaApiKey: "",
 | 
			
		||||
 | 
			
		||||
  // moonshot
 | 
			
		||||
  moonshotUrl: DEFAULT_MOONSHOT_URL,
 | 
			
		||||
  moonshotApiKey: "",
 | 
			
		||||
 | 
			
		||||
  //stability
 | 
			
		||||
  stabilityUrl: DEFAULT_STABILITY_URL,
 | 
			
		||||
  stabilityApiKey: "",
 | 
			
		||||
 | 
			
		||||
  // tencent
 | 
			
		||||
  tencentUrl: DEFAULT_TENCENT_URL,
 | 
			
		||||
  tencentSecretKey: "",
 | 
			
		||||
  tencentSecretId: "",
 | 
			
		||||
 | 
			
		||||
  // iflytek
 | 
			
		||||
  iflytekUrl: DEFAULT_IFLYTEK_URL,
 | 
			
		||||
  iflytekApiKey: "",
 | 
			
		||||
  iflytekApiSecret: "",
 | 
			
		||||
 | 
			
		||||
  // server config
 | 
			
		||||
  needCode: true,
 | 
			
		||||
  hideUserApiKey: false,
 | 
			
		||||
@@ -126,6 +160,17 @@ export const useAccessStore = createPersistStore(
 | 
			
		||||
      return ensure(get(), ["alibabaApiKey"]);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    isValidTencent() {
 | 
			
		||||
      return ensure(get(), ["tencentSecretKey", "tencentSecretId"]);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    isValidMoonshot() {
 | 
			
		||||
      return ensure(get(), ["moonshotApiKey"]);
 | 
			
		||||
    },
 | 
			
		||||
    isValidIflytek() {
 | 
			
		||||
      return ensure(get(), ["iflytekApiKey"]);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    isAuthorized() {
 | 
			
		||||
      this.fetch();
 | 
			
		||||
 | 
			
		||||
@@ -138,6 +183,9 @@ export const useAccessStore = createPersistStore(
 | 
			
		||||
        this.isValidBaidu() ||
 | 
			
		||||
        this.isValidByteDance() ||
 | 
			
		||||
        this.isValidAlibaba() ||
 | 
			
		||||
        this.isValidTencent ||
 | 
			
		||||
        this.isValidMoonshot() ||
 | 
			
		||||
        this.isValidIflytek() ||
 | 
			
		||||
        !this.enabledAccessControl() ||
 | 
			
		||||
        (this.enabledAccessControl() && ensure(get(), ["accessCode"]))
 | 
			
		||||
      );
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ import { nanoid } from "nanoid";
 | 
			
		||||
import { createPersistStore } from "../utils/store";
 | 
			
		||||
import { collectModelsWithDefaultModel } from "../utils/model";
 | 
			
		||||
import { useAccessStore } from "./access";
 | 
			
		||||
import { isDalle3 } from "../utils";
 | 
			
		||||
 | 
			
		||||
export type ChatMessage = RequestMessage & {
 | 
			
		||||
  date: string;
 | 
			
		||||
@@ -90,7 +91,7 @@ function createEmptySession(): ChatSession {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getSummarizeModel(currentModel: string) {
 | 
			
		||||
  // if it is using gpt-* models, force to use 3.5 to summarize
 | 
			
		||||
  // if it is using gpt-* models, force to use 4o-mini to summarize
 | 
			
		||||
  if (currentModel.startsWith("gpt")) {
 | 
			
		||||
    const configStore = useAppConfig.getState();
 | 
			
		||||
    const accessStore = useAccessStore.getState();
 | 
			
		||||
@@ -541,8 +542,13 @@ export const useChatStore = createPersistStore(
 | 
			
		||||
        const config = useAppConfig.getState();
 | 
			
		||||
        const session = get().currentSession();
 | 
			
		||||
        const modelConfig = session.mask.modelConfig;
 | 
			
		||||
        // skip summarize when using dalle3?
 | 
			
		||||
        if (isDalle3(modelConfig.model)) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const api: ClientApi = getClientApi(modelConfig.providerName);
 | 
			
		||||
        const providerName = modelConfig.providerName;
 | 
			
		||||
        const api: ClientApi = getClientApi(providerName);
 | 
			
		||||
 | 
			
		||||
        // remove error messages if any
 | 
			
		||||
        const messages = session.messages;
 | 
			
		||||
@@ -565,6 +571,7 @@ export const useChatStore = createPersistStore(
 | 
			
		||||
            config: {
 | 
			
		||||
              model: getSummarizeModel(session.mask.modelConfig.model),
 | 
			
		||||
              stream: false,
 | 
			
		||||
              providerName,
 | 
			
		||||
            },
 | 
			
		||||
            onFinish(message) {
 | 
			
		||||
              get().updateCurrentSession(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { LLMModel } from "../client/api";
 | 
			
		||||
import { DalleSize, DalleQuality, DalleStyle } from "../typing";
 | 
			
		||||
import { getClientConfig } from "../config/client";
 | 
			
		||||
import {
 | 
			
		||||
  DEFAULT_INPUT_TEMPLATE,
 | 
			
		||||
@@ -33,6 +34,7 @@ export const DEFAULT_CONFIG = {
 | 
			
		||||
  submitKey: SubmitKey.Enter,
 | 
			
		||||
  avatar: "1f603",
 | 
			
		||||
  fontSize: 14,
 | 
			
		||||
  fontFamily: "",
 | 
			
		||||
  theme: Theme.Auto as Theme,
 | 
			
		||||
  tightBorder: !!config?.isApp,
 | 
			
		||||
  sendPreviewBubble: true,
 | 
			
		||||
@@ -60,6 +62,9 @@ export const DEFAULT_CONFIG = {
 | 
			
		||||
    compressMessageLengthThreshold: 1000,
 | 
			
		||||
    enableInjectSystemPrompts: true,
 | 
			
		||||
    template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
 | 
			
		||||
    size: "1024x1024" as DalleSize,
 | 
			
		||||
    quality: "standard" as DalleQuality,
 | 
			
		||||
    style: "vivid" as DalleStyle,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import { BUILTIN_MASKS } from "../masks";
 | 
			
		||||
import { getLang, Lang } from "../locales";
 | 
			
		||||
import { DEFAULT_TOPIC, ChatMessage } from "./chat";
 | 
			
		||||
import { ModelConfig, useAppConfig } from "./config";
 | 
			
		||||
import { StoreKey } from "../constant";
 | 
			
		||||
import { StoreKey, Plugin } from "../constant";
 | 
			
		||||
import { nanoid } from "nanoid";
 | 
			
		||||
import { createPersistStore } from "../utils/store";
 | 
			
		||||
 | 
			
		||||
@@ -17,6 +17,7 @@ export type Mask = {
 | 
			
		||||
  modelConfig: ModelConfig;
 | 
			
		||||
  lang: Lang;
 | 
			
		||||
  builtin: boolean;
 | 
			
		||||
  plugin?: Plugin[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_MASK_STATE = {
 | 
			
		||||
@@ -37,6 +38,7 @@ export const createEmptyMask = () =>
 | 
			
		||||
    lang: getLang(),
 | 
			
		||||
    builtin: false,
 | 
			
		||||
    createdAt: Date.now(),
 | 
			
		||||
    plugin: [Plugin.Artifacts],
 | 
			
		||||
  }) as Mask;
 | 
			
		||||
 | 
			
		||||
export const useMaskStore = createPersistStore(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										163
									
								
								app/store/sd.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								app/store/sd.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,163 @@
 | 
			
		||||
import {
 | 
			
		||||
  Stability,
 | 
			
		||||
  StoreKey,
 | 
			
		||||
  ACCESS_CODE_PREFIX,
 | 
			
		||||
  ApiPath,
 | 
			
		||||
} from "@/app/constant";
 | 
			
		||||
import { getBearerToken } from "@/app/client/api";
 | 
			
		||||
import { createPersistStore } from "@/app/utils/store";
 | 
			
		||||
import { nanoid } from "nanoid";
 | 
			
		||||
import { uploadImage, base64Image2Blob } from "@/app/utils/chat";
 | 
			
		||||
import { models, getModelParamBasicData } from "@/app/components/sd/sd-panel";
 | 
			
		||||
import { useAccessStore } from "./access";
 | 
			
		||||
 | 
			
		||||
const defaultModel = {
 | 
			
		||||
  name: models[0].name,
 | 
			
		||||
  value: models[0].value,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const defaultParams = getModelParamBasicData(models[0].params({}), {});
 | 
			
		||||
 | 
			
		||||
const DEFAULT_SD_STATE = {
 | 
			
		||||
  currentId: 0,
 | 
			
		||||
  draw: [],
 | 
			
		||||
  currentModel: defaultModel,
 | 
			
		||||
  currentParams: defaultParams,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useSdStore = createPersistStore<
 | 
			
		||||
  {
 | 
			
		||||
    currentId: number;
 | 
			
		||||
    draw: any[];
 | 
			
		||||
    currentModel: typeof defaultModel;
 | 
			
		||||
    currentParams: any;
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    getNextId: () => number;
 | 
			
		||||
    sendTask: (data: any, okCall?: Function) => void;
 | 
			
		||||
    updateDraw: (draw: any) => void;
 | 
			
		||||
    setCurrentModel: (model: any) => void;
 | 
			
		||||
    setCurrentParams: (data: any) => void;
 | 
			
		||||
  }
 | 
			
		||||
>(
 | 
			
		||||
  DEFAULT_SD_STATE,
 | 
			
		||||
  (set, _get) => {
 | 
			
		||||
    function get() {
 | 
			
		||||
      return {
 | 
			
		||||
        ..._get(),
 | 
			
		||||
        ...methods,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const methods = {
 | 
			
		||||
      getNextId() {
 | 
			
		||||
        const id = ++_get().currentId;
 | 
			
		||||
        set({ currentId: id });
 | 
			
		||||
        return id;
 | 
			
		||||
      },
 | 
			
		||||
      sendTask(data: any, okCall?: Function) {
 | 
			
		||||
        data = { ...data, id: nanoid(), status: "running" };
 | 
			
		||||
        set({ draw: [data, ..._get().draw] });
 | 
			
		||||
        this.getNextId();
 | 
			
		||||
        this.stabilityRequestCall(data);
 | 
			
		||||
        okCall?.();
 | 
			
		||||
      },
 | 
			
		||||
      stabilityRequestCall(data: any) {
 | 
			
		||||
        const accessStore = useAccessStore.getState();
 | 
			
		||||
        let prefix: string = ApiPath.Stability as string;
 | 
			
		||||
        let bearerToken = "";
 | 
			
		||||
        if (accessStore.useCustomConfig) {
 | 
			
		||||
          prefix = accessStore.stabilityUrl || (ApiPath.Stability as string);
 | 
			
		||||
          bearerToken = getBearerToken(accessStore.stabilityApiKey);
 | 
			
		||||
        }
 | 
			
		||||
        if (!bearerToken && accessStore.enabledAccessControl()) {
 | 
			
		||||
          bearerToken = getBearerToken(
 | 
			
		||||
            ACCESS_CODE_PREFIX + accessStore.accessCode,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        const headers = {
 | 
			
		||||
          Accept: "application/json",
 | 
			
		||||
          Authorization: bearerToken,
 | 
			
		||||
        };
 | 
			
		||||
        const path = `${prefix}/${Stability.GeneratePath}/${data.model}`;
 | 
			
		||||
        const formData = new FormData();
 | 
			
		||||
        for (let paramsKey in data.params) {
 | 
			
		||||
          formData.append(paramsKey, data.params[paramsKey]);
 | 
			
		||||
        }
 | 
			
		||||
        fetch(path, {
 | 
			
		||||
          method: "POST",
 | 
			
		||||
          headers,
 | 
			
		||||
          body: formData,
 | 
			
		||||
        })
 | 
			
		||||
          .then((response) => response.json())
 | 
			
		||||
          .then((resData) => {
 | 
			
		||||
            if (resData.errors && resData.errors.length > 0) {
 | 
			
		||||
              this.updateDraw({
 | 
			
		||||
                ...data,
 | 
			
		||||
                status: "error",
 | 
			
		||||
                error: resData.errors[0],
 | 
			
		||||
              });
 | 
			
		||||
              this.getNextId();
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
            const self = this;
 | 
			
		||||
            if (resData.finish_reason === "SUCCESS") {
 | 
			
		||||
              uploadImage(base64Image2Blob(resData.image, "image/png"))
 | 
			
		||||
                .then((img_data) => {
 | 
			
		||||
                  console.debug("uploadImage success", img_data, self);
 | 
			
		||||
                  self.updateDraw({
 | 
			
		||||
                    ...data,
 | 
			
		||||
                    status: "success",
 | 
			
		||||
                    img_data,
 | 
			
		||||
                  });
 | 
			
		||||
                })
 | 
			
		||||
                .catch((e) => {
 | 
			
		||||
                  console.error("uploadImage error", e);
 | 
			
		||||
                  self.updateDraw({
 | 
			
		||||
                    ...data,
 | 
			
		||||
                    status: "error",
 | 
			
		||||
                    error: JSON.stringify(e),
 | 
			
		||||
                  });
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
              self.updateDraw({
 | 
			
		||||
                ...data,
 | 
			
		||||
                status: "error",
 | 
			
		||||
                error: JSON.stringify(resData),
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
            this.getNextId();
 | 
			
		||||
          })
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
            this.updateDraw({ ...data, status: "error", error: error.message });
 | 
			
		||||
            console.error("Error:", error);
 | 
			
		||||
            this.getNextId();
 | 
			
		||||
          });
 | 
			
		||||
      },
 | 
			
		||||
      updateDraw(_draw: any) {
 | 
			
		||||
        const draw = _get().draw || [];
 | 
			
		||||
        draw.some((item, index) => {
 | 
			
		||||
          if (item.id === _draw.id) {
 | 
			
		||||
            draw[index] = _draw;
 | 
			
		||||
            set(() => ({ draw }));
 | 
			
		||||
            return true;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
      setCurrentModel(model: any) {
 | 
			
		||||
        set({ currentModel: model });
 | 
			
		||||
      },
 | 
			
		||||
      setCurrentParams(data: any) {
 | 
			
		||||
        set({
 | 
			
		||||
          currentParams: data,
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return methods;
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: StoreKey.SdList,
 | 
			
		||||
    version: 1.0,
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
@@ -7,3 +7,7 @@ export interface RequestMessage {
 | 
			
		||||
  role: MessageRole;
 | 
			
		||||
  content: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type DalleSize = "1024x1024" | "1792x1024" | "1024x1792";
 | 
			
		||||
export type DalleQuality = "standard" | "hd";
 | 
			
		||||
export type DalleStyle = "vivid" | "natural";
 | 
			
		||||
 
 | 
			
		||||
@@ -194,6 +194,7 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
 | 
			
		||||
  measureDom.style.width = width + "px";
 | 
			
		||||
  measureDom.innerText = dom.value !== "" ? dom.value : "1";
 | 
			
		||||
  measureDom.style.fontSize = dom.style.fontSize;
 | 
			
		||||
  measureDom.style.fontFamily = dom.style.fontFamily;
 | 
			
		||||
  const endWithEmptyLine = dom.value.endsWith("\n");
 | 
			
		||||
  const height = parseFloat(window.getComputedStyle(measureDom).height);
 | 
			
		||||
  const singleLineHeight = parseFloat(
 | 
			
		||||
@@ -265,3 +266,7 @@ export function isVisionModel(model: string) {
 | 
			
		||||
    visionKeywords.some((keyword) => model.includes(keyword)) || isGpt4Turbo
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isDalle3(model: string) {
 | 
			
		||||
  return "dall-e-3" === model;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -112,7 +112,7 @@ export function base64Image2Blob(base64Data: string, contentType: string) {
 | 
			
		||||
  return new Blob([byteArray], { type: contentType });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function uploadImage(file: File): Promise<string> {
 | 
			
		||||
export function uploadImage(file: Blob): Promise<string> {
 | 
			
		||||
  if (!window._SW_ENABLED) {
 | 
			
		||||
    // if serviceWorker register error, using compressImage
 | 
			
		||||
    return compressImage(file, 256 * 1024);
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,8 @@ export function createWebDavClient(store: SyncStore) {
 | 
			
		||||
  return {
 | 
			
		||||
    async check() {
 | 
			
		||||
      try {
 | 
			
		||||
        const res = await fetch(this.path(folder, proxyUrl), {
 | 
			
		||||
          method: "MKCOL",
 | 
			
		||||
        const res = await fetch(this.path(folder, proxyUrl, "MKCOL"), {
 | 
			
		||||
          method: "GET",
 | 
			
		||||
          headers: this.headers(),
 | 
			
		||||
        });
 | 
			
		||||
        const success = [201, 200, 404, 405, 301, 302, 307, 308].includes(
 | 
			
		||||
@@ -42,6 +42,10 @@ export function createWebDavClient(store: SyncStore) {
 | 
			
		||||
 | 
			
		||||
      console.log("[WebDav] get key = ", key, res.status, res.statusText);
 | 
			
		||||
 | 
			
		||||
      if (404 == res.status) {
 | 
			
		||||
        return "";
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return await res.text();
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@@ -62,7 +66,7 @@ export function createWebDavClient(store: SyncStore) {
 | 
			
		||||
        authorization: `Basic ${auth}`,
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    path(path: string, proxyUrl: string = "") {
 | 
			
		||||
    path(path: string, proxyUrl: string = "", proxyMethod: string = "") {
 | 
			
		||||
      if (path.startsWith("/")) {
 | 
			
		||||
        path = path.slice(1);
 | 
			
		||||
      }
 | 
			
		||||
@@ -78,9 +82,13 @@ export function createWebDavClient(store: SyncStore) {
 | 
			
		||||
        let u = new URL(proxyUrl + pathPrefix + path);
 | 
			
		||||
        // add query params
 | 
			
		||||
        u.searchParams.append("endpoint", config.endpoint);
 | 
			
		||||
        proxyMethod && u.searchParams.append("proxy_method", proxyMethod);
 | 
			
		||||
        url = u.toString();
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        url = pathPrefix + path + "?endpoint=" + config.endpoint;
 | 
			
		||||
        if (proxyMethod) {
 | 
			
		||||
          url += "&proxy_method=" + proxyMethod;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return url;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										246
									
								
								app/utils/hmac.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								app/utils/hmac.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,246 @@
 | 
			
		||||
// From https://gist.github.com/guillermodlpa/f6d955f838e9b10d1ef95b8e259b2c58
 | 
			
		||||
// From https://gist.github.com/stevendesu/2d52f7b5e1f1184af3b667c0b5e054b8
 | 
			
		||||
 | 
			
		||||
// To ensure cross-browser support even without a proper SubtleCrypto
 | 
			
		||||
// impelmentation (or without access to the impelmentation, as is the case with
 | 
			
		||||
// Chrome loaded over HTTP instead of HTTPS), this library can create SHA-256
 | 
			
		||||
// HMAC signatures using nothing but raw JavaScript
 | 
			
		||||
 | 
			
		||||
/* eslint-disable no-magic-numbers, id-length, no-param-reassign, new-cap */
 | 
			
		||||
 | 
			
		||||
// By giving internal functions names that we can mangle, future calls to
 | 
			
		||||
// them are reduced to a single byte (minor space savings in minified file)
 | 
			
		||||
const uint8Array = Uint8Array;
 | 
			
		||||
const uint32Array = Uint32Array;
 | 
			
		||||
const pow = Math.pow;
 | 
			
		||||
 | 
			
		||||
// Will be initialized below
 | 
			
		||||
// Using a Uint32Array instead of a simple array makes the minified code
 | 
			
		||||
// a bit bigger (we lose our `unshift()` hack), but comes with huge
 | 
			
		||||
// performance gains
 | 
			
		||||
const DEFAULT_STATE = new uint32Array(8);
 | 
			
		||||
const ROUND_CONSTANTS: number[] = [];
 | 
			
		||||
 | 
			
		||||
// Reusable object for expanded message
 | 
			
		||||
// Using a Uint32Array instead of a simple array makes the minified code
 | 
			
		||||
// 7 bytes larger, but comes with huge performance gains
 | 
			
		||||
const M = new uint32Array(64);
 | 
			
		||||
 | 
			
		||||
// After minification the code to compute the default state and round
 | 
			
		||||
// constants is smaller than the output. More importantly, this serves as a
 | 
			
		||||
// good educational aide for anyone wondering where the magic numbers come
 | 
			
		||||
// from. No magic numbers FTW!
 | 
			
		||||
function getFractionalBits(n: number) {
 | 
			
		||||
  return ((n - (n | 0)) * pow(2, 32)) | 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let n = 2;
 | 
			
		||||
let nPrime = 0;
 | 
			
		||||
while (nPrime < 64) {
 | 
			
		||||
  // isPrime() was in-lined from its original function form to save
 | 
			
		||||
  // a few bytes
 | 
			
		||||
  let isPrime = true;
 | 
			
		||||
  // Math.sqrt() was replaced with pow(n, 1/2) to save a few bytes
 | 
			
		||||
  // var sqrtN = pow(n, 1 / 2);
 | 
			
		||||
  // So technically to determine if a number is prime you only need to
 | 
			
		||||
  // check numbers up to the square root. However this function only runs
 | 
			
		||||
  // once and we're only computing the first 64 primes (up to 311), so on
 | 
			
		||||
  // any modern CPU this whole function runs in a couple milliseconds.
 | 
			
		||||
  // By going to n / 2 instead of sqrt(n) we net 8 byte savings and no
 | 
			
		||||
  // scaling performance cost
 | 
			
		||||
  for (let factor = 2; factor <= n / 2; factor++) {
 | 
			
		||||
    if (n % factor === 0) {
 | 
			
		||||
      isPrime = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (isPrime) {
 | 
			
		||||
    if (nPrime < 8) {
 | 
			
		||||
      DEFAULT_STATE[nPrime] = getFractionalBits(pow(n, 1 / 2));
 | 
			
		||||
    }
 | 
			
		||||
    ROUND_CONSTANTS[nPrime] = getFractionalBits(pow(n, 1 / 3));
 | 
			
		||||
 | 
			
		||||
    nPrime++;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  n++;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// For cross-platform support we need to ensure that all 32-bit words are
 | 
			
		||||
// in the same endianness. A UTF-8 TextEncoder will return BigEndian data,
 | 
			
		||||
// so upon reading or writing to our ArrayBuffer we'll only swap the bytes
 | 
			
		||||
// if our system is LittleEndian (which is about 99% of CPUs)
 | 
			
		||||
const LittleEndian = !!new uint8Array(new uint32Array([1]).buffer)[0];
 | 
			
		||||
 | 
			
		||||
function convertEndian(word: number) {
 | 
			
		||||
  if (LittleEndian) {
 | 
			
		||||
    return (
 | 
			
		||||
      // byte 1 -> byte 4
 | 
			
		||||
      (word >>> 24) |
 | 
			
		||||
      // byte 2 -> byte 3
 | 
			
		||||
      (((word >>> 16) & 0xff) << 8) |
 | 
			
		||||
      // byte 3 -> byte 2
 | 
			
		||||
      ((word & 0xff00) << 8) |
 | 
			
		||||
      // byte 4 -> byte 1
 | 
			
		||||
      (word << 24)
 | 
			
		||||
    );
 | 
			
		||||
  } else {
 | 
			
		||||
    return word;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function rightRotate(word: number, bits: number) {
 | 
			
		||||
  return (word >>> bits) | (word << (32 - bits));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sha256(data: Uint8Array) {
 | 
			
		||||
  // Copy default state
 | 
			
		||||
  const STATE = DEFAULT_STATE.slice();
 | 
			
		||||
 | 
			
		||||
  // Caching this reduces occurrences of ".length" in minified JavaScript
 | 
			
		||||
  // 3 more byte savings! :D
 | 
			
		||||
  const legth = data.length;
 | 
			
		||||
 | 
			
		||||
  // Pad data
 | 
			
		||||
  const bitLength = legth * 8;
 | 
			
		||||
  const newBitLength = 512 - ((bitLength + 64) % 512) - 1 + bitLength + 65;
 | 
			
		||||
 | 
			
		||||
  // "bytes" and "words" are stored BigEndian
 | 
			
		||||
  const bytes = new uint8Array(newBitLength / 8);
 | 
			
		||||
  const words = new uint32Array(bytes.buffer);
 | 
			
		||||
 | 
			
		||||
  bytes.set(data, 0);
 | 
			
		||||
  // Append a 1
 | 
			
		||||
  bytes[legth] = 0b10000000;
 | 
			
		||||
  // Store length in BigEndian
 | 
			
		||||
  words[words.length - 1] = convertEndian(bitLength);
 | 
			
		||||
 | 
			
		||||
  // Loop iterator (avoid two instances of "var") -- saves 2 bytes
 | 
			
		||||
  let round;
 | 
			
		||||
 | 
			
		||||
  // Process blocks (512 bits / 64 bytes / 16 words at a time)
 | 
			
		||||
  for (let block = 0; block < newBitLength / 32; block += 16) {
 | 
			
		||||
    const workingState = STATE.slice();
 | 
			
		||||
 | 
			
		||||
    // Rounds
 | 
			
		||||
    for (round = 0; round < 64; round++) {
 | 
			
		||||
      let MRound;
 | 
			
		||||
      // Expand message
 | 
			
		||||
      if (round < 16) {
 | 
			
		||||
        // Convert to platform Endianness for later math
 | 
			
		||||
        MRound = convertEndian(words[block + round]);
 | 
			
		||||
      } else {
 | 
			
		||||
        const gamma0x = M[round - 15];
 | 
			
		||||
        const gamma1x = M[round - 2];
 | 
			
		||||
        MRound =
 | 
			
		||||
          M[round - 7] +
 | 
			
		||||
          M[round - 16] +
 | 
			
		||||
          (rightRotate(gamma0x, 7) ^
 | 
			
		||||
            rightRotate(gamma0x, 18) ^
 | 
			
		||||
            (gamma0x >>> 3)) +
 | 
			
		||||
          (rightRotate(gamma1x, 17) ^
 | 
			
		||||
            rightRotate(gamma1x, 19) ^
 | 
			
		||||
            (gamma1x >>> 10));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // M array matches platform endianness
 | 
			
		||||
      M[round] = MRound |= 0;
 | 
			
		||||
 | 
			
		||||
      // Computation
 | 
			
		||||
      const t1 =
 | 
			
		||||
        (rightRotate(workingState[4], 6) ^
 | 
			
		||||
          rightRotate(workingState[4], 11) ^
 | 
			
		||||
          rightRotate(workingState[4], 25)) +
 | 
			
		||||
        ((workingState[4] & workingState[5]) ^
 | 
			
		||||
          (~workingState[4] & workingState[6])) +
 | 
			
		||||
        workingState[7] +
 | 
			
		||||
        MRound +
 | 
			
		||||
        ROUND_CONSTANTS[round];
 | 
			
		||||
      const t2 =
 | 
			
		||||
        (rightRotate(workingState[0], 2) ^
 | 
			
		||||
          rightRotate(workingState[0], 13) ^
 | 
			
		||||
          rightRotate(workingState[0], 22)) +
 | 
			
		||||
        ((workingState[0] & workingState[1]) ^
 | 
			
		||||
          (workingState[2] & (workingState[0] ^ workingState[1])));
 | 
			
		||||
      for (let i = 7; i > 0; i--) {
 | 
			
		||||
        workingState[i] = workingState[i - 1];
 | 
			
		||||
      }
 | 
			
		||||
      workingState[0] = (t1 + t2) | 0;
 | 
			
		||||
      workingState[4] = (workingState[4] + t1) | 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Update state
 | 
			
		||||
    for (round = 0; round < 8; round++) {
 | 
			
		||||
      STATE[round] = (STATE[round] + workingState[round]) | 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Finally the state needs to be converted to BigEndian for output
 | 
			
		||||
  // And we want to return a Uint8Array, not a Uint32Array
 | 
			
		||||
  return new uint8Array(
 | 
			
		||||
    new uint32Array(
 | 
			
		||||
      STATE.map(function (val) {
 | 
			
		||||
        return convertEndian(val);
 | 
			
		||||
      }),
 | 
			
		||||
    ).buffer,
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function hmac(key: Uint8Array, data: ArrayLike<number>) {
 | 
			
		||||
  if (key.length > 64) key = sha256(key);
 | 
			
		||||
 | 
			
		||||
  if (key.length < 64) {
 | 
			
		||||
    const tmp = new Uint8Array(64);
 | 
			
		||||
    tmp.set(key, 0);
 | 
			
		||||
    key = tmp;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Generate inner and outer keys
 | 
			
		||||
  const innerKey = new Uint8Array(64);
 | 
			
		||||
  const outerKey = new Uint8Array(64);
 | 
			
		||||
  for (let i = 0; i < 64; i++) {
 | 
			
		||||
    innerKey[i] = 0x36 ^ key[i];
 | 
			
		||||
    outerKey[i] = 0x5c ^ key[i];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Append the innerKey
 | 
			
		||||
  const msg = new Uint8Array(data.length + 64);
 | 
			
		||||
  msg.set(innerKey, 0);
 | 
			
		||||
  msg.set(data, 64);
 | 
			
		||||
 | 
			
		||||
  // Has the previous message and append the outerKey
 | 
			
		||||
  const result = new Uint8Array(64 + 32);
 | 
			
		||||
  result.set(outerKey, 0);
 | 
			
		||||
  result.set(sha256(msg), 64);
 | 
			
		||||
 | 
			
		||||
  // Hash the previous message
 | 
			
		||||
  return sha256(result);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Convert a string to a Uint8Array, SHA-256 it, and convert back to string
 | 
			
		||||
const encoder = new TextEncoder();
 | 
			
		||||
 | 
			
		||||
export function sign(
 | 
			
		||||
  inputKey: string | Uint8Array,
 | 
			
		||||
  inputData: string | Uint8Array,
 | 
			
		||||
) {
 | 
			
		||||
  const key =
 | 
			
		||||
    typeof inputKey === "string" ? encoder.encode(inputKey) : inputKey;
 | 
			
		||||
  const data =
 | 
			
		||||
    typeof inputData === "string" ? encoder.encode(inputData) : inputData;
 | 
			
		||||
  return hmac(key, data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function hex(bin: Uint8Array) {
 | 
			
		||||
  return bin.reduce((acc, val) => {
 | 
			
		||||
    const hexVal = "00" + val.toString(16);
 | 
			
		||||
    return acc + hexVal.substring(hexVal.length - 2);
 | 
			
		||||
  }, "");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function hash(str: string) {
 | 
			
		||||
  return hex(sha256(encoder.encode(str)));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function hashWithSecret(str: string, secret: string) {
 | 
			
		||||
  return hex(sign(secret, str)).toString();
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +1,42 @@
 | 
			
		||||
import { DEFAULT_MODELS } from "../constant";
 | 
			
		||||
import { LLMModel } from "../client/api";
 | 
			
		||||
 | 
			
		||||
const CustomSeq = {
 | 
			
		||||
  val: -1000, //To ensure the custom model located at front, start from -1000, refer to constant.ts
 | 
			
		||||
  cache: new Map<string, number>(),
 | 
			
		||||
  next: (id: string) => {
 | 
			
		||||
    if (CustomSeq.cache.has(id)) {
 | 
			
		||||
      return CustomSeq.cache.get(id) as number;
 | 
			
		||||
    } else {
 | 
			
		||||
      let seq = CustomSeq.val++;
 | 
			
		||||
      CustomSeq.cache.set(id, seq);
 | 
			
		||||
      return seq;
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const customProvider = (providerName: string) => ({
 | 
			
		||||
  id: providerName.toLowerCase(),
 | 
			
		||||
  providerName: providerName,
 | 
			
		||||
  providerType: "custom",
 | 
			
		||||
  sorted: CustomSeq.next(providerName),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Sorts an array of models based on specified rules.
 | 
			
		||||
 *
 | 
			
		||||
 * First, sorted by provider; if the same, sorted by model
 | 
			
		||||
 */
 | 
			
		||||
const sortModelTable = (models: ReturnType<typeof collectModels>) =>
 | 
			
		||||
  models.sort((a, b) => {
 | 
			
		||||
    if (a.provider && b.provider) {
 | 
			
		||||
      let cmp = a.provider.sorted - b.provider.sorted;
 | 
			
		||||
      return cmp === 0 ? a.sorted - b.sorted : cmp;
 | 
			
		||||
    } else {
 | 
			
		||||
      return a.sorted - b.sorted;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export function collectModelTable(
 | 
			
		||||
  models: readonly LLMModel[],
 | 
			
		||||
  customModels: string,
 | 
			
		||||
@@ -17,6 +47,7 @@ export function collectModelTable(
 | 
			
		||||
      available: boolean;
 | 
			
		||||
      name: string;
 | 
			
		||||
      displayName: string;
 | 
			
		||||
      sorted: number;
 | 
			
		||||
      provider?: LLMModel["provider"]; // Marked as optional
 | 
			
		||||
      isDefault?: boolean;
 | 
			
		||||
    }
 | 
			
		||||
@@ -84,6 +115,7 @@ export function collectModelTable(
 | 
			
		||||
            displayName: displayName || customModelName,
 | 
			
		||||
            available,
 | 
			
		||||
            provider, // Use optional chaining
 | 
			
		||||
            sorted: CustomSeq.next(`${customModelName}@${provider?.id}`),
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@@ -99,12 +131,21 @@ export function collectModelTableWithDefaultModel(
 | 
			
		||||
) {
 | 
			
		||||
  let modelTable = collectModelTable(models, customModels);
 | 
			
		||||
  if (defaultModel && defaultModel !== "") {
 | 
			
		||||
    modelTable[defaultModel] = {
 | 
			
		||||
      ...modelTable[defaultModel],
 | 
			
		||||
      name: defaultModel,
 | 
			
		||||
      available: true,
 | 
			
		||||
      isDefault: true,
 | 
			
		||||
    };
 | 
			
		||||
    if (defaultModel.includes("@")) {
 | 
			
		||||
      if (defaultModel in modelTable) {
 | 
			
		||||
        modelTable[defaultModel].isDefault = true;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      for (const key of Object.keys(modelTable)) {
 | 
			
		||||
        if (
 | 
			
		||||
          modelTable[key].available &&
 | 
			
		||||
          key.split("@").shift() == defaultModel
 | 
			
		||||
        ) {
 | 
			
		||||
          modelTable[key].isDefault = true;
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return modelTable;
 | 
			
		||||
}
 | 
			
		||||
@@ -117,7 +158,9 @@ export function collectModels(
 | 
			
		||||
  customModels: string,
 | 
			
		||||
) {
 | 
			
		||||
  const modelTable = collectModelTable(models, customModels);
 | 
			
		||||
  const allModels = Object.values(modelTable);
 | 
			
		||||
  let allModels = Object.values(modelTable);
 | 
			
		||||
 | 
			
		||||
  allModels = sortModelTable(allModels);
 | 
			
		||||
 | 
			
		||||
  return allModels;
 | 
			
		||||
}
 | 
			
		||||
@@ -132,7 +175,10 @@ export function collectModelsWithDefaultModel(
 | 
			
		||||
    customModels,
 | 
			
		||||
    defaultModel,
 | 
			
		||||
  );
 | 
			
		||||
  const allModels = Object.values(modelTable);
 | 
			
		||||
  let allModels = Object.values(modelTable);
 | 
			
		||||
 | 
			
		||||
  allModels = sortModelTable(allModels);
 | 
			
		||||
 | 
			
		||||
  return allModels;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										102
									
								
								app/utils/tencent.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								app/utils/tencent.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
			
		||||
import { sign, hash as getHash, hex } from "./hmac";
 | 
			
		||||
 | 
			
		||||
// 使用 SHA-256 和 secret 进行 HMAC 加密
 | 
			
		||||
function sha256(message: any, secret: any, encoding?: string) {
 | 
			
		||||
  const result = sign(secret, message);
 | 
			
		||||
  return encoding == "hex" ? hex(result).toString() : result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getDate(timestamp: number) {
 | 
			
		||||
  const date = new Date(timestamp * 1000);
 | 
			
		||||
  const year = date.getUTCFullYear();
 | 
			
		||||
  const month = ("0" + (date.getUTCMonth() + 1)).slice(-2);
 | 
			
		||||
  const day = ("0" + date.getUTCDate()).slice(-2);
 | 
			
		||||
  return `${year}-${month}-${day}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getHeader(
 | 
			
		||||
  payload: any,
 | 
			
		||||
  SECRET_ID: string,
 | 
			
		||||
  SECRET_KEY: string,
 | 
			
		||||
) {
 | 
			
		||||
  // https://cloud.tencent.com/document/api/1729/105701
 | 
			
		||||
 | 
			
		||||
  const endpoint = "hunyuan.tencentcloudapi.com";
 | 
			
		||||
  const service = "hunyuan";
 | 
			
		||||
  const region = ""; // optional
 | 
			
		||||
  const action = "ChatCompletions";
 | 
			
		||||
  const version = "2023-09-01";
 | 
			
		||||
  const timestamp = Math.floor(Date.now() / 1000);
 | 
			
		||||
  //时间处理, 获取世界时间日期
 | 
			
		||||
  const date = getDate(timestamp);
 | 
			
		||||
 | 
			
		||||
  // ************* 步骤 1:拼接规范请求串 *************
 | 
			
		||||
 | 
			
		||||
  const hashedRequestPayload = getHash(payload);
 | 
			
		||||
  const httpRequestMethod = "POST";
 | 
			
		||||
  const contentType = "application/json";
 | 
			
		||||
  const canonicalUri = "/";
 | 
			
		||||
  const canonicalQueryString = "";
 | 
			
		||||
  const canonicalHeaders =
 | 
			
		||||
    `content-type:${contentType}\n` +
 | 
			
		||||
    "host:" +
 | 
			
		||||
    endpoint +
 | 
			
		||||
    "\n" +
 | 
			
		||||
    "x-tc-action:" +
 | 
			
		||||
    action.toLowerCase() +
 | 
			
		||||
    "\n";
 | 
			
		||||
  const signedHeaders = "content-type;host;x-tc-action";
 | 
			
		||||
 | 
			
		||||
  const canonicalRequest = [
 | 
			
		||||
    httpRequestMethod,
 | 
			
		||||
    canonicalUri,
 | 
			
		||||
    canonicalQueryString,
 | 
			
		||||
    canonicalHeaders,
 | 
			
		||||
    signedHeaders,
 | 
			
		||||
    hashedRequestPayload,
 | 
			
		||||
  ].join("\n");
 | 
			
		||||
 | 
			
		||||
  // ************* 步骤 2:拼接待签名字符串 *************
 | 
			
		||||
  const algorithm = "TC3-HMAC-SHA256";
 | 
			
		||||
  const hashedCanonicalRequest = getHash(canonicalRequest);
 | 
			
		||||
  const credentialScope = date + "/" + service + "/" + "tc3_request";
 | 
			
		||||
  const stringToSign =
 | 
			
		||||
    algorithm +
 | 
			
		||||
    "\n" +
 | 
			
		||||
    timestamp +
 | 
			
		||||
    "\n" +
 | 
			
		||||
    credentialScope +
 | 
			
		||||
    "\n" +
 | 
			
		||||
    hashedCanonicalRequest;
 | 
			
		||||
 | 
			
		||||
  // ************* 步骤 3:计算签名 *************
 | 
			
		||||
  const kDate = sha256(date, "TC3" + SECRET_KEY);
 | 
			
		||||
  const kService = sha256(service, kDate);
 | 
			
		||||
  const kSigning = sha256("tc3_request", kService);
 | 
			
		||||
  const signature = sha256(stringToSign, kSigning, "hex");
 | 
			
		||||
 | 
			
		||||
  // ************* 步骤 4:拼接 Authorization *************
 | 
			
		||||
  const authorization =
 | 
			
		||||
    algorithm +
 | 
			
		||||
    " " +
 | 
			
		||||
    "Credential=" +
 | 
			
		||||
    SECRET_ID +
 | 
			
		||||
    "/" +
 | 
			
		||||
    credentialScope +
 | 
			
		||||
    ", " +
 | 
			
		||||
    "SignedHeaders=" +
 | 
			
		||||
    signedHeaders +
 | 
			
		||||
    ", " +
 | 
			
		||||
    "Signature=" +
 | 
			
		||||
    signature;
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    Authorization: authorization,
 | 
			
		||||
    "Content-Type": contentType,
 | 
			
		||||
    Host: endpoint,
 | 
			
		||||
    "X-TC-Action": action,
 | 
			
		||||
    "X-TC-Timestamp": timestamp.toString(),
 | 
			
		||||
    "X-TC-Version": version,
 | 
			
		||||
    "X-TC-Region": region,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								package.json
									
									
									
									
									
								
							@@ -4,14 +4,14 @@
 | 
			
		||||
  "license": "mit",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "mask": "npx tsx app/masks/build.ts",
 | 
			
		||||
    "mask:watch": "npx watch 'yarn mask' app/masks",
 | 
			
		||||
    "dev": "yarn run mask:watch & next dev",
 | 
			
		||||
    "mask:watch": "npx watch \"yarn mask\" app/masks",
 | 
			
		||||
    "dev": "concurrently -r \"yarn run mask:watch\" \"next dev\"",
 | 
			
		||||
    "build": "yarn mask && cross-env BUILD_MODE=standalone next build",
 | 
			
		||||
    "start": "next start",
 | 
			
		||||
    "lint": "next lint",
 | 
			
		||||
    "export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
 | 
			
		||||
    "export:dev": "yarn mask:watch & cross-env BUILD_MODE=export BUILD_APP=1 next dev",
 | 
			
		||||
    "app:dev": "yarn mask:watch & yarn tauri dev",
 | 
			
		||||
    "export:dev": "concurrently -r \"yarn mask:watch\"  \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"",
 | 
			
		||||
    "app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
 | 
			
		||||
    "app:build": "yarn mask && yarn tauri build",
 | 
			
		||||
    "prompts": "node ./scripts/fetch-prompts.mjs",
 | 
			
		||||
    "prepare": "husky install",
 | 
			
		||||
@@ -28,6 +28,7 @@
 | 
			
		||||
    "fuse.js": "^7.0.0",
 | 
			
		||||
    "heic2any": "^0.0.4",
 | 
			
		||||
    "html-to-image": "^1.11.11",
 | 
			
		||||
    "lodash-es": "^4.17.21",
 | 
			
		||||
    "mermaid": "^10.6.1",
 | 
			
		||||
    "nanoid": "^5.0.3",
 | 
			
		||||
    "next": "^14.1.1",
 | 
			
		||||
@@ -48,11 +49,13 @@
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@tauri-apps/cli": "1.5.11",
 | 
			
		||||
    "@types/lodash-es": "^4.17.12",
 | 
			
		||||
    "@types/node": "^20.11.30",
 | 
			
		||||
    "@types/react": "^18.2.70",
 | 
			
		||||
    "@types/react-dom": "^18.2.7",
 | 
			
		||||
    "@types/react-katex": "^3.0.0",
 | 
			
		||||
    "@types/spark-md5": "^3.0.4",
 | 
			
		||||
    "concurrently": "^8.2.2",
 | 
			
		||||
    "cross-env": "^7.0.3",
 | 
			
		||||
    "eslint": "^8.49.0",
 | 
			
		||||
    "eslint-config-next": "13.4.19",
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,10 @@ self.addEventListener("install", function (event) {
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function jsonify(data) {
 | 
			
		||||
  return new Response(JSON.stringify(data), { headers: { 'content-type': 'application/json' } })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function upload(request, url) {
 | 
			
		||||
  const formData = await request.formData()
 | 
			
		||||
  const file = formData.getAll('file')[0]
 | 
			
		||||
@@ -33,13 +37,13 @@ async function upload(request, url) {
 | 
			
		||||
      'server': 'ServiceWorker',
 | 
			
		||||
    }
 | 
			
		||||
  }))
 | 
			
		||||
  return Response.json({ code: 0, data: fileUrl })
 | 
			
		||||
  return jsonify({ code: 0, data: fileUrl })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function remove(request, url) {
 | 
			
		||||
  const cache = await caches.open(CHATGPT_NEXT_WEB_FILE_CACHE)
 | 
			
		||||
  const res = await cache.delete(request.url)
 | 
			
		||||
  return Response.json({ code: 0 })
 | 
			
		||||
  return jsonify({ code: 0 })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
self.addEventListener("fetch", (e) => {
 | 
			
		||||
@@ -56,4 +60,3 @@ self.addEventListener("fetch", (e) => {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
  },
 | 
			
		||||
  "package": {
 | 
			
		||||
    "productName": "NextChat",
 | 
			
		||||
    "version": "2.13.1"
 | 
			
		||||
    "version": "2.14.1"
 | 
			
		||||
  },
 | 
			
		||||
  "tauri": {
 | 
			
		||||
    "allowlist": {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										111
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										111
									
								
								yarn.lock
									
									
									
									
									
								
							@@ -1035,6 +1035,13 @@
 | 
			
		||||
  dependencies:
 | 
			
		||||
    regenerator-runtime "^0.14.0"
 | 
			
		||||
 | 
			
		||||
"@babel/runtime@^7.21.0":
 | 
			
		||||
  version "7.25.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.0.tgz#3af9a91c1b739c569d5d80cc917280919c544ecb"
 | 
			
		||||
  integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    regenerator-runtime "^0.14.0"
 | 
			
		||||
 | 
			
		||||
"@babel/template@^7.18.10", "@babel/template@^7.20.7":
 | 
			
		||||
  version "7.20.7"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8"
 | 
			
		||||
@@ -1697,6 +1704,18 @@
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.14.0.tgz#b84c0afc3218069a5ad64fe2a95321881021b5fe"
 | 
			
		||||
  integrity sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==
 | 
			
		||||
 | 
			
		||||
"@types/lodash-es@^4.17.12":
 | 
			
		||||
  version "4.17.12"
 | 
			
		||||
  resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b"
 | 
			
		||||
  integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/lodash" "*"
 | 
			
		||||
 | 
			
		||||
"@types/lodash@*":
 | 
			
		||||
  version "4.17.7"
 | 
			
		||||
  resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612"
 | 
			
		||||
  integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==
 | 
			
		||||
 | 
			
		||||
"@types/mdast@^3.0.0":
 | 
			
		||||
  version "3.0.11"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0"
 | 
			
		||||
@@ -2269,7 +2288,7 @@ chalk@^2.0.0, chalk@^2.4.2:
 | 
			
		||||
    escape-string-regexp "^1.0.5"
 | 
			
		||||
    supports-color "^5.3.0"
 | 
			
		||||
 | 
			
		||||
chalk@^4.0.0:
 | 
			
		||||
chalk@^4.0.0, chalk@^4.1.2:
 | 
			
		||||
  version "4.1.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
 | 
			
		||||
  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
 | 
			
		||||
@@ -2335,6 +2354,15 @@ client-only@0.0.1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
 | 
			
		||||
  integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
 | 
			
		||||
 | 
			
		||||
cliui@^8.0.1:
 | 
			
		||||
  version "8.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
 | 
			
		||||
  integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    string-width "^4.2.0"
 | 
			
		||||
    strip-ansi "^6.0.1"
 | 
			
		||||
    wrap-ansi "^7.0.0"
 | 
			
		||||
 | 
			
		||||
color-convert@^1.9.0:
 | 
			
		||||
  version "1.9.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
 | 
			
		||||
@@ -2394,6 +2422,21 @@ concat-map@0.0.1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
 | 
			
		||||
  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
 | 
			
		||||
 | 
			
		||||
concurrently@^8.2.2:
 | 
			
		||||
  version "8.2.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-8.2.2.tgz#353141985c198cfa5e4a3ef90082c336b5851784"
 | 
			
		||||
  integrity sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    chalk "^4.1.2"
 | 
			
		||||
    date-fns "^2.30.0"
 | 
			
		||||
    lodash "^4.17.21"
 | 
			
		||||
    rxjs "^7.8.1"
 | 
			
		||||
    shell-quote "^1.8.1"
 | 
			
		||||
    spawn-command "0.0.2"
 | 
			
		||||
    supports-color "^8.1.1"
 | 
			
		||||
    tree-kill "^1.2.2"
 | 
			
		||||
    yargs "^17.7.2"
 | 
			
		||||
 | 
			
		||||
convert-source-map@^1.7.0:
 | 
			
		||||
  version "1.9.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
 | 
			
		||||
@@ -2801,6 +2844,13 @@ data-uri-to-buffer@^4.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
 | 
			
		||||
  integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
 | 
			
		||||
 | 
			
		||||
date-fns@^2.30.0:
 | 
			
		||||
  version "2.30.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
 | 
			
		||||
  integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/runtime" "^7.21.0"
 | 
			
		||||
 | 
			
		||||
dayjs@^1.11.7:
 | 
			
		||||
  version "1.11.7"
 | 
			
		||||
  resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"
 | 
			
		||||
@@ -3562,6 +3612,11 @@ gensync@^1.0.0-beta.2:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
 | 
			
		||||
  integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
 | 
			
		||||
 | 
			
		||||
get-caller-file@^2.0.5:
 | 
			
		||||
  version "2.0.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
 | 
			
		||||
  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
 | 
			
		||||
 | 
			
		||||
get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0:
 | 
			
		||||
  version "1.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f"
 | 
			
		||||
@@ -5480,6 +5535,11 @@ remark-rehype@^10.0.0:
 | 
			
		||||
    mdast-util-to-hast "^12.1.0"
 | 
			
		||||
    unified "^10.0.0"
 | 
			
		||||
 | 
			
		||||
require-directory@^2.1.1:
 | 
			
		||||
  version "2.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
 | 
			
		||||
  integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
 | 
			
		||||
 | 
			
		||||
resolve-from@^4.0.0:
 | 
			
		||||
  version "4.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
 | 
			
		||||
@@ -5557,6 +5617,13 @@ rxjs@^7.8.0:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    tslib "^2.1.0"
 | 
			
		||||
 | 
			
		||||
rxjs@^7.8.1:
 | 
			
		||||
  version "7.8.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
 | 
			
		||||
  integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    tslib "^2.1.0"
 | 
			
		||||
 | 
			
		||||
sade@^1.7.3:
 | 
			
		||||
  version "1.8.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
 | 
			
		||||
@@ -5639,6 +5706,11 @@ shebang-regex@^3.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
 | 
			
		||||
  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 | 
			
		||||
 | 
			
		||||
shell-quote@^1.8.1:
 | 
			
		||||
  version "1.8.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
 | 
			
		||||
  integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
 | 
			
		||||
 | 
			
		||||
side-channel@^1.0.4:
 | 
			
		||||
  version "1.0.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
 | 
			
		||||
@@ -5717,6 +5789,11 @@ spark-md5@^3.0.2:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc"
 | 
			
		||||
  integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==
 | 
			
		||||
 | 
			
		||||
spawn-command@0.0.2:
 | 
			
		||||
  version "0.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e"
 | 
			
		||||
  integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==
 | 
			
		||||
 | 
			
		||||
stable@^0.1.8:
 | 
			
		||||
  version "0.1.8"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
 | 
			
		||||
@@ -5739,7 +5816,7 @@ string-argv@^0.3.1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
 | 
			
		||||
  integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==
 | 
			
		||||
 | 
			
		||||
string-width@^4.1.0, string-width@^4.2.0:
 | 
			
		||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
 | 
			
		||||
  version "4.2.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
 | 
			
		||||
  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
 | 
			
		||||
@@ -5860,7 +5937,7 @@ supports-color@^7.1.0:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    has-flag "^4.0.0"
 | 
			
		||||
 | 
			
		||||
supports-color@^8.0.0:
 | 
			
		||||
supports-color@^8.0.0, supports-color@^8.1.1:
 | 
			
		||||
  version "8.1.1"
 | 
			
		||||
  resolved "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
 | 
			
		||||
  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
 | 
			
		||||
@@ -5956,6 +6033,11 @@ to-regex-range@^5.0.1:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    is-number "^7.0.0"
 | 
			
		||||
 | 
			
		||||
tree-kill@^1.2.2:
 | 
			
		||||
  version "1.2.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
 | 
			
		||||
  integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
 | 
			
		||||
 | 
			
		||||
trim-lines@^3.0.0:
 | 
			
		||||
  version "3.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
 | 
			
		||||
@@ -6355,6 +6437,11 @@ wrappy@1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
 | 
			
		||||
  integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
 | 
			
		||||
 | 
			
		||||
y18n@^5.0.5:
 | 
			
		||||
  version "5.0.8"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
 | 
			
		||||
  integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
 | 
			
		||||
 | 
			
		||||
yallist@^3.0.2:
 | 
			
		||||
  version "3.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
 | 
			
		||||
@@ -6375,6 +6462,24 @@ yaml@^2.2.2:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
 | 
			
		||||
  integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
 | 
			
		||||
 | 
			
		||||
yargs-parser@^21.1.1:
 | 
			
		||||
  version "21.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
 | 
			
		||||
  integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
 | 
			
		||||
 | 
			
		||||
yargs@^17.7.2:
 | 
			
		||||
  version "17.7.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
 | 
			
		||||
  integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    cliui "^8.0.1"
 | 
			
		||||
    escalade "^3.1.1"
 | 
			
		||||
    get-caller-file "^2.0.5"
 | 
			
		||||
    require-directory "^2.1.1"
 | 
			
		||||
    string-width "^4.2.3"
 | 
			
		||||
    y18n "^5.0.5"
 | 
			
		||||
    yargs-parser "^21.1.1"
 | 
			
		||||
 | 
			
		||||
yocto-queue@^0.1.0:
 | 
			
		||||
  version "0.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user